Fallback механизмы

При локализации интерфейсов далеко не всегда существует полный набор переводов, форматов дат, чисел и валют для каждого языка и региона. Библиотека Globalize использует fallback-механизмы для автоматического поиска ближайших подходящих данных, если точная локаль отсутствует.

Fallback позволяет:

  • избегать ошибок при отсутствии части CLDR-данных;
  • использовать общие языковые настройки вместо региональных;
  • формировать предсказуемое поведение приложения;
  • уменьшать объём подключаемых переводов;
  • централизованно управлять деградацией локализации.

Типичный пример:

const globalize = new Globalize("fr-CA");

Если для fr-CA отсутствуют определённые данные, библиотека может использовать данные fr.


Иерархия локалей

Globalize строит цепочку поиска данных на основе языковой и региональной структуры локали.

Например:

pt-BR → pt → root

или:

zh-Hant-TW → zh-Hant → zh → root

Порядок fallback:

  1. Полная локаль
  2. Базовый язык
  3. Root-данные CLDR

Роль CLDR в fallback-механизмах

Globalize полностью опирается на данные Unicode CLDR. Именно CLDR определяет:

  • иерархию локалей;
  • наследование форматов;
  • региональные переопределения;
  • root-значения.

Без CLDR fallback невозможен.

Подключение данных:

const Globalize = require("globalize");
const cldrData = require("cldr-data");

Globalize.load(
    cldrData.entireSupplemental(),
    cldrData.entireMainFor("en", "en-GB", "fr")
);

Root-локаль

root — базовая локаль CLDR, содержащая универсальные значения по умолчанию.

Если библиотека не находит данные ни в одной цепочке локалей, используется root.

Пример fallback:

de-AT → de → root

В root обычно находятся:

  • базовые шаблоны дат;
  • общие правила чисел;
  • нейтральные обозначения;
  • системные настройки.

Автоматический fallback форматов

Форматирование чисел

const globalize = new Globalize("es-MX");

const formatter = globalize.numberFormatter();

console.log(formatter(12345.67));

Если специфичные мексиканские настройки отсутствуют, будут использованы испанские.


Форматирование дат

const globalize = new Globalize("en-AU");

const formatter = globalize.dateFormatter({
    datetime: "medium"
});

console.log(formatter(new Date()));

При отсутствии австралийских шаблонов библиотека возьмёт данные en.


Форматирование валют

const formatter = globalize.currencyFormatter("USD");

console.log(formatter(199.99));

Fallback применяется к:

  • шаблонам валют;
  • символам;
  • позициям валюты;
  • разделителям.

Fallback переводов сообщений

Модуль globalize/message не выполняет автоматическое наследование переводов между локалями. Этот механизм необходимо реализовывать самостоятельно.

Пример структуры переводов:

Globalize.loadMessages({
    en: {
        hello: "Hello"
    },

    "en-GB": {
        currency: "Pounds"
    }
});

Проблема:

const globalize = new Globalize("en-GB");

globalize.formatMessage("hello");

Сообщение "hello" не будет найдено автоматически, потому что en-GB не наследует en.


Ручная реализация fallback переводов

Наиболее распространённый подход — объединение словарей.

Пример merge-стратегии

const messages = {
    en: {
        hello: "Hello",
        save: "Save"
    },

    "en-GB": {
        currency: "Pound"
    }
};

function buildMessages(locale) {
    const parts = locale.split("-");

    const chain = [];

    while (parts.length) {
        chain.unshift(parts.join("-"));
        parts.pop();
    }

    return Object.assign({}, ...chain.map(l => messages[l] || {}));
}

Globalize.loadMessages({
    "en-GB": buildMessages("en-GB")
});

Результат:

{
    hello: "Hello",
    save: "Save",
    currency: "Pound"
}

Создание универсальной fallback-цепочки

Практически всегда fallback организуют через массив локалей.

Пример

function localeChain(locale) {
    const result = [];
    const parts = locale.split("-");

    while (parts.length) {
        result.push(parts.join("-"));
        parts.pop();
    }

    result.push("root");

    return result;
}

console.log(localeChain("zh-Hant-TW"));

Результат:

[
    "zh-Hant-TW",
    "zh-Hant",
    "zh",
    "root"
]

Проверка существования локали

Перед использованием локали полезно убедиться, что данные загружены.

Проверка через try/catch

function safeGlobalize(locale) {
    try {
        return new Globalize(locale);
    } catch (e) {
        return new Globalize("en");
    }
}

Fallback пользовательских настроек

Во многих приложениях fallback используется не только для переводов, но и для пользовательских конфигураций.

Пример

const settings = {
    en: {
        timezone: "UTC"
    },

    "en-US": {
        currency: "USD"
    }
};

Алгоритм:

function resolveSetting(locale, key) {
    const chain = localeChain(locale);

    for (const loc of chain) {
        if (settings[loc] && settings[loc][key]) {
            return settings[loc][key];
        }
    }

    return null;
}

Fallback pluralization

Plural rules также наследуются через CLDR.

Пример:

const globalize = new Globalize("ru-KZ");

const plural = globalize.pluralGenerator();

console.log(plural(5));

Если региональные правила отсутствуют, используются языковые:

ru-KZ → ru

Региональные переопределения

Fallback не означает полное копирование данных родительской локали. CLDR позволяет переопределять отдельные поля.

Например:

en
 ├── date format
 ├── decimal separator
 └── currency style

en-GB
 └── currency symbol override

Globalize автоматически объединяет данные.


Стратегии fallback в крупных приложениях

Стратегия strict locale

Используется только полная локаль.

if (!supportedLocales.includes(userLocale)) {
    throw new Error("Unsupported locale");
}

Подходит для:

  • банковских систем;
  • юридических платформ;
  • систем отчётности.

Стратегия graceful fallback

Постепенное снижение точности локализации.

fr-CA → fr → en

Подходит для:

  • SaaS;
  • интернет-магазинов;
  • контентных платформ.

Стратегия default locale

Если локаль неизвестна — используется язык приложения.

const locale = supported.includes(userLocale)
    ? userLocale
    : "en";

Fallback и lazy loading

При динамической загрузке локалей fallback особенно важен.

Пример

async function loadLocale(locale) {
    try {
        await import(`./locales/${locale}.json`);
        return locale;
    } catch {
        return locale.split("-")[0];
    }
}

Комбинированный fallback

Часто fallback объединяет:

  • переводы;
  • форматы дат;
  • числовые шаблоны;
  • настройки интерфейса;
  • timezone;
  • валюты.

Пример архитектуры

{
    locale: "fr-CA",

    fallback: [
        "fr-CA",
        "fr",
        "en"
    ]
}

Кэширование fallback-результатов

Построение цепочек локалей может вызываться очень часто.

Для оптимизации используют кэш.

Пример

const cache = new Map();

function getLocaleChain(locale) {
    if (cache.has(locale)) {
        return cache.get(locale);
    }

    const chain = localeChain(locale);

    cache.set(locale, chain);

    return chain;
}

Обработка отсутствующих сообщений

Безопасный formatter

function safeMessage(globalize, key) {
    try {
        return globalize.formatMessage(key);
    } catch {
        return `[missing:${key}]`;
    }
}

Использование fallback-языка

function formatWithFallback(primary, fallback, key) {
    try {
        return primary.formatMessage(key);
    } catch {
        return fallback.formatMessage(key);
    }
}

Отладка fallback-цепочек

При сложной локализации важно видеть, какая локаль реально используется.

Логирование

function resolveLocale(locale, available) {
    const chain = localeChain(locale);

    for (const item of chain) {
        console.log("Checking:", item);

        if (available.includes(item)) {
            console.log("Resolved:", item);

            return item;
        }
    }

    return "root";
}

Проблемы fallback-механизмов

Смешивание региональных стандартов

Пример:

fr-CA → fr

В результате могут смешиваться:

  • валюты;
  • форматы адресов;
  • единицы измерения.

Частично переведённый интерфейс

Если часть сообщений отсутствует:

Главная
Settings
Профиль
Logout

Интерфейс становится неоднородным.


Избыточная деградация

Слишком глубокий fallback иногда приводит к неожиданным результатам:

zh-Hant-HK → zh-Hant → zh → root

На последнем этапе форматирование может стать слишком общим.


Лучшие практики

Использование базового языка

Минимальный набор переводов должен существовать для основного языка:

en
fr
de

Хранение полной fallback-цепочки

const config = {
    locale: "pt-BR",

    fallbacks: [
        "pt-BR",
        "pt",
        "en"
    ]
};

Централизация логики fallback

Вместо разрозненных проверок:

if (!message) {
    ...
}

лучше использовать единый resolver.


Явное логирование отсутствующих ключей

function missingKey(locale, key) {
    console.warn(`[${locale}] Missing key: ${key}`);
}

Тестирование fallback-сценариев

Проверяются:

  • отсутствующие переводы;
  • неполные CLDR-данные;
  • неизвестные локали;
  • повреждённые словари;
  • lazy-loading ошибки.

Пример полноценного fallback-resolver

class LocaleResolver {
    constructor(messages, defaultLocale = "en") {
        this.messages = messages;
        this.defaultLocale = defaultLocale;
    }

    chain(locale) {
        const result = [];
        const parts = locale.split("-");

        while (parts.length) {
            result.push(parts.join("-"));
            parts.pop();
        }

        result.push(this.defaultLocale);

        return result;
    }

    resolve(locale, key) {
        const chain = this.chain(locale);

        for (const loc of chain) {
            const dict = this.messages[loc];

            if (dict && key in dict) {
                return dict[key];
            }
        }

        return `[missing:${key}]`;
    }
}

Использование:

const resolver = new LocaleResolver({
    en: {
        save: "Save"
    },

    fr: {
        save: "Enregistrer"
    }
});

console.log(
    resolver.resolve("fr-CA", "save")
);

Результат:

Enregistrer

Интеграция fallback с Globalize

function createGlobalize(locale, supported) {
    const chain = localeChain(locale);

    for (const loc of chain) {
        if (supported.includes(loc)) {
            return new Globalize(loc);
        }
    }

    return new Globalize("en");
}

Fallback для динамических модулей

При модульной архитектуре локали часто разбиваются по пакетам.

Пример:

auth/
dashboard/
profile/
billing/

Fallback может применяться отдельно для каждого модуля.

{
    auth: ["fr-CA", "fr", "en"],
    billing: ["fr-CA", "en"]
}

Каскадное наследование локалей

Иногда используется расширенный fallback:

es-MX
   ↓
es-419
   ↓
es
   ↓
en

Где:

  • es-419 — испанский для Латинской Америки;
  • es — общий испанский;
  • en — резервный язык приложения.

Такая схема особенно распространена в международных enterprise-системах.