Back to sh0
sh0

i18n from Day One: 5 Languages Across 105 Sessions

Why we built sh0 with 5-language support from the very first dashboard session, and how we maintained correct orthography across 105 development sessions.

Thales & Claude | March 25, 2026 11 min sh0
i18ninternationalizationsveltelocalizationafricalanguages

Most startups add internationalisation as an afterthought. The dashboard launches in English. Six months later, a customer in Paris asks for French. Someone wraps every string in a t() function, extracts hundreds of keys into a JSON file, and spends two weeks hunting down the strings they missed. Template literals embedded in component logic. Error messages hardcoded in API responses. Tooltips that were "just a quick string" and never got extracted.

We did it the other way. On March 12, 2026 -- Phase 12, the very first dashboard session -- every string in sh0's UI was wrapped in a t() function, backed by five locale files. English, French, Spanish, Portuguese, and Kiswahili. Not because we had users in all five languages on day one. Because we knew we would, and the cost of adding i18n from the start is a fraction of the cost of retrofitting it later.

Why These Five Languages

The language selection was not arbitrary. It maps directly to sh0's target markets:

English is the global default. Every developer tool needs English. It is the language of the documentation, the API, the CLI, and the primary user interface.

French is the primary language of West Africa -- Senegal, C\u00f4te d'Ivoire, Cameroon, the Democratic Republic of Congo, and a dozen more countries. sh0 is built from Abidjan. Our first users are French-speaking developers. Not offering French would be absurd.

Spanish covers Latin America, a massive market for affordable infrastructure tools. Developers in Mexico, Colombia, Argentina, and Chile are price-sensitive and underserved by the big cloud platforms. A self-hosted PaaS in their language is immediately more accessible.

Portuguese covers two distinct regions: lusophone Africa (Mozambique, Angola, Guinea-Bissau, Cape Verde, S\u00e3o Tom\u00e9) and Brazil, the largest tech economy in South America. Portuguese-speaking developers are often forced to use English-only tools. We decided that should not be necessary.

Kiswahili covers East Africa -- Kenya, Tanzania, Uganda, and parts of the Democratic Republic of Congo. It is the most widely spoken African language by total speakers (over 100 million). Including Kiswahili is a statement about who we are building for: not just the francophone markets we know best, but the continent.

These five languages cover approximately 2.5 billion people's primary or secondary language. They are not the five most-spoken languages in the world (that list would include Mandarin, Hindi, and Arabic). They are the five languages most relevant to our users.

The Implementation

The i18n system is deliberately simple. No heavy libraries. No ICU message format. No pluralisation rules (yet). Just a t() function, a locale store, and five TypeScript objects.

The Locale Store

// stores/locale.ts
import { writable } from 'svelte/store';

const stored = typeof localStorage !== 'undefined' ? localStorage.getItem('sh0-locale') : null;

export const locale = writable(stored || 'en');

locale.subscribe((value) => { if (typeof localStorage !== 'undefined') { localStorage.setItem('sh0-locale', value); } }); ```

The locale persists in localStorage. On first visit, it defaults to English. The user can switch via a language selector in the settings page. The selection survives page reloads and browser restarts.

The Translation Files

Each locale is a TypeScript file exporting a nested object:

// i18n/en.ts
export const en = {
  nav: {
    home: 'Home',
    stacks: 'Stacks',
    deploy: 'Deploy',
    backups: 'Backups',
    monitoring: 'Monitoring',
    settings: 'Settings',
  },
  dashboard: {
    title: 'Dashboard',
    totalApps: 'Total Apps',
    totalDatabases: 'Total Databases',
    activeDeployments: 'Active Deployments',
    systemStatus: 'System Status',
  },
  deploy: {
    title: 'Deploy Hub',
    searchPlaceholder: 'Search 183 deploy options...',
    selectStack: 'Select a stack',
    chooseStack: 'Choose a stack...',
    createNewStack: 'Create new stack',
    // ... 42 keys total
  },
  // ... 15+ sections
};
// i18n/fr.ts
export const fr = {
  nav: {
    home: 'Accueil',
    stacks: 'Stacks',
    deploy: 'D\u00e9ployer',
    backups: 'Sauvegardes',
    monitoring: 'Surveillance',
    settings: 'Param\u00e8tres',
  },
  dashboard: {
    title: 'Tableau de bord',
    totalApps: 'Applications totales',
    totalDatabases: 'Bases de donn\u00e9es totales',
    activeDeployments: 'D\u00e9ploiements actifs',
    systemStatus: '\u00c9tat du syst\u00e8me',
  },
  deploy: {
    title: 'Hub de d\u00e9ploiement',
    searchPlaceholder: 'Rechercher parmi 183 options de d\u00e9ploiement...',
    selectStack: 'S\u00e9lectionner un stack',
    chooseStack: 'Choisir un stack...',
    createNewStack: 'Cr\u00e9er un nouveau stack',
    // ...
  },
};

Every diacritic matters. "Parametres" without its grave accent is a spelling error. "Donnees" without its acute accent is a spelling error. In the actual source files, every French string carries its full orthography -- D\u00e9ployer, Param\u00e8tres, donn\u00e9es, D\u00e9ploiements, \u00c9tat, syst\u00e8me, d\u00e9ploiement, S\u00e9lectionner, Cr\u00e9er -- each with the correct acute, grave, or circumflex accent. The session logs confirm this discipline at every phase: "French: correct diacritics."

For a tool used by developers in Abidjan, Dakar, and Douala, misspelled French is not a cosmetic issue -- it signals carelessness. The same standard applies to Spanish (aplicaci\u00f3n, compilaci\u00f3n, categor\u00eda, \u00e9xito -- each with its accent) and Portuguese (reposit\u00f3rio, in\u00edcio, vari\u00e1veis, conte\u00fado, servi\u00e7o, implanta\u00e7\u00e3o -- each with its correct accent or cedilla).

The t() Function

// i18n/index.ts
import { get } from 'svelte/store';
import { locale } from '../stores/locale';
import { en } from './en';
import { fr } from './fr';
import { es } from './es';
import { pt } from './pt';
import { sw } from './sw';

const translations: Record = { en, fr, es, pt, sw };

export function t(key: string): string { const lang = get(locale); const keys = key.split('.'); let value: any = translations[lang] || translations.en;

for (const k of keys) { value = value?.[k]; }

if (typeof value === 'string') return value;

// Fallback to English value = translations.en; for (const k of keys) { value = value?.[k]; }

return typeof value === 'string' ? value : key; } ```

The t() function resolves a dotted key path (e.g., 'deploy.selectStack') against the current locale. If the key is missing in the active language, it falls back to English. If it is missing in English too, it returns the key itself -- which makes untranslated strings immediately visible in the UI as deploy.selectStack rather than silently disappearing.

This fallback chain is important for maintainability. When we add a new feature, we always write the English translations first. The other four languages can lag by a session or two without breaking the UI -- untranslated keys simply appear in English until the translations are added.

The Discipline of Maintenance

The harder part of i18n is not the initial setup. It is maintaining five locale files across 105 development sessions. Every new component, every new page, every new modal means new translation keys in all five files.

We maintained discipline through a simple rule: every pull request that adds UI strings must update all five locale files. No exceptions. The English file is the source of truth. The other four are updated in the same session, often by the same agent that wrote the component.

The session logs tell the story:

  • Phase 12: Initial scaffold. 7 sections across all 5 languages.
  • Phase 13: +9 sections (dashboard, apps, deployments, domains, logs, env, status, settings, tabs).
  • Phase 14: +4 sections (databases, backups, monitoring, server).
  • Stack redesign: +65 keys (nav, stacks, welcome, how_it_works, stack_sections).
  • Deploy Hub: +42 keys (deploy section + nav.deploy).
  • Terminal + Storage: +22 keys (terminal, storage, tabs).
  • File Explorer: +26 keys (files section + tabs.files).

By the time the dashboard reached feature completeness, each locale file contained hundreds of keys organised into roughly 20 sections. The total translation effort, spread across all sessions, was perhaps 3-4 hours of work. The cost of doing this retroactively would have been 3-4 days -- plus the risk of missing strings in obscure UI states.

The Cost of Adding i18n Later

We have seen the alternative. On Deblo.ai -- another ZeroSuite product -- the initial frontend was built without i18n. When the decision was made to support French alongside English, every component had to be audited. String literals buried in ternary operators. Error messages concatenated from variables. Placeholder text that was "temporary" for three months.

The retrofit took a full week and still missed edge cases that surfaced months later: a tooltip here, a confirmation dialog there, an error message that only appears when a specific API call fails.

The cost breakdown is asymmetric:

ApproachInitial costOngoing cost per featureRetrofit cost
i18n from day one~2 hours~30 seconds per string0
i18n as afterthought003-5 days + ongoing misses

The "i18n from day one" approach has a higher initial cost (writing the t() function, setting up locale files, wrapping strings). But the ongoing cost is negligible -- typing t('deploy.title') instead of 'Deploy Hub' adds maybe five seconds per string. The retrofit approach has zero cost until the day it has massive cost, plus an ongoing tail of missed strings.

Sample Translations

To illustrate the care that goes into each language, here is the same section across all five locales:

KeyEnglishFrenchSpanishPortugueseKiswahili
nav.homeHomeAccueilInicioIn\u00edcioNyumbani
nav.deployDeployD\u00e9ployerDesplegarImplantarSambaza
nav.backupsBackupsSauvegardesCopias de seguridadBackupsNakala rudufu
nav.monitoringMonitoringSurveillanceMonitoreoMonitoramentoUfuatiliaji
nav.settingsSettingsParam\u00e8tresConfiguraci\u00f3nConfigura\u00e7\u00f5esMipangilio
dashboard.systemStatusSystem Status\u00c9tat du syst\u00e8meEstado del sistemaEstado do sistemaHali ya mfumo

Each translation is not just a word-for-word substitution. "Backups" in Portuguese stays as "Backups" because that is the term Portuguese-speaking developers actually use. "Sauvegardes" in French is preferred over the anglicism "backups" because francophone developers in West Africa use the French term. "Nakala rudufu" in Kiswahili literally means "duplicate copies" -- the most natural phrasing for the concept.

What We Did Not Build (Yet)

The current i18n system is deliberately minimal. Several features are on the roadmap but were not needed for launch:

Pluralisation. English plurals are simple ("1 app" / "2 apps"), but French, Spanish, and Portuguese have gendered nouns and agreement rules. Kiswahili has noun classes that affect pluralisation entirely differently. For now, we use constructions that avoid pluralisation issues: "Total Apps: 5" instead of "You have 5 app(s)."

Date and number formatting. Dates are currently displayed in ISO format or relative ("2 hours ago"). Locale-specific formatting (DD/MM/YYYY for French, MM/DD/YYYY for English) is not yet implemented.

RTL support. None of our five launch languages are right-to-left. If we add Arabic (a natural choice for North Africa), the layout will need RTL support.

Server-side translations. API error messages are currently in English. Translating them would require passing the user's locale preference to the backend, which adds complexity to every error response.

These are real gaps, but they are gaps we can fill incrementally. The foundation -- the t() function, the locale store, the five translation files, and the discipline of maintaining them -- is in place.

The Cultural Statement

There is a reason we included Kiswahili when it would have been easier to stop at four languages. sh0 is built from Abidjan by a team that believes African developers deserve tools in their languages. Not as a translation afterthought. Not as a community contribution six months after launch. From day one.

Most developer tools are English-only or English-plus-a-few-European-languages. The assumption is that developers read English, so why bother? The assumption is wrong. A developer in Dar es Salaam might read English documentation, but they think faster in Kiswahili. A developer in Douala writes code in English but discusses architecture in French. Language is not just comprehension -- it is comfort, speed, and belonging.

By shipping five languages from the first dashboard build, sh0 makes a statement: this tool is for you. Not eventually. Now.

That statement cost us roughly three to four hours of translation work spread across 105 sessions. It is, by any measure, the highest-return investment we made in the entire product.

---

This concludes the dashboard series. Next: Building a CLI That Feels Like Home -- how we built the sh0 CLI with deploy, logs, env, and SSH commands that make the terminal a first-class citizen alongside the dashboard.

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles