Internationalization

Put language packs in src/locales and Gemkit generates the i18n instance, language-prefixed URLs and the server-side language detection.

src/locales/
  en.json    default language, /about
  zh.json    /zh/about

A JSON file is a language, the file name is the language code. The default language has no path prefix by default, other languages use /<lang>.

Configuration

pluginGemkit({
  defaultLanguage: 'en',
  prefixDefaultLanguage: false,
});
Option Description
defaultLanguage The default language. Defaults to en when present, otherwise the first language by file name.
prefixDefaultLanguage Put the default language under a prefix too (/en/about). Unprefixed paths redirect to it.

The default language must have a language file, otherwise the build fails.

Every page exists in every language. There are no duplicated page files in src/pages: /zh/about renders the same src/pages/about.ts module with the zh language pack.

Generated module

$gemkit/i18n exports:

Export Description
i18n The I18n instance, typed with the default language pack.
languages The language codes, sorted by file name.
defaultLanguage The default language code.
prefixDefaultLanguage Whether the default language has a prefix.
localizePath (lang, path?) => string, the path with the language prefix.
import { languages, localizePath } from '$gemkit/i18n';

html`<p>${i18n.get('title')}</p>`;

// Switch language with a normal link, the page reloads.
html`${languages.map((lang) => html`<a href=${localizePath(lang)}>${lang}</a>`)}`;

i18n is a Gem I18n instance. See the Gem i18n documentation for variable substitution, fallback languages and cache.

Page content

Pages, layouts and elements read translations from i18n.get. Since the language pack is loaded before rendering, the first server render and hydration use the same language:

@page({ title: () => i18n.get('about') })
export default class AboutPage extends GemElement {
  @template()
  #content = () => html`<h1>${i18n.get('about')}</h1>`;
}

The language is selected from the URL, not from navigator.language. A request without a prefix uses the default language; a request with an unknown prefix is a normal 404 (or the path is treated as part of the default language path).

Switching language

Use a normal <a href> with localizePath. Switching language reloads the page: the base path, the language pack and all content change, so a full reload is the simplest and most consistent behavior. It also matches the server-rendered result.

html`<a href=${localizePath('zh')}>中文</a>`;

Dynamic paths

paths is called for each language, so a route can have different pages in different languages:

export function paths({ lang }: { lang: string }) {
  return posts[lang].map(({ slug }) => ({ slug }));
}

When prefixDefaultLanguage is enabled, Gemkit also writes redirect pages for unprefixed paths, so static hosting redirects to the default language without a server.

Template

The <html lang> attribute is set by the server to the current language. Add the language to src/index.html only if you want a default for non-rendered documents; Gemkit replaces it on rendered pages.

API routes

API routes are not localized. They always use their path without a language prefix, for example /api/likes/1, regardless of the current language.