mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
feat: separate i18n dict pages
This commit is contained in:
@@ -96,9 +96,10 @@
|
|||||||
- Опция `allowPaths: string[]` — dot-path ключи, исключаемые из всех проверок
|
- Опция `allowPaths: string[]` — dot-path ключи, исключаемые из всех проверок
|
||||||
(осознанные отклонения до достижения zero-warn).
|
(осознанные отклонения до достижения zero-warn).
|
||||||
- `baseLocaleFallback: string[]` — паттерны ключей, которые допустимо
|
- `baseLocaleFallback: string[]` — паттерны ключей, которые допустимо
|
||||||
отсутствовать только в базовой локали; в текущей конфигурации это registry
|
отсутствовать только в базовой локали; в текущей конфигурации это тексты
|
||||||
metadata `tools.*.title` и `tools.*.description`. Пустые значения и
|
страниц `pages.*.title` и `pages.*.description` (EN-строка берётся из
|
||||||
placeholders для присутствующих ключей всё равно проверяются.
|
реестра). Пустые значения и placeholders для присутствующих ключей всё равно
|
||||||
|
проверяются.
|
||||||
- `ignoreMissingPatterns: string[]` — паттерны ключей, которые временно не
|
- `ignoreMissingPatterns: string[]` — паттерны ключей, которые временно не
|
||||||
проверяются на missing; в текущей конфигурации это неиспользуемые
|
проверяются на missing; в текущей конфигурации это неиспользуемые
|
||||||
`tools.*.params`. Эта опция не отключает empty/placeholder checks.
|
`tools.*.params`. Эта опция не отключает empty/placeholder checks.
|
||||||
|
|||||||
@@ -12,10 +12,14 @@ export const de: Dict = {
|
|||||||
errors: {
|
errors: {
|
||||||
sourceRequired: "Quelle erforderlich",
|
sourceRequired: "Quelle erforderlich",
|
||||||
},
|
},
|
||||||
tools: {
|
pages: {
|
||||||
addBorder: {
|
addBorder: {
|
||||||
title: "Rahmen hinzufügen",
|
title: "Rahmen hinzufügen",
|
||||||
description: "Zeichnet einen {kind}-Rahmen.",
|
description: "Zeichnet einen {kind}-Rahmen.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
|
addBorder: {
|
||||||
results: {
|
results: {
|
||||||
done: "Rahmen {what}",
|
done: "Rahmen {what}",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,11 +6,15 @@ export type Dict = {
|
|||||||
header: Record<string, string>;
|
header: Record<string, string>;
|
||||||
home: Record<string, string>;
|
home: Record<string, string>;
|
||||||
errors: Record<string, string>;
|
errors: Record<string, string>;
|
||||||
|
pages?: Record<string, PageStrings>;
|
||||||
tools: Record<string, ToolStrings>;
|
tools: Record<string, ToolStrings>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ToolStrings = {
|
export type PageStrings = {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ToolStrings = {
|
||||||
results?: Record<string, string>;
|
results?: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ export const en: Dict = {
|
|||||||
errors: {
|
errors: {
|
||||||
sourceRequired: "Source image required",
|
sourceRequired: "Source image required",
|
||||||
},
|
},
|
||||||
|
// Базовая локаль: тексты страниц берутся из EN-строк реестра.
|
||||||
|
pages: {},
|
||||||
tools: {
|
tools: {
|
||||||
addBorder: {
|
addBorder: {
|
||||||
title: "Add border",
|
|
||||||
description: "Draws a {kind} frame.",
|
|
||||||
results: {
|
results: {
|
||||||
done: "Border {what}",
|
done: "Border {what}",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,10 +12,14 @@ export const ru: Dict = {
|
|||||||
errors: {
|
errors: {
|
||||||
sourceRequired: "Нужен исходник",
|
sourceRequired: "Нужен исходник",
|
||||||
},
|
},
|
||||||
tools: {
|
pages: {
|
||||||
addBorder: {
|
addBorder: {
|
||||||
title: "Добавить рамку",
|
title: "Добавить рамку",
|
||||||
description: "Рисует рамку {kind}.",
|
description: "Рисует рамку {kind}.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
|
addBorder: {
|
||||||
results: {
|
results: {
|
||||||
done: "Рамка {what}",
|
done: "Рамка {what}",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -51,24 +51,28 @@ function messageIds(messages: { messageId?: string }[]) {
|
|||||||
return messages.map((m) => m.messageId);
|
return messages.map((m) => m.messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const registryPolicy = {
|
||||||
|
baseLocaleFallback: ["pages.*.title", "pages.*.description"],
|
||||||
|
ignoreMissingPatterns: ["tools.*.params"],
|
||||||
|
};
|
||||||
|
|
||||||
describe("i18n/dict-consistency", () => {
|
describe("i18n/dict-consistency", () => {
|
||||||
it("stays clean on the consistent fixture dicts", () => {
|
it("stays clean on the consistent fixture dicts", () => {
|
||||||
expect(messageIds(lint(fixture("en.ts"), "lib/i18n/en.ts"))).toEqual([]);
|
expect(
|
||||||
expect(messageIds(lint(fixture("ru.ts"), "lib/i18n/ru.ts"))).toEqual([]);
|
messageIds(lint(fixture("en.ts"), "lib/i18n/en.ts", registryPolicy)),
|
||||||
expect(messageIds(lint(fixture("de.ts"), "lib/i18n/de.ts"))).toEqual([]);
|
).toEqual([]);
|
||||||
|
expect(
|
||||||
|
messageIds(lint(fixture("ru.ts"), "lib/i18n/ru.ts", registryPolicy)),
|
||||||
|
).toEqual([]);
|
||||||
|
expect(
|
||||||
|
messageIds(lint(fixture("de.ts"), "lib/i18n/de.ts", registryPolicy)),
|
||||||
|
).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
const registryPolicy = {
|
it("allows page texts only in the base locale", () => {
|
||||||
baseLocaleFallback: ["tools.*.title", "tools.*.description"],
|
// EN не переводит заголовки страниц: их берёт EN-строка реестра.
|
||||||
ignoreMissingPatterns: ["tools.*.params"],
|
|
||||||
};
|
|
||||||
|
|
||||||
it("allows registry metadata only in the base locale", () => {
|
|
||||||
const enMissing = fixture("en.ts")
|
|
||||||
.replace('\t\t\ttitle: "Add border",\n', "")
|
|
||||||
.replace('\t\t\tdescription: "Draws a {kind} frame.",\n', "");
|
|
||||||
expect(
|
expect(
|
||||||
messageIds(lint(enMissing, "lib/i18n/en.ts", registryPolicy)),
|
messageIds(lint(fixture("en.ts"), "lib/i18n/en.ts", registryPolicy)),
|
||||||
).toEqual([]);
|
).toEqual([]);
|
||||||
|
|
||||||
const ruMissing = fixture("ru.ts").replace(
|
const ruMissing = fixture("ru.ts").replace(
|
||||||
@@ -80,7 +84,7 @@ describe("i18n/dict-consistency", () => {
|
|||||||
).toEqual(["missingKey"]);
|
).toEqual(["missingKey"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not warn for a missing tool object with only fallback metadata", () => {
|
it("does not warn for a tool object with nothing but ignored results", () => {
|
||||||
const enMissingTool = fixture("en.ts").replace(
|
const enMissingTool = fixture("en.ts").replace(
|
||||||
/\t\taddBorder: \{[\s\S]*?\n\t\t\},\n/,
|
/\t\taddBorder: \{[\s\S]*?\n\t\t\},\n/,
|
||||||
"",
|
"",
|
||||||
@@ -106,18 +110,19 @@ describe("i18n/dict-consistency", () => {
|
|||||||
expect(
|
expect(
|
||||||
messageIds(
|
messageIds(
|
||||||
lint(enMissingResults, "lib/i18n/en.ts", {
|
lint(enMissingResults, "lib/i18n/en.ts", {
|
||||||
|
...registryPolicy,
|
||||||
ignoreMissingPatterns: ["tools.*.results"],
|
ignoreMissingPatterns: ["tools.*.results"],
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).toEqual([]);
|
).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("still reports empty fallback metadata", () => {
|
it("still reports empty page text", () => {
|
||||||
const enEmpty = fixture("en.ts").replace(
|
const ruEmpty = fixture("ru.ts").replace(
|
||||||
'title: "Add border",',
|
'title: "Добавить рамку",',
|
||||||
'title: "",',
|
'title: "",',
|
||||||
);
|
);
|
||||||
expect(messageIds(lint(enEmpty, "lib/i18n/en.ts", registryPolicy))).toEqual(
|
expect(messageIds(lint(ruEmpty, "lib/i18n/ru.ts", registryPolicy))).toEqual(
|
||||||
["emptyValue"],
|
["emptyValue"],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -127,7 +132,7 @@ describe("i18n/dict-consistency", () => {
|
|||||||
'\t\trestoreLast: "Restore {title}",',
|
'\t\trestoreLast: "Restore {title}",',
|
||||||
"",
|
"",
|
||||||
);
|
);
|
||||||
const messages = lint(enMissing, "lib/i18n/en.ts");
|
const messages = lint(enMissing, "lib/i18n/en.ts", registryPolicy);
|
||||||
expect(messages).toHaveLength(1);
|
expect(messages).toHaveLength(1);
|
||||||
expect(messageIds(messages)).toEqual(["missingKey"]);
|
expect(messageIds(messages)).toEqual(["missingKey"]);
|
||||||
expect(messages[0].message).toContain('"home.restoreLast"');
|
expect(messages[0].message).toContain('"home.restoreLast"');
|
||||||
@@ -136,7 +141,7 @@ describe("i18n/dict-consistency", () => {
|
|||||||
|
|
||||||
it("reports a missing whole section once, not per child key", () => {
|
it("reports a missing whole section once, not per child key", () => {
|
||||||
const enNoHeader = fixture("en.ts").replace(/\theader: \{[\s\S]*?\},/, "");
|
const enNoHeader = fixture("en.ts").replace(/\theader: \{[\s\S]*?\},/, "");
|
||||||
const messages = lint(enNoHeader, "lib/i18n/en.ts");
|
const messages = lint(enNoHeader, "lib/i18n/en.ts", registryPolicy);
|
||||||
expect(messageIds(messages)).toEqual(["missingKey"]);
|
expect(messageIds(messages)).toEqual(["missingKey"]);
|
||||||
expect(messages[0].message).toContain('"header"');
|
expect(messages[0].message).toContain('"header"');
|
||||||
expect(messages[0].message).not.toContain("header.workspace");
|
expect(messages[0].message).not.toContain("header.workspace");
|
||||||
@@ -147,7 +152,7 @@ describe("i18n/dict-consistency", () => {
|
|||||||
'\t\tsourceRequired: "Source image required",',
|
'\t\tsourceRequired: "Source image required",',
|
||||||
"",
|
"",
|
||||||
);
|
);
|
||||||
const messages = lint(enMissing, "lib/i18n/en.ts");
|
const messages = lint(enMissing, "lib/i18n/en.ts", registryPolicy);
|
||||||
expect(messages).toHaveLength(1);
|
expect(messages).toHaveLength(1);
|
||||||
expect(messageIds(messages)).toEqual(["missingKey"]);
|
expect(messageIds(messages)).toEqual(["missingKey"]);
|
||||||
expect(messages[0].message).toContain('"errors.sourceRequired"');
|
expect(messages[0].message).toContain('"errors.sourceRequired"');
|
||||||
@@ -158,7 +163,7 @@ describe("i18n/dict-consistency", () => {
|
|||||||
const enEmpty = fixture("en.ts")
|
const enEmpty = fixture("en.ts")
|
||||||
.replace('workspace: "Workspace",', 'workspace: "",')
|
.replace('workspace: "Workspace",', 'workspace: "",')
|
||||||
.replace('catalog: "Catalog",', 'catalog: " ",');
|
.replace('catalog: "Catalog",', 'catalog: " ",');
|
||||||
const messages = lint(enEmpty, "lib/i18n/en.ts");
|
const messages = lint(enEmpty, "lib/i18n/en.ts", registryPolicy);
|
||||||
expect(messageIds(messages)).toEqual(["emptyValue", "emptyValue"]);
|
expect(messageIds(messages)).toEqual(["emptyValue", "emptyValue"]);
|
||||||
expect(messages[0].message).toContain('"header.workspace"');
|
expect(messages[0].message).toContain('"header.workspace"');
|
||||||
expect(messages[0].message).toContain(" in en");
|
expect(messages[0].message).toContain(" in en");
|
||||||
@@ -178,14 +183,14 @@ describe("i18n/dict-consistency", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("flags an extra placeholder introduced in one locale", () => {
|
it("flags an extra placeholder introduced in one locale", () => {
|
||||||
const enExtraPlaceholder = fixture("en.ts").replace(
|
const ruExtraPlaceholder = fixture("ru.ts").replace(
|
||||||
'description: "Draws a {kind} frame.",',
|
'description: "Рисует рамку {kind}.",',
|
||||||
'description: "Draws a {kind} frame by {author}.",',
|
'description: "Рисует рамку {kind} от {author}.",',
|
||||||
);
|
);
|
||||||
const messages = lint(enExtraPlaceholder, "lib/i18n/en.ts");
|
const messages = lint(ruExtraPlaceholder, "lib/i18n/ru.ts");
|
||||||
expect(messages).toHaveLength(1);
|
expect(messages).toHaveLength(1);
|
||||||
expect(messageIds(messages)).toEqual(["placeholderMismatch"]);
|
expect(messageIds(messages)).toEqual(["placeholderMismatch"]);
|
||||||
expect(messages[0].message).toContain('"tools.addBorder.description"');
|
expect(messages[0].message).toContain('"pages.addBorder.description"');
|
||||||
expect(messages[0].message).toContain("{author, kind} vs expected {kind}");
|
expect(messages[0].message).toContain("{author, kind} vs expected {kind}");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -195,6 +200,7 @@ describe("i18n/dict-consistency", () => {
|
|||||||
"",
|
"",
|
||||||
);
|
);
|
||||||
const messages = lint(enMissing, "lib/i18n/en.ts", {
|
const messages = lint(enMissing, "lib/i18n/en.ts", {
|
||||||
|
...registryPolicy,
|
||||||
allowPaths: ["errors.sourceRequired"],
|
allowPaths: ["errors.sourceRequired"],
|
||||||
});
|
});
|
||||||
expect(messageIds(messages)).toEqual([]);
|
expect(messageIds(messages)).toEqual([]);
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export default tseslint.config(
|
|||||||
"i18n/dict-consistency": [
|
"i18n/dict-consistency": [
|
||||||
"warn",
|
"warn",
|
||||||
{
|
{
|
||||||
baseLocaleFallback: ["tools.*.title", "tools.*.description"],
|
baseLocaleFallback: ["pages.*.title", "pages.*.description"],
|
||||||
ignoreMissingPatterns: ["tools.*.params"],
|
ignoreMissingPatterns: ["tools.*.params"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -16,9 +16,14 @@ export function isLocale(value: unknown): value is Locale {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ToolStrings = {
|
/** Тексты страницы (`pages.<slug>`): адрес, имя файла и иконка — по slug. */
|
||||||
|
export type PageStrings = {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Тексты инструмента (`tools.<id>`): подписи полей, verdict-ключи. */
|
||||||
|
export type ToolStrings = {
|
||||||
/** Подписи параметров по их id. */
|
/** Подписи параметров по их id. */
|
||||||
params?: Record<string, string>;
|
params?: Record<string, string>;
|
||||||
/** Подписи опций select: paramId -> value -> label. */
|
/** Подписи опций select: paramId -> value -> label. */
|
||||||
@@ -54,6 +59,8 @@ export type Dict = {
|
|||||||
search: Record<string, string>;
|
search: Record<string, string>;
|
||||||
ui: Record<string, string>;
|
ui: Record<string, string>;
|
||||||
errors: Record<string, string>;
|
errors: Record<string, string>;
|
||||||
|
/** Заголовки и описания страниц; в базовой локали их нет — берётся EN из реестра. */
|
||||||
|
pages?: Record<string, PageStrings>;
|
||||||
tools: Record<string, ToolStrings>;
|
tools: Record<string, ToolStrings>;
|
||||||
/** Действия панели результата/генерации (Generate, Open image…). */
|
/** Действия панели результата/генерации (Generate, Open image…). */
|
||||||
actions?: Record<string, string>;
|
actions?: Record<string, string>;
|
||||||
|
|||||||
@@ -334,6 +334,9 @@ export const en: Dict = {
|
|||||||
tile: "Tile",
|
tile: "Tile",
|
||||||
watermark: "Watermark",
|
watermark: "Watermark",
|
||||||
},
|
},
|
||||||
|
// Заголовки и описания страниц в базовой локали не дублируются: их берёт
|
||||||
|
// EN-строка из реестра страниц (`web/src/lib/registry/pages/`).
|
||||||
|
pages: {},
|
||||||
tools: {
|
tools: {
|
||||||
"png-file-size": {
|
"png-file-size": {
|
||||||
results: {
|
results: {
|
||||||
|
|||||||
+527
-314
File diff suppressed because it is too large
Load Diff
@@ -13,11 +13,11 @@ function labelOf(id: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function pageTitle(page: Page): string {
|
export function pageTitle(page: Page): string {
|
||||||
return getMergedDict().tools[page.slug]?.title ?? page.title;
|
return getMergedDict().pages?.[page.slug]?.title ?? page.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pageDescription(page: Page): string {
|
export function pageDescription(page: Page): string {
|
||||||
return getMergedDict().tools[page.slug]?.description ?? page.description;
|
return getMergedDict().pages?.[page.slug]?.description ?? page.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fieldLabel(field: Field<unknown>, id: string): string {
|
export function fieldLabel(field: Field<unknown>, id: string): string {
|
||||||
@@ -65,17 +65,17 @@ function dedupe(values: string[]): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function pageSearchDoc(page: Page): SearchDoc {
|
export function pageSearchDoc(page: Page): SearchDoc {
|
||||||
const active = getMergedDict().tools[page.slug];
|
const active = getMergedDict().pages?.[page.slug];
|
||||||
return {
|
return {
|
||||||
id: page.slug,
|
id: page.slug,
|
||||||
titles: dedupe([
|
titles: dedupe([
|
||||||
active?.title ?? "",
|
active?.title ?? "",
|
||||||
ru.tools[page.slug]?.title ?? "",
|
ru.pages?.[page.slug]?.title ?? "",
|
||||||
page.title,
|
page.title,
|
||||||
]),
|
]),
|
||||||
descriptions: dedupe([
|
descriptions: dedupe([
|
||||||
active?.description ?? "",
|
active?.description ?? "",
|
||||||
ru.tools[page.slug]?.description ?? "",
|
ru.pages?.[page.slug]?.description ?? "",
|
||||||
page.description,
|
page.description,
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ function selectOptions(
|
|||||||
describe("полнота словарей для нового registry", () => {
|
describe("полнота словарей для нового registry", () => {
|
||||||
it("у каждой страницы есть перевод ru с непустыми title/description", () => {
|
it("у каждой страницы есть перевод ru с непустыми title/description", () => {
|
||||||
for (const page of PAGES) {
|
for (const page of PAGES) {
|
||||||
const strings = ru.tools[page.slug];
|
const strings = ru.pages?.[page.slug];
|
||||||
expect(strings, `нет перевода ru для ${page.slug}`).toBeDefined();
|
expect(strings, `нет перевода ru для ${page.slug}`).toBeDefined();
|
||||||
expect(strings?.title, `${page.slug}: title`).toBeTruthy();
|
expect(strings?.title, `${page.slug}: title`).toBeTruthy();
|
||||||
expect(strings?.description, `${page.slug}: description`).toBeTruthy();
|
expect(strings?.description, `${page.slug}: description`).toBeTruthy();
|
||||||
@@ -75,6 +75,20 @@ describe("полнота словарей для нового registry", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("в секции pages нет лишних страниц", () => {
|
||||||
|
const slugs = new Set(PAGES.map((page) => page.slug));
|
||||||
|
for (const [locale, dict] of [
|
||||||
|
["ru", ru],
|
||||||
|
["en", en],
|
||||||
|
] as const) {
|
||||||
|
for (const key of Object.keys(dict.pages ?? {})) {
|
||||||
|
expect(slugs.has(key), `лишняя страница в ${locale}: ${key}`).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("в словарях нет ключей без записи в реестре", () => {
|
it("в словарях нет ключей без записи в реестре", () => {
|
||||||
const known = new Set([
|
const known = new Set([
|
||||||
...TOOLS.map((tool) => tool.id),
|
...TOOLS.map((tool) => tool.id),
|
||||||
|
|||||||
Reference in New Issue
Block a user