actualbudget/actual · error

Unknown locale ${language}

Error message

Unknown locale ${language}

What it means

loadLanguage checks isLanguageAvailable(language), which tests whether the bundled language map has a key /locale/<language>.json. If the requested language has no bundled locale file, it throws 'Unknown locale <language>'. The available locales are determined at build time by the languages import map in i18n.ts.

Source

Thrown at packages/desktop-client/src/i18n.ts:18

import { initReactI18next } from 'react-i18next';

import * as Platform from '@actual-app/core/shared/platform';
import i18n from 'i18next';
import resourcesToBackend from 'i18next-resources-to-backend';

import { languages } from './languages';

export const availableLanguages = Platform.isPlaywright
  ? []
  : Object.keys(languages).map(path => path.split('/')[2].split('.')[0]);

const isLanguageAvailable = (language: string) =>
  Object.hasOwn(languages, `/locale/${language}.json`);

const loadLanguage = (language: string) => {
  if (!isLanguageAvailable(language)) {
    throw new Error(`Unknown locale ${language}`);
  }
  return languages[`/locale/${language}.json`]();
};

void i18n
  .use(initReactI18next)
  .use(resourcesToBackend(loadLanguage))
  .init({
    lng: 'en',

    // allow keys to be phrases having `:`, `.`
    nsSeparator: false,
    keySeparator: false,
    // do not load a fallback
    fallbackLng: false,
    interpolation: {
      escapeValue: false,
    },

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Use one of the bundled locale codes listed in the languages object in packages/desktop-client/src/i18n.ts
  2. Normalize the stored language code to lowercase before calling loadLanguage
  3. If a new locale is needed, add its JSON file under the locale directory so Vite bundles it into the languages map
  4. Catch the error and fall back to 'en' instead of failing startup

Example fix

// before
await i18n.changeLanguage(userLanguage); // 'en-US' not bundled
// after
const available = Object.keys(languages).map(k => k.replace('/locale/', '').replace('.json', ''));
const lang = userLanguage.toLowerCase();
await i18n.changeLanguage(available.includes(lang) ? lang : 'en');
Defensive patterns

Strategy: validation

Validate before calling

const bundledLocales = new Set(Object.keys(languages).map(k => k.replace('/locale/', '').replace(/\.json$/, '')));
const resolveLanguage = (lang: string) => {
  const l = lang.toLowerCase();
  if (bundledLocales.has(l)) return l;
  const base = l.split('-')[0];
  return bundledLocales.has(base) ? base : 'en';
};

Type guard

const isAvailableLocale = (lang: string): lang is keyof typeof languages =>
  Object.hasOwn(languages, `/locale/${lang}.json`);

Try / catch

try {
  await i18n.changeLanguage(userLanguage);
} catch (err) {
  if (err.message.startsWith('Unknown locale')) {
    await i18n.changeLanguage('en');
  } else throw err;
}

Prevention

When it happens

Trigger: Calling i18n.changeLanguage (or the loader path used by initReactI18next) with a locale code that is not in the languages map — e.g. 'en-gb', 'pt-BR' or a custom code with wrong casing when only 'en', 'es', etc. are bundled.

Common situations: Reading a language preference from the OS or storage that was never bundled; a user manually editing settings; adding a new locale to the UI list without adding its /locale/*.json file; case mismatches like 'EN' vs 'en'.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/43c1d5ab902acd3f. Report an issue: GitHub.