Locales for major projects with SvelteKit

Translate content into multiple locales.

How this site supports multiple locales end to end: content, web routing, UI chrome, and bugs.

Read languages via file locales.tsv. It lists ISO 639-1 language codes with their endonyms; a locale code adds a region to one of them, as <language>-<region>, such as cy-gb or zh-001 (see ../locales/index.md). Never use a bare language code such as en as a locale code, directory, or URL path.

Locale code priority order: see ../locales/locales-by-priority.md, starting en-001, cy-001, zh-001, hi-001, es-001, fr-001, ar-001.

In this project

This guidance is general. In this project:

  • The full locales spec, with the status of every locale, is ../locales/index.md.
  • English documents are the monorepo's own Markdown files, at their own paths. Each other locale's documents are in locales/<code>/, with translated folder names, an index.md, a README.md symlink, and a .locale-peer-id in every document directory. scripts/locales.py places, copies, and checks them.
  • Locale codes and labels are in manual-testing-to-automatic-testing.github.io/src/lib/i18n/locale-codes.js (LOCALES, DEFAULT_LOCALE) and src/lib/i18n/locales.ts (LOCALE_META, LOCALE_LABELS, localeHref, browserLocale). The picker lists locales in LOCALES order.
  • UI chrome strings are in src/lib/i18n/chrome.ts, one typed message object per locale, so a missing key is a type error. Page strings are in each page's own messages, keyed by locale, as in testingexamples.github.io.
  • The URL carries the locale: every page is under /<code>/, including en-001, as in testingexamples.github.io. The bare home page, /, sends a visitor to the locale matching the browser's languages, unless they have chosen one with the picker.

.locale-peer.id file

.locale-peer-id file is a byte-identical 32-character hexadecimal lowercase number then newline, across every locale's version of "the same" topic, regardless of slug.

.locale-peer-id id is how the project resolves "this page, in locale X".

Guidance

  • en-us: consistent American spelling; fix any stray en-gb forms (organisation→organization, licence→license, programme→program, cancelled→canceled, analogue→analog).

  • en-gb: the -ize/-ise family (optimise, realise, organise, prioritise, utilise, etc.), -or/-our (colour, behaviour, favour, labour, neighbours), -er/-re (centre, theatre for the metaphorical sense), -ense/-ce (defence, licence), doubled-L forms (modelled, labelled, cancelled, enrol/enrolment), analogue, programme, and math→maths.

  • en-gb-oxendict: use en-gb then revert just the -ise family back to Oxford -ize spelling (optimize, realise→realize, organise→organize, etc.), while correctly keeping -yse forms (analyse/analysable) unchanged, since Oxford style never uses -yze, and keeping all other British forms (colour, centre, defence, licence, programme, maths, modelled) intact.

Guard against corruption

Keep proper nouns unconverted. Example: "Hospital Readmissions Reduction Program" (a real United States federal program name).

Verify

For each locale subdirectory:

  • File exists: index.md
  • Symlink exists: README.md
  • Locale peer id tracking file exists: .locale-peer-id

Then:

  • Fix any broken internal links
  • Fix any residual wrong-dialect spellings
  • Update ./spec/locales/index.md

Content structure (book side)

Each locale is locales/<code>/ in the book repo, containing:

  • locales/<code>/topics/<slug>/index.md + .locale-peer-id — one per topic. README.md is a symlink to index.md.
  • locales/<code>/index.md + .locale-peer-id + README.md symlink — the locale's own translated README (site home/contents page source). Every locale gets this file scaffolded (matching the topic-file pattern) even before it has a translation; it starts empty.

Slugs

Slugs are per-locale, not shared.** Translated locales rename topic directories to native-script/accented slugs.

Example: es-001 año-de-vida-ajustado-por-calidad, ur-001 صحت-ایڈجسٹڈ-متوقع-زندگی.

Nothing in the site assumes slugs match across locales.

Locale picker (labels + ordering)

  • Labels live in locales.js's LOCALE_LABELS, one entry per code, in that language (e.g. 'fr-001': 'Français (Monde)'). Falls back to the raw code via localeLabel() if a code has no label yet.
  • Header PickerBar order comes from content.js's locales() (sorted by code) — the -001 suffix happens to sort before any letter-starting regional suffix, so variants already come first there.
  • Home page's locale list (+page.server.js) sorts explicitly: default locale first, then grouped by language name (label text before the (), with the -001/World variant sorted before its regional siblings within each group, then alphabetically by label. This does NOT fall out of alphabetical-by-label sort on its own (e.g. "España" < "Mundo") — it needs the explicit -001 check.

Bug fixes (regression watch-list)

Bug: ASCII-only \w regexes broke every non-Latin/non-accented slug

Bug: matched topic slugs with [\w.-]+ (ASCII word chars only). Any locale with an accented or native-script slug (Spanish, French, Russian, Chinese, Arabic, Welsh, Hindi, Bengali, Portuguese, Indonesian, Urdu) silently failed peer-id resolution and cross-topic links.

Fix by widening the slug capture group to [^/]+.

Bug: Every locale's home/contents page showed canonical English content

Bug: code and content always read a single top-level /README.md for title, intro, "New here?" picks, part headings, and blurbs — only topic links were ever localized.

Fix: populate the previously-empty locales/<code>/index.md per locale.

Bug: link silently found nothing once the README was translated.

Fix: extract all links from the whole pre-## intro block instead of regex-matching the English sentence.

Bug: UI chrome was hardcoded English in the .svelte templates

Bug: nav labels, subtitles, page titles, intros, breadcrumbs, topic position, pagination, picker/share labels.

Fix: add i18n.js and threading ui(locale) through every locale-scoped route and +layout.svelte.

Bug: header/footer brand wordmark stayed English

Bug: wordmark came only from the root (locale-agnostic) +layout.server.js, which deliberately never picks a locale.

Fix: have locales/[locale]/+layout.server.js supply this locale's own title, which overrides the root layout's canonical one via SvelteKit's merged page.data on any route under /locales/<locale>/ — the root picker and /about/ (no locale in the URL) correctly keep the canonical English title.

View this document on GitHub spec/locales-for-global-sharing-with-svelte/index.md