i18n-locales
The language setup of sahin.io/angel-investments — English-only public pages, the i18next client registry in client/src/i18n, the dormant locale JSON files and LanguageSwitcher, and why visible copy lives in content/ YAML rather than locale files. Use before adding a language, translating anything, touching client/src/i18n or LanguageSwitcher, or when someone asks why the site is English only.
- Source file
.claude/skills/i18n-locales/SKILL.md
i18n and locales
What it is today
The public site is English only. Every public page is an English canonical URL; there are
no /{lang}/ route families in the manifest (server/seoRouteManifest.ts) and no hreflang tags.
The page language comes from data/entity-facts.json site.inLanguage ("en"): it sets
<html lang> in renderDocument(), og:locale (en_US) in renderHeadMeta() and
inLanguage in every JSON-LD page entity (server/publicSchema.ts).
Files
client/src/i18n/index.ts— initialises i18next + react-i18next with one resource (enfromlocales/en.json),lng: 'en',fallbackLng: 'en';SUPPORTED_LANGUAGESlists only English (the comment explains why: keeping the registry to one locale prevents legacy translated content from being bundled or exposed through retired URL families). OnlanguageChangedit forcesdir="ltr". Imported for side effects at the top ofclient/src/main.tsx.client/src/i18n/locales/*.json— 20 files (ar, bn, de, en, es, fr, hi, id, it, ja, ko, nl, pl, pt, ru, th, tr, uk, vi, zh). Onlyen.jsonis imported; the others are not bundled.client/src/components/LanguageSwitcher.tsx— dropdown overSUPPORTED_LANGUAGES; it is not mounted anywhere in the current app (with one language there is nothing to switch).useTranslationcallers:LanguageSwitcher.tsxand legacy SPA pages inclient/src/pages/(StatusPage.tsx,CoInvestors.tsx,Sectors.tsx,NewsFeed.tsx,InvestmentThesis.tsx,Timeline.tsx,GeoMap.tsx).App.tsx'sHumanRouterroutes only/(Home) and NotFound, so these pages are not routed; the public versions are the React pages inclient/src/publicPages/.- The
vendor-i18nchunk is defined invite.config.cloudflare.ts.
Rules
- New visible copy never goes in locale JSON. All words live in
content/YAML/Markdown and reach the browser throughclient/src/generated/copy/(page-content-yaml).server/content-in-yaml.test.tstreats locale codes as code, not copy. - Do not add a language to
SUPPORTED_LANGUAGES, mountLanguageSwitcher, or add/{lang}/routes without an explicit owner decision: that is a new URL family (public-url-conventions), needs canonical/hreflang/sitemap work (seo-head-and-schema), and every translated string must pass the public content policy (private-data-boundary). - Number formatting for pages uses
NUMBER_LOCALEfromclient/src/lib/siteCopy(English).
If a language is ever approved
- Decide the URL scheme with the owner (
public-url-conventions); add manifest routes per locale. - Move translations into per-locale YAML under
content/, generated like the English copy; do not revive the old locale JSON as the source. - Set
<html lang>,og:locale, JSON-LDinLanguageper route; addhreflangalternates and sitemap entries. - Register the language in
SUPPORTED_LANGUAGES, load only its resource, mountLanguageSwitcher. deploy-gate→ui-before-after→ship-to-production.
Rebuild from scratch
Recreate client/src/i18n/index.ts with the single English resource and import it in main.tsx;
keep en.json for the few legacy keys; leave all page copy in content/. Then run the gate.
Tests
server/content-in-yaml.test.ts, server/public-schema.test.ts (inLanguage),
server/public-site.test.ts (document lang), server/legacy-redirects.test.ts (retired URL families).