Skip to content

Internationalization ​

The library includes translations for its controls and dialogs. English (en) is the default. Changing the locale changes interface text; it does not translate document content.

Choose a language ​

The lightweight /locale entry includes English only. Import and register each additional dictionary before selecting its language. Use localeActions.setLang during application initialization or in an event handler:

tsx
import { localeActions, useLocale } from 'ai-sparkwrite-editor/locale';
import vi from 'ai-sparkwrite-editor/locales/vi';
import ja from 'ai-sparkwrite-editor/locales/ja';

localeActions.setMessage('vi', vi);
localeActions.setMessage('ja', ja);

export function LanguagePicker() {
  const { lang } = useLocale();

  return (
    <select
      aria-label='Editor language'
      value={lang}
      onChange={(event) => localeActions.setLang(event.target.value)}
    >
      <option value='en'>English</option>
      <option value='vi'>Tiếng Việt</option>
      <option value='ja'>日本語</option>
    </select>
  );
}

useLocale() returns lang (the language code) and t (a translation function). Call the hook inside a React component; ai-sparkwrite-editor/vue exports a useLocale() composable with the same shape. Use localeActions.setLang to change the language by code.

Locale state is shared across editor instances in the application. The library does not automatically persist a language choice across reloads; restore your application's preference when initializing the client.

Included languages ​

Ordered by the number of speakers worldwide.

LanguageCodeDictionary subpath
Englishen/locales/en
Simplified Chinesezh_CN/locales/zh-cn
Hindihi/locales/hi
Spanishes/locales/es
Frenchfr/locales/fr
Bengalibn/locales/bn
Brazilian Portuguesept_BR/locales/pt-br
Russianru/locales/ru
Indonesianid/locales/id
Germande/locales/de
Japaneseja/locales/ja
Turkishtr/locales/tr
Vietnamesevi/locales/vi
Koreanko/locales/ko
Italianit/locales/it
Hungarianhu_HU/locales/hu
Finnishfi/locales/fi

Use the exact code, including underscores and capitalization: three codes differ from their file name (zh_CN → /locales/zh-cn, pt_BR → /locales/pt-br, hu_HU → /locales/hu). Each dictionary has a default export. For example, register /locales/pt-br under the language code pt_BR.

No right-to-left language ships yet: the editor's own chrome (toolbar, dialogs, bubble menus) is laid out left-to-right, so an RTL dictionary alone would leave the interface mirrored incorrectly. Right-to-left document content is supported through the Text Direction control regardless of the interface language.

Load languages on demand ​

Importing all sixteen non-English dictionaries costs about 61 kB gzipped. Loading one only when the reader picks it keeps the initial bundle at English alone; each dictionary is its own chunk of roughly 3.5 to 4.5 kB.

The loader has to be written out per language. A bundler cannot follow a computed specifier, so import(`ai-sparkwrite-editor/locales/${code}`) either fails to build or quietly pulls in all of them — the map below keeps every specifier a literal:

tsx
import { useState } from 'react';
import { localeActions, useLocale } from 'ai-sparkwrite-editor/locale';

type Loader = () => Promise<{ default: Record<string, string> }>;

/** English needs no loader: the `/locale` entry already registers it. */
const LANGUAGES: { code: string; label: string; load?: Loader }[] = [
  { code: 'en', label: 'English' },
  { code: 'zh_CN', label: '中文', load: () => import('ai-sparkwrite-editor/locales/zh-cn') },
  { code: 'hi', label: 'हिन्दी', load: () => import('ai-sparkwrite-editor/locales/hi') },
  { code: 'es', label: 'Español', load: () => import('ai-sparkwrite-editor/locales/es') },
  { code: 'fr', label: 'Français', load: () => import('ai-sparkwrite-editor/locales/fr') },
  { code: 'bn', label: 'বাংলা', load: () => import('ai-sparkwrite-editor/locales/bn') },
  { code: 'pt_BR', label: 'Português', load: () => import('ai-sparkwrite-editor/locales/pt-br') },
  { code: 'ru', label: 'Русский', load: () => import('ai-sparkwrite-editor/locales/ru') },
  { code: 'id', label: 'Bahasa Indonesia', load: () => import('ai-sparkwrite-editor/locales/id') },
  { code: 'de', label: 'Deutsch', load: () => import('ai-sparkwrite-editor/locales/de') },
  { code: 'ja', label: '日本語', load: () => import('ai-sparkwrite-editor/locales/ja') },
  { code: 'tr', label: 'Türkçe', load: () => import('ai-sparkwrite-editor/locales/tr') },
  { code: 'vi', label: 'Tiếng Việt', load: () => import('ai-sparkwrite-editor/locales/vi') },
  { code: 'ko', label: '한국어', load: () => import('ai-sparkwrite-editor/locales/ko') },
  { code: 'it', label: 'Italiano', load: () => import('ai-sparkwrite-editor/locales/it') },
  { code: 'hu_HU', label: 'Magyar', load: () => import('ai-sparkwrite-editor/locales/hu') },
  { code: 'fi', label: 'Suomi', load: () => import('ai-sparkwrite-editor/locales/fi') },
];

/** Locale state is global, so the cache of loaded dictionaries can be too. */
const registered = new Set(['en']);

export async function selectLanguage(code: string) {
  const entry = LANGUAGES.find((language) => language.code === code);

  if (entry?.load && !registered.has(code)) {
    const { default: messages } = await entry.load();

    localeActions.setMessage(code, messages);
    registered.add(code);
  }

  // Register before selecting, or the interface flashes English first.
  localeActions.setLang(code);
}

export function LanguagePicker() {
  const { lang } = useLocale();
  const [pending, setPending] = useState<string | null>(null);

  async function onSelect(code: string) {
    setPending(code);

    try {
      await selectLanguage(code);
    } catch {
      // A failed chunk leaves the current language in place rather than
      // dropping the reader back to English.
    } finally {
      setPending(null);
    }
  }

  return (
    <select
      aria-label='Editor language'
      disabled={pending !== null}
      value={lang}
      onChange={(event) => onSelect(event.target.value)}
    >
      {LANGUAGES.map(({ code, label }) => (
        <option key={code} value={code}>
          {label}
        </option>
      ))}
    </select>
  );
}

Match the browser language ​

navigator.language is a BCP 47 tag (pt-BR, de-AT, ja), which does not line up with the codes above: three of them use an underscore. Match the exact tag first, then fall back to the base language:

ts
/** Reuses LANGUAGES and selectLanguage from the previous example. */
function matchBrowserLanguage(tag: string) {
  const wanted = tag.toLowerCase().replace('_', '-');
  const base = wanted.split('-')[0];
  const normalize = (code: string) => code.toLowerCase().replace('_', '-');

  return (
    LANGUAGES.find((language) => normalize(language.code) === wanted) ??
    LANGUAGES.find((language) => normalize(language.code).split('-')[0] === base)
  );
}

// In a client-only effect, or wherever you initialize the editor:
const match = matchBrowserLanguage(navigator.language);

if (match) {
  await selectLanguage(match.code);
}

Base matching is approximate where one base language ships in a single variant: zh-TW falls back to zh_CN, so a reader in Taiwan sees Simplified Chinese. Map those cases explicitly if it matters to your users.

Compatibility entry ​

Existing imports from ai-sparkwrite-editor/locale-bundle still work and register all included languages automatically. Use that entry when you need all languages; use /locale and individual dictionaries to avoid loading unused translations. Both entries share the same locale state.

Override existing messages ​

setMessage merges the supplied keys into the language's current messages. You can override a single label without copying the entire dictionary:

ts
import { localeActions } from 'ai-sparkwrite-editor/locale';

localeActions.setMessage('en', {
  'editor.remove': 'Delete',
});

Add a language ​

Start from the exported English dictionary, override the keys you have translated, then select the new language:

ts
import { en, localeActions } from 'ai-sparkwrite-editor/locale';

localeActions.setMessage('fr', {
  ...en,
  'editor.remove': 'Supprimer',
});
localeActions.setLang('fr');

The English spread is optional: missing translations fall back to the current English messages, then to the message key if English also has no value. Register messages before selecting a new code to avoid showing English while its dictionary loads.

Use translations in custom controls ​

tsx
import { useLocale } from 'ai-sparkwrite-editor/locale';

export function RemoveLabel() {
  const { t } = useLocale();
  return <span>{t('editor.remove')}</span>;
}

For messages containing placeholders such as {count}, pass a values object as the second argument to t. Preserve placeholder names when translating. The exported en object and TypeScript completion provide the available message keys.

Made with ❤️