add ru locale to cli
This commit is contained in:
@@ -46,16 +46,21 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
data_sep: ":"
|
||||
- os: windows-latest
|
||||
data_sep: ";"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- run: pip install -e ".[dev]"
|
||||
- run: pybabel compile -d src/nexus_sync/locale -D nexus
|
||||
- run: pyinstaller --onefile src/nexus_sync/client/__main__.py --name nexus-sync-client
|
||||
- run: pyinstaller --onefile src/nexus_sync/server/__main__.py --name nexus-sync-server
|
||||
- run: pyinstaller --onefile src/nexus_sync/cli/__main__.py --name nexus-cli
|
||||
- run: pyinstaller --onefile src/nexus_sync/cli/__main__.py --name nexus-cli --add-data "src/nexus_sync/locale${{ matrix.data_sep }}nexus_sync/locale"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nexus-sync-${{ matrix.os }}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
.PHONY: all client server cli test clean
|
||||
.PHONY: all client server cli test clean i18n-extract i18n-init i18n-update i18n-compile
|
||||
|
||||
# CLI localization (nexus-cli strings only; daemon/server logs stay in English).
|
||||
LOCALE_DIR := src/nexus_sync/locale
|
||||
POT := $(LOCALE_DIR)/nexus.pot
|
||||
LANG ?= ru
|
||||
# PyInstaller --add-data separator is ':' on Unix, ';' on Windows.
|
||||
LOCALE_DATA := $(LOCALE_DIR):nexus_sync/locale
|
||||
|
||||
all: client server cli
|
||||
|
||||
@@ -8,8 +15,25 @@ client:
|
||||
server:
|
||||
pyinstaller --onefile src/nexus_sync/server/__main__.py --name nexus-sync-server
|
||||
|
||||
cli:
|
||||
pyinstaller --onefile src/nexus_sync/cli/__main__.py --name nexus-cli
|
||||
cli: i18n-compile
|
||||
pyinstaller --onefile src/nexus_sync/cli/__main__.py --name nexus-cli \
|
||||
--add-data "$(LOCALE_DATA)"
|
||||
|
||||
# Rebuild the message template from strings wrapped in _()/gettext()/ngettext().
|
||||
i18n-extract:
|
||||
pybabel extract -F babel.cfg -k _ -o $(POT) src
|
||||
|
||||
# Create a catalog for a new language, e.g. `make i18n-init LANG=de`.
|
||||
i18n-init: i18n-extract
|
||||
pybabel init -i $(POT) -d $(LOCALE_DIR) -D nexus -l $(LANG)
|
||||
|
||||
# Merge new/changed strings into existing catalogs.
|
||||
i18n-update: i18n-extract
|
||||
pybabel update -i $(POT) -d $(LOCALE_DIR) -D nexus
|
||||
|
||||
# Compile .po catalogs to the .mo files bundled with nexus-cli.
|
||||
i18n-compile:
|
||||
pybabel compile -d $(LOCALE_DIR) -D nexus
|
||||
|
||||
test:
|
||||
pytest
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Babel extraction config for CLI localization.
|
||||
# Paths are relative to the extraction root passed to pybabel (the `src` dir).
|
||||
# Only strings wrapped in _()/gettext()/ngettext() are extracted, so logger.*
|
||||
# messages are never picked up.
|
||||
[python: **.py]
|
||||
+28
-1
@@ -61,10 +61,37 @@ nexus-cli --json client linux-client
|
||||
nexus-cli --json command cmd_123
|
||||
```
|
||||
|
||||
## Localization
|
||||
|
||||
CLI output is localized with `gettext` (catalogs managed by Babel). Only
|
||||
user-facing CLI strings are translatable; daemon/server log messages are
|
||||
deliberately left untranslated.
|
||||
|
||||
Select a language with the `NEXUS_SYNC_LANG` env var (falls back to the system
|
||||
locale, then to the source English strings):
|
||||
|
||||
```bash
|
||||
NEXUS_SYNC_LANG=ru nexus-cli command cmd_123
|
||||
```
|
||||
|
||||
Translation sources live in `src/nexus_sync/locale/<lang>/LC_MESSAGES/nexus.po`.
|
||||
Workflow (requires `pip install -e ".[dev]"`):
|
||||
|
||||
```bash
|
||||
make i18n-extract # rebuild the .pot template from _()-wrapped strings
|
||||
make i18n-update # merge new/changed strings into existing catalogs
|
||||
make i18n-init LANG=de # start a new language
|
||||
# edit the .po file, then:
|
||||
make i18n-compile # build the .mo files shipped with the binary
|
||||
```
|
||||
|
||||
`make cli` compiles catalogs automatically and bundles them into the binary.
|
||||
|
||||
## Build standalone binary
|
||||
|
||||
```bash
|
||||
make cli
|
||||
```
|
||||
|
||||
This creates `dist/nexus-cli` through PyInstaller.
|
||||
This creates `dist/nexus-cli` through PyInstaller (with localization catalogs
|
||||
bundled via `--add-data`).
|
||||
|
||||
+5
-1
@@ -25,7 +25,8 @@ dev = [
|
||||
"pyinstaller>=6.0",
|
||||
"mypy>=1.0",
|
||||
"types-PyYAML>=6.0.12",
|
||||
"pre-commit>=4.0"
|
||||
"pre-commit>=4.0",
|
||||
"Babel>=2.14.0"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
@@ -34,6 +35,9 @@ nexus-cli = "nexus_sync.cli.__main__:main"
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
nexus_sync = ["locale/**/*.mo"]
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
target-version = ["py311"]
|
||||
|
||||
@@ -8,6 +8,9 @@ from collections.abc import Callable
|
||||
from types import TracebackType
|
||||
from typing import Any, Protocol, Self
|
||||
|
||||
from nexus_sync import i18n
|
||||
from nexus_sync.i18n import _
|
||||
|
||||
DEFAULT_SERVER_URL = "http://127.0.0.1:5852"
|
||||
API_PREFIX = "/api/v1"
|
||||
JsonObject = dict[str, Any]
|
||||
@@ -53,62 +56,62 @@ def request_json(
|
||||
body = response.read()
|
||||
except urllib.error.HTTPError as error:
|
||||
detail = error.read().decode(errors="replace")
|
||||
raise CLIError(f"HTTP {error.code}: {detail}") from error
|
||||
raise CLIError(_("HTTP {code}: {detail}").format(code=error.code, detail=detail)) from error
|
||||
except urllib.error.URLError as error:
|
||||
raise CLIError(f"request failed: {error.reason}") from error
|
||||
raise CLIError(_("request failed: {error}").format(error=error.reason)) from error
|
||||
except OSError as error:
|
||||
raise CLIError(f"request failed: {error}") from error
|
||||
raise CLIError(_("request failed: {error}").format(error=error)) from error
|
||||
|
||||
try:
|
||||
result = json.loads(body.decode())
|
||||
except json.JSONDecodeError as error:
|
||||
raise CLIError(f"server returned invalid JSON: {error}") from error
|
||||
raise CLIError(_("server returned invalid JSON: {error}").format(error=error)) from error
|
||||
if not isinstance(result, dict):
|
||||
raise CLIError("server returned JSON that is not an object")
|
||||
raise CLIError(_("server returned JSON that is not an object"))
|
||||
return result
|
||||
|
||||
|
||||
def format_clients(payload: JsonObject) -> str:
|
||||
clients = payload.get("clients", [])
|
||||
if not isinstance(clients, list) or not clients:
|
||||
return "clients:\n- none"
|
||||
return _("clients:") + "\n- " + _("none")
|
||||
|
||||
lines = ["clients:"]
|
||||
lines = [_("clients:")]
|
||||
for item in clients:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
parts = [
|
||||
str(item.get("id", "<unknown>")),
|
||||
str(item.get("platform", "unknown")),
|
||||
str(item.get("hostname", "unknown")),
|
||||
f"version={item.get('version', 'unknown')}",
|
||||
f"last_seen={item.get('last_seen_at', 'unknown')}",
|
||||
str(item.get("id", _("<unknown>"))),
|
||||
str(item.get("platform", _("unknown"))),
|
||||
str(item.get("hostname", _("unknown"))),
|
||||
f"version={item.get('version', _('unknown'))}",
|
||||
f"last_seen={item.get('last_seen_at', _('unknown'))}",
|
||||
]
|
||||
lines.append(f"- {' '.join(parts)}")
|
||||
commands = _command_names(item.get("available_commands", []))
|
||||
if commands:
|
||||
lines.append(f" commands: {', '.join(commands)}")
|
||||
lines.append(f" {_('commands:')} {', '.join(commands)}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def format_client(payload: JsonObject) -> str:
|
||||
lines = [
|
||||
f"id: {payload.get('id', '<unknown>')}",
|
||||
f"hostname: {payload.get('hostname', 'unknown')}",
|
||||
f"platform: {payload.get('platform', 'unknown')}",
|
||||
f"version: {payload.get('version', 'unknown')}",
|
||||
f"created_at: {payload.get('created_at', 'unknown')}",
|
||||
f"last_seen_at: {payload.get('last_seen_at', 'unknown')}",
|
||||
"available_commands:",
|
||||
_field_line("id", payload.get("id", _("<unknown>"))),
|
||||
_field_line("hostname", payload.get("hostname", _("unknown"))),
|
||||
_field_line("platform", payload.get("platform", _("unknown"))),
|
||||
_field_line("version", payload.get("version", _("unknown"))),
|
||||
_field_line("created_at", payload.get("created_at", _("unknown"))),
|
||||
_field_line("last_seen_at", payload.get("last_seen_at", _("unknown"))),
|
||||
_("available_commands:"),
|
||||
]
|
||||
commands = payload.get("available_commands", [])
|
||||
if not isinstance(commands, list) or not commands:
|
||||
lines.append("- none")
|
||||
lines.append("- " + _("none"))
|
||||
return "\n".join(lines)
|
||||
|
||||
for command in commands:
|
||||
if isinstance(command, dict):
|
||||
name = command.get("name", "<unknown>")
|
||||
name = command.get("name", _("<unknown>"))
|
||||
description = command.get("description", "")
|
||||
suffix = f" - {description}" if description else ""
|
||||
lines.append(f"- {name}{suffix}")
|
||||
@@ -116,7 +119,7 @@ def format_client(payload: JsonObject) -> str:
|
||||
|
||||
|
||||
def format_command(payload: JsonObject, *, queued: bool = False) -> str:
|
||||
lines = ["queued command:" if queued else "command:"]
|
||||
lines = [_("queued command:") if queued else _("command:")]
|
||||
for key in (
|
||||
"id",
|
||||
"client_id",
|
||||
@@ -129,43 +132,45 @@ def format_command(payload: JsonObject, *, queued: bool = False) -> str:
|
||||
"finished_at",
|
||||
):
|
||||
if key in payload:
|
||||
lines.append(f"{key}: {payload.get(key)}")
|
||||
lines.append(_field_line(key, payload.get(key)))
|
||||
|
||||
result = payload.get("result")
|
||||
if isinstance(result, dict):
|
||||
lines.append("result:")
|
||||
lines.append(_("result:"))
|
||||
if "status" in result:
|
||||
lines.append(f" status: {result.get('status')}")
|
||||
lines.append(" " + _field_line("status", result.get("status")))
|
||||
if "return_code" in result:
|
||||
lines.append(f" return_code: {result.get('return_code')}")
|
||||
lines.append(" stdout:")
|
||||
lines.append(" " + _field_line("return_code", result.get("return_code")))
|
||||
lines.append(" " + _("stdout:"))
|
||||
lines.extend(_indent_block(str(result.get("stdout", ""))))
|
||||
lines.append(" stderr:")
|
||||
lines.append(" " + _("stderr:"))
|
||||
lines.extend(_indent_block(str(result.get("stderr", ""))))
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(prog="nexus-cli", description="CLI for nexus-sync server API")
|
||||
parser.add_argument(
|
||||
"--server-url", "-s", default=DEFAULT_SERVER_URL, help="nexus-sync server URL"
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="nexus-cli", description=_("CLI for nexus-sync server API")
|
||||
)
|
||||
parser.add_argument("--list", action="store_true", help="list clients")
|
||||
parser.add_argument("--json", action="store_true", help="print raw JSON response")
|
||||
parser.add_argument(
|
||||
"--server-url", "-s", default=DEFAULT_SERVER_URL, help=_("nexus-sync server URL")
|
||||
)
|
||||
parser.add_argument("--list", action="store_true", help=_("list clients"))
|
||||
parser.add_argument("--json", action="store_true", help=_("print raw JSON response"))
|
||||
|
||||
subparsers = parser.add_subparsers(dest="resource")
|
||||
client = subparsers.add_parser("client", help="show client info or queue a command")
|
||||
client.add_argument("id", help="client id")
|
||||
client.add_argument("--run-command", metavar="NAME", help="queue a command for this client")
|
||||
client = subparsers.add_parser("client", help=_("show client info or queue a command"))
|
||||
client.add_argument("id", help=_("client id"))
|
||||
client.add_argument("--run-command", metavar="NAME", help=_("queue a command for this client"))
|
||||
client.add_argument(
|
||||
"--timeout-seconds",
|
||||
type=int,
|
||||
default=30,
|
||||
help="command timeout in seconds for --run-command",
|
||||
help=_("command timeout in seconds for --run-command"),
|
||||
)
|
||||
|
||||
command = subparsers.add_parser("command", help="show command execution info")
|
||||
command.add_argument("id", help="command id")
|
||||
command = subparsers.add_parser("command", help=_("show command execution info"))
|
||||
command.add_argument("id", help=_("command id"))
|
||||
return parser
|
||||
|
||||
|
||||
@@ -174,6 +179,7 @@ def main(
|
||||
*,
|
||||
requester: Requester = request_json,
|
||||
) -> int:
|
||||
i18n.setup()
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
server_url = str(args.server_url).rstrip("/")
|
||||
@@ -215,7 +221,7 @@ def main(
|
||||
_print_payload(payload, raw_json=args.json, formatter=format_command)
|
||||
return 0
|
||||
except CLIError as error:
|
||||
print(f"nexus-cli error: {error}", file=sys.stderr)
|
||||
print(_("nexus-cli error: {error}").format(error=error), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
parser.print_help()
|
||||
@@ -234,6 +240,30 @@ def _print_payload(
|
||||
print(formatter(payload))
|
||||
|
||||
|
||||
def _field_line(key: str, value: object) -> str:
|
||||
return f"{_field_label(key)}: {value}"
|
||||
|
||||
|
||||
def _field_label(key: str) -> str:
|
||||
labels = {
|
||||
"id": _("id"),
|
||||
"client_id": _("client_id"),
|
||||
"hostname": _("hostname"),
|
||||
"platform": _("platform"),
|
||||
"version": _("version"),
|
||||
"created_at": _("created_at"),
|
||||
"last_seen_at": _("last_seen_at"),
|
||||
"kind": _("kind"),
|
||||
"name": _("name"),
|
||||
"status": _("status"),
|
||||
"timeout_seconds": _("timeout_seconds"),
|
||||
"delivered_at": _("delivered_at"),
|
||||
"finished_at": _("finished_at"),
|
||||
"return_code": _("return_code"),
|
||||
}
|
||||
return labels.get(key, key)
|
||||
|
||||
|
||||
def _command_names(commands: object) -> list[str]:
|
||||
if not isinstance(commands, list):
|
||||
return []
|
||||
@@ -246,7 +276,7 @@ def _command_names(commands: object) -> list[str]:
|
||||
|
||||
def _indent_block(value: str) -> list[str]:
|
||||
if not value:
|
||||
return [" <empty>"]
|
||||
return [" " + _("<empty>")]
|
||||
return [f" {line}" if line else "" for line in value.rstrip("\n").splitlines()]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"""CLI localization (i18n).
|
||||
|
||||
User-facing CLI strings are wrapped in :func:`_` so they can be translated.
|
||||
Log messages (``logger.*``) are intentionally left unwrapped and always stay
|
||||
in English for grep-ability and operations.
|
||||
|
||||
Translation only becomes active after :func:`setup` is called (done in the CLI
|
||||
entry points). Until then, and whenever no catalog matches the requested
|
||||
language, ``gettext`` falls back to returning the original (English) message.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import gettext as _gettext
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
DOMAIN = "nexus"
|
||||
LANG_ENV = "NEXUS_SYNC_LANG"
|
||||
|
||||
_translation: _gettext.NullTranslations = _gettext.NullTranslations()
|
||||
|
||||
|
||||
def _locale_dir() -> str:
|
||||
"""Locate the compiled message catalogs, both in-source and inside a PyInstaller bundle."""
|
||||
bundled = getattr(sys, "_MEIPASS", None)
|
||||
if bundled is not None:
|
||||
return os.path.join(bundled, "nexus_sync", "locale")
|
||||
return str(Path(__file__).resolve().parent / "locale")
|
||||
|
||||
|
||||
def setup(lang: str | None = None) -> None:
|
||||
"""Activate the message catalog for ``lang``.
|
||||
|
||||
When ``lang`` is ``None`` the ``NEXUS_SYNC_LANG`` env var is consulted, and
|
||||
failing that the system locale (``LANGUAGE``/``LANG``/...) is used. Missing
|
||||
catalogs fall back silently to the original English strings.
|
||||
"""
|
||||
global _translation
|
||||
if lang is None:
|
||||
lang = os.environ.get(LANG_ENV) or None
|
||||
languages = [lang] if lang else None
|
||||
_translation = _gettext.translation(DOMAIN, _locale_dir(), languages=languages, fallback=True)
|
||||
|
||||
|
||||
def gettext(message: str) -> str:
|
||||
return _translation.gettext(message)
|
||||
|
||||
|
||||
def ngettext(singular: str, plural: str, n: int) -> str:
|
||||
return _translation.ngettext(singular, plural, n)
|
||||
|
||||
|
||||
# Conventional alias used to mark translatable strings; recognized by pybabel.
|
||||
_ = gettext
|
||||
@@ -0,0 +1,192 @@
|
||||
# Russian translations for nexus-sync.
|
||||
# Copyright (C) 2026 ORGANIZATION
|
||||
# This file is distributed under the same license as the nexus-sync project.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: nexus-sync 0.1.0\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-06-24 22:58+0300\n"
|
||||
"PO-Revision-Date: 2026-06-24 23:01+0300\n"
|
||||
"Last-Translator: nexus-sync\n"
|
||||
"Language: ru\n"
|
||||
"Language-Team: ru <LL@li.org>\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.18.0\n"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:59
|
||||
#, python-brace-format
|
||||
msgid "HTTP {code}: {detail}"
|
||||
msgstr ""
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:61 src/nexus_sync/cli/__main__.py:63
|
||||
#, python-brace-format
|
||||
msgid "request failed: {error}"
|
||||
msgstr "запрос не выполнен: {error}"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:69
|
||||
#, python-brace-format
|
||||
msgid "server returned invalid JSON: {error}"
|
||||
msgstr "сервер вернул некорректный JSON: {error}"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:72
|
||||
msgid "server returned JSON that is not an object"
|
||||
msgstr "сервер вернул JSON, который не является объектом"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:79 src/nexus_sync/cli/__main__.py:81
|
||||
msgid "clients:"
|
||||
msgstr "клиенты:"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:79 src/nexus_sync/cli/__main__.py:111
|
||||
msgid "none"
|
||||
msgstr "нет"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:86 src/nexus_sync/cli/__main__.py:101
|
||||
#: src/nexus_sync/cli/__main__.py:116
|
||||
msgid "<unknown>"
|
||||
msgstr "<неизвестно>"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:87 src/nexus_sync/cli/__main__.py:88
|
||||
#: src/nexus_sync/cli/__main__.py:89 src/nexus_sync/cli/__main__.py:90
|
||||
#: src/nexus_sync/cli/__main__.py:102 src/nexus_sync/cli/__main__.py:103
|
||||
#: src/nexus_sync/cli/__main__.py:104 src/nexus_sync/cli/__main__.py:105
|
||||
#: src/nexus_sync/cli/__main__.py:106
|
||||
msgid "unknown"
|
||||
msgstr "неизвестно"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:95
|
||||
msgid "commands:"
|
||||
msgstr "команды:"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:107
|
||||
msgid "available_commands:"
|
||||
msgstr "доступные команды:"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:124
|
||||
msgid "queued command:"
|
||||
msgstr "команда поставлена в очередь:"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:124
|
||||
msgid "command:"
|
||||
msgstr "команда:"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:141
|
||||
msgid "result:"
|
||||
msgstr "результат:"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:146
|
||||
msgid "stdout:"
|
||||
msgstr ""
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:148
|
||||
msgid "stderr:"
|
||||
msgstr ""
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:155
|
||||
msgid "CLI for nexus-sync server API"
|
||||
msgstr "CLI для API сервера nexus-sync"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:158
|
||||
msgid "nexus-sync server URL"
|
||||
msgstr "URL сервера nexus-sync"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:160
|
||||
msgid "list clients"
|
||||
msgstr "показать список клиентов"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:161
|
||||
msgid "print raw JSON response"
|
||||
msgstr "вывести необработанный JSON-ответ"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:164
|
||||
msgid "show client info or queue a command"
|
||||
msgstr "показать информацию о клиенте или поставить команду в очередь"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:165
|
||||
msgid "client id"
|
||||
msgstr "идентификатор клиента"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:167
|
||||
msgid "queue a command for this client"
|
||||
msgstr "поставить команду в очередь для этого клиента"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:173
|
||||
msgid "command timeout in seconds for --run-command"
|
||||
msgstr "таймаут команды в секундах для --run-command"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:176
|
||||
msgid "show command execution info"
|
||||
msgstr "показать информацию о выполнении команды"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:177
|
||||
msgid "command id"
|
||||
msgstr "идентификатор команды"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:228
|
||||
#, python-brace-format
|
||||
msgid "nexus-cli error: {error}"
|
||||
msgstr "ошибка nexus-cli: {error}"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:253
|
||||
msgid "id"
|
||||
msgstr ""
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:254
|
||||
msgid "client_id"
|
||||
msgstr "id клиента"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:255
|
||||
msgid "hostname"
|
||||
msgstr "имя хоста"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:256
|
||||
msgid "platform"
|
||||
msgstr "платформа"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:257
|
||||
msgid "version"
|
||||
msgstr "версия"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:258
|
||||
msgid "created_at"
|
||||
msgstr "создано"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:259
|
||||
msgid "last_seen_at"
|
||||
msgstr "последняя активность"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:260
|
||||
msgid "kind"
|
||||
msgstr "тип"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:261
|
||||
msgid "name"
|
||||
msgstr "имя"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:262
|
||||
msgid "status"
|
||||
msgstr "статус"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:263
|
||||
msgid "timeout_seconds"
|
||||
msgstr "таймаут (сек)"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:264
|
||||
msgid "delivered_at"
|
||||
msgstr "доставлено"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:265
|
||||
msgid "finished_at"
|
||||
msgstr "завершено"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:266
|
||||
msgid "return_code"
|
||||
msgstr "код возврата"
|
||||
|
||||
#: src/nexus_sync/cli/__main__.py:283
|
||||
msgid "<empty>"
|
||||
msgstr "<пусто>"
|
||||
@@ -0,0 +1,13 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _force_source_locale(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Keep CLI output in the source language during tests.
|
||||
|
||||
``nexus-cli`` activates a locale from ``NEXUS_SYNC_LANG`` / the system
|
||||
locale, so assertions on English output would break on a machine whose
|
||||
locale is, e.g., Russian. Pinning to a language with no catalog makes
|
||||
gettext fall back to the source strings regardless of the dev's environment.
|
||||
"""
|
||||
monkeypatch.setenv("NEXUS_SYNC_LANG", "en")
|
||||
Reference in New Issue
Block a user