Версионирование переводов

В экосистеме FormatJS управление переводами строится вокруг строгой структуры сообщений, извлечения строк и использования ICU MessageFormat. При росте приложения ключевой проблемой становится синхронизация исходных сообщений и их локализованных версий, особенно когда интерфейс развивается параллельно с переводами. Версионирование переводов решает задачу согласованности между кодом и многоязычными ресурсами, предотвращая рассинхронизацию интерфейса, устаревшие строки и некорректные форматы данных.


Модель данных переводов в FormatJS

FormatJS опирается на концепцию сообщений (messages), где каждая строка имеет:

  • стабильный идентификатор (id)
  • дефолтный текст (defaultMessage)
  • описание (description, опционально)

Пример:

{
  "cart.itemCount": {
    "defaultMessage": "В корзине {count, plural, one {# товар} few {# товара} many {# товаров} other {# товаров}}",
    "description": "Количество товаров в корзине"
  }
}

Ключевое свойство такой модели — независимость перевода от конкретной локали. Однако именно это приводит к необходимости отслеживания изменений структуры сообщений, поскольку изменение defaultMessage может ломать смысл перевода, даже если id остаётся прежним.


Проблема рассинхронизации переводов

Версионирование становится критичным в следующих случаях:

  • изменяется текст сообщения без изменения id
  • добавляются или удаляются переменные ({count}, {name})
  • меняется тип форматирования (plural, select, number, date)
  • изменяется семантика строки при сохранении ключа
  • происходит рефакторинг UI без обновления локалей

Пример конфликтного изменения:

- "cart.itemCount": "В корзине {count} товаров"
+ "cart.itemCount": "В корзине {count} товаров на сумму {price}"

Если переводы для разных языков не обновлены, приложение продолжит использовать старые структуры, что может привести к ошибкам форматирования ICU.


Подходы к версионированию переводов

1. Версионирование по идентификатору сообщения

Один из самых строгих подходов — включение версии прямо в id:

"cart.itemCount.v1"
"cart.itemCount.v2"

Преимущества:

  • полная изоляция изменений
  • отсутствие конфликтов между версиями
  • простое откатывание

Недостатки:

  • быстрый рост количества ключей
  • необходимость миграции всех локалей
  • усложнение поиска строк

Такой подход используется редко в больших системах, но полезен при радикальных переработках интерфейса.


2. Семантическое версионирование каталогов

Переводы разделяются по версиям приложения:

/locales
  /v1
    ru.json
    en.json
  /v2
    ru.json
    en.json

В коде выбирается версия:

const messages = loadMessages(locale, appVersion);

Преимущества:

  • чёткая привязка переводов к версии приложения
  • простая стратегия отката
  • отсутствие конфликтов форматов

Недостатки:

  • дублирование неизменённых переводов
  • рост объёма файлов
  • сложность частичных обновлений

3. Контентное версионирование через хэши сообщений

Более гибкий подход — вычисление хэша от defaultMessage:

import { createHash } from "crypto";

function messageId(defaultMessage) {
  return createHash("md5").update(defaultMessage).digest("hex");
}

Пример:

"e4b7c3a91d2f..."

Преимущества:

  • автоматическое отслеживание изменений текста
  • отсутствие ручного управления версиями
  • гарантированная уникальность при изменениях

Недостатки:

  • невозможность стабильных ссылок на сообщения
  • сложность поддержки переводчиков
  • плохая читаемость ключей

4. Стабильные ключи + контроль структуры

Наиболее распространённый подход в FormatJS — стабильные id с контролем изменений через CI.

"cart.checkout.buttonLabel"

Версионирование осуществляется не через ключи, а через анализ изменений структуры сообщения.


Контроль изменений ICU-сообщений

FormatJS использует ICU MessageFormat, где критичны:

  • количество аргументов
  • типы аргументов (number, date, plural)
  • вложенные конструкции

Изменение структуры без обновления переводов приводит к ошибкам runtime.

Пример несовместимого изменения:

- "{count, plural, one {# item} other {# items}}"
+ "{count} items"

Для предотвращения таких проблем используется:

  • статический анализ сообщений
  • CI-проверки ICU-валидности
  • сравнение AST сообщений

Использование formatjs CLI для контроля версий

Инструменты FormatJS позволяют автоматизировать извлечение и проверку переводов:

formatjs extract "src/**/*.{js,ts,tsx}" --out-file messages.json

Это создаёт единый файл сообщений, который может использоваться как источник истины.

Дальнейшая проверка изменений:

formatjs compile messages.json --out-file compiled.json

В процессе CI можно сравнивать:

  • новые сообщения
  • удалённые сообщения
  • изменённые ICU-структуры

Стратегия диффов переводов

При обновлении приложения критически важно различать:

Изменение текста без изменения структуры

- "Добавить в корзину"
+ "Положить в корзину"

Перевод можно оставить без изменений или обновить без влияния на ICU.


Изменение структуры сообщения

- "В корзине {count} товаров"
+ "В корзине {count, plural, one {# товар} other {# товаров}}"

Требует обязательного обновления всех локалей.


Добавление новых параметров

- "Привет, {name}"
+ "Привет, {name}. У вас {count} уведомлений"

Это уже изменение контракта сообщения, требующее синхронизации переводов.


Совместимость версий переводов

При работе с несколькими версиями важно учитывать fallback-механизм:

const messages = {
  ru: {
    v2: {...},
    v1: {...}
  }
};

Если ключ отсутствует в новой версии, система может:

  • использовать fallback на предыдущую версию
  • подставлять defaultMessage
  • логировать отсутствующие ключи

Псевдолокализация как инструмент версионирования

Для выявления проблем совместимости используется псевдолокализация:

"Hello {name}" → "Ħëļļø {name}"

Это позволяет обнаружить:

  • переполнения UI
  • сломанные плейсхолдеры
  • потерю аргументов при обновлениях

Интеграция версионирования в CI/CD

Типичный пайплайн включает:

  1. извлечение сообщений (extract)
  2. сравнение с предыдущей версией
  3. проверка ICU-валидности
  4. генерация diff отчёта
  5. блокировка деплоя при критических изменениях

Пример логики проверки:

if (removedMessages.length > 0 && env === "production") {
  throw new Error("Translation keys removed");
}

Управление изменениями в больших проектах

При масштабировании системы переводов ключевыми становятся следующие принципы:

  • неизменяемость id после публикации
  • запрет на изменение структуры ICU без версии
  • централизованное хранение message catalog
  • автоматическое обнаружение drift между языками
  • контроль семантики через описание сообщений

Стратегии миграции переводов

При переходе между версиями интерфейса применяются сценарии:

  • backfill: добавление новых ключей во все локали
  • cleanup: удаление устаревших сообщений после деактивации UI
  • shadow migration: параллельное использование старых и новых ключей
  • gradual rollout: частичное внедрение новых переводов

Роль стабильных идентификаторов

Стабильный id становится контрактом между:

  • кодом интерфейса
  • системой локализации
  • переводчиками
  • CI-инфраструктурой

Любое нарушение этого контракта фактически является изменением версии сообщения, даже если формально ключ не изменился.