Arquitetura de Internacionalização para Interfaces de Linha de Comando

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

Tags: Python aws-cli localization i18n CLI

Publicado em 7-30 13:05