mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
Merge PR #491: vitest-чек синхронности en/ru локалей + запуск тестов в CI
This commit is contained in:
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
@@ -10,8 +10,26 @@ jobs:
|
|||||||
lint:
|
lint:
|
||||||
uses: ./.github/workflows/lint.yml
|
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:
|
build:
|
||||||
needs: lint
|
needs: [lint, test]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|||||||
@@ -1703,6 +1703,7 @@
|
|||||||
"totalUpload": "Upload",
|
"totalUpload": "Upload",
|
||||||
"totalTraffic": "Total",
|
"totalTraffic": "Total",
|
||||||
"online": "online",
|
"online": "online",
|
||||||
|
"realtimeTitle": "Realtime traffic",
|
||||||
"inbounds": "Inbounds",
|
"inbounds": "Inbounds",
|
||||||
"outbounds": "Outbounds"
|
"outbounds": "Outbounds"
|
||||||
},
|
},
|
||||||
|
|||||||
91
src/locales/locales.test.ts
Normal file
91
src/locales/locales.test.ts
Normal file
@@ -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<string>([
|
||||||
|
// Пусто — так и держать: новые ключи переводите в en.json, а не вносите сюда.
|
||||||
|
]);
|
||||||
|
|
||||||
|
const KNOWN_PLACEHOLDER_MISMATCHES = new Set<string>([]);
|
||||||
|
|
||||||
|
// Плюральные категории 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<string, string> {
|
||||||
|
const out = new Map<string, string>();
|
||||||
|
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<string, string>): Set<string> {
|
||||||
|
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([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2924,6 +2924,7 @@
|
|||||||
"namePlaceholder": "Введите название тарифа",
|
"namePlaceholder": "Введите название тарифа",
|
||||||
"description": "Описание",
|
"description": "Описание",
|
||||||
"descriptionPlaceholder": "Краткое описание тарифа",
|
"descriptionPlaceholder": "Краткое описание тарифа",
|
||||||
|
"descriptionPlaceholder2": "Краткое описание тарифа",
|
||||||
"trafficLimit": "Лимит трафика",
|
"trafficLimit": "Лимит трафика",
|
||||||
"trafficHint": "0 = безлимитный трафик",
|
"trafficHint": "0 = безлимитный трафик",
|
||||||
"deviceLimit": "Лимит устройств",
|
"deviceLimit": "Лимит устройств",
|
||||||
@@ -3814,6 +3815,7 @@
|
|||||||
"purchases": "Покупок",
|
"purchases": "Покупок",
|
||||||
"campaign": "Рекламная кампания",
|
"campaign": "Рекламная кампания",
|
||||||
"referralsList": "Список рефералов",
|
"referralsList": "Список рефералов",
|
||||||
|
"referralEarnings": "Реф. доход",
|
||||||
"noReferrals": "Нет рефералов",
|
"noReferrals": "Нет рефералов",
|
||||||
"panelConfig": "Конфигурация панели",
|
"panelConfig": "Конфигурация панели",
|
||||||
"trojanPassword": "Trojan пароль",
|
"trojanPassword": "Trojan пароль",
|
||||||
|
|||||||
Reference in New Issue
Block a user