Picking a language and translating a site
i18n (internationalization: 18 letters between i and n) is making a site able to speak several languages. l10n (localization) is the work of actually adding one.
Rule one: no text in components
Every visible string lives in a table, keyed by an id:
// strings/en.ts (the source language)
export const en = {
'notes.title': 'Notes',
'notes.count': '{count} notes',
};
// strings/zh.ts
export const zh = { 'notes.title': '笔记', 'notes.count': '{count} 篇笔记' };
Components ask for t('notes.title'). Then:
- Adding a language means adding a table, not touching components.
- Placeholders (
{count}) let each language put the variable where its grammar wants it. Never build sentences by concatenating fragments. - Markup inside a sentence uses tags in the string (
'Read the <link>notes</link>') that the component maps to elements, so translators can move the link too. - Shared UI libraries take their words from the app (a provider or props) instead of carrying their own language.
Choosing the language
Ask in order, first answer wins:
- An explicit choice in the URL (
?lang=zh): shareable and testable. - A cookie remembering what the visitor picked with the switch.
- The visitor's country, from a CDN header (
CF-IPCountry) or an IP lookup. Cache lookups per IP, keep the timeout short, and remember that sending IPs to a lookup service is sharing visitor data. - The browser's
Accept-Languageheader. - The default.
Country is a guess, not a language: people travel and use VPNs. Always offer a visible switch, and let it win.
Set <html lang="..."> to the chosen language, for screen readers, fonts and search engines.
Machine translation
For long content you won't translate by hand, translate on the server and cache:
- Cache key = target language + a hash of the source text. Unchanged text is never translated twice, and an edit re-translates only what changed.
- Translate the rendered HTML in HTML mode, so tags survive. Mark things that must not change (
<span translate="no">, placeholders, code). - If the service is down or unconfigured, fall back to the source language instead of failing the page.
- Free options: Azure AI Translator F0 (2 million characters a month) or a self-hosted LibreTranslate. Both are statistical/neural MT, not LLMs.
Hand-written translations stay better for short UI text; use machine translation to fill gaps and for long content.
Related: the day it was built, Afternoon on the live site.