Como implementar localização e internacionalização em APIs REST

1. Fundamentos de i18n e l10n em APIs REST

A internacionalização (i18n) e a localização (l10n) são conceitos fundamentais para construir APIs que atendam usuários globais. A internacionalização refere-se ao processo de projetar o sistema para suportar múltiplos idiomas e regiões sem exigir alterações no código-fonte. Já a localização é a adaptação concreta do conteúdo para um locale específico, incluindo traduções, formatos de data, moeda e regras culturais.

Em APIs REST, os desafios são particulares. Diferentemente de aplicações web tradicionais, onde o idioma pode ser detectado pelo navegador, as APIs precisam lidar com headers HTTP, parâmetros de query e até subdomínios para determinar o locale. A escolha mais comum e alinhada com os padrões REST é utilizar o header Accept-Language, que segue a especificação HTTP e permite que clientes indiquem suas preferências linguísticas com quality values.

// Exemplo de header Accept-Language
Accept-Language: pt-BR, pt;q=0.9, en;q=0.8, es;q=0.5

Alternativas incluem passar o locale como parâmetro de query (?lang=pt-BR) ou usar subdomínios (pt.api.exemplo.com). A abordagem via header é recomendada por ser semântica e não poluir a URL, mas cada caso deve ser avaliado conforme o ecossistema de clientes.

2. Estruturação de mensagens e recursos traduzíveis

A organização dos arquivos de tradução é o alicerce de uma implementação robusta. Formatos como JSON, YAML ou PO (usado em sistemas GNU gettext) são populares. Recomenda-se estruturar as mensagens por domínio e chave, separando arquivos por idioma.

// Estrutura de diretórios
locales/
  en/
    common.json
    errors.json
    validation.json
  pt-BR/
    common.json
    errors.json
    validation.json
  es/
    common.json
    errors.json
    validation.json

O suporte a pluralização é crítico. Idiomas como o inglês têm duas formas (singular/plural), enquanto o russo ou árabe possuem regras complexas. Uma boa prática é usar chaves com sufixos ou bibliotecas que implementem a CLDR (Common Locale Data Repository).

// Exemplo de arquivo de tradução com pluralização (en/common.json)
{
  "items_count": {
    "one": "1 item",
    "other": "{count} items"
  }
}

O gerenciamento de fallback é essencial. Defina um idioma padrão (geralmente inglês) e uma cadeia de fallbacks: se pt-BR não existir, tente pt; se pt não existir, use en. Isso garante que a API nunca retorne chaves não traduzidas.

3. Implementação do middleware de localização

O middleware de localização é responsável por interceptar cada requisição, extrair o locale preferido e disponibilizá-lo para os endpoints. Em Node.js com Express, por exemplo, podemos implementar:

// Middleware de localização
const supportedLocales = ['pt-BR', 'pt', 'en', 'es'];
const defaultLocale = 'en';

function localeMiddleware(req, res, next) {
  const acceptLanguage = req.headers['accept-language'];

  if (acceptLanguage) {
    const parsed = parseAcceptLanguage(acceptLanguage, supportedLocales);
    req.locale = parsed || defaultLocale;
  } else {
    req.locale = defaultLocale;
  }

  // Define o cabeçalho de resposta
  res.setHeader('Content-Language', req.locale);
  next();
}

O parsing de quality values deve respeitar a especificação HTTP, onde valores mais altos indicam maior preferência. Bibliotecas como accept-language-parser facilitam esse processo. A validação de códigos de idioma deve seguir ISO 639-1 (duas letras) ou BCP 47 (com variações regionais).

4. Tradução de respostas de erro e validação

Mensagens de erro padronizadas com suporte a múltiplos idiomas são cruciais para a experiência do desenvolvedor. Cada mensagem de erro deve ser referenciada por uma chave única, e o sistema de tradução deve ser invocado antes de retornar a resposta.

// Exemplo de resposta de erro internacionalizada
HTTP/1.1 422 Unprocessable Entity
Content-Language: pt-BR
Content-Type: application/json

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "O campo 'email' é obrigatório",
    "details": [
      {
        "field": "email",
        "rule": "required",
        "message": "O email deve ser fornecido"
      }
    ]
  }
}

A tradução de campos de validação exige cuidado com nomes de atributos. Em vez de hardcodar "email", use uma chave de tradução como field.email que pode ser localizada. O cabeçalho Content-Language na resposta informa ao cliente qual idioma foi utilizado, permitindo que ele ajuste sua interface se necessário.

5. Localização de dados dinâmicos e conteúdos do banco

Dados que variam conforme o locale (como descrições de produtos ou nomes de categorias) exigem estratégias específicas em bancos relacionais. Duas abordagens comuns são:

Tabelas separadas: Uma tabela principal com dados invariantes e uma tabela de traduções com chave estrangeira e coluna de idioma.

-- Modelo de tabelas separadas
CREATE TABLE products (
  id SERIAL PRIMARY KEY,
  sku VARCHAR(50) NOT NULL,
  price DECIMAL(10,2) NOT NULL
);

CREATE TABLE product_translations (
  product_id INTEGER REFERENCES products(id),
  locale VARCHAR(10) NOT NULL,
  name VARCHAR(200) NOT NULL,
  description TEXT,
  PRIMARY KEY (product_id, locale)
);

Coluna JSON: Uma coluna JSONB que armazena todas as traduções em um único campo.

-- Modelo com coluna JSON
CREATE TABLE products (
  id SERIAL PRIMARY KEY,
  sku VARCHAR(50) NOT NULL,
  price DECIMAL(10,2) NOT NULL,
  translations JSONB NOT NULL
);

-- Exemplo de conteúdo da coluna translations
{
  "en": {"name": "Wireless Mouse", "description": "Ergonomic design"},
  "pt-BR": {"name": "Mouse sem fio", "description": "Design ergonômico"},
  "es": {"name": "Ratón inalámbrico", "description": "Diseño ergonómico"}
}

Para performance, implemente cache de traduções em memória (Redis ou similar) e evite consultas repetitivas ao banco. Estratégias de fallback devem buscar o idioma mais específico primeiro e depois cair para o padrão.

6. Versionamento e testes de internacionalização

Arquivos de tradução devem ser versionados junto com o código, e mudanças devem seguir o mesmo fluxo de revisão. Ferramentas como Lokalise, Crowdin ou POEditor ajudam a gerenciar traduções com equipes distribuídas.

Testes automatizados são indispensáveis:

// Teste de cobertura de idiomas
describe('Localization Tests', () => {
  it('should have all keys in all supported locales', () => {
    const supportedLocales = ['en', 'pt-BR', 'es'];
    const referenceKeys = getKeysForLocale('en');

    supportedLocales.forEach(locale => {
      const localeKeys = getKeysForLocale(locale);
      const missingKeys = referenceKeys.filter(k => !localeKeys.includes(k));
      expect(missingKeys).toHaveLength(0);
    });
  });

  it('should respond in Portuguese when Accept-Language is pt-BR', async () => {
    const response = await request(app)
      .get('/api/products/1')
      .set('Accept-Language', 'pt-BR');

    expect(response.headers['content-language']).toBe('pt-BR');
    expect(response.body.name).toBe('Mouse sem fio');
  });
});

Simule diferentes headers de idioma em testes de integração para garantir que o middleware funciona corretamente e que as traduções são aplicadas em todos os endpoints.

7. Boas práticas e considerações de performance

O carregamento de traduções pode impactar a performance. Em vez de lazy loading (carregar sob demanda), que adiciona latência, considere carregar todas as traduções no startup da aplicação, especialmente se o número de idiomas for limitado (até 20). Para muitos idiomas, use um cache com TTL e carregamento sob demanda com fallback.

A Content Negotiation deve ir além do idioma. Para formatos de data, moeda e número, utilize bibliotecas como Intl (nativa do JavaScript) ou moment.js com locale configurado.

// Exemplo de formatação de data com locale
const dateFormatter = new Intl.DateTimeFormat('pt-BR', {
  dateStyle: 'long',
  timeStyle: 'short'
});
console.log(dateFormatter.format(new Date())); // "15 de janeiro de 2025 14:30"

Monitore métricas importantes: taxa de fallback (indica lacunas nas traduções), tempo médio de tradução por requisição e distribuição de idiomas usados. Ferramentas como Prometheus e Grafana podem capturar essas métricas via middleware.

Por fim, documente claramente como os clientes devem especificar o idioma e quais locales são suportados. Uma documentação clara reduz erros de integração e melhora a experiência dos desenvolvedores que consomem sua API.

Referências