i18n Developer Guide — Building Translated Pages

2026-05-06 · VPN.com Engineering · handoff

i18n Developer Guide

Rules and patterns for building pages and components that work across all configured languages (see src/config/languages.ts).

Golden Rule

Every visible string must go through t() or tx(). Hardcoded English in a template shows up in English on every translated page. The QA script (translation-qa.mjs) catches this at build time.


New Component Checklist

Visible text

---
import { t } from '@/i18n/translations';
interface Props { lang?: string; }
const { lang = 'en' } = Astro.props;
---
<p>{t(lang, 'ui.someKey')}</p>
  1. Add lang to Props
  2. Import t from @/i18n/translations
  3. Add all configured languages (see src/config/languages.ts) to translations.ts
  4. Use t(lang, 'key') for every visible string
---
import { getRouteMap, resolveUrl } from '@/i18n/routes';
const { routeMap, reverseMap } = await getRouteMap();
const localizeUrl = (url: string) => resolveUrl(routeMap, reverseMap, url, lang);
---
<a href={localizeUrl('/vpn/nordvpn/')}>NordVPN</a>

Never use a bare /{lang}/ prefix. Translated pages have translated slugs — /fr/vpn/nordvpn/ is wrong, the correct URL might be /fr/vpn/meilleur-vpn/. Always use resolveUrl().

Provider data

---
import { getDetails, getPlans } from '@/data/providers';
const provider = getDetails(slug, lang);
const plans = getPlans(slug, lang);
---

Pass lang to every provider data call. The data layer merges English base with language overlays automatically.

Client-side JavaScript strings

JS can't call t(). Pass strings via a JSON config element:

---
const jsStrings = JSON.stringify({
  copied: t(lang, 'ui.linkCopied'),
  error: t(lang, 'broker.somethingWrong'),
});
---
<script type="application/json" id="i18n-config" set:html={jsStrings} />
<script>
  const i18n = JSON.parse(document.getElementById('i18n-config')?.textContent || '{}');
</script>

New Hub Page

Follow the pattern of VpnPage.astro:

  1. Create src/components/pages/NewPage.astro with lang prop
  2. Add translations to translations.ts
  3. Create src/pages/newpage/index.astro (English, thin wrapper)
  4. Add src/pages/[lang]/newpage/index.astro:
---
import NewPage from '@/components/pages/NewPage.astro';
import { LANGUAGES } from '@/config/languages';
export function getStaticPaths() {
  return LANGUAGES.map(lang => ({ params: { lang }, props: { lang } }));
}
const { lang } = Astro.props;
---
<NewPage lang={lang} />

Adding Translation Keys

Add to src/i18n/translations.ts:

'section.keyName': {
  en: 'English text',
  fr: 'Texte français',
  de: 'Deutscher Text',
  es: 'Texto español',
  pt: 'Texto português',
  it: 'Testo italiano',
  nl: 'Nederlandse tekst',
  ja: '日本語テキスト',
  ar: 'النص العربي',
  da: 'Dansk tekst',
},

Convention: section.camelCaseKey — e.g., footer.topics, speedlab.getDeal.

If translations aren't ready, use the English value as a placeholder. The QA script flags sentinel strings.


lang Flow Through the Tree

Base.astro
  → Header.astro, Footer.astro, Popup.astro
  → [page content]
    → SpeedLab.astro, PostGrid.astro, ProviderMetrics.astro, etc.

If you add a component that needs lang, make sure its parent passes it.


Provider Translation Overlays

Overlays live at src/data/providers/i18n/{lang}/{slug}.json — translatable fields only. Missing fields fall back to English.

node scripts/translate-provider-json.mjs --execute              # all providers
node scripts/translate-provider-json.mjs --execute --slug nordvpn  # one provider
node scripts/translate-provider-json.mjs --execute --lang ko    # one language

To fix manually: edit the overlay JSON directly. Keys match the English base.


QA Script

node scripts/translation-qa.mjs              # all languages
node scripts/translation-qa.mjs --lang fr    # single language
node scripts/translation-qa.mjs --verbose    # show all checks

Update scripts/sentinel-strings.mjs when adding new UI strings. Add brand terms that trigger false positives to BRAND_ALLOWLIST.


Pre-Ship Checklist


File Reference

FilePurpose
src/config/languages.tsLanguage list — single source of truth
src/i18n/translations.tsUI string translations
src/i18n/routes.tsRoute map: resolveUrl, getRouteMap
src/data/providers.tsProvider data with lang-aware overlay merging
src/data/providers/i18n/{lang}/{slug}.jsonProvider overlays
src/components/LocalizedContent.astroRewrites links in rendered markdown
src/pages/[lang]/Dynamic routes for translated hub pages
scripts/translation-qa.mjsQA — checks for English leaks
scripts/sentinel-strings.mjsKnown English strings + brand allowlist
scripts/translate-provider-json.mjsBulk translate provider data
scripts/translate-md.mjsBulk translate markdown content