# Arquitetura de i18n — Sistema de Tradução Automático

Guia para reproduzir do zero o sistema de internacionalização usado no Genesis/Galaxia.

---

## Conceito central

O sistema não usa chaves de tradução (como `t('menu.home')`). O texto original **é** a chave. Isso tem uma consequência importante: **nunca quebra em produção** — se uma string ainda não foi traduzida, ela aparece no idioma original enquanto o arquivo JSON é atualizado automaticamente em background.

---

## Estrutura de arquivos

```
/storage/linguagens/
├── {setor-a}/
│   ├── pt-br.json
│   ├── en.json
│   └── es.json
└── {setor-b}/
    ├── pt-br.json
    └── ...
```

O diretório é particionado por **setor** (tenant/cliente). Isso permite traduções customizadas por cliente — o setor A pode ter "Pedido" enquanto o setor B chama de "Ordem".

---

## Formato do JSON

A chave é o **MD5 do texto original**. Isso evita problemas com caracteres especiais, espaços e encoding nas chaves JSON.

```json
{
    "574893d1aa72e1263f2d1e062db88ba2": {
        "original": "Não foi possível enviar sua solicitação",
        "traducao": "Não foi possível enviar sua solicitação",
        "solicitacoes": [
            "/app/modules/pedidos/PedidoService.php",
            "/app/views/pedidos/criar.php"
        ]
    },
    "3fa8d51dfe47bcaad0c6198f1b912d35": {
        "original": "Ops.",
        "traducao": "Oops.",
        "solicitacoes": [
            "/app/helpers/Message.php"
        ]
    }
}
```

| Campo | Descrição |
|---|---|
| `original` | Texto na língua-base (pt-br) — nunca muda |
| `traducao` | Texto traduzido para o idioma do arquivo |
| `solicitacoes` | Lista de arquivos que usam essa string — rastreabilidade automática |

---

## Implementação — função `show()`

```php
// Cache em memória para evitar leitura de disco a cada chamada
$_traducoesSalvas = [];

function show(string $texto): string
{
    $idioma = $_COOKIE['app_language'] ?? 'pt-br';
    $setor  = getCurrentSector(); // ex: "acme-corp" ou "default"

    return _retornarTraducao($idioma, $texto, $setor, debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]);
}

function _retornarTraducao(string $idioma, string $texto, string $setor, array $backtrace): string
{
    global $_traducoesSalvas;

    $arquivoJson = "/storage/linguagens/{$setor}/{$idioma}.json";
    $arquivo     = $backtrace['file'];
    $hash        = md5($texto);

    // Garante que o diretório existe
    if (!is_dir(dirname($arquivoJson))) {
        mkdir(dirname($arquivoJson), 0755, true);
    }

    // Inicializa o arquivo se não existir
    if (!is_file($arquivoJson)) {
        file_put_contents($arquivoJson, json_encode([], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
        $_traducoesSalvas[$idioma] = [];
    }

    // Carrega do disco uma única vez por request por idioma
    if (!isset($_traducoesSalvas[$idioma])) {
        $_traducoesSalvas[$idioma] = json_decode(file_get_contents($arquivoJson), true) ?? [];
    }

    $traducoes = &$_traducoesSalvas[$idioma];

    // String ainda não cadastrada — auto-registra com a tradução = original
    if (empty($traducoes[$hash])) {
        // Re-lê do disco para evitar race condition com outro request
        $traducoes = json_decode(file_get_contents($arquivoJson), true) ?? [];

        if (empty($traducoes[$hash])) {
            $traducoes[$hash] = [
                'original'    => $texto,
                'traducao'    => $texto, // fallback: exibe o original
                'solicitacoes' => [$arquivo],
            ];
            file_put_contents($arquivoJson, json_encode($traducoes, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
        }
    }

    // Registra o arquivo como solicitante (rastreabilidade)
    if (!in_array($arquivo, $traducoes[$hash]['solicitacoes'])) {
        $traducoes[$hash]['solicitacoes'][] = $arquivo;
        file_put_contents($arquivoJson, json_encode($traducoes, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
    }

    return $traducoes[$hash]['traducao'];
}
```

### Uso

```php
// Em views PHP
<h1><?= show("Edição de Rota") ?></h1>
<button><?= show("Salvar") ?></button>

// Em classes de serviço
throw new Exception(show("Registro não encontrado"));
$this->message->warning(show("Campos obrigatórios não preenchidos"));
```

---

## Configurando o idioma do usuário

O idioma é lido de um cookie de longa duração (não de sessão, para sobreviver logout/login):

```php
function setUserLanguage(string $idioma): void
{
    // Idiomas suportados
    $suportados = ['pt-br', 'en', 'es'];
    if (!in_array($idioma, $suportados)) {
        $idioma = 'pt-br';
    }

    setcookie('app_language', $idioma, time() + (750 * 24 * 3600), '/');
}
```

Chamar `setUserLanguage()` após o login (ou numa rota de preferências do usuário).

---

## Multi-tenant: como funciona o setor

Cada cliente/empresa tem seu próprio diretório de traduções. O `getCurrentSector()` pode ser implementado de várias formas:

```php
function getCurrentSector(): string
{
    // Opção 1: baseado no subdomínio
    // acme.myapp.com → "acme"
    $host = $_SERVER['HTTP_HOST'] ?? '';
    $partes = explode('.', $host);
    if (count($partes) >= 3) {
        return $partes[0];
    }

    // Opção 2: baseado na sessão do usuário logado
    return $_SESSION['user']['setor_pathname'] ?? 'default';
}
```

Se o sistema for single-tenant, usar sempre `'default'` como setor.

---

## Fluxo completo de uma string nova

```
show("Novo texto aqui")
        │
        ▼
    md5("Novo texto aqui") = "abc123..."
        │
        ▼
    Existe abc123... no JSON?
        │
    NÃO ├──────────────────────────────────────┐
        │                                      ▼
        │                          Grava no JSON:
        │                          { original: "Novo texto aqui",
        │                            traducao: "Novo texto aqui",  ← fallback = original
        │                            solicitacoes: ["views/foo.php"] }
        │
    SIM ├──────────────────────────────────────┐
        │                                      ▼
        │                          Já traduzido? Retorna traducao
        │                          Não traduzido? Retorna original (sem quebrar)
        ▼
    Retorna traducao
```

---

## Workflow de tradução

1. **Deploy de nova feature** → strings novas aparecem com `traducao = original` no JSON
2. **Tradutor acessa o JSON** (ou uma UI de admin, se construída) e preenche `traducao` nos idiomas desejados
3. **Próximo request** já carrega a tradução correta

Não há etapa de build, não há keys para manter em sync, não há quebra em produção.

---

## UI Admin para tradução (opcional)

Para facilitar o trabalho dos tradutores, uma UI simples pode:

1. Ler todos os JSONs de um setor
2. Listar as strings com `traducao == original` (pendentes de tradução)
3. Mostrar o campo `solicitacoes` para dar contexto (em qual tela aparece)
4. Salvar o `traducao` atualizado de volta no JSON

```
┌─────────────────────────────────────────────────────────┐
│ Strings pendentes — en.json (setor: acme)               │
├──────────────────────────┬──────────────────────────────┤
│ Original (pt-br)         │ Tradução (en)                │
├──────────────────────────┼──────────────────────────────┤
│ Edição de Rota           │ [Route Editing             ] │
│ Salvar                   │ [Save                      ] │
│ Campos obrigatórios      │ [Required fields           ] │
└──────────────────────────┴──────────────────────────────┘
  📍 Encontrado em: views/rotas/editar.php
```

---

## Cuidados e limitações

**Race condition em escrita**
Múltiplos requests simultâneos podem tentar gravar no mesmo JSON. Mitigação simples: usar `LOCK_EX` no `file_put_contents`:

```php
file_put_contents($arquivoJson, json_encode($traducoes, ...), LOCK_EX);
```

Para alto volume, substituir por um banco de dados ou fila de escrita assíncrona.

**Cache em memória é por request**
O `$_traducoesSalvas` é reiniciado a cada request HTTP. Para performance em aplicações de alta escala, usar APCu ou Redis:

```php
$cacheKey = "linguagens:{$setor}:{$idioma}";
$traducoes = apcu_fetch($cacheKey);
if ($traducoes === false) {
    $traducoes = json_decode(file_get_contents($arquivoJson), true) ?? [];
    apcu_store($cacheKey, $traducoes, 300); // TTL 5min
}
```

**Tamanho do arquivo**
Com muitas strings, o JSON cresce. Particionar por módulo (`pedidos/pt-br.json`, `produtos/pt-br.json`) é uma evolução natural quando necessário.

**Strings dinâmicas**
Não passar variáveis diretamente no texto base — o MD5 seria diferente a cada valor:

```php
// ERRADO — gera uma entrada diferente por ID
show("Pedido #{$id} não encontrado");

// CORRETO — traduz o template, interpola depois
sprintf(show("Pedido #%s não encontrado"), $id);
```
