A internacionalização (i18n) em interfaces de linha de comando (CLI) exige uma arquitetura robusta capaz de lidar com detecção de ambiente, gerenciamento de recursos e renderização específica para diferentes sistemas operacionais. Ferramentas de grande escala utilizam abordagens em camadas para garantir que a experiência do usuário seja consistente independentemente do idioma ou configuração de codificação do terminal.
Detecção de Ambiente e Configuração de Locale
O primeiro passo para qualquer sistema i18n é identificar a preferência linguística do usuário. Isso é feito analisando variáveis de ambiente do sistema com uma ordem de precedência estrita, garantindo que as configurações regionais específicas tenham prioridade sobre as globais.
import os
import locale
def resolve_active_language():
"""Determina o idioma ativo baseado nas variáveis de ambiente."""
precedence = ('LC_ALL', 'LC_MESSAGES', 'LANG')
for env_key in precedence:
env_val = os.getenv(env_key)
if env_val:
# Remove sufixos de codificação (ex: .UTF-8)
return env_val.split('.')[0]
try:
sys_lang, _ = locale.getdefaultlocale()
return sys_lang if sys_lang else 'en_US'
except Exception:
return 'en_US'
Estrutura de Dados para Recursos Multilíngues
A segregação de textos em arquivos estruturados facilita a manutenção e a tradução. O formato JSON é amplamente adotado por sua interoperabilidade. Abaixo, um exemplo de organização que separa ações da CLI de mensagens de exceção:
{
"pt_BR": {
"cli_actions": {
"init": {
"desc": "Inicializar configurações do ambiente",
"syntax": "app [flags] <acao> <subacao> [args]"
}
},
"exceptions": {
"auth_missing": "Credenciais ausentes. Execute 'app init'.",
"timeout": "A conexão expirou."
}
}
}
</subacao></acao>
Motor de Carregamento Dinâmico
Para evitar sobrecarregar a memória, os arquivos de tradução devem ser lidos dinamicamente. O componente central atua como um provedor que carrega os dicionários de idiomas disponíveis e implementa uma lógica de fallback caso um idioma ou chave específica não seja encontrado.
import json
from pathlib import Path
class TranslationEngine:
def __init__(self, assets_dir: str = "locales"):
self.assets_path = Path(assets_dir)
self.active_lang = "en_US"
self.dictionary = {}
self._bootstrap()
def _bootstrap(self):
for lang_dir in self.assets_path.iterdir():
if lang_dir.is_dir():
self.dictionary[lang_dir.name] = {}
for file in lang_dir.glob("*.json"):
module = file.stem
with file.open("r", encoding="utf-8") as f:
self.dictionary[lang_dir.name][module] = json.load(f)
def switch_language(self, lang_code: str):
if lang_code in self.dictionary:
self.active_lang = lang_code
else:
base = lang_code.split('_')[0]
match = next((k for k in self.dictionary if k.startswith(base)), "en_US")
self.active_lang = match
def translate(self, module: str, key: str, fallback: str = "") -> str:
try:
return self.dictionary[self.active_lang][module][key]
except KeyError:
return self.dictionary.get("en_US", {}).get(module, {}).get(key, fallback or key)
Sanitização de Texto e Compatibilidade de Codificação
Terminais possuem configurações de codificação variadas. O processamento de strings deve garantir que a saída seja segura, lidando com erros de decodificação e adaptando-se ao codec do fluxo de saída padrão.
import sys
class StreamSanitizer:
@staticmethod
def decode_payload(data, default_codec='utf-8'):
if not isinstance(data, bytes):
return data
for codec in [default_codec, 'latin-1', 'cp1252']:
try:
return data.decode(codec)
except UnicodeDecodeError:
continue
return data.decode(default_codec, errors='ignore')
@staticmethod
def emit(target_stream, message):
codec = getattr(target_stream, 'encoding', None) or 'utf-8'
if codec.lower() == 'utf-8':
target_stream.write(message)
else:
safe_bytes = message.encode(codec, errors='replace')
target_stream.write(safe_bytes.decode(codec))
Ajuste de Largura do Terminal
A quebra de linha automática previne que textos longos sejam truncados ou renderizados incorretamente em janelas redimensionadas.
import shutil
def wrap_console_text(content: str, margin: int = 2) -> str:
try:
cols = shutil.get_terminal_size().columns - margin
except OSError:
cols = 80
cols = max(cols, 20)
paragraphs = content.split('\n')
wrapped_lines = []
for para in paragraphs:
words = para.split()
line_buffer = []
line_len = 0
for w in words:
if line_len + len(w) + len(line_buffer) > cols:
wrapped_lines.append(" ".join(line_buffer))
line_buffer = [w]
line_len = len(w)
else:
line_buffer.append(w)
line_len += len(w)
if line_buffer:
wrapped_lines.append(" ".join(line_buffer))
return "\n".join(wrapped_lines)
Renderização Específica por Plataforma
Sistemas POSIX e Windows exigem abordagens distintas para paginar e formatar textos de documentação interativa.
Ambientes Unix-like
import subprocess
import shutil
class UnixDocRenderer:
PAGER_CMD = ['less', '-R']
def render_manpage(self, raw_rst: str, lang: str) -> str:
man_data = self._generate_man(raw_rst, lang)
formatter = 'groff' if shutil.which('groff') else 'mandoc'
cmd = ['groff', '-m', 'man', '-T', 'utf8'] if formatter == 'groff' else ['mandoc', '-T', 'utf8']
process = subprocess.run(cmd, input=man_data, capture_output=True, text=True)
return process.stdout
Ambientes Windows
class WindowsDocRenderer:
PAGER_CMD = ['more']
def render_text(self, raw_rst: str, lang: str) -> str:
cp = self._fetch_codepage()
return self._generate_text(raw_rst, lang, cp)
def _fetch_codepage(self) -> str:
try:
import ctypes
return str(ctypes.windll.kernel32.GetConsoleOutputCP())
except Exception:
return 'utf-8'
Otimizações de Desempenho
Carregamento Sob Demanda (Lazy Loading)
Carregar todos os dicionários de idioma na inicialização impacta o tempo de resposta da CLI. A estratégia de carregaemnto sob demanda lê os arquivos apenas quando um idioma é requisitado pela priemira vez.
import os
import json
class OnDemandTranslator:
def __init__(self):
self._cache = {}
self._active = None
def fetch(self, domain: str, key: str) -> str:
if self._active not in self._cache:
self._hydrate(self._active)
return self._cache[self._active].get(domain, {}).get(key, key)
def _hydrate(self, lang_code: str):
target_dir = f"locales/{lang_code}"
if not os.path.isdir(target_dir):
target_dir = "locales/en_US"
self._cache[lang_code] = {}
for f in os.listdir(target_dir):
if f.endswith('.json'):
with open(os.path.join(target_dir, f), 'r', encoding='utf-8') as fh:
self._cache[lang_code][f[:-5]] = json.load(fh)
Gerenciamento de Cache em Memória
class TranslationCache:
def __init__(self, capacity=500):
self._store = {}
self._capacity = capacity
def resolve(self, lang, domain, key):
cache_key = f"{lang}:{domain}:{key}"
if cache_key in self._store:
return self._store[cache_key]
value = self._disk_lookup(lang, domain, key)
if len(self._store) >= self._capacity:
# Remove o item mais antigo
self._store.pop(next(iter(self._store)))
self._store[cache_key] = value
return value
Validação Automatizada e Testes
A suíte de testes deve garantir que a detecção de idioma e o fallback funcionem corretamente sob diferentes configurações de ambiente.
import pytest
import os
from unittest.mock import patch
def test_locale_resolution():
scenarios = [
({'LC_ALL': 'fr_FR.UTF-8'}, 'fr_FR'),
({'LANG': 'de_DE.UTF-8'}, 'de_DE'),
({}, 'en_US')
]
for env_mock, expected in scenarios:
with patch.dict(os.environ, env_mock, clear=True):
assert resolve_active_language() == expected
def test_fallback_mechanism():
engine = TranslationEngine()
engine.switch_language('xx_XX')
assert engine.active_lang == 'en_US'
Matriz de Compatibilidade de Codificação
| Cenário | Input | Output Esperado | Comportamento |
|---|---|---|---|
| Terminal UTF-8 | UTF-8 | UTF-8 | Renderização direta |
| Terminal CP1252 | UTF-8 | CP1252 | Transcodificação automática |
| Bytes inválidos | Dados corrompidos | UTF-8 | Substituição por caractere de escape |
Implantação Contínua de Recursos i18n
A estrutura de diretórios no repositório deve espelhar os códigos de locale, mantendo os domínios de tradução separados:
locales/
├── en_US/
│ ├── cli_actions.json
│ └── exceptions.json
├── pt_BR/
│ ├── cli_actions.json
│ └── exceptions.json
└── fr_FR/
└── ...
O pipeline de Integração Contínua (CI) valida as traduções contra múltiplos locales simultaneamente, assegurando que novas chaves não quebrem a compatibilidade retroativa:
name: Validation i18n
on: [push]
jobs:
verify-locales:
runs-on: ubuntu-latest
strategy:
matrix:
lang: [en_US, pt_BR, es_ES, fr_FR]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Execute tests
env:
LANG: ${{ matrix.lang }}.UTF-8
run: pytest tests/i18n/ -v