При использовании библиотеки Globalize процессы интернационализации быстро становятся критически важной частью сборки приложения. Ошибки в CLDR-данных, отсутствие переводов, несовместимость форматов дат и чисел, повреждённые JSON-файлы локалей — всё это должно обнаруживаться автоматически ещё до публикации приложения.
Continuous Integration (CI) позволяет:
В экосистеме Javascript чаще всего используются:
Одной из главных задач CI становится проверка всех локализационных файлов до момента слияния изменений в основную ветку.
Типичный набор файлов:
locales/
├── en.json
├── ru.json
├── de.json
└── fr.json
Проблемы, которые необходимо выявлять автоматически:
Для полноценной интеграции обычно устанавливаются:
npm install globalize cldr-data cldrjs
Для тестирования:
npm install --save-dev jest
Для сборки production-версии:
npm install --save-dev globalize-compiler
Простейшая задача CI — убедиться, что локализационные JSON-файлы корректны.
const fs = require("fs");
const path = require("path");
const localesDir = "./locales";
const files = fs.readdirSync(localesDir);
for (const file of files) {
const fullPath = path.join(localesDir, file);
try {
JSON.parse(fs.readFileSync(fullPath, "utf8"));
console.log(`${file} OK`);
} catch (e) {
console.error(`${file} INVALID`);
process.exit(1);
}
}
Добавление в package.json:
{
"scripts": {
"validate:locales": "node scripts/validate-locales.js"
}
}
Очень распространённая проблема — разные локали содержат разные ключи.
Например:
// en.json
{
"title": "Home",
"logout": "Logout"
}
// ru.json
{
"title": "Главная"
}
В production это приведёт к ошибке отсутствующего сообщения.
const fs = require("fs");
const en = require("../locales/en.json");
const ru = require("../locales/ru.json");
function compareKeys(base, target, path = "") {
for (const key in base) {
const fullPath = path ? `${path}.${key}` : key;
if (!(key in target)) {
console.error(`Missing key: ${fullPath}`);
process.exitCode = 1;
}
if (typeof base[key] === "object") {
compareKeys(base[key], target[key] || {}, fullPath);
}
}
}
compareKeys(en, ru);
Минимальный workflow:
name: Localization CI
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install
- run: npm run validate:locales
- run: npm test
CI автоматически запускается:
Очень важно проверять, что библиотека действительно может инициализироваться со всеми локалями.
const Globalize = require("globalize");
const cldrData = require("cldr-data");
Globalize.load(
cldrData.entireSupplemental(),
cldrData.entireMainFor("en", "ru")
);
Globalize.locale("ru");
test("number formatting", () => {
const formatter = Globalize.numberFormatter();
expect(formatter(12345.67))
.toBe("12 345,67");
});
test("date formatting", () => {
const formatter = Globalize.dateFormatter({
datetime: "medium"
});
const result = formatter(new Date());
expect(typeof result).toBe("string");
});
Globalize использует CLDR plural rules, поэтому их необходимо тестировать отдельно.
test("plural rules", () => {
const plural = Globalize.pluralGenerator();
expect(plural(1)).toBe("one");
expect(plural(5)).toBe("many");
});
Одно из важнейших преимуществ Globalize Compiler — возможность компилировать форматтеры заранее.
Это уменьшает:
const GlobalizeCompiler = require("globalize-compiler");
GlobalizeCompiler.compile([
{
output: "dist/compiled.js",
locales: ["en", "ru"],
messages: "locales/{locale}.json"
}
]);
- run: node scripts/compile-globalize.js
Если:
CI завершится с ошибкой.
В крупных проектах CLDR-файлы могут занимать значительный объём.
Для ускорения CI используется cache.
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
CI должен проверять приложение на всех поддерживаемых языках.
strategy:
matrix:
locale: [en, ru, de, fr]
- run: npm run test:locale -- ${{ matrix.locale }}
const locale = process.argv[2];
Globalize.locale(locale);
Очень часто переводчики добавляют новые строки не во все языки.
Автоматическая проверка:
function findMissing(base, target) {
return Object.keys(base)
.filter(key => !(key in target));
}
Более сложный вариант:
function deepDiff(base, target, path = "") {
const missing = [];
for (const key in base) {
const current = path
? `${path}.${key}`
: key;
if (!(key in target)) {
missing.push(current);
continue;
}
if (typeof base[key] === "object") {
missing.push(
...deepDiff(
base[key],
target[key],
current
)
);
}
}
return missing;
}
Globalize поддерживает ICU Message syntax.
Ошибочный шаблон:
{
"items": "{count, plural, one{# item} other{# items}"
}
Проблема — отсутствует закрывающая скобка.
const Globalize = require("globalize");
try {
Globalize.loadMessages({
en: require("./locales/en.json")
});
Globalize.messageFormatter("items");
} catch (e) {
console.error(e.message);
process.exit(1);
}
После успешной сборки CI может:
name: Release
on:
push:
tags:
- "v*"
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm test
- run: npm run build
После подключения новых локалей размер приложения может резко увеличиться.
CI может автоматически контролировать размер.
Установка:
npm install --save-dev size-limit
Конфигурация:
{
"size-limit": [
{
"path": "dist/app.js",
"limit": "200 KB"
}
]
}
Запуск:
- run: npx size-limit
Некоторые formatter’ы Globalize могут быть тяжёлыми при динамической генерации.
CI способен запускать benchmark-тесты.
const benchmark = require("benchmark");
const suite = new benchmark.Suite();
suite
.add("number formatter", () => {
const formatter = Globalize.numberFormatter();
formatter(1000000);
})
.on("complete", function() {
console.log(this[0].hz);
})
.run();
Контейнеризация позволяет:
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm test
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t globalize-app .
Unicode CLDR регулярно обновляется.
После обновления могут измениться:
CI помогает обнаруживать несовместимости сразу после обновления зависимостей.
test("currency snapshot", () => {
const formatter =
Globalize.currencyFormatter("USD");
expect(formatter(100))
.toMatchSnapshot();
});
Если CLDR изменит формат валюты, snapshot-тест покажет различия.
Для JSON-файлов часто подключают:
Пример схемы:
{
"type": "object",
"properties": {
"title": {
"type": "string"
}
},
"required": ["title"]
}
Проверка через ajv:
const Ajv = require("ajv");
const ajv = new Ajv();
const validate = ajv.compile(schema);
const valid = validate(data);
if (!valid) {
console.error(validate.errors);
process.exit(1);
}
Иногда локализация работает по-разному:
Для проверки используют:
test("russian locale", async ({ page }) => {
await page.goto("http://localhost:3000");
await page.selectOption("#locale", "ru");
await expect(page.locator("h1"))
.toContainText("Главная");
});
Важно тестировать fallback-механизм.
Globalize.locale("fr");
const formatter =
Globalize.messageFormatter("title");
Если fr отсутствует:
Оба сценария должны проверяться в CI.
При работе с локализацией важно останавливать pipeline немедленно после первой ошибки.
Пример:
strategy:
fail-fast: true
Это уменьшает:
Крупные проекты разделяют CI на этапы:
name: Full CI
on:
push:
pull_request:
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run validate:locales
test:
needs: validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
CI особенно важен при работе больших команд переводчиков и разработчиков.
Branch protection rules позволяют:
В крупных системах переводы часто versioned отдельно.
Например:
locales/
├── v1/
├── v2/
└── v3/
CI может:
Некоторые команды создают отдельный pipeline:
Это позволяет безопасно поддерживать актуальность локализационной инфраструктуры без ручной проверки каждого изменения Unicode-данных.