Add a language to your website

Add Japanese language configuration, UI resources, documentation content, and release checks.

This article adds Japanese to the website. The goal goes beyond adding an option to the language menu: the UI, links, business text, and content you plan to publish all need to work together.

The current template enables English, Simplified Chinese, and Traditional Chinese. Check your project's config/i18n.ts for its actual configuration. This example uses ja as the new language code, consistently across filenames, configuration, and content directories.

1. Distinguish UI resources from content

ContentLocationExamples
Available languages and default languageconfig/i18n.tsLanguage codes, names, and text direction
UI textlocales/<locale>.jsonNavigation, forms, messages, and email template text
Documentationcontent/docs/<locale>/Tutorial bodies and sidebar titles
Blogcontent/blog/<locale>/Post titles, summaries, and bodies

Adding a language to the configuration does not automatically translate JSON or generate documentation and blog posts for it. Decide which content to publish in this release before preparing the resources.

2. Complete the Japanese resources

Check whether locales/ja.json exists. An existing file does not mean the translation is complete. First check whether it contains only an empty object or partial placeholder content.

Complete the Japanese file using the key structure in locales/en.json as the reference. Preserve object nesting, key names, array structures, and interpolation variables. Change only the displayed text.

If you followed the previous tutorials, also merge the following workspace text into the Japanese file:

"pages": {
  "workspace": {
    "title": "マイワークスペース",
    "description": "保存したリンクをここで管理できます。"
  }
}

This is only a snippet to merge. The complete Japanese file should also include keys for the other existing pages. Translate the recipes.savedLinks entries added in the paid entitlements tutorial as well, rather than checking only text included in the original template.

Pay particular attention to the following when translating:

  • Keep interpolation names unchanged, such as {count} and {name}. Do not translate the names inside the braces.
  • Preserve tags, links, and variables in HTML or rich text templates.
  • The | between plural branches is message syntax. Escape a literal vertical bar as {'|'}, following the existing convention.
  • Keep the JSON valid. Do not add comments or trailing commas.

The existing server and client loaders discover resources through locales/*.json. You do not need to add a Japanese branch to the loaders manually.

3. Enable the language

Enable or add the following entry in websiteI18nDefinitions.languages in config/i18n.ts:

{
    code: 'ja',
    name: '日本語',
    direction: 'ltr',
},

If this entry is already commented out in the file, uncomment it instead of adding a duplicate. This change only adds an available language. Keep the existing defaultLanguage. Changing the default language affects the default entry point and fallback behavior, so handle it as a separate, deliberate change.

Language types are inferred from the configuration. Components read text through the project's existing translation APIs and do not need locale === 'ja' checks.

After updating the configuration, restart the development server. Open /ja/, then visit /ja/workspace from the previous tutorial. Confirm that both the page routes and text are correct.

4. Check that components use translation resources

Continue using the following on server-rendered pages:

serverLocalized({ c, id: 'pages.workspace.title' })

Existing client components should continue using Localized, localized, or the project's translation hook. Do not create a separate language mapping object.

If some text is still in English or Chinese, first check whether it is hardcoded in JSX or in a configuration's value. Move text that needs translation into the locale resources and reference it by its resource key. Date and number displays should also use the existing formatting utilities or Intl, passing the current language.

Use the existing internationalized routing utilities for page links, such as getFullPath('/workspace', locale). Business APIs usually remain under /api/.... Do not create /ja/api/... endpoints for Japanese. The existing request wrapper carries language information. If you write requests yourself, pass Accept-Language according to the API's language conventions.

5. Prepare Japanese documentation and blog posts

If the website publishes documentation, create content/docs/ja/. Copy the articles you actually intend to publish and their directory configuration from an existing language directory. Preserve the corresponding file numbers and paths, then translate the frontmatter, body, and display titles in meta.json.

We recommend starting with a small but complete content collection:

content/docs/ja/
├── meta.json
└── 000_index.mdx

The safest approach is to copy these two files from an existing language directory in the same project, then update pages in meta.json to match the pages that actually exist in the Japanese directory. Do not copy a full sidebar that points to chapters you have not translated yet.

Check every internal documentation link when adding a language. Links in the Japanese directory must point to existing Japanese articles under /ja/docs/..., or to an anchor in the current article. If the target has not been translated, add that translation first or temporarily replace the link with plain text. Do not link to another language as a fallback or invent a URL for a page that does not exist. Use the same English anchor IDs in corresponding articles across languages.

Follow the same approach for the blog, adding posts that are ready to publish under content/blog/ja/. You only need these files if the project enables the blog and this release includes Japanese posts. Each language's content directory determines which posts it has.

6. Check for missing resource keys

You can create a temporary check-ja-keys.mjs file in the project root to compare the structure of the English and Japanese resources:

import { readFileSync } from 'node:fs';

const readLocale = (name) => JSON.parse(
    readFileSync(new URL(`./locales/${name}.json`, import.meta.url), 'utf8'),
);

function collect(value, prefix = '', result = new Map()) {
    if (value !== null && typeof value === 'object') {
        result.set(prefix, Array.isArray(value) ? 'array' : 'object');
        for (const [key, child] of Object.entries(value)) {
            collect(child, prefix ? `${prefix}.${key}` : key, result);
        }
    } else {
        result.set(prefix, typeof value);
    }
    return result;
}

const source = collect(readLocale('en'));
const target = collect(readLocale('ja'));
const missing = [...source.keys()].filter((key) => !target.has(key));
const mismatched = [...source.keys()].filter(
    (key) => target.has(key) && target.get(key) !== source.get(key),
);
const extra = [...target.keys()].filter((key) => !source.has(key));
console.log({ missing, mismatched, extra });
if (missing.length || mismatched.length) process.exitCode = 1;

Run:

node check-ja-keys.mjs

The missing list contains missing keys, and mismatched lists differences in structure or value types. Review extra to determine whether those keys are obsolete. This check detects only structural issues. It cannot assess translation quality or replace manual checks of interpolation variables and the actual UI. You can delete the temporary script when you are done.

7. Generate content and verify the release

Run the project checks:

npm run lint
npm run typecheck
npm run build

The build generates content collections and search indexes. To troubleshoot documentation compilation or search separately, run:

npm run gen:search-index

If your deployment uses KV for documentation content, use npm run kv:sync:local for local synchronization and npm run kv:sync:remote for remote synchronization. The existing deploy:update flow handles the synchronization needed for publishing. Running a build alone does not update remote content.

Finally, verify the release through real user flows:

  • Switch to Japanese from the language menu and open the homepage and workspace.
  • Check that sign-in, signup, form validation, empty states, and failure messages all have corresponding text.
  • Confirm that the purchase section displays Japanese names and descriptions, with amounts that still match the actual plans.
  • Check that the documentation sidebar lists only valid pages and that links and images in the body work.
  • Confirm that Japanese search finds published Japanese documentation.
  • Check text outside the current page, such as emails, through the corresponding flows as well.
  • Confirm that the English, Simplified Chinese, and Traditional Chinese entry points still work.

If some pages fall back to another language, first check resource keys, content availability, and publishing synchronization. Do not add temporary Japanese-specific branches to components, or you will need to change business code again the next time you add a language. For more on how this works, see Internationalization and Documentation.