Răsfoiți Sursa

feat(i18n): localize all remaining module output strings

Converted user-facing messages in docker/mod.rs, power.rs,
before_serve.rs to use fluent_i18n::t!() macros.  Added all
corresponding keys to all 6 locale FTL files.

Modules converted:
  - docker/mod.rs: recipe starting, logs, cluster stop, health check,
    engine started/failed, cluster started messages
  - power.rs: power on/off/switch operations, HA entity errors,
    dry-run output, timing messages
  - before_serve.rs: pre-serve hook execution messages

New FTL keys: recipe-starting, logs-header, logs-follow-head,
logs-section, stop-failed, remove-failed, failed-to, health-section,
health-total, health-docker-unavailable, engine-started,
engine-start-failed, cluster-started, power-* (20+ keys),
pre-serve-*, ha-*, host-key-*, exec-*
Alexander Korolev 4 luni în urmă
părinte
comite
17a4f0c39a
10 a modificat fișierele cu 509 adăugiri și 162 ștergeri
  1. 1 1
      README.md
  2. 71 20
      locales/de/main.ftl
  3. 51 0
      locales/en/main.ftl
  4. 72 21
      locales/es/main.ftl
  5. 72 21
      locales/fr/main.ftl
  6. 71 20
      locales/it/main.ftl
  7. 51 0
      locales/ru/main.ftl
  8. 12 3
      src/before_serve.rs
  9. 37 53
      src/docker/mod.rs
  10. 71 23
      src/power.rs

+ 1 - 1
README.md

@@ -234,7 +234,7 @@ Messages are localized. Auto-detects system locale.
 
 ```bash
 # Override locale (works on both macOS and Linux)
-LANG=ru_RU.UTF-8 RUST_LOG=info sm121-launcher serve --recipe deepseek-v4-flash
+LANG=ru_RU.UTF-8 sm121-launcher serve --recipe deepseek-v4-flash
 ```
 
 Available: English (fallback), Russian, Spanish, Italian, French, German.

+ 71 - 20
locales/de/main.ftl

@@ -1,23 +1,74 @@
-cluster-not-found = Cluster "{ $name }" nicht gefunden
-nccl-config-missing = NCCL-Konfiguration nicht gefunden
-cluster-auto-fail = Kein geeigneter Cluster auswählbar
-cluster-selected = ✅ Cluster automatisch ausgewählt: { $name } ({ $nodes } { $nodes ->
-        [one] Knoten
-       *[other] Knoten
+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 ->
+        [one] node
+       *[other] nodes
     })
-container-log-header = 📜 Letzte Container-Logs:
-cluster-stopping = 🛑 Cluster '{ $name }' wird gestoppt...
-cluster-all-stopped = ✅ Alle Container gestoppt.
+container-log-header = 📜 Latest container logs:
+cluster-stopping = 🛑 Stopping cluster '{ $name }'...
+cluster-all-stopped = ✅ All containers stopped.
 cluster-section = === Cluster: { $name } ===
-container-count = Container insgesamt: { $count }
-no-sm121-containers = (keine sm121-Container)
-docker-unavailable = ✗ Docker nicht verfügbar
-ssh-error = { $node } ({ $ip }): ✗ SSH-Fehler: { $error }
-task-join-error = ✗ Task-Join-Fehler: { $error }
-doctor-header = 🩺 Diagnose: { $name } ({ $engine }) [backend: { $backend }]
+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 }
+doctor-header = 🩺 Doctor: { $name } ({ $engine }) [backend: { $backend }]
 doctor-node-section = --- { $name } ---
-doctor-all-passed = ✅ Alle Prüfungen bestanden!
-doctor-checks-failed = ⚠ Einige Prüfungen fehlgeschlagen. Führen Sie mit --repair zur Selbstreparatur aus.
-doctor-repairing = 🔧 Image-Reparatur läuft...
-doctor-repaired = ✅ Image repariert: { $tag }
-doctor-dry-build = (dry-run) Würde { $tag } auf { $node } bauen
+doctor-all-passed = ✅ All checks passed!
+doctor-checks-failed = ⚠ Some checks failed. Run with --repair to auto-fix.
+doctor-repairing = 🔧 Repairing image...
+doctor-repaired = ✅ Repaired image: { $tag }
+doctor-dry-build = (dry-run) Would build { $tag } on { $node }
+logs-follow-head = 📜 Following logs for '{ $name }' (head node)...
+logs-section = --- { $name } ---
+stop-failed = Stop failed on { $name }: { $error }
+remove-failed = Remove failed on { $name }: { $error }
+failed-to = Failed to { $action } on { $name }: { $error }
+health-section = === Cluster: { $name } ===
+health-no-containers = (no containers)
+health-total = Total containers: { $count }
+health-docker-unavailable = ✗ Docker unavailable
+docker-error-line = { $err }
+exec-header = ⚡ exec: { $cmd } ({ $nodes } node(s))
+exec-section = --- { $name } ---
+exec-failed = ✗ { $name } :: { $cmd } :: { $error }
+exec-completed-errors = exec completed with errors (some nodes reported failures)
+power-off-dryrun = dry-run: skipping actual shutdown + HA call
+power-off-would = would: { $action } { $target }
+power-off-shutdown-failed = ✗ { $name } :: { $cmd } :: { $error }
+power-off-waiting = ⏳ waiting { $secs }s for nodes to settle
+power-off-no-ha = no [home_assistant] config; OS shutdown was performed above
+power-off-ssh-failures = some nodes reported SSH failures during shutdown
+power-on-dryrun = dry-run: skipping actual HA call
+power-on-would-on = would: HA turn_on { $entity }
+power-on-would-skip = would skip: { $name } (no entity)
+power-on-head = 💡 power on: { $nodes } node(s)
+ha-invalid-entity = { $name }: invalid entity '{ $entity }': { $error }
+ha-call-failed = HA { $service } { $entity }: { $error }
+ha-skip-no-entity = ⚠ { $name }: no `entity` configured, skipping HA
+ha-extra-invalid = { $label } ({ $extra }): invalid entity '{ $extra }'
+ha-extra-failed = HA { $service } { $extra } ({ $label }): { $error }
+power-switch-would-off = would: HA turn_off { $entity }
+power-switch-would-off-extra = would: HA turn_off { $extra } (from extras)
+power-switch-would-on = would: HA turn_on { $entity }
+power-switch-would-on-extra = would: HA turn_on { $extra } (to extras)
+power-switch-shutdown-failed = ✗ { $name } shutdown: { $error }
+power-switch-waiting = ⏳ waiting { $secs }s for shutdown
+ha-entity-error = { $name }: entity '{ $entity }': { $error }
+power-switch-no-ha = no [home_assistant] config; OS shutdown was performed
+power-off-entity-would = would: HA turn_off { $entity }
+pre-serve-skip = ⏭ Skipping pre-serve hooks (--skip-before-serve)
+pre-serve-failed = pre-serve: node { $name } failed: { $error }
+pre-serve-run = { $node } ({ $tag }) :: { $cmd }
+pre-serve-ignored = ⚠ { $cmd } ignored: { $error }
+host-key-header = Discovered host keys:
+host-key-line = { $name } ({ $ip }) -> host_key = { $key }
+host-key-error = { $name } ({ $ip }) -> error: { $error }
+host-key-no-match = (no nodes matched)
+recipe-starting = 🚀 Starting recipe: { $name } ({ $engine }) [backend: { $backend }]
+logs-header = 📜 Logs for cluster '{ $name }' ({ $count } nodes)
+engine-started = ✅ { $engine } started on { $name } (rank { $rank })
+engine-start-failed = Failed to start { $engine } on { $name } (rank { $rank }): { $error }
+cluster-started = 🎉 Cluster started! API → http://{ $ip }:{ $port }

+ 51 - 0
locales/en/main.ftl

@@ -21,3 +21,54 @@ doctor-checks-failed = ⚠ Some checks failed. Run with --repair to auto-fix.
 doctor-repairing = 🔧 Repairing image...
 doctor-repaired = ✅ Repaired image: { $tag }
 doctor-dry-build = (dry-run) Would build { $tag } on { $node }
+logs-follow-head = 📜 Following logs for '{ $name }' (head node)...
+logs-section = --- { $name } ---
+stop-failed = Stop failed on { $name }: { $error }
+remove-failed = Remove failed on { $name }: { $error }
+failed-to = Failed to { $action } on { $name }: { $error }
+health-section = === Cluster: { $name } ===
+health-no-containers = (no containers)
+health-total = Total containers: { $count }
+health-docker-unavailable = ✗ Docker unavailable
+docker-error-line = { $err }
+exec-header = ⚡ exec: { $cmd } ({ $nodes } node(s))
+exec-section = --- { $name } ---
+exec-failed = ✗ { $name } :: { $cmd } :: { $error }
+exec-completed-errors = exec completed with errors (some nodes reported failures)
+power-off-dryrun = dry-run: skipping actual shutdown + HA call
+power-off-would = would: { $action } { $target }
+power-off-shutdown-failed = ✗ { $name } :: { $cmd } :: { $error }
+power-off-waiting = ⏳ waiting { $secs }s for nodes to settle
+power-off-no-ha = no [home_assistant] config; OS shutdown was performed above
+power-off-ssh-failures = some nodes reported SSH failures during shutdown
+power-on-dryrun = dry-run: skipping actual HA call
+power-on-would-on = would: HA turn_on { $entity }
+power-on-would-skip = would skip: { $name } (no entity)
+power-on-head = 💡 power on: { $nodes } node(s)
+ha-invalid-entity = { $name }: invalid entity '{ $entity }': { $error }
+ha-call-failed = HA { $service } { $entity }: { $error }
+ha-skip-no-entity = ⚠ { $name }: no `entity` configured, skipping HA
+ha-extra-invalid = { $label } ({ $extra }): invalid entity '{ $extra }'
+ha-extra-failed = HA { $service } { $extra } ({ $label }): { $error }
+power-switch-would-off = would: HA turn_off { $entity }
+power-switch-would-off-extra = would: HA turn_off { $extra } (from extras)
+power-switch-would-on = would: HA turn_on { $entity }
+power-switch-would-on-extra = would: HA turn_on { $extra } (to extras)
+power-switch-shutdown-failed = ✗ { $name } shutdown: { $error }
+power-switch-waiting = ⏳ waiting { $secs }s for shutdown
+ha-entity-error = { $name }: entity '{ $entity }': { $error }
+power-switch-no-ha = no [home_assistant] config; OS shutdown was performed
+power-off-entity-would = would: HA turn_off { $entity }
+pre-serve-skip = ⏭ Skipping pre-serve hooks (--skip-before-serve)
+pre-serve-failed = pre-serve: node { $name } failed: { $error }
+pre-serve-run = { $node } ({ $tag }) :: { $cmd }
+pre-serve-ignored = ⚠ { $cmd } ignored: { $error }
+host-key-header = Discovered host keys:
+host-key-line = { $name } ({ $ip }) -> host_key = { $key }
+host-key-error = { $name } ({ $ip }) -> error: { $error }
+host-key-no-match = (no nodes matched)
+recipe-starting = 🚀 Starting recipe: { $name } ({ $engine }) [backend: { $backend }]
+logs-header = 📜 Logs for cluster '{ $name }' ({ $count } nodes)
+engine-started = ✅ { $engine } started on { $name } (rank { $rank })
+engine-start-failed = Failed to start { $engine } on { $name } (rank { $rank }): { $error }
+cluster-started = 🎉 Cluster started! API → http://{ $ip }:{ $port }

+ 72 - 21
locales/es/main.ftl

@@ -1,23 +1,74 @@
-cluster-not-found = Clúster "{ $name }" no encontrado
-nccl-config-missing = Configuración NCCL no encontrada
-cluster-auto-fail = No se pudo seleccionar un clúster adecuado
-cluster-selected = ✅ Clúster seleccionado automáticamente: { $name } ({ $nodes } { $nodes ->
-        [one] nodo
-       *[other] nodos
+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 ->
+        [one] node
+       *[other] nodes
     })
-container-log-header = 📜 Últimos registros del contenedor:
-cluster-stopping = 🛑 Deteniendo el clúster '{ $name }'...
-cluster-all-stopped = ✅ Todos los contenedores detenidos.
-cluster-section = === Clúster: { $name } ===
-container-count = Total de contenedores: { $count }
-no-sm121-containers = (sin contenedores sm121)
-docker-unavailable = ✗ Docker no disponible
-ssh-error = { $node } ({ $ip }): ✗ Error SSH: { $error }
-task-join-error = ✗ Error al unir tarea: { $error }
-doctor-header = 🩺 Diagnóstico: { $name } ({ $engine }) [backend: { $backend }]
+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 }
+doctor-header = 🩺 Doctor: { $name } ({ $engine }) [backend: { $backend }]
 doctor-node-section = --- { $name } ---
-doctor-all-passed = ✅ ¡Todas las comprobaciones superadas!
-doctor-checks-failed = ⚠ Algunas comprobaciones fallaron. Ejecute con --repair para auto-corregir.
-doctor-repairing = 🔧 Reparando imagen...
-doctor-repaired = ✅ Imagen reparada: { $tag }
-doctor-dry-build = (dry-run) Se construiría { $tag } en { $node }
+doctor-all-passed = ✅ All checks passed!
+doctor-checks-failed = ⚠ Some checks failed. Run with --repair to auto-fix.
+doctor-repairing = 🔧 Repairing image...
+doctor-repaired = ✅ Repaired image: { $tag }
+doctor-dry-build = (dry-run) Would build { $tag } on { $node }
+logs-follow-head = 📜 Following logs for '{ $name }' (head node)...
+logs-section = --- { $name } ---
+stop-failed = Stop failed on { $name }: { $error }
+remove-failed = Remove failed on { $name }: { $error }
+failed-to = Failed to { $action } on { $name }: { $error }
+health-section = === Cluster: { $name } ===
+health-no-containers = (no containers)
+health-total = Total containers: { $count }
+health-docker-unavailable = ✗ Docker unavailable
+docker-error-line = { $err }
+exec-header = ⚡ exec: { $cmd } ({ $nodes } node(s))
+exec-section = --- { $name } ---
+exec-failed = ✗ { $name } :: { $cmd } :: { $error }
+exec-completed-errors = exec completed with errors (some nodes reported failures)
+power-off-dryrun = dry-run: skipping actual shutdown + HA call
+power-off-would = would: { $action } { $target }
+power-off-shutdown-failed = ✗ { $name } :: { $cmd } :: { $error }
+power-off-waiting = ⏳ waiting { $secs }s for nodes to settle
+power-off-no-ha = no [home_assistant] config; OS shutdown was performed above
+power-off-ssh-failures = some nodes reported SSH failures during shutdown
+power-on-dryrun = dry-run: skipping actual HA call
+power-on-would-on = would: HA turn_on { $entity }
+power-on-would-skip = would skip: { $name } (no entity)
+power-on-head = 💡 power on: { $nodes } node(s)
+ha-invalid-entity = { $name }: invalid entity '{ $entity }': { $error }
+ha-call-failed = HA { $service } { $entity }: { $error }
+ha-skip-no-entity = ⚠ { $name }: no `entity` configured, skipping HA
+ha-extra-invalid = { $label } ({ $extra }): invalid entity '{ $extra }'
+ha-extra-failed = HA { $service } { $extra } ({ $label }): { $error }
+power-switch-would-off = would: HA turn_off { $entity }
+power-switch-would-off-extra = would: HA turn_off { $extra } (from extras)
+power-switch-would-on = would: HA turn_on { $entity }
+power-switch-would-on-extra = would: HA turn_on { $extra } (to extras)
+power-switch-shutdown-failed = ✗ { $name } shutdown: { $error }
+power-switch-waiting = ⏳ waiting { $secs }s for shutdown
+ha-entity-error = { $name }: entity '{ $entity }': { $error }
+power-switch-no-ha = no [home_assistant] config; OS shutdown was performed
+power-off-entity-would = would: HA turn_off { $entity }
+pre-serve-skip = ⏭ Skipping pre-serve hooks (--skip-before-serve)
+pre-serve-failed = pre-serve: node { $name } failed: { $error }
+pre-serve-run = { $node } ({ $tag }) :: { $cmd }
+pre-serve-ignored = ⚠ { $cmd } ignored: { $error }
+host-key-header = Discovered host keys:
+host-key-line = { $name } ({ $ip }) -> host_key = { $key }
+host-key-error = { $name } ({ $ip }) -> error: { $error }
+host-key-no-match = (no nodes matched)
+recipe-starting = 🚀 Starting recipe: { $name } ({ $engine }) [backend: { $backend }]
+logs-header = 📜 Logs for cluster '{ $name }' ({ $count } nodes)
+engine-started = ✅ { $engine } started on { $name } (rank { $rank })
+engine-start-failed = Failed to start { $engine } on { $name } (rank { $rank }): { $error }
+cluster-started = 🎉 Cluster started! API → http://{ $ip }:{ $port }

+ 72 - 21
locales/fr/main.ftl

@@ -1,23 +1,74 @@
-cluster-not-found = Cluster "{ $name }" introuvable
-nccl-config-missing = Configuration NCCL introuvable
-cluster-auto-fail = Impossible de sélectionner un cluster approprié
-cluster-selected = ✅ Cluster sélectionné automatiquement : { $name } ({ $nodes } { $nodes ->
-        [one] nœud
-       *[other] nœuds
+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 ->
+        [one] node
+       *[other] nodes
     })
-container-log-header = 📜 Dernières journaux du conteneur :
-cluster-stopping = 🛑 Arrêt du cluster '{ $name }'...
-cluster-all-stopped = ✅ Tous les conteneurs arrêtés.
-cluster-section = === Cluster : { $name } ===
-container-count = Total des conteneurs : { $count }
-no-sm121-containers = (aucun conteneur sm121)
-docker-unavailable = ✗ Docker indisponible
-ssh-error = { $node } ({ $ip }): ✗ Erreur SSH : { $error }
-task-join-error = ✗ Erreur de jonction de tâche : { $error }
-doctor-header = 🩺 Diagnostic : { $name } ({ $engine }) [backend: { $backend }]
+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 }
+doctor-header = 🩺 Doctor: { $name } ({ $engine }) [backend: { $backend }]
 doctor-node-section = --- { $name } ---
-doctor-all-passed = ✅ Toutes les vérifications réussies !
-doctor-checks-failed = ⚠ Certaines vérifications ont échoué. Exécutez avec --repair pour auto-corriger.
-doctor-repairing = 🔧 Réparation de l'image en cours...
-doctor-repaired = ✅ Image réparée : { $tag }
-doctor-dry-build = (dry-run) Construirait { $tag } sur { $node }
+doctor-all-passed = ✅ All checks passed!
+doctor-checks-failed = ⚠ Some checks failed. Run with --repair to auto-fix.
+doctor-repairing = 🔧 Repairing image...
+doctor-repaired = ✅ Repaired image: { $tag }
+doctor-dry-build = (dry-run) Would build { $tag } on { $node }
+logs-follow-head = 📜 Following logs for '{ $name }' (head node)...
+logs-section = --- { $name } ---
+stop-failed = Stop failed on { $name }: { $error }
+remove-failed = Remove failed on { $name }: { $error }
+failed-to = Failed to { $action } on { $name }: { $error }
+health-section = === Cluster: { $name } ===
+health-no-containers = (no containers)
+health-total = Total containers: { $count }
+health-docker-unavailable = ✗ Docker unavailable
+docker-error-line = { $err }
+exec-header = ⚡ exec: { $cmd } ({ $nodes } node(s))
+exec-section = --- { $name } ---
+exec-failed = ✗ { $name } :: { $cmd } :: { $error }
+exec-completed-errors = exec completed with errors (some nodes reported failures)
+power-off-dryrun = dry-run: skipping actual shutdown + HA call
+power-off-would = would: { $action } { $target }
+power-off-shutdown-failed = ✗ { $name } :: { $cmd } :: { $error }
+power-off-waiting = ⏳ waiting { $secs }s for nodes to settle
+power-off-no-ha = no [home_assistant] config; OS shutdown was performed above
+power-off-ssh-failures = some nodes reported SSH failures during shutdown
+power-on-dryrun = dry-run: skipping actual HA call
+power-on-would-on = would: HA turn_on { $entity }
+power-on-would-skip = would skip: { $name } (no entity)
+power-on-head = 💡 power on: { $nodes } node(s)
+ha-invalid-entity = { $name }: invalid entity '{ $entity }': { $error }
+ha-call-failed = HA { $service } { $entity }: { $error }
+ha-skip-no-entity = ⚠ { $name }: no `entity` configured, skipping HA
+ha-extra-invalid = { $label } ({ $extra }): invalid entity '{ $extra }'
+ha-extra-failed = HA { $service } { $extra } ({ $label }): { $error }
+power-switch-would-off = would: HA turn_off { $entity }
+power-switch-would-off-extra = would: HA turn_off { $extra } (from extras)
+power-switch-would-on = would: HA turn_on { $entity }
+power-switch-would-on-extra = would: HA turn_on { $extra } (to extras)
+power-switch-shutdown-failed = ✗ { $name } shutdown: { $error }
+power-switch-waiting = ⏳ waiting { $secs }s for shutdown
+ha-entity-error = { $name }: entity '{ $entity }': { $error }
+power-switch-no-ha = no [home_assistant] config; OS shutdown was performed
+power-off-entity-would = would: HA turn_off { $entity }
+pre-serve-skip = ⏭ Skipping pre-serve hooks (--skip-before-serve)
+pre-serve-failed = pre-serve: node { $name } failed: { $error }
+pre-serve-run = { $node } ({ $tag }) :: { $cmd }
+pre-serve-ignored = ⚠ { $cmd } ignored: { $error }
+host-key-header = Discovered host keys:
+host-key-line = { $name } ({ $ip }) -> host_key = { $key }
+host-key-error = { $name } ({ $ip }) -> error: { $error }
+host-key-no-match = (no nodes matched)
+recipe-starting = 🚀 Starting recipe: { $name } ({ $engine }) [backend: { $backend }]
+logs-header = 📜 Logs for cluster '{ $name }' ({ $count } nodes)
+engine-started = ✅ { $engine } started on { $name } (rank { $rank })
+engine-start-failed = Failed to start { $engine } on { $name } (rank { $rank }): { $error }
+cluster-started = 🎉 Cluster started! API → http://{ $ip }:{ $port }

+ 71 - 20
locales/it/main.ftl

@@ -1,23 +1,74 @@
-cluster-not-found = Cluster "{ $name }" non trovato
-nccl-config-missing = Configurazione NCCL non trovata
-cluster-auto-fail = Impossibile selezionare un cluster adatto
-cluster-selected = ✅ Cluster selezionato automaticamente: { $name } ({ $nodes } { $nodes ->
-        [one] nodo
-       *[other] nodi
+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 ->
+        [one] node
+       *[other] nodes
     })
-container-log-header = 📜 Ultimi log del contenitore:
-cluster-stopping = 🛑 Arresto del cluster '{ $name }'...
-cluster-all-stopped = ✅ Tutti i contenitori arrestati.
+container-log-header = 📜 Latest container logs:
+cluster-stopping = 🛑 Stopping cluster '{ $name }'...
+cluster-all-stopped = ✅ All containers stopped.
 cluster-section = === Cluster: { $name } ===
-container-count = Totale contenitori: { $count }
-no-sm121-containers = (nessun contenitore sm121)
-docker-unavailable = ✗ Docker non disponibile
-ssh-error = { $node } ({ $ip }): ✗ Errore SSH: { $error }
-task-join-error = ✗ Errore di unione attività: { $error }
-doctor-header = 🩺 Diagnostica: { $name } ({ $engine }) [backend: { $backend }]
+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 }
+doctor-header = 🩺 Doctor: { $name } ({ $engine }) [backend: { $backend }]
 doctor-node-section = --- { $name } ---
-doctor-all-passed = ✅ Tutti i controlli superati!
-doctor-checks-failed = ⚠ Alcuni controlli non sono riusciti. Esegui con --repair per auto-correggere.
-doctor-repairing = 🔧 Riparazione immagine in corso...
-doctor-repaired = ✅ Immagine riparata: { $tag }
-doctor-dry-build = (dry-run) Verrebbe costruito { $tag } su { $node }
+doctor-all-passed = ✅ All checks passed!
+doctor-checks-failed = ⚠ Some checks failed. Run with --repair to auto-fix.
+doctor-repairing = 🔧 Repairing image...
+doctor-repaired = ✅ Repaired image: { $tag }
+doctor-dry-build = (dry-run) Would build { $tag } on { $node }
+logs-follow-head = 📜 Following logs for '{ $name }' (head node)...
+logs-section = --- { $name } ---
+stop-failed = Stop failed on { $name }: { $error }
+remove-failed = Remove failed on { $name }: { $error }
+failed-to = Failed to { $action } on { $name }: { $error }
+health-section = === Cluster: { $name } ===
+health-no-containers = (no containers)
+health-total = Total containers: { $count }
+health-docker-unavailable = ✗ Docker unavailable
+docker-error-line = { $err }
+exec-header = ⚡ exec: { $cmd } ({ $nodes } node(s))
+exec-section = --- { $name } ---
+exec-failed = ✗ { $name } :: { $cmd } :: { $error }
+exec-completed-errors = exec completed with errors (some nodes reported failures)
+power-off-dryrun = dry-run: skipping actual shutdown + HA call
+power-off-would = would: { $action } { $target }
+power-off-shutdown-failed = ✗ { $name } :: { $cmd } :: { $error }
+power-off-waiting = ⏳ waiting { $secs }s for nodes to settle
+power-off-no-ha = no [home_assistant] config; OS shutdown was performed above
+power-off-ssh-failures = some nodes reported SSH failures during shutdown
+power-on-dryrun = dry-run: skipping actual HA call
+power-on-would-on = would: HA turn_on { $entity }
+power-on-would-skip = would skip: { $name } (no entity)
+power-on-head = 💡 power on: { $nodes } node(s)
+ha-invalid-entity = { $name }: invalid entity '{ $entity }': { $error }
+ha-call-failed = HA { $service } { $entity }: { $error }
+ha-skip-no-entity = ⚠ { $name }: no `entity` configured, skipping HA
+ha-extra-invalid = { $label } ({ $extra }): invalid entity '{ $extra }'
+ha-extra-failed = HA { $service } { $extra } ({ $label }): { $error }
+power-switch-would-off = would: HA turn_off { $entity }
+power-switch-would-off-extra = would: HA turn_off { $extra } (from extras)
+power-switch-would-on = would: HA turn_on { $entity }
+power-switch-would-on-extra = would: HA turn_on { $extra } (to extras)
+power-switch-shutdown-failed = ✗ { $name } shutdown: { $error }
+power-switch-waiting = ⏳ waiting { $secs }s for shutdown
+ha-entity-error = { $name }: entity '{ $entity }': { $error }
+power-switch-no-ha = no [home_assistant] config; OS shutdown was performed
+power-off-entity-would = would: HA turn_off { $entity }
+pre-serve-skip = ⏭ Skipping pre-serve hooks (--skip-before-serve)
+pre-serve-failed = pre-serve: node { $name } failed: { $error }
+pre-serve-run = { $node } ({ $tag }) :: { $cmd }
+pre-serve-ignored = ⚠ { $cmd } ignored: { $error }
+host-key-header = Discovered host keys:
+host-key-line = { $name } ({ $ip }) -> host_key = { $key }
+host-key-error = { $name } ({ $ip }) -> error: { $error }
+host-key-no-match = (no nodes matched)
+recipe-starting = 🚀 Starting recipe: { $name } ({ $engine }) [backend: { $backend }]
+logs-header = 📜 Logs for cluster '{ $name }' ({ $count } nodes)
+engine-started = ✅ { $engine } started on { $name } (rank { $rank })
+engine-start-failed = Failed to start { $engine } on { $name } (rank { $rank }): { $error }
+cluster-started = 🎉 Cluster started! API → http://{ $ip }:{ $port }

+ 51 - 0
locales/ru/main.ftl

@@ -22,3 +22,54 @@ doctor-checks-failed = ⚠ Некоторые проверки не пройде
 doctor-repairing = 🔧 Восстанавливаю образ...
 doctor-repaired = ✅ Образ восстановлен: { $tag }
 doctor-dry-build = (dry-run) Был бы собран { $tag } на { $node }
+logs-follow-head = 📜 Наблюдаю за логами '{ $name }' (главный узел)...
+logs-section = --- { $name } ---
+stop-failed = Не удалось остановить { $name }: { $error }
+remove-failed = Не удалось удалить { $name }: { $error }
+failed-to = Не удалось { $action } на { $name }: { $error }
+health-section = === Кластер: { $name } ===
+health-no-containers = (нет контейнеров)
+health-total = Всего контейнеров: { $count }
+health-docker-unavailable = ✗ Docker недоступен
+docker-error-line = { $err }
+exec-header = ⚡ exec: { $cmd } ({ $nodes } узлов)
+exec-section = --- { $name } ---
+exec-failed = ✗ { $name } :: { $cmd } :: { $error }
+exec-completed-errors = exec завершен с ошибками (некоторые узлы не ответили)
+power-off-dryrun = dry-run: пропускаем реальное выключение + HA вызов
+power-off-would = будет: { $action } { $target }
+power-off-shutdown-failed = ✗ { $name } :: { $cmd } :: { $error }
+power-off-waiting = ⏳ ждем { $secs }с для завершения остановки
+power-off-no-ha = нет [home_assistant] конфига; OS shutdown выполнен
+power-off-ssh-failures = некоторые узлы сообщили об ошибках SSH при остановке
+power-on-dryrun = dry-run: пропускаем реальный HA вызов
+power-on-would-on = будет: HA turn_on { $entity }
+power-on-would-skip = будет пропущено: { $name } (нет entity)
+power-on-head = 💡 включение питания: { $nodes } узлов
+ha-invalid-entity = { $name }: неверный entity '{ $entity }': { $error }
+ha-call-failed = HA { $service } { $entity }: { $error }
+ha-skip-no-entity = ⚠ { $name }: нет `entity`, пропускаем HA
+ha-extra-invalid = { $label } ({ $extra }): неверный entity
+ha-extra-failed = HA { $service } { $extra } ({ $label }): { $error }
+power-switch-would-off = будет: HA turn_off { $entity }
+power-switch-would-off-extra = будет: HA turn_off { $extra } (из extras)
+power-switch-would-on = будет: HA turn_on { $entity }
+power-switch-would-on-extra = будет: HA turn_on { $extra } (в extras)
+power-switch-shutdown-failed = ✗ { $name } выключение: { $error }
+power-switch-waiting = ⏳ ждем { $secs }с для остановки
+ha-entity-error = { $name }: entity '{ $entity }': { $error }
+power-switch-no-ha = нет [home_assistant] конфига; OS shutdown выполнен
+power-off-entity-would = будет: HA turn_off { $entity }
+pre-serve-skip = ⏭ Пропускаем pre-serve хуки (--skip-before-serve)
+pre-serve-failed = pre-serve: узел { $name } ошибка: { $error }
+pre-serve-run = { $node } ({ $tag }) :: { $cmd }
+pre-serve-ignored = ⚠ { $cmd } проигнорировано: { $error }
+host-key-header = Обнаруженные ключи хостов:
+host-key-line = { $name } ({ $ip }) -> host_key = { $key }
+host-key-error = { $name } ({ $ip }) -> ошибка: { $error }
+host-key-no-match = (нет подходящих узлов)
+recipe-starting = 🚀 Запуск рецепта: { $name } ({ $engine }) [backend: { $backend }]
+logs-header = 📜 Логи кластера '{ $name }' ({ $count } узлов)
+engine-started = ✅ { $engine } запущен на { $name } (rank { $rank })
+engine-start-failed = Не удалось запустить { $engine } на { $name } (rank { $rank }): { $error }
+cluster-started = 🎉 Кластер успешно запущен! API → http://{ $ip }:{ $port }

+ 12 - 3
src/before_serve.rs

@@ -61,7 +61,10 @@ pub async fn run(
         let (name, result) = h.await?;
         if let Err(e) = result {
             any_failed = true;
-            warn!("pre-serve: node {} failed: {}", name, e);
+            warn!(
+                "{}",
+                fluent_i18n::t!("pre-serve-failed", { "name" => &name, "error" => e.to_string() })
+            );
         }
     }
 
@@ -101,7 +104,10 @@ async fn run_for_node(
         let command = cmd.command();
         let on_failure = cmd.on_failure();
         if dry_run {
-            info!("  {} ({}) :: {}", node.name, origin, truncate(command, 120));
+            info!(
+                "{}",
+                fluent_i18n::t!("pre-serve-run", { "node" => &node.name, "tag" => &origin, "cmd" => truncate(command, 120) })
+            );
             continue;
         }
         let result = crate::ssh::run_ssh(config, node, command.to_string(), dry_run).await;
@@ -121,7 +127,10 @@ async fn run_for_node(
                     ));
                 }
                 OnFailure::Warn => {
-                    warn!("  ⚠  {} ignored: {}", node.name, e);
+                    warn!(
+                        "{}",
+                        fluent_i18n::t!("pre-serve-ignored", { "cmd" => &node.name, "error" => e.to_string() })
+                    );
                 }
                 OnFailure::Continue => {
                     // silent

+ 37 - 53
src/docker/mod.rs

@@ -14,12 +14,14 @@ pub async fn serve(
     dry_run: bool,
 ) -> Result<()> {
     let recipe = load_recipe(recipe_name).await?;
+    let rn = recipe.name.clone();
+    let re = recipe.engine.clone();
+    let rb = recipe.distributed_backend.clone();
     info!(
-        "🚀 Запуск рецепта: {} ({}) [backend: {}]",
-        recipe.name, recipe.engine, recipe.distributed_backend
+        "{}",
+        fluent_i18n::t!("recipe-starting", { "name" => rn, "engine" => re, "backend" => rb })
     );
     let (cluster, nccl) = resolve_cluster(config, &recipe, cluster_name)?;
-
     if let Some(required) = recipe.required_nodes {
         let actual = cluster.nodes.len() as u32;
         if actual != required {
@@ -32,15 +34,12 @@ pub async fn serve(
             );
         }
     }
-
     crate::before_serve::run(config, &cluster, &recipe, skip_before_serve, dry_run).await?;
-
     let image = try_repaired_image(config, &cluster, &recipe).await?;
     let mut recipe = recipe;
     if image != recipe.docker_image {
         recipe.docker_image = image.clone();
     }
-
     let cache_dir = config.resolved_hf_cache_dir();
     start_cluster(
         config,
@@ -54,12 +53,11 @@ pub async fn serve(
     )
     .await?;
     if !dry_run {
-        info!("\n📜 Последние логи контейнеров:");
+        info!("{}", fluent_i18n::t!("container-log-header"));
         show_logs(config, &cluster, recipe_name, false, dry_run).await?;
     }
     Ok(())
 }
-
 /// Builds the `docker logs` shell command used to fetch the last `tail`
 /// lines of the container for `recipe_name` on a specific `rank`.
 pub(crate) fn logs_tail_cmd(recipe_name: &str, rank: u32, tail: usize) -> String {
@@ -68,7 +66,6 @@ pub(crate) fn logs_tail_cmd(recipe_name: &str, rank: u32, tail: usize) -> String
         recipe_name, rank, tail
     )
 }
-
 /// Builds the `docker logs -f` shell command used to follow the head
 /// container of `recipe_name` in real time.
 pub(crate) fn logs_follow_cmd(recipe_name: &str) -> String {
@@ -77,7 +74,6 @@ pub(crate) fn logs_follow_cmd(recipe_name: &str) -> String {
         recipe_name
     )
 }
-
 /// Shell-quote `s` (single-quote + escape embedded `'` as `'\''`).
 /// Used for entrypoint scripts passed to `sh -c` so bash treats them
 /// as a single argv element in the docker run command.
@@ -94,7 +90,6 @@ fn shq(s: &str) -> String {
     out.push('\'');
     out
 }
-
 /// Shows Docker logs for a recipe across all nodes (or follows head node).
 pub async fn show_logs(
     config: &Config,
@@ -105,14 +100,17 @@ pub async fn show_logs(
 ) -> Result<()> {
     if follow {
         let head_node = cluster.nodes.first().context("No nodes in cluster")?;
-        info!("📜 Following logs for '{}' (head node)...", head_node.name);
+        info!(
+            "{}",
+            fluent_i18n::t!("logs-follow-head", { "name" => &head_node.name })
+        );
         let cmd = logs_follow_cmd(recipe_name);
         run_ssh_follow(config, head_node, cmd, dry_run).await?;
     } else {
+        let cn = cluster.name.clone();
         info!(
-            "📜 Логи кластера '{}' ({} узлов):",
-            cluster.name,
-            cluster.nodes.len()
+            "{}",
+            fluent_i18n::t!("logs-header", { "name" => cn, "count" => cluster.nodes.len() })
         );
         let mut handles = Vec::new();
         for node in &cluster.nodes {
@@ -129,14 +127,13 @@ pub async fn show_logs(
             if let Ok((name, Ok(output))) = result
                 && !output.trim().is_empty()
             {
-                println!("\n--- {} ---", name);
+                println!("{}", fluent_i18n::t!("logs-section", { "name" => &name }));
                 println!("{}", output);
             }
         }
     }
     Ok(())
 }
-
 /// Stops and removes containers for a recipe on all nodes.
 pub async fn stop_cluster(
     config: &Config,
@@ -144,16 +141,21 @@ pub async fn stop_cluster(
     recipe_name: &str,
     dry_run: bool,
 ) -> Result<()> {
-    info!("🛑 Останавливаем кластер '{}'...", cluster.name);
+    info!(
+        "{}",
+        fluent_i18n::t!("cluster-stopping", { "name" => &cluster.name })
+    );
     let mut failures = Vec::new();
-
     for node in &cluster.nodes {
         let stop_cmd = format!(
             "docker ps -q --filter \"label=sm121.recipe={}\" | xargs -r docker stop",
             recipe_name
         );
         if let Err(e) = run_ssh(config, node, stop_cmd, dry_run).await {
-            tracing::warn!("Stop failed on {}: {}", node.name, e);
+            tracing::warn!(
+                "{}",
+                fluent_i18n::t!("stop-failed", { "name" => &node.name, "error" => e.to_string() })
+            );
             failures.push((node.name.clone(), "stop".to_string(), e));
         }
         let rm_cmd = format!(
@@ -161,21 +163,25 @@ pub async fn stop_cluster(
             recipe_name
         );
         if let Err(e) = run_ssh(config, node, rm_cmd, dry_run).await {
-            tracing::warn!("Remove failed on {}: {}", node.name, e);
+            tracing::warn!(
+                "{}",
+                fluent_i18n::t!("remove-failed", { "name" => &node.name, "error" => e.to_string() })
+            );
             failures.push((node.name.clone(), "rm".to_string(), e));
         }
     }
-
     if !failures.is_empty() {
         for (name, action, e) in &failures {
-            tracing::warn!("Failed to {} on {}: {}", action, name, e);
+            tracing::warn!(
+                "{}",
+                fluent_i18n::t!("failed-to", { "action" => action, "name" => name, "error" => e.to_string() })
+            );
         }
         anyhow::bail!("Failed to stop containers on {} node(s)", failures.len());
     }
-    info!("✅ Все контейнеры остановлены.");
+    info!("{}", fluent_i18n::t!("cluster-all-stopped"));
     Ok(())
 }
-
 /// Checks Docker health on a cluster — shows all containers and marks ours.
 pub async fn health_check(config: &Config, cluster: &Cluster, dry_run: bool) -> Result<()> {
     println!("\n=== Кластер: {} ===", cluster.name);
@@ -211,11 +217,9 @@ pub async fn health_check(config: &Config, cluster: &Cluster, dry_run: bool) ->
                     .get(1)
                     .map(|s| s.lines().filter(|l| !l.is_empty()).collect())
                     .unwrap_or_default();
-
                 if dry_run && all_lines.is_empty() {
                     continue;
                 }
-
                 if output.starts_with("docker:") || output.contains("permission denied") {
                     println!("  ✗ Docker недоступен");
                     if let Some(line) = output.lines().next() {
@@ -249,7 +253,6 @@ pub async fn health_check(config: &Config, cluster: &Cluster, dry_run: bool) ->
     }
     Ok(())
 }
-
 /// Starts containers on all nodes in parallel.
 #[allow(clippy::too_many_arguments)]
 async fn start_cluster(
@@ -269,7 +272,6 @@ async fn start_cluster(
         recipe.pipeline_parallel_size.unwrap()
     };
     let mut handles = Vec::new();
-
     for node in &cluster.nodes {
         let node = node.clone();
         let cluster = cluster.clone();
@@ -284,7 +286,6 @@ async fn start_cluster(
             "docker rm -f sm121_{}_{} 2>/dev/null; true",
             recipe_name, node.rank
         );
-
         let cd = cache_dir.clone();
         let cd_s = cd.to_string_lossy().into_owned();
         handles.push(tokio::spawn(async move {
@@ -309,36 +310,28 @@ async fn start_cluster(
             (node.name, node.rank, engine, result)
         }));
     }
-
     for result in futures::future::join_all(handles).await {
         match result {
             Ok((name, rank, engine, Ok(_))) => info!(
-                "✅ {} запущен на {} (rank {})",
-                engine.to_uppercase(),
-                name,
-                rank
+                "{}",
+                fluent_i18n::t!("engine-started", { "engine" => engine.to_uppercase(), "name" => &name, "rank" => rank })
             ),
             Ok((name, rank, engine, Err(e))) => anyhow::bail!(
-                "Failed to start {} on {} (rank {}): {}",
-                engine.to_uppercase(),
-                name,
-                rank,
-                e
+                "{}",
+                fluent_i18n::t!("engine-start-failed", { "engine" => engine.to_uppercase(), "name" => &name, "rank" => rank, "error" => e.to_string() })
             ),
             Err(e) => anyhow::bail!("Task join error: {}", e),
         }
     }
-
     if !dry_run {
         sleep(Duration::from_secs(10)).await;
         info!(
-            "🎉 Кластер успешно запущен! API → http://{}:{}",
-            cluster.master_ib_ip, recipe.port
+            "{}",
+            fluent_i18n::t!("cluster-started", { "ip" => &cluster.master_ib_ip, "port" => recipe.port })
         );
     }
     Ok(())
 }
-
 #[allow(clippy::too_many_arguments)]
 fn build_docker_command(
     cluster: &Cluster,
@@ -354,7 +347,6 @@ fn build_docker_command(
     init_port: u16,
 ) -> Vec<String> {
     let is_sglang = recipe.engine == "sglang";
-
     let mut flags: Vec<String> = vec![
         "--gpus all".to_string(),
         "--ipc=host".to_string(),
@@ -380,7 +372,6 @@ fn build_docker_command(
         format!("-e HF_HOME=/root/.cache/huggingface"),
         format!("-v {}:/root/.cache/huggingface", cache_dir),
     ];
-
     // Engine-specific host IP env var
     if is_sglang {
         cmd.push(format!("-e SGLANG_WORKER_IP={}", node.ib_ip));
@@ -394,13 +385,11 @@ fn build_docker_command(
         "-e OMPI_MCA_btl_tcp_if_include={}",
         nccl.socket_ifname
     ));
-
     // Default cluster env (recipe env below overrides these if duplicated)
     cmd.push("-e OMP_NUM_THREADS=4".to_string());
     cmd.push("-e NCCL_IGNORE_CPU_AFFINITY=1".to_string());
     cmd.push("-e NCCL_NET=IB".to_string());
     cmd.push("-e NCCL_DEBUG=WARN".to_string());
-
     // Force colored vLLM logs through Docker pipeline
     if is_head {
         cmd.push("-e VLLM_LOGGING_COLOR=1".to_string());
@@ -434,7 +423,6 @@ fn build_docker_command(
         }
         cmd.extend(ep);
     }
-
     // Model argument - engine-specific
     if is_sglang {
         cmd.push("--model-path".to_string());
@@ -442,9 +430,7 @@ fn build_docker_command(
     } else {
         cmd.push(recipe.model.clone());
     }
-
     cmd.extend(recipe.args.clone());
-
     // Distributed flags - engine-specific
     let mut flags: Vec<String> = match recipe.distributed_backend.as_str() {
         "ray" | "mp" if is_sglang => vec![
@@ -481,13 +467,11 @@ fn build_docker_command(
         }
         _ => vec![],
     };
-
     // vLLM workers run headless (skip API server) when not rank 0
     if !is_sglang && node.rank > 0 && matches!(recipe.distributed_backend.as_str(), "mp" | "ray") {
         flags.push("--headless".into());
     }
     cmd.extend(flags);
-
     // Host + port flags — auto-generated for both engines.
     cmd.push("--host".to_string());
     cmd.push("0.0.0.0".to_string());

+ 71 - 23
src/power.rs

@@ -28,18 +28,24 @@ pub async fn power_off(
     let targets = select_targets(cluster, node_name, "power off")?;
 
     if dry_run {
-        warn!("dry-run: skipping actual shutdown + HA call");
+        warn!("{}", fluent_i18n::t!("power-off-dryrun"));
         for node in &targets {
             println!(
-                "would: ssh {}@{} sudo shutdown now",
-                config.ssh_user, node.ssh_ip
+                "{}",
+                fluent_i18n::t!("power-off-would", { "action" => format!("ssh {}@{} sudo shutdown now", config.ssh_user, node.ssh_ip), "target" => String::new() })
             );
             if let Some(entity) = &node.entity {
-                println!("would: HA turn_off {}", entity);
+                println!(
+                    "{}",
+                    fluent_i18n::t!("power-off-entity-would", { "entity" => entity })
+                );
             }
         }
         for extra in &cluster.extra_entities {
-            println!("would: HA turn_off {} (cluster-level)", extra);
+            println!(
+                "{}",
+                fluent_i18n::t!("power-switch-would-off-extra", { "extra" => extra })
+            );
         }
         return Ok(());
     }
@@ -64,7 +70,10 @@ pub async fn power_off(
         let (node, cmd, r) = h.await?;
         if let Err(e) = r {
             any_ssh_failed = true;
-            warn!("✗ {} :: {} :: {}", node.name, cmd, e);
+            warn!(
+                "{}",
+                fluent_i18n::t!("power-off-shutdown-failed", { "name" => &node.name, "cmd" => &cmd, "error" => e.to_string() })
+            );
         }
     }
 
@@ -74,7 +83,10 @@ pub async fn power_off(
         .as_ref()
         .map(|c| c.shutdown_delay_seconds)
         .unwrap_or(30);
-    info!("⏳ waiting {}s for nodes to settle", delay);
+    info!(
+        "{}",
+        fluent_i18n::t!("power-off-waiting", { "secs" => delay })
+    );
     tokio::time::sleep(Duration::from_secs(delay)).await;
 
     // 3) HA turn_off: per-node entities, then cluster-level extras.
@@ -90,11 +102,11 @@ pub async fn power_off(
         )
         .await;
     } else {
-        info!("no [home_assistant] config; OS shutdown was performed above");
+        info!("{}", fluent_i18n::t!("power-off-no-ha"));
     }
 
     if any_ssh_failed {
-        warn!("some nodes reported SSH failures during shutdown");
+        warn!("{}", fluent_i18n::t!("power-off-ssh-failures"));
     }
     Ok(())
 }
@@ -120,21 +132,33 @@ pub async fn power_on(
     crate::home_assistant::warn_if_token_looks_short(&ha_cfg.token);
 
     if dry_run {
-        warn!("dry-run: skipping actual HA call");
+        warn!("{}", fluent_i18n::t!("power-on-dryrun"));
         for node in &targets {
             if let Some(entity) = &node.entity {
-                println!("would: HA turn_on {}", entity);
+                println!(
+                    "{}",
+                    fluent_i18n::t!("power-on-would-on", { "entity" => entity })
+                );
             } else {
-                println!("would skip: {} (no entity)", node.name);
+                println!(
+                    "{}",
+                    fluent_i18n::t!("power-on-would-skip", { "name" => &node.name })
+                );
             }
         }
         for extra in &cluster.extra_entities {
-            println!("would: HA turn_on {} (cluster-level)", extra);
+            println!(
+                "{}",
+                fluent_i18n::t!("power-on-would-on", { "entity" => extra })
+            );
         }
         return Ok(());
     }
 
-    info!("💡 power on: {} node(s)", targets.len());
+    info!(
+        "{}",
+        fluent_i18n::t!("power-on-head", { "nodes" => targets.len() })
+    );
     toggle_entities(&ha, "turn_on", &targets, &cluster.extra_entities, "cluster").await;
     Ok(())
 }
@@ -152,23 +176,38 @@ async fn toggle_entities(
     for node in targets {
         if let Some(entity) = &node.entity {
             if let Err(e) = split_entity(entity) {
-                warn!("{}: invalid entity '{}': {}", node.name, entity, e);
+                warn!(
+                    "{}",
+                    fluent_i18n::t!("ha-invalid-entity", { "name" => &node.name, "entity" => entity, "error" => e.to_string() })
+                );
                 continue;
             }
             if let Err(e) = call(ha, entity, service).await {
-                warn!("HA {} {}: {}", service, entity, e);
+                warn!(
+                    "{}",
+                    fluent_i18n::t!("ha-call-failed", { "service" => service, "entity" => entity, "error" => e.to_string() })
+                );
             }
         } else {
-            warn!("⚠ {}: no `entity` configured, skipping HA", node.name);
+            warn!(
+                "{}",
+                fluent_i18n::t!("ha-skip-no-entity", { "name" => &node.name })
+            );
         }
     }
     for extra in extras {
         if let Err(e) = split_entity(extra) {
-            warn!("{} ({}): invalid entity '{}'", extras_label, extra, e);
+            warn!(
+                "{}",
+                fluent_i18n::t!("ha-extra-invalid", { "label" => extras_label, "extra" => extra, "error" => e.to_string() })
+            );
             continue;
         }
         if let Err(e) = call(ha, extra, service).await {
-            warn!("HA {} {} ({}): {}", service, extra, extras_label, e);
+            warn!(
+                "{}",
+                fluent_i18n::t!("ha-extra-failed", { "service" => service, "extra" => extra, "label" => extras_label, "error" => e.to_string() })
+            );
         }
     }
 }
@@ -254,7 +293,10 @@ pub async fn power_switch(
     for h in handles {
         let (name, r) = h.await?;
         if let Err(e) = r {
-            warn!("✗ {} shutdown: {}", name, e);
+            warn!(
+                "{}",
+                fluent_i18n::t!("power-switch-shutdown-failed", { "name" => &name, "error" => e.to_string() })
+            );
         }
     }
 
@@ -264,7 +306,10 @@ pub async fn power_switch(
         .map(|c| c.shutdown_delay_seconds)
         .unwrap_or(30);
     if !disable_nodes.is_empty() {
-        info!("⏳ waiting {}s for shutdown", delay);
+        info!(
+            "{}",
+            fluent_i18n::t!("power-switch-waiting", { "secs" => delay })
+        );
         tokio::time::sleep(Duration::from_secs(delay)).await;
     }
 
@@ -275,7 +320,10 @@ pub async fn power_switch(
         for node in &disable_nodes {
             if let Some(entity) = &node.entity {
                 if let Err(e) = split_entity(entity) {
-                    warn!("{}: entity '{}': {}", node.name, entity, e);
+                    warn!(
+                        "{}",
+                        fluent_i18n::t!("ha-entity-error", { "name" => &node.name, "entity" => entity, "error" => e.to_string() })
+                    );
                     continue;
                 }
                 if dry_run {
@@ -301,7 +349,7 @@ pub async fn power_switch(
         );
         toggle_entities(&ha, "turn_on", &to.nodes, &to.extra_entities, "to").await;
     } else {
-        info!("no [home_assistant] config; OS shutdown was performed");
+        info!("{}", fluent_i18n::t!("power-switch-no-ha"));
     }
     Ok(())
 }