Skip to content

feat(cli): suporte i18n completo — 42 locales, --lang flag, config lang get/set/list - #2285

Merged
diegosouzapw merged 2 commits into
release/v3.8.0from
feat/cli-v4
May 15, 2026
Merged

diegosouzapw merged 2 commits into
release/v3.8.0from
feat/cli-v4

Conversation

@diegosouzapw

Copy link
Copy Markdown
Collaborator

O que foi feito

Adiciona suporte completo a internacionalização (i18n) na CLI do OmniRoute — 42 locales, seleção de idioma por flag/env/sistema, e subcomandos para gerenciar o idioma ativo.

Novos recursos

  • --lang <code> — flag global para sobrescrever o idioma por execução (ex: omniroute --lang de status)
  • omniroute config lang get — exibe o locale atualmente ativo
  • omniroute config lang set <code> — salva o idioma em ~/.omniroute/.env (persiste entre sessões)
  • omniroute config lang list — lista todos os 42 locales disponíveis com flag, nome nativo e indicador do ativo
  • 42 arquivos de locale em bin/cli/locales/ — en e pt-BR totalmente traduzidos; 29 locales com common + program; 11 scaffolds (fallback automático para en)
  • Script de geração bin/cli/scripts/generate-locales.mjs para scaffoldar novos locales a partir de config/i18n.json

Ordem de detecção do locale

--lang flag → OMNIROUTE_LANG env → LC_ALL → LC_MESSAGES → LANG → en

Correções de segurança e qualidade (code review)

  • i18n.mjs: proteção contra path traversal em normalize() via /^[a-zA-Z0-9-]+$/
  • i18n.mjs: replaceAll("_", "-") para tratar múltiplos underscores (zh_Hans_CN)
  • config.mjs: fileURLToPath(import.meta.url) no lugar de .replace("file://", "") (fix Windows)
  • config.mjs: profundidade correta de dirname() para localizar config/i18n.json (4 níveis)
  • config.mjs: escrita atômica do .env via writeFileSync(tmp) + renameSync — elimina risco de truncamento
  • program.mjs: remove .env("OMNIROUTE_LANG") do option --lang para evitar opts.lang ambíguo
  • Mensagem config.lang.saved corrigida — remove "Effective on next run" já que setLocale() é chamado em-processo

Testes

13 novos testes em tests/unit/cli-lang-commands.test.ts cobrindo:

  • Path traversal em normalize()
  • runConfigLangGetCommand (texto e JSON)
  • runConfigLangListCommand (JSON com campo active)
  • runConfigLangSetCommand (persistência, código inválido, --force, atualização sem duplicar)

Documentação atualizada

  • bin/cli/README.md — seção de locale selection, estrutura de diretórios atualizada
  • bin/cli/CONVENTIONS.md — seção §6 i18n atualizada com 42 locales, regras de PR, guia para adicionar locale
  • docs/guides/I18N.md — nova seção "CLI i18n" com tabela de detecção, exemplos de uso, lista de locales

Test plan

  • node --import tsx/esm --test tests/unit/cli-lang-commands.test.ts → 13/13 pass
  • node --import tsx/esm --test tests/unit/cli-i18n-catalog.test.ts → 7/7 pass
  • omniroute config lang list → exibe 42 locales
  • omniroute config lang set pt-BR → salva em ~/.omniroute/.env
  • omniroute --lang de providers list → saída em alemão (ou fallback en se chave ausente)
  • OMNIROUTE_LANG=../etc/passwd omniroute status → fallback para en (sem crash)

…et/list

- Adiciona --lang <code> como opção global no CLI (envia via OMNIROUTE_LANG)
- Aplica setLocale() em omniroute.mjs antes do Commander parsear (antes de t() rodar)
- Adiciona 3 subcomandos em `config lang`: get, set, list
  - `set` persiste OMNIROUTE_LANG no ~/.omniroute/.env (lido no boot)
  - `list` exibe tabela com flag, código, nome em inglês e nativo, marca active
- Cria 40 arquivos de locale (ar, az, bg, bn, cs, da, de, es, fa, fi, fr, gu, he,
  hi, hu, id, in, it, ja, ko, mr, ms, nl, no, phi, pl, pt, ro, ru, sk, sv, sw,
  ta, te, th, tr, uk-UA, ur, vi, zh-CN)
  - 29 línguas com common+program traduzidos (falantes >30M ou usuários técnicos)
  - 11 línguas em scaffold (fallback automático para en via i18n.mjs)
- Adiciona bin/cli/scripts/generate-locales.mjs para regenerar/forçar atualização
- Chaves novas em en.json e pt-BR.json: program.lang, config.lang.*
…, docs

- i18n.mjs: path traversal protection via /^[a-zA-Z0-9-]+$/ in normalize()
- i18n.mjs: replaceAll("_", "-") to handle multi-underscore locales (zh_Hans_CN)
- config.mjs: fileURLToPath(import.meta.url) instead of .replace("file://", "")
- config.mjs: correct dirname depth (4 levels) to reach repo root from bin/cli/commands/
- config.mjs: atomic .env write via tmp file + renameSync
- config.mjs: export runConfigLang{Get,Set,List}Command for testability
- program.mjs: remove .env("OMNIROUTE_LANG") from --lang option
- en.json/pt-BR.json: drop misleading "Effective on next run" from saved message
- tests: cli-lang-commands.test.ts with 13 new unit tests
- docs: update CONVENTIONS.md §6, README.md, docs/guides/I18N.md with CLI i18n section

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a robust internationalization system for the CLI, adding support for 42 locales, language persistence in configuration, and a scaffold generation script. The review feedback highlights several areas for improvement, including making the initial argument detection more flexible, simplifying path resolution for configuration files, and refining the environment variable update logic to handle empty files. Furthermore, the reviewer suggested centralizing imports and moving hardcoded strings to locale files to maintain consistency with the project's i18n goals and CLI color settings.

Comment thread bin/omniroute.mjs
Comment on lines +72 to +75
const langIdx = process.argv.findIndex((a) => a === "--lang");
const langArg = langIdx >= 0 ? process.argv[langIdx + 1] : null;
const langEnv = process.env.OMNIROUTE_LANG;
const chosen = langArg || langEnv;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

A detecção manual do argumento --lang no bootstrap é frágil. Ela não suporta a sintaxe --lang=valor (padrão no Commander) e pode falhar ou capturar o argumento errado se o flag for passado sem valor no final da linha de comando. Como o normalize já valida o código, podemos tornar a captura mais flexível.

  const langIdx = process.argv.findIndex((a) => a.startsWith("--lang"));
  const langArg = langIdx >= 0 ? (process.argv[langIdx].split("=")[1] || process.argv[langIdx + 1]) : null;
  const chosen = langArg || process.env.OMNIROUTE_LANG;

Comment on lines +146 to +150
const cfgPath = path.join(
path.dirname(path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))))),
"config",
"i18n.json"
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

O cálculo do caminho para i18n.json usando múltiplos path.dirname encadeados é difícil de ler e propenso a erros se a estrutura de arquivos for alterada. Uma abordagem usando o construtor URL com caminho relativo ao import.meta.url é mais limpa e robusta.

  const cfgPath = path.join(fileURLToPath(new URL("../../../../config/i18n.json", import.meta.url)));

Comment on lines +165 to +173
const lines = content.split("\n");
const idx = lines.findIndex((l) => l.trimStart().startsWith(`${key}=`));
const newLine = `${key}=${value}`;
if (idx >= 0) {
lines[idx] = newLine;
} else {
if (content && !content.endsWith("\n")) lines.push("");
lines.push(newLine);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Se o arquivo .env estiver vazio ou não existir, content.split("\n") retorna [""]. Isso faz com que o arquivo final comece com uma linha em branco indesejada. Tratar o conteúdo vazio explicitamente resolve o problema.

  const lines = content ? content.trim().split("\n") : [];
  const idx = lines.findIndex((l) => l.trimStart().startsWith(`${key}=`));
  const newLine = `${key}=${value}`;
  if (idx >= 0) {
    lines[idx] = newLine;
  } else {
    lines.push(newLine);
  }

Comment on lines +234 to +241
console.log(`\n\x1b[1m\x1b[36m${t("config.lang.listTitle")}\x1b[0m\n`);
for (const loc of locales) {
const active = loc.code === current ? " \x1b[32m◀ active\x1b[0m" : "";
console.log(
` ${loc.flag} ${loc.code.padEnd(8)} ${loc.english.padEnd(28)} ${loc.native}${active}`
);
}
console.log("");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Uso de códigos de escape ANSI diretamente no console.log ignora as configurações globais de cor da CLI (como o flag --no-color) e é inconsistente com o restante do código que utiliza os helpers do io.mjs. Além disso, a string " active" está hardcoded, o que vai contra o objetivo de i18n total da CLI.

Suggested change
console.log(`\n\x1b[1m\x1b[36m${t("config.lang.listTitle")}\x1b[0m\n`);
for (const loc of locales) {
const active = loc.code === current ? " \x1b[32m◀ active\x1b[0m" : "";
console.log(
` ${loc.flag} ${loc.code.padEnd(8)} ${loc.english.padEnd(28)} ${loc.native}${active}`
);
}
console.log("");
printHeading(t("config.lang.listTitle"));
for (const loc of locales) {
const active = loc.code === current ? ` \x1b[32m◀ ${t("config.lang.activeLabel")}\x1b[0m` : "";
console.log(
` ${loc.flag} ${loc.code.padEnd(8)} ${loc.english.padEnd(28)} ${loc.native}${active}`
);
}
console.log("");

@@ -2,6 +2,8 @@ import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
import { t } from "../i18n.mjs";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Importe getLocale e setLocale no topo do arquivo. Como o módulo já é carregado para a função t, os imports dinâmicos dentro das funções de comando (linhas 182, 206, 221) tornam-se redundantes e menos eficientes.

Suggested change
import { t } from "../i18n.mjs";
import { t, getLocale, setLocale } from "../i18n.mjs";

Comment thread bin/cli/locales/en.json
Comment on lines +1169 to +1170
"alreadySet": "Language is already set to {code}.",
"envHint": "Tip: you can also set OMNIROUTE_LANG={code} in your environment."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Adicione a chave activeLabel para permitir a tradução do indicador de idioma ativo na listagem de configurações, evitando strings hardcoded no código.

      "alreadySet": "Language is already set to {code}.",
      "activeLabel": "active",
      "envHint": "Tip: you can also set OMNIROUTE_LANG={code} in your environment."

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5f4eaf1013

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +148 to +149
"config",
"i18n.json"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid runtime dependency on unpackaged config/i18n.json

loadI18nLocales() now reads ROOT/config/i18n.json, but the published package allowlist does not include config/ (only bin/, app/, etc.). In an installed omniroute CLI this read falls into the catch path and returns [], which makes config lang list empty and causes config lang set <code> to fail with unknown for every language. This breaks the new language-management commands outside a source checkout.

Useful? React with 👍 / 👎.

Comment thread bin/omniroute.mjs
Comment on lines +72 to +73
const langIdx = process.argv.findIndex((a) => a === "--lang");
const langArg = langIdx >= 0 ? process.argv[langIdx + 1] : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse --lang=code form before creating the program

The pre-parse locale bootstrap only detects an argument equal to "--lang" and then reads the next token. When users pass --lang=<code>, langArg is never set, so setLocale() is not called before createProgram() invokes t(...); the process stays on env/default locale and the new flag appears ignored. This affects help text and command output for a common long-option syntax.

Useful? React with 👍 / 👎.

@diegosouzapw
diegosouzapw merged commit 79d0357 into release/v3.8.0 May 15, 2026
3 checks passed
diegosouzapw added a commit that referenced this pull request May 16, 2026
@diegosouzapw
diegosouzapw deleted the feat/cli-v4 branch May 20, 2026 11:48
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…ng get/set/list (cheaper-inference#2285)

feat(cli): suporte i18n completo — 42 locales, --lang flag, config lang get/set/list

- 42 locale files in bin/cli/locales/ (en + pt-BR fully translated, 29 with common/program, 11 scaffolds)
- --lang <code> global flag for per-execution override
- config lang get/set/list subcommands
- Locale persistence via ~/.omniroute/.env
- Path traversal protection via regex validation in normalize()
- Script generate-locales.mjs for scaffolding new locales
- Unit tests for lang commands + normalization security

Integrated into release/v3.8.0
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
…ng get/set/list (cheaper-inference#2285)

feat(cli): suporte i18n completo — 42 locales, --lang flag, config lang get/set/list

- 42 locale files in bin/cli/locales/ (en + pt-BR fully translated, 29 with common/program, 11 scaffolds)
- --lang <code> global flag for per-execution override
- config lang get/set/list subcommands
- Locale persistence via ~/.omniroute/.env
- Path traversal protection via regex validation in normalize()
- Script generate-locales.mjs for scaffolding new locales
- Unit tests for lang commands + normalization security

Integrated into release/v3.8.0
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…ng get/set/list (cheaper-inference#2285)

feat(cli): suporte i18n completo — 42 locales, --lang flag, config lang get/set/list

- 42 locale files in bin/cli/locales/ (en + pt-BR fully translated, 29 with common/program, 11 scaffolds)
- --lang <code> global flag for per-execution override
- config lang get/set/list subcommands
- Locale persistence via ~/.omniroute/.env
- Path traversal protection via regex validation in normalize()
- Script generate-locales.mjs for scaffolding new locales
- Unit tests for lang commands + normalization security

Integrated into release/v3.8.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant