sahin.io

All skills and agents

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 (en from locales/en.json), lng: 'en', fallbackLng: 'en'; SUPPORTED_LANGUAGES lists 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). On languageChanged it forces dir="ltr". Imported for side effects at the top of client/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). Only en.json is imported; the others are not bundled.
  • client/src/components/LanguageSwitcher.tsx — dropdown over SUPPORTED_LANGUAGES; it is not mounted anywhere in the current app (with one language there is nothing to switch).
  • useTranslation callers: LanguageSwitcher.tsx and legacy SPA pages in client/src/pages/ (StatusPage.tsx, CoInvestors.tsx, Sectors.tsx, NewsFeed.tsx, InvestmentThesis.tsx, Timeline.tsx, GeoMap.tsx). App.tsx's HumanRouter routes only / (Home) and NotFound, so these pages are not routed; the public versions are the React pages in client/src/publicPages/.
  • The vendor-i18n chunk is defined in vite.config.cloudflare.ts.

Rules

  • New visible copy never goes in locale JSON. All words live in content/ YAML/Markdown and reach the browser through client/src/generated/copy/ (page-content-yaml). server/content-in-yaml.test.ts treats locale codes as code, not copy.
  • Do not add a language to SUPPORTED_LANGUAGES, mount LanguageSwitcher, 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_LOCALE from client/src/lib/siteCopy (English).

If a language is ever approved

  1. Decide the URL scheme with the owner (public-url-conventions); add manifest routes per locale.
  2. Move translations into per-locale YAML under content/, generated like the English copy; do not revive the old locale JSON as the source.
  3. Set <html lang>, og:locale, JSON-LD inLanguage per route; add hreflang alternates and sitemap entries.
  4. Register the language in SUPPORTED_LANGUAGES, load only its resource, mount LanguageSwitcher.
  5. 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).