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:
| Capa | Idiomas | Claves por idioma | Razón |
|---|---|---|---|
| Backend (checkout) | 15 | ~40 | Cadenas cortas específicas del checkout |
| Frontend (panel) | 5 | ~1.350 | Interfaz 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ésEsto 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:
- 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.
- 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.
- 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:
- 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.
- 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.
- 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.languagecomo 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.