Back to 0fee
0fee

Internacionalización: 15 idiomas en el backend, 5 en el frontend

Cómo 0fee.dev implementa 15 idiomas en el backend y 5 en el frontend con SolidJS Context API, RTL en árabe y más de 1.350 claves.

Juste A. Gnimavo (Thales) & Claude | March 27, 2026 8 min 0fee
EN/ FR/ ES
i18ninternationalizationsolidjs-contextrtltranslations

Una plataforma de pagos que sirve a desarrolladores en más de 200 países no puede ser solo en inglés. Las páginas de checkout del backend soportan 15 idiomas incluyendo árabe con diseño RTL. El panel de control del frontend soporta 5 idiomas con más de 1.350 claves de traducción cada uno. Todo el sistema fue construido sin ninguna biblioteca i18n de terceros -- solo SolidJS Context API, interfaces TypeScript, detección de idioma del navegador y persistencia en localStorage.

Este artículo cubre la arquitectura de traducción de doble capa, la implementación con SolidJS Context, el desafío RTL, la detección de idioma del navegador y la sesión de corrección de acentos que arregló docenas de diacríticos faltantes.

Dos capas de traducción

El sistema usa diferentes cantidades de traducciones para diferentes superficies:

CapaIdiomasClaves por idiomaRazón
Backend (checkout)15~40Cadenas cortas específicas del checkout
Frontend (panel)5~1.350Interfaz completa de la aplicación

El backend necesita más idiomas porque las páginas de checkout están orientadas al cliente -- un cliente en Japón pagando a través del checkout alojado debería ver texto en japonés. El frontend necesita menos idiomas pero más profundidad porque es el panel del desarrollador -- y la mayoría de desarrolladores que construyen integraciones de pago leen inglés, francés, español, portugués o chino.

Backend: 15 idiomas

El sistema de traducción del backend es un único módulo Python con diccionarios anidados:

python# backend/locales/translations.py

SUPPORTED_LANGUAGES = [
    ("en", "English"),
    ("fr", "Francais"),
    ("es", "Espanol"),
    ("pt", "Portugues"),
    ("de", "Deutsch"),
    ("it", "Italiano"),
    ("nl", "Nederlands"),
    ("ar", "العربية"),
    ("zh", "中文"),
    ("ja", "日本語"),
    ("ko", "한국어"),
    ("tr", "Turkce"),
    ("ru", "Русский"),
    ("sw", "Kiswahili"),
    ("ha", "Hausa"),
]

TRANSLATIONS = {
    "en": {
        "checkout_title": "Checkout",
        "select_country": "Select your country",
        "select_method": "Choose payment method",
        "enter_phone": "Enter your phone number",
        "phone_placeholder": "Phone number",
        "enter_otp": "Enter the verification code sent to your phone",
        "otp_placeholder": "6-digit code",
        "pay_button": "Pay {amount} {currency}",
        "processing": "Processing your payment...",
        "check_phone": "Check your phone for the payment prompt",
        "success_title": "Payment Successful",
        "success_message": "Your payment of {amount} {currency} has been processed",
        "failed_title": "Payment Failed",
        "failed_message": "We could not process your payment",
        "retry": "Try Again",
        "cancel": "Cancel",
        "back": "Back",
        "powered_by": "Powered by 0fee.dev",
        "sandbox_notice": "Test mode - no real charges",
    },
    "fr": {
        "checkout_title": "Paiement",
        # ... traducciones en francés
    },
    "ar": {
        "checkout_title": "الدفع",
        # ... traducciones en árabe
    },
    "sw": {
        "checkout_title": "Malipo",
        # ... traducciones en suajili
    },
    "ha": {
        "checkout_title": "Biya",
        # ... traducciones en hausa
    },
    # ... 10 idiomas más
}

El suajili y el hausa están incluidos porque son ampliamente hablados en África Oriental y Occidental respectivamente -- mercados clave para 0fee.

Soporte RTL para árabe

El árabe requiere diseño de derecha a izquierda. La plantilla de checkout detecta el árabe y aplica RTL:

html<html lang="{{ lang }}" {% if lang == 'ar' %}dir="rtl"{% endif %}>
css/* Sobreescrituras específicas para RTL */
[dir="rtl"] .checkout-form {
    direction: rtl;
    text-align: right;
}

[dir="rtl"] .phone-input {
    flex-direction: row-reverse;
}

[dir="rtl"] .back-button {
    flex-direction: row-reverse;
}

[dir="rtl"] .back-button svg {
    transform: rotate(180deg);
}

La flecha de retroceso, el diseño de entrada de teléfono y la alineación del texto se invierten. Cada icono que implica dirección (flechas, chevrones) debe ser reflejado.

Frontend: 5 idiomas con más de 1.350 claves

El frontend usa un enfoque más estructurado con interfaces TypeScript para seguridad de tipos:

Definiciones de tipos

typescript// frontend/src/locales/types.ts
export interface Translations {
  common: {
    loading: string;
    error: string;
    success: string;
    cancel: string;
    save: string;
    // ... más de 50 cadenas comunes
  };
  nav: {
    dashboard: string;
    apps: string;
    transactions: string;
    // ... etiquetas de navegación
  };
  dashboard: {
    title: string;
    totalTransactions: string;
    // ... cadenas específicas del panel
  };
  // ... más de 15 secciones más
}

SolidJS Context API

Las traducciones se proporcionan a través de un Context de SolidJS:

typescript// frontend/src/contexts/LanguageContext.tsx
import { createContext, useContext, createSignal, ParentComponent } from "solid-js";
import { en } from "../locales/en";
import { fr } from "../locales/fr";
import { es } from "../locales/es";
import { pt } from "../locales/pt";
import { zh } from "../locales/zh";
import type { Translations } from "../locales/types";

const LANGUAGES: Record<string, Translations> = { en, fr, es, pt, zh };

export const LanguageProvider: ParentComponent = (props) => {
  const detectLanguage = (): string => {
    const saved = localStorage.getItem("lang");
    if (saved && LANGUAGES[saved]) return saved;

    const browserLang = navigator.language.split("-")[0];
    if (LANGUAGES[browserLang]) return browserLang;

    return "en";
  };

  const [lang, setLang] = createSignal(detectLanguage());
  const t = () => LANGUAGES[lang()] || en;

  const updateLang = (newLang: string) => {
    setLang(newLang);
    localStorage.setItem("lang", newLang);
  };

  return (
    <LanguageContext.Provider
      value={{ t, lang, setLang: updateLang, availableLanguages: [
        { code: "en", name: "English" },
        { code: "fr", name: "Francais" },
        { code: "es", name: "Espanol" },
        { code: "pt", name: "Portugues" },
        { code: "zh", name: "中文" },
      ] }}
    >
      {props.children}
    </LanguageContext.Provider>
  );
};

Uso en componentes

tsximport { useLanguage } from "../contexts/LanguageContext";

function TransactionsPage() {
  const { t } = useLanguage();

  return (
    <div>
      <h1>{t().transactions.title}</h1>
      <table>
        <thead>
          <tr>
            <th>{t().transactions.tableHeaders.id}</th>
            <th>{t().transactions.tableHeaders.amount}</th>
            <th>{t().transactions.tableHeaders.status}</th>
          </tr>
        </thead>
      </table>
    </div>
  );
}

La sesión de corrección de acentos (083)

La sesión 082 creó los archivos de traducción en español, portugués y chino y completó las 1.350 claves por idioma. Sin embargo, las traducciones al francés carecían de acentos -- un problema crítico para la base de usuarios francófonos africanos de 0fee.

La sesión 083 fue dedicada a arreglar esto. Más de 50 palabras recibieron diacríticos franceses correctos. El compilador TypeScript no detectó ninguno de estos errores porque son valores de cadena, no problemas de sintaxis. Por eso es importante el testing automatizado para traducciones -- y por qué tenemos una regla a nivel de proyecto contra reproducir defectos de entrada en el contenido generado.

Detección de idioma del navegador

La detección de idioma sigue una prioridad de tres pasos:

1. localStorage (elección explícita del usuario)
2. navigator.language (configuración de idioma del navegador)
3. Predeterminado: Inglés

Esto significa que un desarrollador francófono que visite 0fee.dev por primera vez verá el panel en francés sin ninguna configuración. Puede cambiar a otro idioma en cualquier momento, y su elección persiste entre sesiones.

Decisiones de arquitectura

Tres elecciones deliberadas dieron forma al sistema i18n:

  1. Sin biblioteca externa. Consideramos i18next y typesafe-i18n pero decidimos que SolidJS Context con interfaces TypeScript proporciona todo lo que necesitamos: claves con seguridad de tipos, cambio reactivo de idioma y cero sobrecarga de dependencias.
  1. Diferentes cantidades de idiomas por capa. El backend necesita amplia cobertura (15 idiomas) para el checkout orientado al cliente. El frontend necesita cobertura profunda (más de 1.350 claves) para el panel del desarrollador. Intentar mantener 1.350 claves en 15 idiomas sería impracticable.
  1. Importaciones estáticas, no carga dinámica. Los cinco archivos de idioma se importan en tiempo de build e incluyen en el bundle. Esto añade ~30KB por idioma al bundle pero elimina los retrasos de carga al cambiar de idioma. Para cinco idiomas, la compensación es aceptable.

Lo que aprendimos

Construir el sistema i18n nos enseñó tres cosas:

  1. Las traducciones con seguridad de tipos detectan errores estructurales pero no errores de contenido. TypeScript asegura que cada archivo de idioma tenga cada clave, pero no puede detectar acentos faltantes, traducciones incorrectas o errores gramaticales. La revisión humana (o en nuestro caso, una sesión de corrección dedicada) sigue siendo necesaria.
  1. El soporte RTL no es solo dirección del texto. El árabe requirió invertir diseños de formularios, reflejar iconos direccionales, ajustar componentes de entrada de teléfono y probar cada elemento visual. Es un cambio de sistema de diseño, no solo una propiedad CSS.
  1. La detección del navegador proporciona un buen valor predeterminado. Los navegadores de la mayoría de los desarrolladores están configurados para su idioma preferido. Usar navigator.language como la suposición inicial significa que el panel "simplemente funciona" en el idioma correcto para la mayoría de usuarios en la primera visita.

El sistema i18n fue construido en la sesión 064, expandido en la sesión 082 y corregido en la sesión 083. Sirve a desarrolladores en 5 idiomas y a clientes en 15, cubriendo los principales mercados donde opera 0fee.


Este artículo es parte de la serie "Cómo construimos 0fee.dev". 0fee.dev es un orquestador de pagos que cubre más de 53 proveedores en más de 200 países, construido por Juste A. GNIMAVO y Claude desde Abiyán sin ingenieros humanos. Sigue la serie para conocer la historia completa de la construcción.

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles

Thales & Claude zerosuite

Funciona, y no está terminado

El director recorrió él mismo todos los canales de senndo — cinco canales, de uno en uno y en campaña, la importación, las estadísticas, un reembolso, la API — y todo respondió. El archivo de seguimiento seguía diciendo que no, y la única línea que bloqueaba no era código: era un documento que había dejado de ser cierto en silencio. Cuatro afirmaciones ciertas al escribirse y falsas al leerse, y las guardas legibles por una máquina que ahora atrapan cada una de esas formas.

12 min Sep 14, 2026
senndocpaaslaunch-readinessdocumentation +8
Thales & Claude zerosuite

El navegador en manos de Claude: manejar el propio Chrome del CEO

Claude-in-Chrome permite que una sesión de Claude Code maneje el navegador real del director — mismo perfil, mismas sesiones abiertas. Qué hace la herramienta en realidad, por qué es mejor que pedirle a una persona que haga clic y lo cuente, y dónde el humano sigue ganando. Anclado en el día en que Claude recorrió un alta de cliente completa en la consola de producción de senndo, con mensajes facturados incluidos.

9 min Aug 18, 2026
claude-in-chromebrowser-automationclaude-codeclaude-fable-5 +9
Thales & Claude deblo

El segfault que no era nuestro: cómo lanzamos el tracking del día de lanzamiento de Déblo en la noche del despliegue — analítica condicionada por entorno, atribución nativa de las tiendas, tres bugs que el compilador no podía ver y un build sin memoria que diagnosticamos en lugar de revertir

El 1 de julio de 2026 — el día del lanzamiento — el riesgo nunca fue el texto. Era que las campañas de pago salieran a ciegas. Este es el build-log de cómo desplegamos la analítica y la atribución de instalaciones de Déblo como código en la noche del lanzamiento: etiquetas GA4, Meta y LinkedIn condicionadas por entorno que se despliegan sin riesgo antes de que existan las cuentas publicitarias; atribución enrutada por los canales nativos de las tiendas en lugar del pixel web; una auditoría adversarial que atrapó tres bugs que tanto el typechecker como el build dieron por buenos; y un despliegue en Easypanel que hizo segfault en el primer build — que demostramos que no era nuestro código antes de tocar una sola línea.

18 min Jul 1, 2026
deblolaunch-dayclaude-opus-4.8claude-code +26