Pārlūkot izejas kodu

feat: integrate fluent-i18n localization (Fluent/FTL)

Adds Mozilla Fluent-based i18n via the fluent-i18n crate:
- locales/en/main.ftl — English translations (fallback)
- locales/ru/main.ftl — Russian translations
- Auto-detects system locale via set_locale(None)
- Converted config.rs error/info messages to t!() macro

Usage: set LANG=ru_RU.UTF-8 or the system locale controls which
translation is used. Falls back to English when the locale doesn't
have a matching translation file.
Alexander Korolev 4 mēneši atpakaļ
vecāks
revīzija
658cf4e935
6 mainītis faili ar 290 papildinājumiem un 7 dzēšanām
  1. 250 0
      Cargo.lock
  2. 1 0
      Cargo.toml
  3. 13 0
      locales/en/main.ftl
  4. 13 0
      locales/ru/main.ftl
  5. 9 7
      src/config.rs
  6. 4 0
      src/main.rs

+ 250 - 0
Cargo.lock

@@ -351,6 +351,16 @@ dependencies = [
  "cipher 0.5.2",
 ]
 
+[[package]]
+name = "bstr"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
 [[package]]
 name = "bumpalo"
 version = "3.20.3"
@@ -602,6 +612,31 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
 [[package]]
 name = "crypto-bigint"
 version = "0.5.5"
@@ -1043,6 +1078,94 @@ dependencies = [
  "miniz_oxide",
 ]
 
+[[package]]
+name = "fluent-bundle"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4"
+dependencies = [
+ "fluent-langneg",
+ "fluent-syntax",
+ "intl-memoizer",
+ "intl_pluralrules",
+ "rustc-hash",
+ "self_cell",
+ "smallvec",
+ "unic-langid",
+]
+
+[[package]]
+name = "fluent-i18n"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8550f449bcf1c66798713df8965e3b6306d99a47a71cc5025dc9ba2bceb76e15"
+dependencies = [
+ "fluent-templates",
+ "sys-locale",
+ "thiserror 2.0.18",
+ "unic-langid",
+]
+
+[[package]]
+name = "fluent-langneg"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0"
+dependencies = [
+ "unic-langid",
+]
+
+[[package]]
+name = "fluent-syntax"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198"
+dependencies = [
+ "memchr",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "fluent-template-macros"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "748050b3fb6fd97b566aedff8e9e021389c963e73d5afbeb92752c2b8d686c6c"
+dependencies = [
+ "flume",
+ "ignore",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "unic-langid",
+]
+
+[[package]]
+name = "fluent-templates"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56264446a01f404469aef9cc5fd4a4d736f68a0f52482bf6d1a54d6e9bbd9476"
+dependencies = [
+ "fluent-bundle",
+ "fluent-langneg",
+ "fluent-syntax",
+ "fluent-template-macros",
+ "flume",
+ "ignore",
+ "intl-memoizer",
+ "log",
+ "thiserror 2.0.18",
+ "unic-langid",
+]
+
+[[package]]
+name = "flume"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
+dependencies = [
+ "spin",
+]
+
 [[package]]
 name = "form_urlencoded"
 version = "1.2.2"
@@ -1228,6 +1351,19 @@ dependencies = [
  "polyval 0.7.1",
 ]
 
+[[package]]
+name = "globset"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "log",
+ "regex-automata",
+ "regex-syntax",
+]
+
 [[package]]
 name = "group"
 version = "0.13.0"
@@ -1555,6 +1691,22 @@ dependencies = [
  "icu_properties",
 ]
 
+[[package]]
+name = "ignore"
+version = "0.4.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b915661dd01db3f05050265b2477bcc6527b3792388e2749b41623cc592be67d"
+dependencies = [
+ "crossbeam-deque",
+ "globset",
+ "log",
+ "memchr",
+ "regex-automata",
+ "same-file",
+ "walkdir",
+ "winapi-util",
+]
+
 [[package]]
 name = "indexmap"
 version = "2.14.0"
@@ -1625,6 +1777,25 @@ dependencies = [
  "rand_core 0.10.1",
 ]
 
+[[package]]
+name = "intl-memoizer"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f"
+dependencies = [
+ "type-map",
+ "unic-langid",
+]
+
+[[package]]
+name = "intl_pluralrules"
+version = "7.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972"
+dependencies = [
+ "unic-langid",
+]
+
 [[package]]
 name = "ipnet"
 version = "2.12.0"
@@ -2359,6 +2530,12 @@ dependencies = [
  "elliptic-curve 0.14.0-rc.33",
 ]
 
+[[package]]
+name = "proc-macro-hack"
+version = "0.5.20+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.106"
@@ -3056,6 +3233,12 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "self_cell"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89"
+
 [[package]]
 name = "semver"
 version = "1.0.28"
@@ -3259,6 +3442,7 @@ dependencies = [
  "base64",
  "clap",
  "dirs",
+ "fluent-i18n",
  "futures",
  "reqwest",
  "russh",
@@ -3293,6 +3477,9 @@ name = "spin"
 version = "0.9.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
 
 [[package]]
 name = "spki"
@@ -3453,6 +3640,15 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "sys-locale"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "thiserror"
 version = "1.0.69"
@@ -3509,6 +3705,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
 dependencies = [
  "displaydoc",
+ "serde_core",
  "zerovec",
 ]
 
@@ -3741,12 +3938,64 @@ version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
 
+[[package]]
+name = "type-map"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90"
+dependencies = [
+ "rustc-hash",
+]
+
 [[package]]
 name = "typenum"
 version = "1.20.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
 
+[[package]]
+name = "unic-langid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05"
+dependencies = [
+ "unic-langid-impl",
+ "unic-langid-macros",
+]
+
+[[package]]
+name = "unic-langid-impl"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658"
+dependencies = [
+ "tinystr",
+]
+
+[[package]]
+name = "unic-langid-macros"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5957eb82e346d7add14182a3315a7e298f04e1ba4baac36f7f0dbfedba5fc25"
+dependencies = [
+ "proc-macro-hack",
+ "tinystr",
+ "unic-langid-impl",
+ "unic-langid-macros-impl",
+]
+
+[[package]]
+name = "unic-langid-macros-impl"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1249a628de3ad34b821ecb1001355bca3940bcb2f88558f1a8bd82e977f75b5"
+dependencies = [
+ "proc-macro-hack",
+ "quote",
+ "syn",
+ "unic-langid-impl",
+]
+
 [[package]]
 name = "unicode-ident"
 version = "1.0.24"
@@ -4471,6 +4720,7 @@ version = "0.11.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
 dependencies = [
+ "serde",
  "yoke",
  "zerofrom",
  "zerovec-derive",

+ 1 - 0
Cargo.toml

@@ -19,3 +19,4 @@ base64 = "0.22"
 futures = "0.3"
 dirs = "6"
 reqwest = { version = "0.13", default-features = false, features = ["json", "rustls"] }
+fluent-i18n = "0.1.0"

+ 13 - 0
locales/en/main.ftl

@@ -0,0 +1,13 @@
+cluster-not-found = Cluster "{ $name }" not found
+nccl-config-missing = NCCL config not found
+cluster-auto-fail = No suitable cluster found
+cluster-selected = ✅ Auto-selected cluster: { $name } ({ $nodes } nodes)
+container-log-header = 📜 Latest container logs:
+cluster-stopping = 🛑 Stopping cluster '{ $name }'...
+cluster-all-stopped = ✅ All containers stopped.
+cluster-section = === Cluster: { $name } ===
+container-count = Total containers: { $count }
+no-sm121-containers = (no sm121 containers)
+docker-unavailable = ✗ Docker unavailable
+ssh-error = { $node } ({ $ip }): ✗ SSH error: { $error }
+task-join-error = ✗ Task join error: { $error }

+ 13 - 0
locales/ru/main.ftl

@@ -0,0 +1,13 @@
+cluster-not-found = Кластер "{ $name }" не найден
+nccl-config-missing = NCCL конфиг не найден
+cluster-auto-fail = Не удалось подобрать подходящий кластер
+cluster-selected = ✅ Автоподбор кластера: { $name } ({ $nodes } узлов)
+container-log-header = 📜 Последние логи контейнеров:
+cluster-stopping = 🛑 Останавливаем кластер '{ $name }'...
+cluster-all-stopped = ✅ Все контейнеры остановлены.
+cluster-section = === Кластер: { $name } ===
+container-count = Всего контейнеров: { $count }
+no-sm121-containers = (нет контейнеров sm121)
+docker-unavailable = ✗ Docker недоступен
+ssh-error = { $node } ({ $ip }): ✗ SSH ошибка: { $error }
+task-join-error = ✗ Task join error: { $error }

+ 9 - 7
src/config.rs

@@ -126,10 +126,10 @@ fn resolve_cluster_name<'a>(clusters: &'a [Cluster], name: &str) -> Result<&'a C
         .filter(|c| c.name.starts_with(name))
         .collect();
     match matches.len() {
-        0 => anyhow::bail!("Кластер {} не найден", name),
+        0 => anyhow::bail!(fluent_i18n::t!("cluster-not-found", { "name" => name.to_string() })),
         1 => Ok(matches[0]),
         n => anyhow::bail!(
-            "Префикс '{}' соответствует {} кластерам: {}",
+            "Pref '{}' matches {} clusters: {}",
             name,
             n,
             matches
@@ -164,7 +164,7 @@ pub fn select_cluster<'c>(
         let nccl = config
             .nccl
             .get(&cluster.name)
-            .context("NCCL конфиг не найден")?;
+            .context(fluent_i18n::t!("nccl-config-missing"))?;
         return Ok((cluster.clone(), nccl));
     }
     let mut candidates: Vec<(&Cluster, &NcclConfig)> = config
@@ -176,13 +176,15 @@ pub fn select_cluster<'c>(
     candidates.sort_by_key(|(c, _)| c.nodes.len());
     if let Some((cluster, nccl)) = candidates.into_iter().next() {
         info!(
-            "✅ Автоподбор кластера: {} ({} узлов)",
-            cluster.name,
-            cluster.nodes.len()
+            "{}",
+            fluent_i18n::t!("cluster-selected", {
+                "name" => cluster.name.to_string(),
+                "nodes" => cluster.nodes.len(),
+            })
         );
         return Ok((cluster.clone(), nccl));
     }
-    anyhow::bail!("Не удалось подобрать подходящий кластер");
+    anyhow::bail!(fluent_i18n::t!("cluster-auto-fail"));
 }
 /// Looks up a cluster by name (or unique prefix) without requiring NCCL config.
 pub fn select_cluster_by_name(config: &Config, cluster_name: &str) -> Result<Cluster> {

+ 4 - 0
src/main.rs

@@ -3,6 +3,8 @@
 use anyhow::Result;
 use clap::Parser;
 
+fluent_i18n::i18n!("locales", fallback = "en");
+
 mod before_serve;
 mod cli;
 mod config;
@@ -55,6 +57,8 @@ fn resolve_config_path(cli_config: &str) -> std::path::PathBuf {
 
 #[tokio::main]
 async fn main() -> Result<()> {
+    // Auto-detect system locale for translations
+    let _ = fluent_i18n::set_locale(None);
     tracing_subscriber::fmt::init();
     let cli = Cli::parse();