Frontend i18n Pattern
This skill outlines the standard pattern for implementing internationalization in Eridu frontend applications. We use Paraglide JS for type-safe, lightweight i18n.
Architecture
- •Tooling:
@inlang/paraglide-js(v2+) - •Configuration: Defined in
project.inlangat the app root. - •Message Storage: JSON files in
src/i18n/messages/{lang}.json. - •Output: Generated code in
src/paraglide(typically gitignored). - •Shared Library:
@eridu/i18n(workspace package) for common translations and locale definitions.
Workflow: Adding Translations
- •Locate the Message File: Open
src/i18n/messages/en.json(English is the source language). - •Add Your Key: Add a new key-value pair. You can nest keys for better organization.
json
{ "dashboard": { "title": "My Dashboard", "welcomeUser": "Welcome back, {name}!" } } - •Automatic Compilation: If the dev server (
pnpm dev) is running, Paraglide will automatically detect changes and regeneratesrc/paraglide. - •Use in Component: Import the messages and use them.
Usage in Components
1. App-Specific Messages
Import from the generated local alias (usually configured as @/paraglide/messages).
tsx
import * as m from '@/paraglide/messages';
export function Dashboard({ userName }) {
// Simple message
const title = m['dashboard.title']();
// Message with parameters
const welcome = m['dashboard.welcomeUser']({ name: userName });
return (
<div>
<h1>{title}</h1>
<p>{welcome}</p>
</div>
);
}
Note: Because we use nested keys in JSON (e.g.
dashboard.title), Paraglide exports them with the exact path as the name. Since these contain dots, you must use bracket notation:m['dashboard.title']().
2. Shared Messages
For common terms (Save, Cancel, Table headers, etc.), use the shared @eridu/i18n package to ensure consistency.
tsx
import * as sharedM from '@eridu/i18n';
// Usage
<Button>{sharedM['common.save']()}</Button>
<Button>{sharedM['common.cancel']()}</Button>
3. Locale Management
Use the LocaleEnum and helper functions from @eridu/i18n when dealing with locale logic (e.g., language switchers).
tsx
import { LocaleEnum, LOCALE_LABELS } from '@eridu/i18n';
// Access available locales and their labels
{Object.entries(LOCALE_LABELS).map(([code, label]) => (
<option key={code} value={code}>{label}</option>
))}
Best Practices
- •Grouping: Group messages by feature or page (e.g.,
admin,auth,settings). - •Naming: Use
camelCasefor keys (e.g.,welcomeUsernotwelcome-user). - •Parameters: Use named parameters
{param}instead of positional arguments. - •No Hardcoding: Never hardcode user-facing strings in components. Always extract them to
en.json. - •Review Shared: Before adding a generic term like "Submit" or "Error", check
packages/i18n/messages/en.jsonto see if it already exists in the shared library.
Troubleshooting
- •"Property '...' does not exist on type...": Run
pnpm devorpnpm buildto force regeneration of the Paraglide files. - •Missing Translations: Ensure you've added the key to
en.json. Other languages (e.g.,zh-TW) can be filled in later or via translation tools.