Internationalization patterns for the Flare Stack Blog using Paraglide-JS. Use when adding new translatable strings, localizing components, or handling locale-specific date/time formatting.
This project uses Paraglide-JS (Inlang) for type-safe internationalization. All localized strings are managed in JSON files and compiled into a type-safe runtime.
Translations are located in the messages/ directory:
messages/zh.json: Chinese (Default)messages/en.json: EnglishUse descriptive, hierarchical keys to avoid collisions:
nav_*: Navigation items (e.g., nav_home, nav_posts)home_*: Homepage specific stringsposts_*: Posts list/archive specific stringspost_*: Post detail specific stringsfriend_links_*: Friend links page stringsformat_*: Shared formatting strings (dates, numbers)Always use the following import pattern to keep components consistent:
import { m } from "@/paraglide/messages";
Replace hardcoded text with function calls:
// Before
<h1>友情链接</h1>
// After
<h1>{m.friend_links_title()}</h1>
Use parameters for dynamic content and plurals. Define these in the JSON files using Inlang's message format.
JSON (en.json):
"posts_count": [
{
"declarations": [
"input count",
"local formattedCountEn = count: number"
],
"match": {
"count=1": "{formattedCountEn} post",
"count=*": "{formattedCountEn} posts"
}
}
]
TSX:
<span>{m.posts_count({ count: posts.length })}</span>
Separate Server API validation from Client UI validation:
inputValidator.(m: Messages) => z.object(...) for localized form errors. Do NOT use dynamic imports await import(...). Pass the imported m object directly in the UI component.// feature.schema.ts
import type { Messages } from "@/lib/i18n";
// 1. Static schema for API
export const SubmitSchema = z.object({ name: z.string().min(1) });
// 2. Factory schema for UI Form
export const createSubmitSchema = (m: Messages) => z.object({
name: z.string().min(1, m.validation_required())
});
Do NOT use Intl.DateTimeFormat directly in components. Define formatting logic in the translation files to ensure locale-appropriate display.
Use the format_date or format_datetime keys:
// messages/zh.json
"format_date": [
{
"declarations": [
"input date",
"local formattedDateZh = date: datetime year=numeric month=long day=numeric"
],
"match": { "date=*": "{formattedDateZh}" }
}
]
// Component
<span>{formatDate(post.publishedAt)}</span> // Uses m.format_date internally
Use the time_ago_* family of messages. The formatTimeAgo utility in src/lib/utils.ts handles the logic of selecting the right message based on the duration.
// Use the utility which internally calls m.time_ago_minutes, etc.
<span>{formatTimeAgo(post.publishedAt)}</span>
Localize page titles and meta description in the Route definition:
// src/routes/_public/posts.tsx
export const Route = createFileRoute("/_public/posts")({
loader: async ({ context }) => {
return {
title: m.posts_title(),
description: blogConfig.description, // Keep user config as-is if global
};
},
head: ({ loaderData }) => ({
meta: [
{ title: loaderData?.title },
{ name: "description", content: loaderData?.description },
],
}),
});
.json files.m with string indexing; use the generated function calls.aria-label, placeholder, and alt text.m.posts_no_posts()).After adding new keys to the JSON files, run the compiler to update the type-safe definitions:
bun paraglide-js compile --project ./project.inlang --outdir ./src/paraglide