fix(rc5): harden frontend runtime and diagnostics
This commit is contained in:
@@ -19,5 +19,11 @@ Frontend собирается production builder'ом из [`tools/build`](../..
|
||||
|
||||
```sh
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm run build:prod
|
||||
bun test test/i18n-runtime.test.ts
|
||||
pnpm run verify
|
||||
```
|
||||
|
||||
Runtime-тест обходит все строковые сообщения русского и английского словарей
|
||||
через установленный `vue-i18n`. Он проверяет не только исключения, но и
|
||||
compiler diagnostics в `console.error`: некоторые версии компилятора сообщают
|
||||
ошибку формата без исключения из `t()`.
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"qrcode.vue": "3.4.1",
|
||||
"vue": "^3.5.42",
|
||||
"vue-echarts": "^8.1.0",
|
||||
"vue-i18n": "^9.14.5",
|
||||
"vue-i18n": "^11.4.10",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -65,17 +65,6 @@
|
||||
"vue-eslint-parser": "10.4.1",
|
||||
"vue-tsc": "^3.3.11"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"lodash": ">=4.18.1",
|
||||
"lodash-es": ">=4.18.1",
|
||||
"brace-expansion": ">=1.1.18",
|
||||
"picomatch": ">=2.3.2",
|
||||
"js-yaml": ">=4.3.1",
|
||||
"table>ajv": ">=8.18.0",
|
||||
"browserslist": "4.28.7"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
|
||||
Generated
+45
-38
@@ -4,15 +4,6 @@ settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
overrides:
|
||||
lodash: '>=4.18.1'
|
||||
lodash-es: '>=4.18.1'
|
||||
brace-expansion: '>=1.1.18'
|
||||
picomatch: '>=2.3.2'
|
||||
js-yaml: '>=4.3.1'
|
||||
table>ajv: '>=8.18.0'
|
||||
browserslist: 4.28.7
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
@@ -54,8 +45,8 @@ importers:
|
||||
specifier: ^8.1.0
|
||||
version: 8.1.0(echarts@6.1.0)(vue@3.5.42(typescript@5.9.3))
|
||||
vue-i18n:
|
||||
specifier: ^9.14.5
|
||||
version: 9.14.5(vue@3.5.42(typescript@5.9.3))
|
||||
specifier: ^11.4.10
|
||||
version: 11.4.10(vue@3.5.42(typescript@5.9.3))
|
||||
vue-router:
|
||||
specifier: ^4.6.4
|
||||
version: 4.6.4(vue@3.5.42(typescript@5.9.3))
|
||||
@@ -492,17 +483,21 @@ packages:
|
||||
'@iconify/utils@3.1.4':
|
||||
resolution: {integrity: sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==}
|
||||
|
||||
'@intlify/core-base@9.14.5':
|
||||
resolution: {integrity: sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==}
|
||||
engines: {node: '>= 16'}
|
||||
'@intlify/core-base@11.4.10':
|
||||
resolution: {integrity: sha512-+yJ74JRWVJokdgG9zYNMyTSzeNV3O9T4vVxk8PvLFHmI+R/BYA//cITh7vhRK37hWLZ4/kTcKcUz1dlWOpypIg==}
|
||||
engines: {node: '>= 22'}
|
||||
|
||||
'@intlify/message-compiler@9.14.5':
|
||||
resolution: {integrity: sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ==}
|
||||
engines: {node: '>= 16'}
|
||||
'@intlify/devtools-types@11.4.10':
|
||||
resolution: {integrity: sha512-xZxzZsAuu6/0zoLRVQWdpXWe5Kjl0LnWpjlQA3r9u9FbLYMhapqt7IwkgQyn0Tm2GUNAqhj9eZiUmYOrB024BQ==}
|
||||
engines: {node: '>= 22'}
|
||||
|
||||
'@intlify/shared@9.14.5':
|
||||
resolution: {integrity: sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==}
|
||||
engines: {node: '>= 16'}
|
||||
'@intlify/message-compiler@11.4.10':
|
||||
resolution: {integrity: sha512-oUB/scz2EJENXDiUJ7JjZffOrH8UIZ1BuZeHvonbi5fWLavLt04aivuk2OIByOZA0tsci1bkeeQRmwhb5M8Imw==}
|
||||
engines: {node: '>= 22'}
|
||||
|
||||
'@intlify/shared@11.4.10':
|
||||
resolution: {integrity: sha512-FeImVdPeoSHTm3NBFFZHv0eRP9gQ3F4lj2puDBX5Kw7iiM1uJW6JTf39ian0K/17pbXCI3ef5i9RVsRrALqI6Q==}
|
||||
engines: {node: '>= 22'}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
@@ -1525,7 +1520,7 @@ packages:
|
||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
picomatch: '>=2.3.2'
|
||||
picomatch: ^3 || ^4
|
||||
peerDependenciesMeta:
|
||||
picomatch:
|
||||
optional: true
|
||||
@@ -1789,8 +1784,8 @@ packages:
|
||||
resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==}
|
||||
peerDependencies:
|
||||
'@types/lodash-es': '*'
|
||||
lodash: '>=4.18.1'
|
||||
lodash-es: '>=4.18.1'
|
||||
lodash: '*'
|
||||
lodash-es: '*'
|
||||
|
||||
lodash.truncate@4.4.2:
|
||||
resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
|
||||
@@ -1957,6 +1952,10 @@ packages:
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
picomatch@2.3.2:
|
||||
resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
picomatch@4.0.7:
|
||||
resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -2406,7 +2405,7 @@ packages:
|
||||
resolution: {integrity: sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
browserslist: 4.28.7
|
||||
browserslist: '>= 4.21.0'
|
||||
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
@@ -2483,10 +2482,9 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
|
||||
vue-i18n@9.14.5:
|
||||
resolution: {integrity: sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g==}
|
||||
engines: {node: '>= 16'}
|
||||
deprecated: v9 and v10 no longer supported. please migrate to v11. about maintenance status, see https://vue-i18n.intlify.dev/guide/maintenance.html
|
||||
vue-i18n@11.4.10:
|
||||
resolution: {integrity: sha512-Lp+BjOxqzOY87DS6Z8KrQrpiTr9IN/Lt4kZEilwyXG2Wrx+AcU6IVsAW92HNXtVcn1HFFPV6ty41p9e/qDpyvg==}
|
||||
engines: {node: '>= 22'}
|
||||
peerDependencies:
|
||||
vue: ^3.0.0
|
||||
|
||||
@@ -2782,17 +2780,23 @@ snapshots:
|
||||
'@iconify/types': 2.0.0
|
||||
import-meta-resolve: 4.2.0
|
||||
|
||||
'@intlify/core-base@9.14.5':
|
||||
'@intlify/core-base@11.4.10':
|
||||
dependencies:
|
||||
'@intlify/message-compiler': 9.14.5
|
||||
'@intlify/shared': 9.14.5
|
||||
'@intlify/devtools-types': 11.4.10
|
||||
'@intlify/message-compiler': 11.4.10
|
||||
'@intlify/shared': 11.4.10
|
||||
|
||||
'@intlify/message-compiler@9.14.5':
|
||||
'@intlify/devtools-types@11.4.10':
|
||||
dependencies:
|
||||
'@intlify/shared': 9.14.5
|
||||
'@intlify/core-base': 11.4.10
|
||||
'@intlify/shared': 11.4.10
|
||||
|
||||
'@intlify/message-compiler@11.4.10':
|
||||
dependencies:
|
||||
'@intlify/shared': 11.4.10
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@intlify/shared@9.14.5': {}
|
||||
'@intlify/shared@11.4.10': {}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
dependencies:
|
||||
@@ -4113,7 +4117,7 @@ snapshots:
|
||||
micromatch@4.0.8:
|
||||
dependencies:
|
||||
braces: 3.0.3
|
||||
picomatch: 4.0.7
|
||||
picomatch: 2.3.2
|
||||
|
||||
mime-db@1.52.0: {}
|
||||
|
||||
@@ -4245,6 +4249,8 @@ snapshots:
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@2.3.2: {}
|
||||
|
||||
picomatch@4.0.7: {}
|
||||
|
||||
pinia@2.3.1(typescript@5.9.3)(vue@3.5.42(typescript@5.9.3)):
|
||||
@@ -4798,10 +4804,11 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vue-i18n@9.14.5(vue@3.5.42(typescript@5.9.3)):
|
||||
vue-i18n@11.4.10(vue@3.5.42(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@intlify/core-base': 9.14.5
|
||||
'@intlify/shared': 9.14.5
|
||||
'@intlify/core-base': 11.4.10
|
||||
'@intlify/devtools-types': 11.4.10
|
||||
'@intlify/shared': 11.4.10
|
||||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.5.42(typescript@5.9.3)
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Настройки resolution принадлежат pnpm, а не npm package manifest.
|
||||
# Единственный workspace-проект — сам frontend.
|
||||
packages:
|
||||
- "."
|
||||
|
||||
overrides:
|
||||
lodash: ">=4.18.1"
|
||||
lodash-es: ">=4.18.1"
|
||||
brace-expansion: ">=1.1.18"
|
||||
picomatch: ">=2.3.2"
|
||||
js-yaml: ">=4.3.1"
|
||||
"table>ajv": ">=8.18.0"
|
||||
browserslist: "4.28.7"
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Контракт имени пира на стороне панели.
|
||||
*
|
||||
* Сервер остаётся конечным авторитетом: эти значения зеркалят константы из
|
||||
* apps/service/peer_import.go и сверяются с ними release-тестом. Общий модуль
|
||||
* не даёт форме, сообщению об ошибке и подсказке завести разные копии правила.
|
||||
*/
|
||||
|
||||
export const PEER_NAME_MIN_LENGTH = 6;
|
||||
export const PEER_NAME_MAX_LENGTH = 32;
|
||||
|
||||
/** Набор знаков без букв и цифр — передаётся в i18n как значение. */
|
||||
export const PEER_NAME_PUNCTUATION = "!@#$%^&*()_+-=";
|
||||
|
||||
/** Полный набор в том виде, в каком его показывают оператору. */
|
||||
export const PEER_NAME_CHARSET = `a-z A-Z 0-9 ${PEER_NAME_PUNCTUATION}`;
|
||||
|
||||
/**
|
||||
* Набор символов в записи регулярного выражения.
|
||||
*
|
||||
* Дефис экранирован намеренно: неэкранированное `+-=` становится диапазоном и
|
||||
* принимает лишние знаки. Числовые границы берутся из тех же констант, что и
|
||||
* тексты формы.
|
||||
*/
|
||||
const PEER_NAME_CHARACTER_CLASS = "a-zA-Z0-9!@#$%^&*()_+\\-=";
|
||||
|
||||
export const PEER_NAME_PATTERN = new RegExp(
|
||||
`^[${PEER_NAME_CHARACTER_CLASS}]{${PEER_NAME_MIN_LENGTH},${PEER_NAME_MAX_LENGTH}}$`
|
||||
);
|
||||
@@ -60,7 +60,8 @@ export default {
|
||||
trafficApiUnreachable: "Unreachable",
|
||||
security: {
|
||||
hysteriaStopped: "Hysteria2 is stopped",
|
||||
hysteriaStateUnknown: "Hysteria2 service state is unknown: systemd did not answer",
|
||||
hysteriaStateUnknown:
|
||||
"Hysteria2 service state is unknown: systemd did not answer",
|
||||
trafficApiUnavailable: "Traffic API is unavailable",
|
||||
},
|
||||
error: {
|
||||
@@ -217,7 +218,7 @@ export default {
|
||||
// The character set is stated as service.IsValidPeerName accepts it. The
|
||||
// previous wording described the rule that was in force before EX-03.
|
||||
nameHint:
|
||||
"Peer identifier: 6 to 32 characters — latin letters, digits and !@#$%^&*()_+-=. The name becomes part of the auto-generated secret and is shown to the client as the profile name.",
|
||||
"Peer identifier: {min} to {max} characters — latin letters, digits and {punctuation}. The name becomes part of the auto-generated secret and is shown to the client as the profile name.",
|
||||
remarkPlaceholder: "laptop",
|
||||
remarkHint: "Optional operator note. It is never shown to the client.",
|
||||
secretPlaceholder: "leave empty to generate one",
|
||||
|
||||
@@ -240,7 +240,7 @@ export default {
|
||||
// действовавшего до EX-03: подсказка обещала более узкий набор, чем
|
||||
// сервер принимает, и оператор не имел причин пробовать остальные.
|
||||
nameHint:
|
||||
"Идентификатор пира: от 6 до 32 символов — латиница, цифры и знаки !@#$%^&*()_+-=. Имя попадает в автогенерируемый секрет и показывается клиенту как название профиля.",
|
||||
"Идентификатор пира: от {min} до {max} символов — латиница, цифры и знаки {punctuation}. Имя попадает в автогенерируемый секрет и показывается клиенту как название профиля.",
|
||||
remarkPlaceholder: "ноутбук",
|
||||
remarkHint: "Необязательная пометка для оператора. Клиент её не видит.",
|
||||
secretPlaceholder: "оставьте пустым — сгенерируем",
|
||||
|
||||
@@ -98,7 +98,9 @@
|
||||
<el-col :xs="24" :sm="12" :lg="6"
|
||||
><el-card shadow="never"
|
||||
>{{ $t("dashboard.onlinePeers") }}:
|
||||
{{ summary.hysteria.apiReachable ? summary.peers.onlinePeers : "?" }}</el-card
|
||||
{{
|
||||
summary.hysteria.apiReachable ? summary.peers.onlinePeers : "?"
|
||||
}}</el-card
|
||||
></el-col
|
||||
>
|
||||
<el-col :xs="24" :sm="12" :lg="6"
|
||||
@@ -128,9 +130,9 @@
|
||||
<div class="chart-header">
|
||||
<span>{{ $t("dashboard.trafficChart") }}</span>
|
||||
<el-radio-group v-model="range" size="small" @change="loadDashboard">
|
||||
<el-radio-button label="1h">1h</el-radio-button>
|
||||
<el-radio-button label="24h">24h</el-radio-button>
|
||||
<el-radio-button label="7d">7d</el-radio-button>
|
||||
<el-radio-button value="1h">1h</el-radio-button>
|
||||
<el-radio-button value="24h">24h</el-radio-button>
|
||||
<el-radio-button value="7d">7d</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -247,7 +247,15 @@
|
||||
v-model="dataForm.name"
|
||||
:placeholder="$t('peer.form.namePlaceholder')"
|
||||
/>
|
||||
<div class="form-hint">{{ $t("peer.form.nameHint") }}</div>
|
||||
<div class="form-hint">
|
||||
{{
|
||||
$t("peer.form.nameHint", {
|
||||
min: PEER_NAME_MIN_LENGTH,
|
||||
max: PEER_NAME_MAX_LENGTH,
|
||||
punctuation: PEER_NAME_PUNCTUATION,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('peer.remark')" :error="serverErrors.remark">
|
||||
<el-input
|
||||
@@ -445,6 +453,13 @@ import { UploadFile, UploadRawFile, UploadRequestOptions } from "element-plus";
|
||||
import { ERR_CODE, isApiError } from "@/utils/api-error";
|
||||
import { describeApiError, fieldErrorMap } from "@/utils/api-message";
|
||||
import { saveResponseAsFile } from "@/utils/download";
|
||||
import {
|
||||
PEER_NAME_CHARSET,
|
||||
PEER_NAME_MAX_LENGTH,
|
||||
PEER_NAME_MIN_LENGTH,
|
||||
PEER_NAME_PATTERN,
|
||||
PEER_NAME_PUNCTUATION,
|
||||
} from "@/constants/peer";
|
||||
|
||||
/**
|
||||
* Ограничение ширины всплывающих подсказок.
|
||||
@@ -582,10 +597,6 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
// Зеркало серверного контракта, а не второй его экземпляр: границы и набор
|
||||
// символов заданы в service.IsValidPeerName и dto.PeerSaveDto, и расхождение
|
||||
// здесь приводит лишь к лишнему запросу, а не к принятому некорректному пиру.
|
||||
const PEER_NAME_PATTERN = /^[a-zA-Z0-9!@#$%^&*()_+\-=]{6,32}$/;
|
||||
const SECRET_MIN_LENGTH = 6;
|
||||
const SECRET_MAX_LENGTH = 128;
|
||||
|
||||
@@ -600,9 +611,9 @@ const rules = {
|
||||
pattern: PEER_NAME_PATTERN,
|
||||
message: t("error.code.peer_name", {
|
||||
field: t("error.field.name"),
|
||||
min: 6,
|
||||
max: 32,
|
||||
charset: "a-z A-Z 0-9 !@#$%^&*()_+-=",
|
||||
min: PEER_NAME_MIN_LENGTH,
|
||||
max: PEER_NAME_MAX_LENGTH,
|
||||
charset: PEER_NAME_CHARSET,
|
||||
}),
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createI18n } from "vue-i18n";
|
||||
|
||||
import {
|
||||
PEER_NAME_MAX_LENGTH,
|
||||
PEER_NAME_MIN_LENGTH,
|
||||
PEER_NAME_PUNCTUATION,
|
||||
} from "../src/constants/peer";
|
||||
import en from "../src/lang/package/en";
|
||||
import ru from "../src/lang/package/ru";
|
||||
|
||||
type Dictionary = Record<string, unknown>;
|
||||
|
||||
function leafMessages(
|
||||
value: unknown,
|
||||
prefix = ""
|
||||
): Array<[key: string, message: string]> {
|
||||
if (typeof value === "string") {
|
||||
return [[prefix, value]];
|
||||
}
|
||||
if (!value || typeof value !== "object") {
|
||||
return [];
|
||||
}
|
||||
return Object.entries(value as Dictionary).flatMap(([key, child]) =>
|
||||
leafMessages(child, prefix ? `${prefix}.${key}` : key)
|
||||
);
|
||||
}
|
||||
|
||||
function interpolationValues(message: string): Record<string, string | number> {
|
||||
const values: Record<string, string | number> = {
|
||||
count: 2,
|
||||
n: 2,
|
||||
min: PEER_NAME_MIN_LENGTH,
|
||||
max: PEER_NAME_MAX_LENGTH,
|
||||
punctuation: PEER_NAME_PUNCTUATION,
|
||||
};
|
||||
for (const match of message.matchAll(/\{([A-Za-z_][A-Za-z0-9_$-]*)\}/g)) {
|
||||
values[match[1]] ??= match[1];
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
for (const [locale, dictionary] of Object.entries({ ru, en }) as Array<
|
||||
["ru" | "en", Dictionary]
|
||||
>) {
|
||||
describe(`runtime-компиляция локализации ${locale}`, () => {
|
||||
test("каждое строковое сообщение компилируется без diagnostics", () => {
|
||||
const compilerDiagnostics: string[] = [];
|
||||
const thrown: string[] = [];
|
||||
const originalConsoleError = console.error;
|
||||
|
||||
console.error = (...args: unknown[]) => {
|
||||
compilerDiagnostics.push(args.map(String).join(" "));
|
||||
};
|
||||
|
||||
try {
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale,
|
||||
fallbackLocale: false,
|
||||
missingWarn: false,
|
||||
fallbackWarn: false,
|
||||
messages: { [locale]: dictionary },
|
||||
});
|
||||
|
||||
for (const [key, message] of leafMessages(dictionary)) {
|
||||
try {
|
||||
const translated = i18n.global.t(key, interpolationValues(message));
|
||||
if (typeof translated !== "string") {
|
||||
thrown.push(`${key}: результат не является строкой`);
|
||||
}
|
||||
} catch (error) {
|
||||
thrown.push(
|
||||
`${key}: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
console.error = originalConsoleError;
|
||||
}
|
||||
|
||||
expect(thrown).toEqual([]);
|
||||
expect(compilerDiagnostics).toEqual([]);
|
||||
});
|
||||
|
||||
test("набор знаков имени пира проходит интерполяцию как данные", () => {
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale,
|
||||
fallbackLocale: false,
|
||||
messages: { [locale]: dictionary },
|
||||
});
|
||||
|
||||
const template = (dictionary as any).peer.form.nameHint as string;
|
||||
expect(
|
||||
i18n.global.t("peer.form.nameHint", {
|
||||
min: PEER_NAME_MIN_LENGTH,
|
||||
max: PEER_NAME_MAX_LENGTH,
|
||||
punctuation: PEER_NAME_PUNCTUATION,
|
||||
})
|
||||
).toBe(
|
||||
template
|
||||
.replace("{min}", String(PEER_NAME_MIN_LENGTH))
|
||||
.replace("{max}", String(PEER_NAME_MAX_LENGTH))
|
||||
.replace("{punctuation}", PEER_NAME_PUNCTUATION)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user