Back to 0fee
0fee

Depuracion en Navidad: i18n, SDKs y errores de moneda

Sesiones 064-067 en el dia de Navidad de 2025: construyendo i18n, reescribiendo SDKs y corrigiendo errores de moneda desde Abiyan. Por Juste A. Gnimavo.

Thales & Claude | March 30, 2026 7 min 0fee
EN/ FR/ ES
christmasdebuggingi18nsdkcurrency

25 de diciembre de 2025. Abiyan, Costa de Marfil. Mientras el resto del mundo abria regalos, Thales abrio su portatil y comenzo la sesion 064. Al final del dia, 0fee.dev tenia infraestructura de internacionalizacion soportando 15 idiomas de backend y 5 de frontend, SDKs de Node.js y Python completamente reescritos, un sistema de monedas corregido, generacion de recibos PDF y una clave de token de localStorage reparada.

Cuatro sesiones. Dia de Navidad. Trabajando desde Abiyan. Asi es como se ve construir una startup con un CTO de IA cuando no hay festivos ni fines de semana.

Sesion 064: infraestructura de i18n

La sesion de internacionalizacion fue la mas grande de las cuatro. 0fee.dev necesitaba servir a desarrolladores en todo el mundo, y solo ingles era una limitacion:

Backend: 15 idiomas

python# i18n/languages.py
SUPPORTED_LANGUAGES = {
    "en": "English",
    "fr": "French",
    "es": "Spanish",
    "pt": "Portuguese",
    "ar": "Arabic",
    "zh": "Chinese (Simplified)",
    "ja": "Japanese",
    "ko": "Korean",
    "de": "German",
    "it": "Italian",
    "nl": "Dutch",
    "ru": "Russian",
    "tr": "Turkish",
    "sw": "Swahili",
    "ha": "Hausa",
}

El suajili y el hausa fueron incluidos desde el principio -- decisiones deliberadas para una plataforma que prioriza Africa. El suajili cubre Africa Oriental (Kenia, Tanzania, Uganda). El hausa cubre Africa Occidental (Nigeria, Niger, Ghana).

El sistema de i18n del backend fue construido como un diccionario de traducciones con respaldo:

python# i18n/translator.py
import json
from pathlib import Path

class Translator:
    def __init__(self):
        self._translations: dict[str, dict[str, str]] = {}
        self._load_translations()

    def _load_translations(self):
        i18n_dir = Path(__file__).parent / "translations"
        for lang_file in i18n_dir.glob("*.json"):
            lang_code = lang_file.stem
            with open(lang_file) as f:
                self._translations[lang_code] = json.load(f)

    def t(self, key: str, lang: str = "en", **kwargs) -> str:
        """Traducir una clave al idioma especificado."""
        translations = self._translations.get(lang, {})
        text = translations.get(key)

        if not text:
            # Respaldo a ingles
            text = self._translations.get("en", {}).get(key, key)

        # Interpolar variables
        if kwargs:
            text = text.format(**kwargs)

        return text

translator = Translator() t = translator.t ```

El idioma se determina desde la cabecera Accept-Language, con un parametro de consulta como anulacion:

python# middleware/i18n.py
async def get_language(request: Request) -> str:
    # El parametro de consulta tiene prioridad
    lang = request.query_params.get("lang")
    if lang and lang in SUPPORTED_LANGUAGES:
        return lang

    # Parsear cabecera Accept-Language
    accept_lang = request.headers.get("accept-language", "en")
    for part in accept_lang.split(","):
        lang_code = part.split(";")[0].strip()[:2].lower()
        if lang_code in SUPPORTED_LANGUAGES:
            return lang_code

    return "en"

Frontend: 5 idiomas

El frontend lanzo con 5 idiomas inicialmente:

typescript// i18n/index.ts
const FRONTEND_LANGUAGES = {
    en: () => import('./locales/en.json'),
    fr: () => import('./locales/fr.json'),
    es: () => import('./locales/es.json'),
    pt: () => import('./locales/pt.json'),
    ar: () => import('./locales/ar.json'),
};

El soporte de arabe requirio consideracion de diseno RTL (derecha a izquierda):

css/* Soporte RTL para arabe */
[dir="rtl"] .sidebar { right: 0; left: auto; }
[dir="rtl"] .content { margin-right: 250px; margin-left: 0; }
[dir="rtl"] .text-left { text-align: right; }

Sesion 065: reescritura de SDK de Node.js y Python

Los SDKs originales de la sesion 002 fueron generados rapidamente y tenian limitaciones. La sesion 065 reescribio los dos SDKs mas populares desde cero:

SDK de Node.js

typescript// @0fee/node - v2.0.0
import { ZeroFee } from '@0fee/node';

const zerofee = new ZeroFee({
    apiKey: 'sk_live_...',
    baseUrl: 'https://api.0fee.dev',  // Opcional, predeterminado a produccion
});

// Crear un pago
const payment = await zerofee.payments.create({
    amount: 10.00,
    currency: 'USD',
    reference: 'order-123',
    metadata: { orderId: '123' },
});

// Verificacion de firma de webhook
const isValid = zerofee.webhooks.verify(
    payload,
    signature,
    webhookSecret,
);

La reescritura introdujo: - Cero dependencias en tiempo de ejecucion (usa fetch nativo) - Tipos TypeScript completos para todos los objetos de solicitud/respuesta - Reintento automatico con retroceso exponencial - Verificacion de firma de webhook integrada - API basada en recursos (zerofee.payments, zerofee.transactions, etc.)

SDK de Python

python# zerofee-python v2.0.0
from zerofee import ZeroFee

client = ZeroFee(api_key="sk_live_...")

# Crear un pago
payment = client.payments.create(
    amount=10.00,
    currency="USD",
    reference="order-123",
)

# Soporte asincrono
from zerofee import AsyncZeroFee

async_client = AsyncZeroFee(api_key="sk_live_...")
payment = await async_client.payments.create(
    amount=10.00,
    currency="USD",
    reference="order-123",
)

Sesion 066: el error de moneda

Esta sesion abordo el persistente problema de visualizacion de moneda. Los datos se almacenaban en dolares (unidades mayores), pero varias rutas de codigo aun dividian por 100, asumiendo que los montos estaban en centavos (unidades menores):

python# El descubrimiento
# Base de datos: amount = 5.00 (dolares)
# Visualizacion del panel: $0.05 (dividido por 100 erroneamente)
# Total de factura: $0.05 (mismo error)
# Payload de webhook: 0.05 (mismo error)

La sesion 066 elimino todas las divisiones erroneas /100 en 8 archivos (documentado en detalle en el articulo 060). La correccion fue simple -- eliminar la division. Pero encontrar cada instancia requirio una busqueda sistematica a traves de todo el codigo.

Sesion 067: generacion de recibos PDF

La sesion final de Navidad construyo el sistema de generacion de recibos:

python# services/receipt.py
from weasyprint import HTML
from jinja2 import Template

async def generate_receipt_pdf(transaction_id: str) -> bytes:
    transaction = await get_transaction(transaction_id)
    app = await get_app(transaction.app_id)

    html_content = Template(RECEIPT_TEMPLATE).render(
        app_name=app.name,
        receipt_number=transaction.id[:12].upper(),
        formatted_amount=format_amount(transaction.source_amount, transaction.source_currency),
        status=transaction.status.title(),
        reference=transaction.reference,
        provider=transaction.provider,
        payment_method=transaction.payment_method,
        date=transaction.created_at.strftime("%B %d, %Y at %H:%M UTC"),
        transaction_id=transaction.id,
        destination_amount=transaction.destination_amount,
        formatted_destination=format_amount(
            transaction.destination_amount, transaction.destination_currency
        ) if transaction.destination_amount else None,
        exchange_rate=transaction.exchange_rate,
    )

    pdf = HTML(string=html_content).write_pdf()
    return pdf

El formato compacto A5 fue una decision deliberada. Los recibos se imprimen frecuentemente en Africa -- particularmente para transacciones de dinero movil donde los registros en papel son importantes para la contabilidad empresarial. A5 (la mitad de A4) usa menos papel mientras permanece legible.

La correccion de la clave de token de localStorage

Una correccion de error pequeña pero impactante tambien ocurrio en esta sesion. El frontend estaba almacenando el token JWT bajo diferentes claves en diferentes componentes:

typescript// Algunos componentes usaban:
localStorage.getItem("token")

// Otros componentes usaban:
localStorage.getItem("access_token")

// El modulo de autenticacion usaba:
localStorage.getItem("0fee_token")

Tres claves diferentes para el mismo token. La correccion fue estandarizar en una sola clave:

typescript// Almacenamiento de token estandarizado
const TOKEN_KEY = "0fee_access_token";

export function getToken(): string | null {
    return localStorage.getItem(TOKEN_KEY);
}

export function setToken(token: string): void {
    localStorage.setItem(TOKEN_KEY, token);
}

export function clearToken(): void {
    localStorage.removeItem(TOKEN_KEY);
}

Trabajando en Navidad desde Abiyan

La Navidad en Abiyan se celebra, pero no es el paron que es en Europa o Norteamerica. La ciudad se mueve a un ritmo mas lento, pero se mueve. Y para una startup sin financiamiento compitiendo con empresas financiadas en San Francisco y Londres, cada dia de construccion es una ventaja.

Las cuatro sesiones en el dia de Navidad produjeron infraestructura que serviria a la plataforma durante meses:

  • i18n abrio 0fee.dev a desarrolladores que no hablan ingles en 15 idiomas
  • Las reescrituras de SDK dieron a los desarrolladores una experiencia de integracion pulida y confiable
  • La correccion del error de moneda elimino toda una clase de errores de visualizacion
  • Los recibos PDF proporcionaron el rastro de papel que los comerciantes africanos necesitan para la contabilidad

No es un mal regalo de Navidad para una plataforma que tenia 15 dias de vida.


Este articulo es parte de la serie "Como construimos 0fee.dev". 0fee.dev es un orquestador de pagos que cubre mas de 53 proveedores en mas de 200 paises, construido por Juste A. GNIMAVO y Claude desde Abiyan sin ningun ingeniero humano. Sigue la serie para conocer la historia completa de construccion.

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles