Rules and patterns for building pages and components that work across all configured languages (see src/config/languages.ts).
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.
---
import { t } from '@/i18n/translations';
interface Props { lang?: string; }
const { lang = 'en' } = Astro.props;
---
<p>{t(lang, 'ui.someKey')}</p>
lang to Propst from @/i18n/translationssrc/config/languages.ts) to translations.tst(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().
---
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.
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>
Follow the pattern of VpnPage.astro:
src/components/pages/NewPage.astro with lang proptranslations.tssrc/pages/newpage/index.astro (English, thin wrapper)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} />
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 TreeBase.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.
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.
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.
lang prop on componentt(lang, 'key')resolveUrl()langtranslation-qa.mjs passes after build| File | Purpose |
|---|---|
src/config/languages.ts | Language list — single source of truth |
src/i18n/translations.ts | UI string translations |
src/i18n/routes.ts | Route map: resolveUrl, getRouteMap |
src/data/providers.ts | Provider data with lang-aware overlay merging |
src/data/providers/i18n/{lang}/{slug}.json | Provider overlays |
src/components/LocalizedContent.astro | Rewrites links in rendered markdown |
src/pages/[lang]/ | Dynamic routes for translated hub pages |
scripts/translation-qa.mjs | QA — checks for English leaks |
scripts/sentinel-strings.mjs | Known English strings + brand allowlist |
scripts/translate-provider-json.mjs | Bulk translate provider data |
scripts/translate-md.mjs | Bulk translate markdown content |