Repository navigation
feat(cli): suporte i18n completo — 42 locales, --lang flag, config lang get/set/list - #2285
Conversation
…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
There was a problem hiding this comment.
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.
| 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; |
There was a problem hiding this comment.
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;| const cfgPath = path.join( | ||
| path.dirname(path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))))), | ||
| "config", | ||
| "i18n.json" | ||
| ); |
There was a problem hiding this comment.
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)));| 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); | ||
| } |
There was a problem hiding this comment.
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);
}| 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(""); |
There was a problem hiding this comment.
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.
| 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"; | |||
There was a problem hiding this comment.
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.
| import { t } from "../i18n.mjs"; | |
| import { t, getLocale, setLocale } from "../i18n.mjs"; |
| "alreadySet": "Language is already set to {code}.", | ||
| "envHint": "Tip: you can also set OMNIROUTE_LANG={code} in your environment." |
There was a problem hiding this comment.
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."There was a problem hiding this comment.
💡 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".
| "config", | ||
| "i18n.json" |
There was a problem hiding this comment.
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 👍 / 👎.
| const langIdx = process.argv.findIndex((a) => a === "--lang"); | ||
| const langArg = langIdx >= 0 ? process.argv[langIdx + 1] : null; |
There was a problem hiding this comment.
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 👍 / 👎.
…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
…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
…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
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 ativoomniroute 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 ativobin/cli/locales/—enept-BRtotalmente traduzidos; 29 locales comcommon+program; 11 scaffolds (fallback automático paraen)bin/cli/scripts/generate-locales.mjspara scaffoldar novos locales a partir deconfig/i18n.jsonOrdem de detecção do locale
--langflag →OMNIROUTE_LANGenv →LC_ALL→LC_MESSAGES→LANG→enCorreções de segurança e qualidade (code review)
i18n.mjs: proteção contra path traversal emnormalize()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 dedirname()para localizarconfig/i18n.json(4 níveis)config.mjs: escrita atômica do.envviawriteFileSync(tmp) + renameSync— elimina risco de truncamentoprogram.mjs: remove.env("OMNIROUTE_LANG")do option--langpara evitaropts.langambíguoconfig.lang.savedcorrigida — remove "Effective on next run" já quesetLocale()é chamado em-processoTestes
13 novos testes em
tests/unit/cli-lang-commands.test.tscobrindo:normalize()runConfigLangGetCommand(texto e JSON)runConfigLangListCommand(JSON com campoactive)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 atualizadabin/cli/CONVENTIONS.md— seção §6 i18n atualizada com 42 locales, regras de PR, guia para adicionar localedocs/guides/I18N.md— nova seção "CLI i18n" com tabela de detecção, exemplos de uso, lista de localesTest plan
node --import tsx/esm --test tests/unit/cli-lang-commands.test.ts→ 13/13 passnode --import tsx/esm --test tests/unit/cli-i18n-catalog.test.ts→ 7/7 passomniroute config lang list→ exibe 42 localesomniroute config lang set pt-BR→ salva em~/.omniroute/.envomniroute --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)