Back to 0fee
0fee

Christmas Day Debugging: i18n, SDKs, and Currency Bugs

Sessions 064-067 on Christmas Day 2025: building i18n, rewriting SDKs, and fixing currency bugs from Abidjan. By Juste A. Gnimavo.

Thales & Claude | March 25, 2026 7 min 0fee
christmasdebuggingi18nsdkcurrency

December 25, 2025. Abidjan, Ivory Coast. While the rest of the world opened presents, Thales opened his laptop and started Session 064. By the end of the day, 0fee.dev had internationalization infrastructure supporting 15 backend languages and 5 frontend languages, completely rewritten Node.js and Python SDKs, a fixed currency system, PDF receipt generation, and a repaired localStorage token key.

Four sessions. Christmas Day. Working from Abidjan. This is what building a startup with an AI CTO looks like when there are no holidays and no weekends.

Session 064: i18n Infrastructure

The internationalization session was the largest of the four. 0fee.dev needed to serve developers worldwide, and English-only was a limitation:

Backend: 15 Languages

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",
}

Swahili and Hausa were included from the start -- deliberate choices for an Africa-first platform. Swahili covers East Africa (Kenya, Tanzania, Uganda). Hausa covers West Africa (Nigeria, Niger, Ghana).

The backend i18n system was built as a translation dictionary with fallback:

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:
        """Translate a key to the specified language."""
        translations = self._translations.get(lang, {})
        text = translations.get(key)

        if not text:
            # Fallback to English
            text = self._translations.get("en", {}).get(key, key)

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

        return text

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

Example translation files:

json// i18n/translations/en.json
{
    "payment.pending": "Payment is being processed",
    "payment.completed": "Payment completed successfully",
    "payment.failed": "Payment failed: {reason}",
    "error.rate_limited": "Too many requests. Please try again in {seconds} seconds.",
    "invoice.subject": "Invoice #{number} from 0fee.dev",
    "email.welcome": "Welcome to 0fee.dev, {name}!"
}
json// i18n/translations/fr.json
{
    "payment.pending": "Le paiement est en cours de traitement",
    "payment.completed": "Paiement effectue avec succes",
    "payment.failed": "Le paiement a echoue : {reason}",
    "error.rate_limited": "Trop de requetes. Veuillez reessayer dans {seconds} secondes.",
    "invoice.subject": "Facture n{number} de 0fee.dev",
    "email.welcome": "Bienvenue sur 0fee.dev, {name} !"
}

The language is determined from the Accept-Language header, with a query parameter override:

python# middleware/i18n.py
async def get_language(request: Request) -> str:
    # Query parameter takes priority
    lang = request.query_params.get("lang")
    if lang and lang in SUPPORTED_LANGUAGES:
        return lang

    # Parse Accept-Language header
    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 Languages

The frontend launched with 5 languages initially:

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'),
};

Arabic support required RTL (right-to-left) layout consideration:

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

Session 065: Node.js and Python SDK Rewrite

The original SDKs from Session 002 were generated rapidly and had limitations. Session 065 rewrote the two most popular SDKs from scratch:

Node.js SDK

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

const zerofee = new ZeroFee({
    apiKey: 'sk_live_...',
    baseUrl: 'https://api.0fee.dev',  // Optional, defaults to production
});

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

// List transactions with filters
const transactions = await zerofee.transactions.list({
    status: 'completed',
    page: 1,
    perPage: 50,
});

// Webhook signature verification
const isValid = zerofee.webhooks.verify(
    payload,
    signature,
    webhookSecret,
);

The rewrite introduced: - Zero runtime dependencies (uses native fetch) - Full TypeScript types for all request/response objects - Automatic retry with exponential backoff - Webhook signature verification built-in - Resource-based API (zerofee.payments, zerofee.transactions, etc.)

Python SDK

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

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

# Create a payment
payment = client.payments.create(
    amount=10.00,
    currency="USD",
    reference="order-123",
)

# Async support
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",
)

The Python SDK rewrite added: - Both sync (httpx) and async (httpx.AsyncClient) support - Pydantic models for type validation - Automatic pagination helpers - Webhook verification - Zero dependencies beyond httpx

Session 066: The Currency Bug

This session addressed the persistent currency display issue. The data was stored in dollars (major units), but several code paths still divided by 100, assuming amounts were in cents (minor units):

python# The discovery
# Database: amount = 5.00 (dollars)
# Dashboard display: $0.05 (divided by 100 erroneously)
# Invoice total: $0.05 (same bug)
# Webhook payload: 0.05 (same bug)

Session 066 removed all erroneous /100 divisions across 8 files (documented in detail in article 060). The fix was simple -- delete the division. But finding every instance required a systematic search through the entire codebase.

Session 067: PDF Receipt Generation

The final Christmas Day session built the receipt generation system:

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

RECEIPT_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
    <style>
        @page { size: A5; margin: 15mm; }
        body { font-family: 'Inter', sans-serif; font-size: 10pt; }
        .header { display: flex; justify-content: space-between; }
        .amount { font-size: 24pt; font-weight: bold; text-align: center; }
        .details-table { width: 100%; border-collapse: collapse; }
        .details-table td { padding: 4pt 0; border-bottom: 0.5pt solid #eee; }
        .footer { text-align: center; color: #666; font-size: 8pt; }
    </style>
</head>
<body>
    <div class="header">
        <div><strong>{{ app_name }}</strong></div>
        <div>Receipt #{{ receipt_number }}</div>
    </div>

    <div class="amount">
        {{ formatted_amount }}
    </div>

    <table class="details-table">
        <tr><td>Status</td><td>{{ status }}</td></tr>
        <tr><td>Reference</td><td>{{ reference }}</td></tr>
        <tr><td>Provider</td><td>{{ provider }}</td></tr>
        <tr><td>Method</td><td>{{ payment_method }}</td></tr>
        <tr><td>Date</td><td>{{ date }}</td></tr>
        {% if destination_amount %}
        <tr><td>Converted</td><td>{{ formatted_destination }}</td></tr>
        <tr><td>Exchange Rate</td><td>{{ exchange_rate }}</td></tr>
        {% endif %}
    </table>

    <div class="footer">
        <p>Powered by 0fee.dev</p>
        <p>Transaction ID: {{ transaction_id }}</p>
    </div>
</body>
</html>
"""

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

The compact A5 layout was a deliberate choice. Receipts are frequently printed in Africa -- particularly for mobile money transactions where paper records are important for business accounting. A5 (half of A4) uses less paper while remaining readable.

The localStorage Token Key Fix

A small but impactful bug fix also happened in this session. The frontend was storing the JWT token under different keys in different components:

typescript// Some components used:
localStorage.getItem("token")

// Other components used:
localStorage.getItem("access_token")

// The auth module used:
localStorage.getItem("0fee_token")

Three different keys for the same token. Depending on which component the user interacted with first, the token would be stored under one key and not found by components looking under a different key. The fix was standardizing on a single key:

typescript// Standardized token storage
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);
}

Working on Christmas Day From Abidjan

Christmas in Abidjan is celebrated, but it is not the shutdown that it is in Europe or North America. The city moves at a slower pace, but it moves. And for a bootstrapped startup competing with funded companies in San Francisco and London, every day of building is an advantage.

The four sessions on Christmas Day produced infrastructure that would serve the platform for months:

  • i18n opened 0fee.dev to non-English-speaking developers across 15 languages
  • The SDK rewrites gave developers a polished, reliable integration experience
  • The currency bug fix eliminated an entire class of display errors
  • PDF receipts provided the paper trail that African merchants need for accounting

Not a bad Christmas present for a platform that was 15 days old.


This article is part of the "How We Built 0fee.dev" series. 0fee.dev is a payment orchestrator covering 53+ providers across 200+ countries, built by Juste A. GNIMAVO and Claude from Abidjan with zero human engineers. Follow the series for the complete build story.

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles