Internationalization
Learn how to configure languages, add translated messages, and maintain localized content in TanStarter
TanStarter includes multilingual support powered by Paraglide JS. English is the base locale, and Chinese uses a URL prefix:
- English page:
/about - Chinese page:
/zh/about
Related Files
Internationalization mainly touches these files:
| File | Purpose |
|---|---|
project.inlang/settings.json | Configures the base locale and supported locales |
project.inlang/messages/en.json | English messages |
project.inlang/messages/zh.json | Chinese messages |
src/lib/locale.ts | Exports locale config, URL helpers, and message parsing helpers |
src/locale/middleware.ts | Connects Paraglide middleware so server requests know the current locale |
src/locale/paraglide/ | Generated Paraglide runtime code; do not edit manually |
src/components/layout/locale-switcher.tsx | Language switcher used by the navbar and menus |
Do not edit files under src/locale/paraglide/ manually. They are generated from project.inlang/messages/*.json.
Add UI Messages
Short UI messages such as buttons, form labels, error messages, email subjects, and component text live in project.inlang/messages/*.json.
1. Add Message Keys
Add the same keys to both English and Chinese files:
{
"settings_profile_title": "Profile",
"settings_profile_description": "Update your public profile information."
}{
"settings_profile_title": "ä¸Ēäēēčĩæ",
"settings_profile_description": "æ´æ°äŊ įå
Ŧåŧä¸ĒäēēčĩæäŋĄæ¯ã"
}Use module prefixes for message keys, for example:
auth_login_titlepricing_plan_pro_namedashboard_empty_state_titlemail_verify_email_subject
This keeps sorting, searching, and maintenance predictable.
2. Use Messages in Components
Read messages from the generated Paraglide m object:
import { m } from '@/locale/paraglide/messages';
export function ProfileCard() {
return (
<section>
<h2>{m.settings_profile_title()}</h2>
<p>{m.settings_profile_description()}</p>
</section>
);
}During development, pnpm dev recompiles the Paraglide runtime automatically. After the new key is generated, you can call it as m.your_key().
3. Sort and Check Keys
After adding or changing messages, run:
pnpm locale:sort
pnpm locale:checkpnpm locale:sortsorts keys in all locale JSON filespnpm locale:checkverifies key parity across locale files
If a key exists in en.json but is missing from zh.json, the check command will report it.
Add Markdown Translations
Long-form content such as blog posts, changelog entries, and legal pages is maintained as Markdown. English uses the base filename, and Chinese adds .zh before .md.
content/blog/getting-started.md
content/blog/getting-started.zh.md
content/changelog/v1.0.0.md
content/changelog/v1.0.0.zh.md
content/pages/privacy.md
content/pages/privacy.zh.mdFor example, add a Chinese version of a blog post:
---
title: My First Post
description: A short introduction to the product.
date: 2026-01-15
category: Tutorial
image: https://example.com/cover.jpg
---
English content...---
title: æįįŦŦä¸į¯æįĢ
description: ä¸į¯įŽįįäē§åäģįģã
date: 2026-01-15
category: æį¨
image: https://example.com/cover.jpg
---
䏿å
厚...TanStarter treats these files as different locale versions of the same slug:
- English:
/blog/my-first-post - Chinese:
/zh/blog/my-first-post
The .zh suffix is only used to identify the locale. It does not appear in the final URL.
Add a New Language
To add another language, such as Japanese ja, update the locale config and add a message file.
1. Update Inlang Settings
Add the new locale to project.inlang/settings.json:
{
"baseLocale": "en",
"locales": ["en", "zh", "ja"]
}2. Add a Message File
Copy an existing locale file and translate it:
project.inlang/messages/ja.jsonThe new file must contain the same keys as en.json and zh.json.
3. Update Locale List
Add the language name, flag, and hreflang in src/lib/locale.ts:
export const localeConfig = {
en: {
flag: 'đēđ¸',
name: 'English',
hreflang: 'en',
},
zh: {
flag: 'đ¨đŗ',
name: '䏿',
hreflang: 'zh-CN',
},
ja: {
flag: 'đ¯đĩ',
name: 'æĨæŦčĒ',
hreflang: 'ja',
},
} satisfies Record<Locale, LocaleConfig>;The locale switcher reads locales and localeConfig, so the new language appears automatically.
4. Add Localized Content Files
If blog posts, changelog entries, or legal pages should support the new language, create localized Markdown files with the same naming pattern:
content/blog/getting-started.ja.md
content/changelog/v1.0.0.ja.md
content/pages/privacy.ja.mdIf the new locale is not en or zh, also update the locale suffix detection in content-collections.ts.
Commands
pnpm dev # Start dev server and compile the locale runtime automatically
pnpm build # Build for production and compile the locale runtime
pnpm locale:sort # Sort message keys
pnpm locale:check # Check message key parity across locales
pnpm locale:compile # Compile the Paraglide runtime manuallyIn daily development, pnpm dev is usually enough. After adding messages, run pnpm locale:sort and pnpm locale:check to catch missing translation keys.
TanStarter Docs