diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5630838..92212a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,8 +10,26 @@ jobs: lint: uses: ./.github/workflows/lint.yml + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm run test + build: - needs: lint + needs: [lint, test] runs-on: ubuntu-latest steps: - name: Checkout code diff --git a/src/locales/en.json b/src/locales/en.json index 7683adf..2acecf4 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1703,6 +1703,7 @@ "totalUpload": "Upload", "totalTraffic": "Total", "online": "online", + "realtimeTitle": "Realtime traffic", "inbounds": "Inbounds", "outbounds": "Outbounds" }, diff --git a/src/locales/locales.test.ts b/src/locales/locales.test.ts new file mode 100644 index 0000000..c3d6827 --- /dev/null +++ b/src/locales/locales.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from 'vitest'; +import en from './en.json'; +import ru from './ru.json'; + +/** + * Синхронность en/ru локалей. i18next настроен с fallbackLng: 'ru' — ключ, + * отсутствующий в en.json, отдаёт англоязычным пользователям РУССКИЙ текст + * (а не инлайн-дефолт из кода). Так весь namespace resetPassword.* уехал в + * прод по-русски. Тест ловит новые дыры при добавлении ключей в одну локаль. + */ + +// Русскоязычные словари бэкенд-настроек — осознанно только в ru +// (en падает на сырые имена настроек, это админ-экран). +const RU_ONLY_NAMESPACES = [ + 'admin.settings.settingNames.', + 'admin.settings.categories.', + 'admin.settings.presets.', +]; + +// Известные дыры. #489 смержен, realtimeTitle переведён — список пуст. +const KNOWN_MISSING_IN_EN = new Set([ + // Пусто — так и держать: новые ключи переводите в en.json, а не вносите сюда. +]); + +const KNOWN_PLACEHOLDER_MISMATCHES = new Set([]); + +// Плюральные категории i18next: ru использует _one/_few/_many, en — _one/_other. +// Сравниваем БАЗОВЫЕ ключи (без плюрального суффикса); context-варианты +// (напр. _trial) — самостоятельные ключи и обязаны существовать в обеих локалях. +const PLURAL_SUFFIX = /_(zero|one|two|few|many|other)$/; + +type Tree = { [key: string]: Tree | string }; + +function flatten(tree: Tree, prefix = ''): Map { + const out = new Map(); + for (const [key, value] of Object.entries(tree)) { + const path = prefix ? `${prefix}.${key}` : key; + if (typeof value === 'string') { + out.set(path, value); + } else { + for (const [k, v] of flatten(value, path)) { + out.set(k, v); + } + } + } + return out; +} + +const enFlat = flatten(en as Tree); +const ruFlat = flatten(ru as Tree); + +function baseKeys(flat: Map): Set { + return new Set([...flat.keys()].map((k) => k.replace(PLURAL_SUFFIX, ''))); +} + +const enBases = baseKeys(enFlat); +const ruBases = baseKeys(ruFlat); + +describe('синхронность локалей en/ru', () => { + it('каждый en-ключ существует в ru', () => { + const missing = [...enBases].filter((k) => !ruBases.has(k)); + expect(missing).toEqual([]); + }); + + it('каждый ru-ключ существует в en (кроме ru-only словарей настроек)', () => { + const missing = [...ruBases].filter( + (k) => + !enBases.has(k) && + !RU_ONLY_NAMESPACES.some((ns) => k.startsWith(ns)) && + !KNOWN_MISSING_IN_EN.has(k), + ); + expect(missing).toEqual([]); + }); + + it('плейсхолдеры {{...}} совпадают в общих ключах', () => { + const PH = /\{\{[^}]+\}\}/g; + const mismatches: string[] = []; + for (const [key, ruValue] of ruFlat) { + const enValue = enFlat.get(key); + if (enValue === undefined || KNOWN_PLACEHOLDER_MISMATCHES.has(key)) { + continue; + } + const ruPh = (ruValue.match(PH) ?? []).sort().join(','); + const enPh = (enValue.match(PH) ?? []).sort().join(','); + if (ruPh !== enPh) { + mismatches.push(`${key}: ru=[${ruPh}] en=[${enPh}]`); + } + } + expect(mismatches).toEqual([]); + }); +}); diff --git a/src/locales/ru.json b/src/locales/ru.json index 79f6820..d9044ea 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -2924,6 +2924,7 @@ "namePlaceholder": "Введите название тарифа", "description": "Описание", "descriptionPlaceholder": "Краткое описание тарифа", + "descriptionPlaceholder2": "Краткое описание тарифа", "trafficLimit": "Лимит трафика", "trafficHint": "0 = безлимитный трафик", "deviceLimit": "Лимит устройств", @@ -3814,6 +3815,7 @@ "purchases": "Покупок", "campaign": "Рекламная кампания", "referralsList": "Список рефералов", + "referralEarnings": "Реф. доход", "noReferrals": "Нет рефералов", "panelConfig": "Конфигурация панели", "trojanPassword": "Trojan пароль",