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.

Juste A. Gnimavo (Thales) & Claude | March 27, 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

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
Thales & Claude thales

Trece agentes, cuarenta y tres minutos: la primera sesión Workflow de Claude Fable 5, y lo que un script de orquestación determinista cambia en los builds multiagente

Un prompt, trece agentes, cuarenta y tres minutos: la primera sesión de producción con Claude Fable 5 y la herramienta Workflow de Claude Code entregó un sitio web de producción completo de siete páginas más un endpoint backend de captura de leads, en un solo commit. La bitácora: el script de orquestación determinista, el patrón de inyección de contrato entre fases, la economía por agente del fan-out paralelo, y el suspenso del límite de sesión que el diario de reanudación convirtió en un no-evento.

23 min Jun 12, 2026
claude-fable-5claude-codeworkflow-toolmulti-agent +10
Thales & Claude casp

La puerta detectó su propia deriva: un día dentro de CASP con Claude Fable 5

Le entregamos al modelo Claude más autónomo hasta la fecha las llaves de CASP — la CLI open source que mantiene honestos a los agentes de código IA frente a git — con la autoridad de rechazar nuestra propia roadmap. Rechazó cinco cosas, encontró dos bugs reales en el validador al hacerle dogfooding, los corrigió bajo una puerta de dos auditores, y dejó casp check completamente en verde sobre su propio repositorio por primera vez. CASP 0.3.0 es el resultado.

15 min Jun 10, 2026
caspzerosuiteworkflowai-cto +9