Page:
Internationalization
No results
1
Internationalization
Ella Mizuki edited this page 2026-07-03 04:33:23 +00:00
Table of contents
Internationalization
beta-website uses a code-first, TypeScript-native i18n system — no JSON files, no external translation services at runtime. All UI strings live in typed dictionary objects per locale.
Overview
| Aspect | Implementation |
|---|---|
| Strategy | Build-time dictionary lookup |
| Source of Truth | src/i18n/locales/{locale}.ts (6 files) |
| Type Safety | Dictionary type in src/i18n/types.ts |
| Runtime Cost | Zero — resolved at build/SSG time |
| Locale Detection | URL prefix (/pt-br/, /ja/, etc.) |
| Default Locale | en (no prefix: /blog/) |
| Fallback | EN content shown with isFallback: true flag |
Current Locale Support
| Code | Language | Native Name | Status | Direction |
|---|---|---|---|---|
en |
English | English | ✅ Canonical | LTR |
pt-br |
Portuguese (Brazil) | Português | ✅ Complete | LTR |
ja |
Japanese | 日本語 | ✅ Complete | LTR |
zh-cn |
Chinese (Simplified) | 中文 | ✅ Complete | LTR |
de |
German | Deutsch | ✅ Complete | LTR |
fr |
French | Français | ✅ Complete | LTR |
Dictionary Structure
File: src/i18n/types.ts — single source of truth for all UI strings.
export type Dictionary = {
meta: { siteDescription: string };
navigation: Record<Section, string>; // blog, snippets, projects, about, contact
common: {
skipToContent: string;
language: string;
openLanguageMenu: string;
latest: string;
featured: string;
viewAll: string;
readMore: string;
email: string;
github: string;
rss: string;
website: string;
externalLink: string;
published: string;
updated: string;
availableInEnglish: string;
backToBlog: string;
backToSnippets: string;
backToProjects: string;
tableOfContents: string;
onThisNote: string;
copyCode: string;
copied: string;
footerNote: string;
minuteRead: (minutes: number) => string; // function for pluralization
};
projectStatuses: Record<ProjectStatus, string>;
home: {
eyebrow: string;
title: string;
titleHighlight: string;
description: string;
statement: string;
statementHighlight: string;
statementDetail: string;
exploreTitle: string;
exploreDescription: string;
};
pages: {
blog: BlogPageDictionary;
snippets: SnippetsPageDictionary;
projects: ProjectsPageDictionary;
about: AboutPageDictionary;
contact: ContactPageDictionary;
};
};
Key Design Decisions
- Functions for pluralization —
minuteRead(minutes)handles locale-specific rules - Nested page dictionaries — co-locates strings with their page
- No flat keys —
t('common.copyCode')not needed; direct object access - Discriminated unions —
Section,ProjectStatusensure exhaustiveness
Routing & URL Structure
File: src/lib/routes.ts
| Page | EN (default) | Other Locales |
|---|---|---|
| Home | / |
/pt-br/, /ja/, /zh-cn/, /de/, /fr/ |
| Blog index | /blog/ |
/pt-br/blog/, /ja/blog/, ... |
| Blog post | /blog/my-post/ |
/pt-br/blog/my-post/, ... |
| Snippets | /snippets/ |
/pt-br/snippets/, ... |
| Projects | /projects/ |
/pt-br/projects/, ... |
| About | /about/ |
/pt-br/about/, ... |
| Contact | /contact/ |
/pt-br/contact/, ... |
Astro i18n Config
File: astro.config.ts
i18n: {
locales: ["en", "pt-br", "ja", "zh-cn", "de", "fr"],
defaultLocale: "en",
routing: {
prefixDefaultLocale: false, // EN has no prefix
},
}
Content Fallback Logic
File: src/lib/content.ts — getLocalizedListing()
- Group entries by
slug(fromslug/localeID format) - For each slug: prefer
localeentry, fallback toen - Return
{ entry, isFallback, entryLocale, slug }[]sorted by date desc - Drafts hidden in production (
import.meta.env.PROD)
Behavior
| Scenario | Result |
|---|---|
| Locale file exists | Shows localized content, isFallback: false |
| Locale missing, EN exists | Shows EN content, isFallback: true |
| Neither exists | Entry excluded from listing |
UI Indicators
- LanguageSwitcher — unavailable locales shown disabled (
aria-disabled) - Content pages —
availableInEnglishstring shown whenisFallback - hreflang — only generated for existing locales (via sitemap config)
Translation Workflow
1. UI Strings (Dictionary)
Location: src/i18n/locales/{locale}.ts
Process:
- Edit
src/i18n/locales/en.ts(canonical) - Copy changes to all 5 locale files
- Translate each string
- Run
pnpm build— TypeScript verifies completeness
2. Content (MDX)
Location: src/content/{collection}/{slug}/{locale}.mdx
Process:
- Create entry folder:
src/content/blog/my-post/ - Create
en.mdxwith full frontmatter - Create locale variants:
pt-br.mdx,ja.mdx, etc. - Translate frontmatter + body
- Set
canonicalSourceLocale: enon translations
Automated Translation Adapter
pnpm content:translate reports missing translations when no adapter is configured. To generate files, configure an external local command:
CONTENT_TRANSLATION_COMMAND=/path/to/translator \
CONTENT_TRANSLATION_ARGS='["--format","mdx"]' \
pnpm content:translate
The command receives one JSON object on stdin:
{
"collection": "blog",
"slug": "example",
"sourceLocale": "en",
"targetLocale": "ja",
"sourcePath": "/absolute/path/en.mdx",
"sourceContent": "---\n..."
}
It must write a complete MDX document to stdout. Existing translations are never overwritten.
Adding a New Locale
- Add to
localesarray insrc/i18n/config.ts - Add
localeMetadataentry - Create
src/i18n/locales/{locale}.tssatisfyingDictionary - Add to
dictionariesinsrc/i18n/index.ts - Update
astro.config.tsi18n config - Update sitemap i18n config
- Run
pnpm content:new:post/pnpm content:new:snippetto scaffold content - Run
pnpm build— TypeScript catches missing keys