mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-09-14 21:46:35 +00:00
style: fix formatting via prettier
This commit is contained in:
@@ -1,17 +1,19 @@
|
||||
import type { CategoryId } from '../categories';
|
||||
import type { CategoryId } from "../categories";
|
||||
|
||||
export const LOCALES = ['ru', 'en'] as const;
|
||||
export const LOCALES = ["ru", "en"] as const;
|
||||
export type Locale = (typeof LOCALES)[number];
|
||||
|
||||
export const BASE_LOCALE: Locale = 'en';
|
||||
export const BASE_LOCALE: Locale = "en";
|
||||
|
||||
export const LOCALE_TAGS: Record<Locale, string> = {
|
||||
ru: 'ru-RU',
|
||||
en: 'en-US'
|
||||
ru: "ru-RU",
|
||||
en: "en-US",
|
||||
};
|
||||
|
||||
export function isLocale(value: unknown): value is Locale {
|
||||
return typeof value === 'string' && (LOCALES as readonly string[]).includes(value);
|
||||
return (
|
||||
typeof value === "string" && (LOCALES as readonly string[]).includes(value)
|
||||
);
|
||||
}
|
||||
|
||||
export type ToolStrings = {
|
||||
|
||||
+130
-127
@@ -1,182 +1,185 @@
|
||||
import type { Dict } from './dict';
|
||||
import type { Dict } from "./dict";
|
||||
|
||||
export const en: Dict = {
|
||||
header: {
|
||||
workspace: 'Workspace',
|
||||
catalog: 'Catalog',
|
||||
sectionsAria: 'Sections',
|
||||
workspace: "Workspace",
|
||||
catalog: "Catalog",
|
||||
sectionsAria: "Sections",
|
||||
footerNote:
|
||||
'All operations run locally in your browser — your files are never uploaded anywhere.'
|
||||
"All operations run locally in your browser — your files are never uploaded anywhere.",
|
||||
},
|
||||
categories: {
|
||||
convert: 'Convert',
|
||||
alpha: 'Transparency',
|
||||
color: 'Color',
|
||||
geometry: 'Geometry',
|
||||
filters: 'Filters',
|
||||
text: 'Text',
|
||||
analyze: 'Analyze',
|
||||
generate: 'Generate'
|
||||
convert: "Convert",
|
||||
alpha: "Transparency",
|
||||
color: "Color",
|
||||
geometry: "Geometry",
|
||||
filters: "Filters",
|
||||
text: "Text",
|
||||
analyze: "Analyze",
|
||||
generate: "Generate",
|
||||
},
|
||||
home: {
|
||||
defaultTitle: 'easy-png-tools — PNG utilities right in your browser',
|
||||
heroTitle: 'What do you want to do with the image?',
|
||||
heroLead: 'Find a tool — everything runs locally in your browser.',
|
||||
restoreLast: '↩ Restore last: {title}',
|
||||
changeTool: '← Change tool'
|
||||
defaultTitle: "easy-png-tools — PNG utilities right in your browser",
|
||||
heroTitle: "What do you want to do with the image?",
|
||||
heroLead: "Find a tool — everything runs locally in your browser.",
|
||||
restoreLast: "↩ Restore last: {title}",
|
||||
changeTool: "← Change tool",
|
||||
},
|
||||
catalog: {
|
||||
pageTitle: 'All tools — easy-png-tools',
|
||||
pageTitle: "All tools — easy-png-tools",
|
||||
metaDescription:
|
||||
'Full catalog of PNG utilities: convert, transparency, color, geometry, analysis and image generation.',
|
||||
heading: 'Tool catalog',
|
||||
lead: '{count} utilities for working with PNG. Everything runs locally in your browser.'
|
||||
"Full catalog of PNG utilities: convert, transparency, color, geometry, analysis and image generation.",
|
||||
heading: "Tool catalog",
|
||||
lead: "{count} utilities for working with PNG. Everything runs locally in your browser.",
|
||||
},
|
||||
toolPage: {
|
||||
fallbackTitle: 'Tool',
|
||||
legendSource: 'Source',
|
||||
legendSummary: 'Summary',
|
||||
legendResult: 'Result',
|
||||
legendParams: 'Parameters',
|
||||
stepHeading: 'Step {n}',
|
||||
removeStepAria: 'Remove step',
|
||||
stepError: 'Step {n} ({title}): {msg}'
|
||||
fallbackTitle: "Tool",
|
||||
legendSource: "Source",
|
||||
legendSummary: "Summary",
|
||||
legendResult: "Result",
|
||||
legendParams: "Parameters",
|
||||
stepHeading: "Step {n}",
|
||||
removeStepAria: "Remove step",
|
||||
stepError: "Step {n} ({title}): {msg}",
|
||||
},
|
||||
chain: {
|
||||
stepLabel: 'Step {n}: {title}',
|
||||
inputLegend: 'Input',
|
||||
resultLegend: 'Result',
|
||||
paramsLegend: 'Parameters',
|
||||
busyTitle: 'Processing…',
|
||||
busyHint: 'Running chain step',
|
||||
removeStepAria: 'Remove step'
|
||||
stepLabel: "Step {n}: {title}",
|
||||
inputLegend: "Input",
|
||||
resultLegend: "Result",
|
||||
paramsLegend: "Parameters",
|
||||
busyTitle: "Processing…",
|
||||
busyHint: "Running chain step",
|
||||
removeStepAria: "Remove step",
|
||||
},
|
||||
sourceCard: {
|
||||
replaceImage: 'Replace image'
|
||||
replaceImage: "Replace image",
|
||||
},
|
||||
resultCard: {
|
||||
emptyTitle: 'The result will appear here',
|
||||
emptyHint: 'First upload a source image on the left',
|
||||
processingTitle: 'Processing…',
|
||||
processingHint: 'The image is being processed, this will take a moment',
|
||||
recalc: 'Recalculating…',
|
||||
nextTool: '⛓ Next tool',
|
||||
breakChain: '✂ Break the chain'
|
||||
emptyTitle: "The result will appear here",
|
||||
emptyHint: "First upload a source image on the left",
|
||||
processingTitle: "Processing…",
|
||||
processingHint: "The image is being processed, this will take a moment",
|
||||
recalc: "Recalculating…",
|
||||
nextTool: "⛓ Next tool",
|
||||
breakChain: "✂ Break the chain",
|
||||
},
|
||||
paramsCard: {
|
||||
noParams: 'This tool has no parameters — the result is ready as is.'
|
||||
noParams: "This tool has no parameters — the result is ready as is.",
|
||||
},
|
||||
textInput: {
|
||||
heading: 'Text',
|
||||
placeholder: 'Paste data here',
|
||||
aria: 'Text data',
|
||||
decode: 'Decode'
|
||||
heading: "Text",
|
||||
placeholder: "Paste data here",
|
||||
aria: "Text data",
|
||||
decode: "Decode",
|
||||
},
|
||||
textResult: {
|
||||
outputAria: 'Text result',
|
||||
copied: 'Copied',
|
||||
copy: 'Copy',
|
||||
downloadTxt: 'Download .txt'
|
||||
outputAria: "Text result",
|
||||
copied: "Copied",
|
||||
copy: "Copy",
|
||||
downloadTxt: "Download .txt",
|
||||
},
|
||||
download: {
|
||||
busy: 'Preparing file…',
|
||||
file: 'Download .{ext}'
|
||||
busy: "Preparing file…",
|
||||
file: "Download .{ext}",
|
||||
},
|
||||
infoPanel: {
|
||||
dimensions: 'Dimensions',
|
||||
alpha: 'Alpha channel',
|
||||
alphaYes: 'yes — there are semi-transparent pixels',
|
||||
alphaNo: 'no',
|
||||
colorCount: 'Unique colors (RGBA)'
|
||||
dimensions: "Dimensions",
|
||||
alpha: "Alpha channel",
|
||||
alphaYes: "yes — there are semi-transparent pixels",
|
||||
alphaNo: "no",
|
||||
colorCount: "Unique colors (RGBA)",
|
||||
},
|
||||
dropZone: {
|
||||
pickDefault: 'Drop an image here or click to choose a file',
|
||||
overlayDefault: 'Release the file to replace the image'
|
||||
pickDefault: "Drop an image here or click to choose a file",
|
||||
overlayDefault: "Release the file to replace the image",
|
||||
},
|
||||
search: {
|
||||
placeholder: 'Find a tool…',
|
||||
aria: 'Search tools',
|
||||
nothingFound: 'Nothing found — try another word.'
|
||||
placeholder: "Find a tool…",
|
||||
aria: "Search tools",
|
||||
nothingFound: "Nothing found — try another word.",
|
||||
},
|
||||
ui: {
|
||||
showMask: 'Show mask',
|
||||
decrease: 'Decrease',
|
||||
increase: 'Increase',
|
||||
reset: 'Reset',
|
||||
pipette: 'Eyedropper',
|
||||
themeLight: 'Light theme',
|
||||
themeDark: 'Dark theme',
|
||||
overlayTitle: 'Watermark',
|
||||
overlayDrop: 'Drop a watermark PNG or click',
|
||||
overlayRemove: 'Remove watermark'
|
||||
showMask: "Show mask",
|
||||
decrease: "Decrease",
|
||||
increase: "Increase",
|
||||
reset: "Reset",
|
||||
pipette: "Eyedropper",
|
||||
themeLight: "Light theme",
|
||||
themeDark: "Dark theme",
|
||||
overlayTitle: "Watermark",
|
||||
overlayDrop: "Drop a watermark PNG or click",
|
||||
overlayRemove: "Remove watermark",
|
||||
},
|
||||
errors: {
|
||||
noImageRun: 'This tool does not process images',
|
||||
workerFailed: 'Worker execution failed',
|
||||
workerUnavailable: 'Worker is unavailable',
|
||||
notFound: 'Tool not found',
|
||||
noWatermark: 'Pick a watermark image first',
|
||||
badTransform: 'Degenerate transformation matrix',
|
||||
skewAngle: 'Skew angles cannot be 90° or -90°',
|
||||
noImageRun: "This tool does not process images",
|
||||
workerFailed: "Worker execution failed",
|
||||
workerUnavailable: "Worker is unavailable",
|
||||
notFound: "Tool not found",
|
||||
noWatermark: "Pick a watermark image first",
|
||||
badTransform: "Degenerate transformation matrix",
|
||||
skewAngle: "Skew angles cannot be 90° or -90°",
|
||||
badHex: 'Invalid HEX color: "{value}"',
|
||||
radiusInt: 'Radius must be a non-negative integer',
|
||||
kernelSize: 'Kernel does not match the image dimensions',
|
||||
sizeInt: 'Width and height must be integers ≥ 1',
|
||||
cropBounds: 'Crop area does not intersect the image',
|
||||
noCanvasCtx: 'Canvas 2D context is unavailable in this environment',
|
||||
badBase64: 'Expected a base64 string or a data-uri of an image',
|
||||
qualityRange: 'quality must be within 0..1',
|
||||
svgSize: 'Could not determine SVG dimensions',
|
||||
svgLoad: 'Failed to load SVG — check the markup',
|
||||
encodeUnsupported: 'The browser does not support encoding to {mime}',
|
||||
radiusInt: "Radius must be a non-negative integer",
|
||||
kernelSize: "Kernel does not match the image dimensions",
|
||||
sizeInt: "Width and height must be integers ≥ 1",
|
||||
cropBounds: "Crop area does not intersect the image",
|
||||
noCanvasCtx: "Canvas 2D context is unavailable in this environment",
|
||||
badBase64: "Expected a base64 string or a data-uri of an image",
|
||||
qualityRange: "quality must be within 0..1",
|
||||
svgSize: "Could not determine SVG dimensions",
|
||||
svgLoad: "Failed to load SVG — check the markup",
|
||||
encodeUnsupported: "The browser does not support encoding to {mime}",
|
||||
unsupportedFile:
|
||||
'Unsupported file format ({type}). Supported formats are PNG, JPEG, WebP, GIF and BMP.',
|
||||
widthInt: 'Image width must be an integer ≥ 1',
|
||||
noHexPixels: 'No hex pixel values found',
|
||||
badPixelToken: 'Each pixel must be 8 hex characters RRGGBBAA, separated by spaces',
|
||||
pixelCountMismatch: 'Pixel count ({count}) is not divisible by width {width} without a remainder',
|
||||
"Unsupported file format ({type}). Supported formats are PNG, JPEG, WebP, GIF and BMP.",
|
||||
widthInt: "Image width must be an integer ≥ 1",
|
||||
noHexPixels: "No hex pixel values found",
|
||||
badPixelToken:
|
||||
"Each pixel must be 8 hex characters RRGGBBAA, separated by spaces",
|
||||
pixelCountMismatch:
|
||||
"Pixel count ({count}) is not divisible by width {width} without a remainder",
|
||||
toolNotFound: 'Tool "{id}" not found',
|
||||
badJson: 'The file is not valid JSON',
|
||||
badPipelineShape: 'The file structure does not look like a chain of steps',
|
||||
pipelineVersion: 'Unsupported chain version: {version}',
|
||||
noSteps: 'The file has no list of steps',
|
||||
badJson: "The file is not valid JSON",
|
||||
badPipelineShape: "The file structure does not look like a chain of steps",
|
||||
pipelineVersion: "Unsupported chain version: {version}",
|
||||
noSteps: "The file has no list of steps",
|
||||
paramNumber: 'Parameter "{id}" must be a number',
|
||||
paramString: 'Parameter "{id}" must be a string',
|
||||
paramBool: 'Parameter "{id}" must be a checkbox value',
|
||||
resizeSize: 'Width and/or height must be positive',
|
||||
cropSize: 'Crop width and height must be positive',
|
||||
sizePositive: 'Dimensions must be positive and finite'
|
||||
resizeSize: "Width and/or height must be positive",
|
||||
cropSize: "Crop width and height must be positive",
|
||||
sizePositive: "Dimensions must be positive and finite",
|
||||
},
|
||||
tools: {
|
||||
'png-file-size': {
|
||||
"png-file-size": {
|
||||
results: {
|
||||
line: 'PNG size: {kb} KB'
|
||||
}
|
||||
line: "PNG size: {kb} KB",
|
||||
},
|
||||
},
|
||||
'verify-is-png': {
|
||||
"verify-is-png": {
|
||||
results: {
|
||||
verifyYes: 'Yes — this is a valid PNG signature.',
|
||||
verifyNo: 'No — the signature does not match a PNG file.'
|
||||
}
|
||||
verifyYes: "Yes — this is a valid PNG signature.",
|
||||
verifyNo: "No — the signature does not match a PNG file.",
|
||||
},
|
||||
},
|
||||
'png-is-grayscale': {
|
||||
"png-is-grayscale": {
|
||||
results: {
|
||||
grayscaleYes: 'Yes — all pixels are shades of gray.',
|
||||
grayscaleNo: 'No — colored pixels were found.'
|
||||
}
|
||||
grayscaleYes: "Yes — all pixels are shades of gray.",
|
||||
grayscaleNo: "No — colored pixels were found.",
|
||||
},
|
||||
},
|
||||
'png-is-transparent': {
|
||||
"png-is-transparent": {
|
||||
results: {
|
||||
transparentYes: 'Yes — there are transparent or semi-transparent pixels.',
|
||||
transparentNo: 'No — all pixels are fully opaque.'
|
||||
}
|
||||
transparentYes:
|
||||
"Yes — there are transparent or semi-transparent pixels.",
|
||||
transparentNo: "No — all pixels are fully opaque.",
|
||||
},
|
||||
},
|
||||
'png-orientation': {
|
||||
"png-orientation": {
|
||||
results: {
|
||||
orientationPortrait: 'Portrait — height is greater than width.',
|
||||
orientationLandscape: 'Landscape — width is greater than height.',
|
||||
orientationSquare: 'Square — the sides are equal.'
|
||||
}
|
||||
}
|
||||
}
|
||||
orientationPortrait: "Portrait — height is greater than width.",
|
||||
orientationLandscape: "Landscape — width is greater than height.",
|
||||
orientationSquare: "Square — the sides are equal.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,80 +1,82 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { getLocale, initLocale, setLocale } from './locale.svelte';
|
||||
import { interpolate, t } from './t';
|
||||
import { en } from './en';
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { getLocale, initLocale, setLocale } from "./locale.svelte";
|
||||
import { interpolate, t } from "./t";
|
||||
import { en } from "./en";
|
||||
|
||||
type Store = Record<string, string>;
|
||||
|
||||
function stubStorage(): { store: Store } {
|
||||
const store: Store = {};
|
||||
vi.stubGlobal('localStorage', {
|
||||
vi.stubGlobal("localStorage", {
|
||||
getItem: (k: string) => (k in store ? store[k] : null),
|
||||
setItem: (k: string, v: string) => {
|
||||
store[k] = v;
|
||||
}
|
||||
},
|
||||
});
|
||||
vi.stubGlobal('window', {});
|
||||
vi.stubGlobal("window", {});
|
||||
return { store };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setLocale('ru');
|
||||
setLocale("ru");
|
||||
delete (en.home as Record<string, string>).onlyEnKey;
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe('t', () => {
|
||||
it('возвращает строку по точечному пути активной локали', () => {
|
||||
expect(t('header.workspace')).toBe('Рабочая область');
|
||||
setLocale('en');
|
||||
expect(t('header.catalog')).toBe('Catalog');
|
||||
describe("t", () => {
|
||||
it("возвращает строку по точечному пути активной локали", () => {
|
||||
expect(t("header.workspace")).toBe("Рабочая область");
|
||||
setLocale("en");
|
||||
expect(t("header.catalog")).toBe("Catalog");
|
||||
});
|
||||
|
||||
it('фолбэк на английскую базу, если в активной локали нет ключа', () => {
|
||||
(en.home as Record<string, string>).onlyEnKey = 'Only English string';
|
||||
setLocale('ru');
|
||||
expect(t('home.onlyEnKey')).toBe('Only English string');
|
||||
it("фолбэк на английскую базу, если в активной локали нет ключа", () => {
|
||||
(en.home as Record<string, string>).onlyEnKey = "Only English string";
|
||||
setLocale("ru");
|
||||
expect(t("home.onlyEnKey")).toBe("Only English string");
|
||||
});
|
||||
|
||||
it('неизвестный путь возвращает сам путь', () => {
|
||||
expect(t('no.such.key')).toBe('no.such.key');
|
||||
it("неизвестный путь возвращает сам путь", () => {
|
||||
expect(t("no.such.key")).toBe("no.such.key");
|
||||
});
|
||||
});
|
||||
|
||||
describe('interpolate', () => {
|
||||
it('подставляет переменные в шаблон', () => {
|
||||
expect(interpolate('Шаг {n} из {total}', { n: 2, total: 5 })).toBe('Шаг 2 из 5');
|
||||
describe("interpolate", () => {
|
||||
it("подставляет переменные в шаблон", () => {
|
||||
expect(interpolate("Шаг {n} из {total}", { n: 2, total: 5 })).toBe(
|
||||
"Шаг 2 из 5",
|
||||
);
|
||||
});
|
||||
|
||||
it('оставляет плейсхолдер без переменной как есть', () => {
|
||||
expect(interpolate('Привет, {name}!', {})).toBe('Привет, {name}!');
|
||||
it("оставляет плейсхолдер без переменной как есть", () => {
|
||||
expect(interpolate("Привет, {name}!", {})).toBe("Привет, {name}!");
|
||||
});
|
||||
|
||||
it('без переменных возвращает строку без изменений', () => {
|
||||
expect(interpolate('Просто текст')).toBe('Просто текст');
|
||||
it("без переменных возвращает строку без изменений", () => {
|
||||
expect(interpolate("Просто текст")).toBe("Просто текст");
|
||||
});
|
||||
});
|
||||
|
||||
describe('персист локали', () => {
|
||||
it('initLocale читает сохранённый выбор', () => {
|
||||
describe("персист локали", () => {
|
||||
it("initLocale читает сохранённый выбор", () => {
|
||||
const { store } = stubStorage();
|
||||
store['locale'] = 'en';
|
||||
store["locale"] = "en";
|
||||
initLocale();
|
||||
expect(getLocale()).toBe('en');
|
||||
expect(getLocale()).toBe("en");
|
||||
});
|
||||
|
||||
it('initLocale игнорирует мусор в хранилище', () => {
|
||||
it("initLocale игнорирует мусор в хранилище", () => {
|
||||
const { store } = stubStorage();
|
||||
store['locale'] = 'fr';
|
||||
store["locale"] = "fr";
|
||||
initLocale();
|
||||
expect(getLocale()).toBe('ru');
|
||||
expect(getLocale()).toBe("ru");
|
||||
});
|
||||
|
||||
it('setLocale сохраняет выбор в localStorage', () => {
|
||||
it("setLocale сохраняет выбор в localStorage", () => {
|
||||
const { store } = stubStorage();
|
||||
initLocale();
|
||||
setLocale('en');
|
||||
expect(store['locale']).toBe('en');
|
||||
expect(getLocale()).toBe('en');
|
||||
setLocale("en");
|
||||
expect(store["locale"]).toBe("en");
|
||||
expect(getLocale()).toBe("en");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { BASE_LOCALE, isLocale, type Dict, type Locale } from './dict';
|
||||
import { ru } from './ru';
|
||||
import { en } from './en';
|
||||
import { BASE_LOCALE, isLocale, type Dict, type Locale } from "./dict";
|
||||
import { ru } from "./ru";
|
||||
import { en } from "./en";
|
||||
|
||||
const STORAGE_KEY = 'locale';
|
||||
const STORAGE_KEY = "locale";
|
||||
|
||||
const DICTS: Record<Locale, Dict> = { ru, en };
|
||||
|
||||
let locale = $state<Locale>(BASE_LOCALE);
|
||||
|
||||
function storage(): Storage | null {
|
||||
return typeof localStorage === 'undefined' ? null : localStorage;
|
||||
return typeof localStorage === "undefined" ? null : localStorage;
|
||||
}
|
||||
|
||||
export function getLocale(): Locale {
|
||||
@@ -29,13 +29,11 @@ export function getMergedDict(): Dict {
|
||||
get(_target, section: string) {
|
||||
const a = (active as unknown as Record<string, unknown>)[section];
|
||||
const b = (base as unknown as Record<string, unknown>)[section];
|
||||
if (
|
||||
a && b && typeof a === 'object' && typeof b === 'object'
|
||||
) {
|
||||
if (a && b && typeof a === "object" && typeof b === "object") {
|
||||
return { ...(b as object), ...(a as object) };
|
||||
}
|
||||
return a ?? b;
|
||||
}
|
||||
},
|
||||
}) as Dict;
|
||||
}
|
||||
|
||||
@@ -47,14 +45,14 @@ export function setLocale(next: Locale): void {
|
||||
|
||||
/** Читает сохранённый выбор и синхронизирует атрибут lang. Вызывается на клиенте. */
|
||||
export function initLocale(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
if (typeof window === "undefined") return;
|
||||
const saved = storage()?.getItem(STORAGE_KEY);
|
||||
if (isLocale(saved)) locale = saved;
|
||||
syncLangAttr();
|
||||
}
|
||||
|
||||
function syncLangAttr(): void {
|
||||
if (typeof document !== 'undefined') {
|
||||
if (typeof document !== "undefined") {
|
||||
document.documentElement.lang = locale;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,94 +1,101 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { TOOLS } from '../registry';
|
||||
import { normalizeForSearch, scoreDoc, type SearchDoc } from './matching';
|
||||
import { setLocale } from './locale.svelte';
|
||||
import { toolSearchDoc } from './tool-strings';
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { TOOLS } from "../registry";
|
||||
import { normalizeForSearch, scoreDoc, type SearchDoc } from "./matching";
|
||||
import { setLocale } from "./locale.svelte";
|
||||
import { toolSearchDoc } from "./tool-strings";
|
||||
|
||||
describe('normalizeForSearch', () => {
|
||||
it('нижний регистр', () => {
|
||||
expect(normalizeForSearch('Rotate PNG')).toBe('rotate png');
|
||||
describe("normalizeForSearch", () => {
|
||||
it("нижний регистр", () => {
|
||||
expect(normalizeForSearch("Rotate PNG")).toBe("rotate png");
|
||||
});
|
||||
|
||||
it('ё заменяется на е', () => {
|
||||
expect(normalizeForSearch('Ёлка и ёж')).toBe('елка и еж');
|
||||
it("ё заменяется на е", () => {
|
||||
expect(normalizeForSearch("Ёлка и ёж")).toBe("елка и еж");
|
||||
});
|
||||
|
||||
it('диакритика снимается через NFD', () => {
|
||||
expect(normalizeForSearch('café')).toBe('cafe');
|
||||
expect(normalizeForSearch('Über')).toBe('uber');
|
||||
it("диакритика снимается через NFD", () => {
|
||||
expect(normalizeForSearch("café")).toBe("cafe");
|
||||
expect(normalizeForSearch("Über")).toBe("uber");
|
||||
});
|
||||
});
|
||||
|
||||
describe('scoreDoc', () => {
|
||||
describe("scoreDoc", () => {
|
||||
const doc: SearchDoc = {
|
||||
id: 'rotate-free-png',
|
||||
titles: ['Повернуть на произвольный угол'],
|
||||
descriptions: ['Поворот на любой угол. Холст расширяется под новые габариты.']
|
||||
id: "rotate-free-png",
|
||||
titles: ["Повернуть на произвольный угол"],
|
||||
descriptions: [
|
||||
"Поворот на любой угол. Холст расширяется под новые габариты.",
|
||||
],
|
||||
};
|
||||
|
||||
it('пустой запрос даёт нейтральный балл', () => {
|
||||
expect(scoreDoc(doc, '')).toBe(1);
|
||||
it("пустой запрос даёт нейтральный балл", () => {
|
||||
expect(scoreDoc(doc, "")).toBe(1);
|
||||
});
|
||||
|
||||
it('префикс названия ценнее подстроки', () => {
|
||||
const prefix = scoreDoc(doc, normalizeForSearch('повер'));
|
||||
const infix = scoreDoc(doc, normalizeForSearch('верну'));
|
||||
it("префикс названия ценнее подстроки", () => {
|
||||
const prefix = scoreDoc(doc, normalizeForSearch("повер"));
|
||||
const infix = scoreDoc(doc, normalizeForSearch("верну"));
|
||||
expect(prefix!).toBe(100);
|
||||
expect(infix!).toBeGreaterThan(30);
|
||||
});
|
||||
|
||||
it('подстрока названия ценнее совпадения в описании', () => {
|
||||
const title = scoreDoc(doc, normalizeForSearch('угол'));
|
||||
const desc = scoreDoc(doc, normalizeForSearch('габариты'));
|
||||
it("подстрока названия ценнее совпадения в описании", () => {
|
||||
const title = scoreDoc(doc, normalizeForSearch("угол"));
|
||||
const desc = scoreDoc(doc, normalizeForSearch("габариты"));
|
||||
expect(title!).toBeGreaterThan(desc!);
|
||||
});
|
||||
|
||||
it('нет совпадения — null', () => {
|
||||
expect(scoreDoc(doc, normalizeForSearch('квант'))).toBeNull();
|
||||
it("нет совпадения — null", () => {
|
||||
expect(scoreDoc(doc, normalizeForSearch("квант"))).toBeNull();
|
||||
});
|
||||
|
||||
it('запрос с опечаткой ё/е всё равно находит', () => {
|
||||
expect(scoreDoc(doc, normalizeForSearch('повёрнут'))).not.toBeNull();
|
||||
it("запрос с опечаткой ё/е всё равно находит", () => {
|
||||
expect(scoreDoc(doc, normalizeForSearch("повёрнут"))).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('кросс-языковой поиск на реальном реестре', () => {
|
||||
it('английский запрос находит инструмент при русской локали', () => {
|
||||
setLocale('ru');
|
||||
describe("кросс-языковой поиск на реальном реестре", () => {
|
||||
it("английский запрос находит инструмент при русской локали", () => {
|
||||
setLocale("ru");
|
||||
const hits = TOOLS.filter((tool) => {
|
||||
const s = scoreDoc(toolSearchDoc(tool), normalizeForSearch('rotate'));
|
||||
const s = scoreDoc(toolSearchDoc(tool), normalizeForSearch("rotate"));
|
||||
return s !== null && s > 0;
|
||||
}).map((tool) => tool.id);
|
||||
expect(hits).toContain('rotate-png');
|
||||
expect(hits).toContain('rotate-free-png');
|
||||
expect(hits).toContain("rotate-png");
|
||||
expect(hits).toContain("rotate-free-png");
|
||||
});
|
||||
|
||||
it('русский запрос находит инструмент при английской локали', () => {
|
||||
setLocale('en');
|
||||
it("русский запрос находит инструмент при английской локали", () => {
|
||||
setLocale("en");
|
||||
try {
|
||||
for (const q of ['повернуть', 'пово']) {
|
||||
for (const q of ["повернуть", "пово"]) {
|
||||
const hits = TOOLS.filter((tool) => {
|
||||
const s = scoreDoc(toolSearchDoc(tool), normalizeForSearch(q));
|
||||
return s !== null && s > 0;
|
||||
}).map((tool) => tool.id);
|
||||
expect(hits, `запрос «${q}» при en-локали`).toContain('rotate-png');
|
||||
expect(hits, `запрос «${q}» при en-локали`).toContain('rotate-free-png');
|
||||
expect(hits, `запрос «${q}» при en-локали`).toContain("rotate-png");
|
||||
expect(hits, `запрос «${q}» при en-локали`).toContain(
|
||||
"rotate-free-png",
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setLocale('ru');
|
||||
setLocale("ru");
|
||||
}
|
||||
});
|
||||
|
||||
it('поиск работает и по описанию, не только по названию', () => {
|
||||
setLocale('en');
|
||||
it("поиск работает и по описанию, не только по названию", () => {
|
||||
setLocale("en");
|
||||
try {
|
||||
const hits = TOOLS.filter((tool) => {
|
||||
const s = scoreDoc(toolSearchDoc(tool), normalizeForSearch('полупрозрачные'));
|
||||
const s = scoreDoc(
|
||||
toolSearchDoc(tool),
|
||||
normalizeForSearch("полупрозрачные"),
|
||||
);
|
||||
return s !== null && s > 0;
|
||||
}).map((tool) => tool.id);
|
||||
expect(hits).toContain('png-is-transparent');
|
||||
expect(hits).toContain("png-is-transparent");
|
||||
} finally {
|
||||
setLocale('ru');
|
||||
setLocale("ru");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
export function normalizeForSearch(value: string): string {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.replaceAll('ё', 'е')
|
||||
.normalize('NFD')
|
||||
.replace(/\p{M}/gu, '');
|
||||
.replaceAll("ё", "е")
|
||||
.normalize("NFD")
|
||||
.replace(/\p{M}/gu, "");
|
||||
}
|
||||
|
||||
export type SearchDoc = {
|
||||
@@ -48,7 +48,10 @@ function bestTitleScore(titles: string[], q: string): number | null {
|
||||
* Скоринг документа против нормализованного запроса.
|
||||
* Пустой запрос — нейтральный балл; отсутствие совпадения — null.
|
||||
*/
|
||||
export function scoreDoc(doc: SearchDoc, normalizedQuery: string): number | null {
|
||||
export function scoreDoc(
|
||||
doc: SearchDoc,
|
||||
normalizedQuery: string,
|
||||
): number | null {
|
||||
if (normalizedQuery.length === 0) return 1;
|
||||
if (normalizeForSearch(doc.id).includes(normalizedQuery)) {
|
||||
const at = normalizeForSearch(doc.id).indexOf(normalizedQuery);
|
||||
|
||||
+1136
-937
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { TOOLS } from '../registry';
|
||||
import { normalizeForSearch, scoreDoc } from './matching';
|
||||
import { toolSearchDoc } from './tool-strings';
|
||||
import { isChainable } from '../registry';
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { TOOLS } from "../registry";
|
||||
import { normalizeForSearch, scoreDoc } from "./matching";
|
||||
import { toolSearchDoc } from "./tool-strings";
|
||||
import { isChainable } from "../registry";
|
||||
|
||||
function hits(q: string): string[] {
|
||||
const nq = normalizeForSearch(q);
|
||||
@@ -12,13 +12,15 @@ function hits(q: string): string[] {
|
||||
}).map((t) => t.id);
|
||||
}
|
||||
|
||||
describe('полнота поиска', () => {
|
||||
it('генератор (не chainable) находится поиском — раньше отфильтровывался', () => {
|
||||
expect(isChainable(TOOLS.find((t) => t.id === 'color-wheel-png')!)).toBe(false);
|
||||
expect(hits('color wheel')).toContain('color-wheel-png');
|
||||
describe("полнота поиска", () => {
|
||||
it("генератор (не chainable) находится поиском — раньше отфильтровывался", () => {
|
||||
expect(isChainable(TOOLS.find((t) => t.id === "color-wheel-png")!)).toBe(
|
||||
false,
|
||||
);
|
||||
expect(hits("color wheel")).toContain("color-wheel-png");
|
||||
});
|
||||
|
||||
it('анализатор-маска тоже ищется', () => {
|
||||
expect(hits('уникальных цветов')).toContain('unique-color-mask-png');
|
||||
it("анализатор-маска тоже ищется", () => {
|
||||
expect(hits("уникальных цветов")).toContain("unique-color-mask-png");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,82 +1,96 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { setLocale } from './locale.svelte';
|
||||
import { t } from './t';
|
||||
import { LOCALE_TAGS } from './dict';
|
||||
import { normalizeForSearch, scoreDoc } from './matching';
|
||||
import { toolSearchDoc } from './tool-strings';
|
||||
import { TOOLS } from '../registry';
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { setLocale } from "./locale.svelte";
|
||||
import { t } from "./t";
|
||||
import { LOCALE_TAGS } from "./dict";
|
||||
import { normalizeForSearch, scoreDoc } from "./matching";
|
||||
import { toolSearchDoc } from "./tool-strings";
|
||||
import { TOOLS } from "../registry";
|
||||
|
||||
afterEach(() => {
|
||||
setLocale('ru');
|
||||
setLocale("ru");
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe('Смоук §6 i18n', () => {
|
||||
it('1. html lang следует за локалью', () => {
|
||||
const doc = { documentElement: { lang: '' } };
|
||||
vi.stubGlobal('document', doc);
|
||||
setLocale('en');
|
||||
expect(doc.documentElement.lang).toBe('en');
|
||||
setLocale('ru');
|
||||
expect(doc.documentElement.lang).toBe('ru');
|
||||
describe("Смоук §6 i18n", () => {
|
||||
it("1. html lang следует за локалью", () => {
|
||||
const doc = { documentElement: { lang: "" } };
|
||||
vi.stubGlobal("document", doc);
|
||||
setLocale("en");
|
||||
expect(doc.documentElement.lang).toBe("en");
|
||||
setLocale("ru");
|
||||
expect(doc.documentElement.lang).toBe("ru");
|
||||
});
|
||||
|
||||
it('3. Ключевые секции переведены без смеси языков', () => {
|
||||
it("3. Ключевые секции переведены без смеси языков", () => {
|
||||
const samples: Array<[string, string, string]> = [
|
||||
['header.workspace', 'Рабочая область', 'Workspace'],
|
||||
['catalog.heading', 'Каталог инструментов', 'Tool catalog'],
|
||||
['home.heroTitle', 'Что делаем с изображением?', 'What do you want to do'],
|
||||
['chain.inputLegend', 'Вход', 'Input'],
|
||||
['resultCard.nextTool', 'Следующий инструмент', 'Next tool'],
|
||||
['download.busy', 'Готовим файл', 'Preparing file']
|
||||
["header.workspace", "Рабочая область", "Workspace"],
|
||||
["catalog.heading", "Каталог инструментов", "Tool catalog"],
|
||||
[
|
||||
"home.heroTitle",
|
||||
"Что делаем с изображением?",
|
||||
"What do you want to do",
|
||||
],
|
||||
["chain.inputLegend", "Вход", "Input"],
|
||||
["resultCard.nextTool", "Следующий инструмент", "Next tool"],
|
||||
["download.busy", "Готовим файл", "Preparing file"],
|
||||
];
|
||||
for (const [key, ruPart, enPart] of samples) {
|
||||
setLocale('ru');
|
||||
expect(t(key), key + ' @ru').toContain(ruPart);
|
||||
setLocale('en');
|
||||
expect(t(key), key + ' @en').toContain(enPart);
|
||||
setLocale("ru");
|
||||
expect(t(key), key + " @ru").toContain(ruPart);
|
||||
setLocale("en");
|
||||
expect(t(key), key + " @en").toContain(enPart);
|
||||
}
|
||||
});
|
||||
|
||||
it('5. Ошибки с vars локализуются на оба языка', () => {
|
||||
setLocale('ru');
|
||||
expect(t('errors.badHex', { value: '#zz' })).toBe('Некорректный HEX-цвет: "#zz"');
|
||||
expect(t('errors.toolNotFound', { id: 'x' })).toContain('не найден');
|
||||
setLocale('en');
|
||||
expect(t('errors.badHex', { value: '#zz' })).toBe('Invalid HEX color: "#zz"');
|
||||
it("5. Ошибки с vars локализуются на оба языка", () => {
|
||||
setLocale("ru");
|
||||
expect(t("errors.badHex", { value: "#zz" })).toBe(
|
||||
'Некорректный HEX-цвет: "#zz"',
|
||||
);
|
||||
expect(t("errors.toolNotFound", { id: "x" })).toContain("не найден");
|
||||
setLocale("en");
|
||||
expect(t("errors.badHex", { value: "#zz" })).toBe(
|
||||
'Invalid HEX color: "#zz"',
|
||||
);
|
||||
});
|
||||
|
||||
it('6. Поиск кросс-языковой в обе стороны', () => {
|
||||
it("6. Поиск кросс-языковой в обе стороны", () => {
|
||||
const ids = (q: string) =>
|
||||
TOOLS.filter((tool) => {
|
||||
const s = scoreDoc(toolSearchDoc(tool), normalizeForSearch(q));
|
||||
return s !== null && s > 0;
|
||||
}).map((tool) => tool.id);
|
||||
|
||||
setLocale('ru');
|
||||
let hits = ids('rotate');
|
||||
expect(hits).toContain('rotate-png');
|
||||
hits = ids('повер');
|
||||
expect(hits).toContain('rotate-png');
|
||||
setLocale("ru");
|
||||
let hits = ids("rotate");
|
||||
expect(hits).toContain("rotate-png");
|
||||
hits = ids("повер");
|
||||
expect(hits).toContain("rotate-png");
|
||||
|
||||
setLocale('en');
|
||||
hits = ids('пово');
|
||||
expect(hits).toContain('rotate-free-png');
|
||||
hits = ids('rotate');
|
||||
expect(hits).toContain('rotate-png');
|
||||
setLocale("en");
|
||||
hits = ids("пово");
|
||||
expect(hits).toContain("rotate-free-png");
|
||||
hits = ids("rotate");
|
||||
expect(hits).toContain("rotate-png");
|
||||
});
|
||||
|
||||
it('6b. Ё не мешает совпадению', () => {
|
||||
const blackWhite = TOOLS.find((tool) => tool.id === 'black-and-white-png')!;
|
||||
setLocale('ru');
|
||||
const hit = scoreDoc(toolSearchDoc(blackWhite), normalizeForSearch('ЧЁРНО'));
|
||||
it("6b. Ё не мешает совпадению", () => {
|
||||
const blackWhite = TOOLS.find((tool) => tool.id === "black-and-white-png")!;
|
||||
setLocale("ru");
|
||||
const hit = scoreDoc(
|
||||
toolSearchDoc(blackWhite),
|
||||
normalizeForSearch("ЧЁРНО"),
|
||||
);
|
||||
expect(hit).not.toBeNull();
|
||||
const withoutYo = scoreDoc(toolSearchDoc(blackWhite), normalizeForSearch('черно'));
|
||||
const withoutYo = scoreDoc(
|
||||
toolSearchDoc(blackWhite),
|
||||
normalizeForSearch("черно"),
|
||||
);
|
||||
expect(withoutYo).not.toBeNull();
|
||||
});
|
||||
|
||||
it('7. Теги локалей для форматирования чисел корректны', () => {
|
||||
expect(LOCALE_TAGS.ru).toBe('ru-RU');
|
||||
expect(LOCALE_TAGS.en).toBe('en-US');
|
||||
it("7. Теги локалей для форматирования чисел корректны", () => {
|
||||
expect(LOCALE_TAGS.ru).toBe("ru-RU");
|
||||
expect(LOCALE_TAGS.en).toBe("en-US");
|
||||
});
|
||||
});
|
||||
|
||||
+17
-7
@@ -1,9 +1,12 @@
|
||||
import { getMergedDict } from './locale.svelte';
|
||||
import { getMergedDict } from "./locale.svelte";
|
||||
|
||||
export function interpolate(template: string, vars?: Record<string, string | number>): string {
|
||||
export function interpolate(
|
||||
template: string,
|
||||
vars?: Record<string, string | number>,
|
||||
): string {
|
||||
if (!vars) return template;
|
||||
return template.replace(/\{(\w+)\}/g, (match, name: string) =>
|
||||
name in vars ? String(vars[name]) : match
|
||||
name in vars ? String(vars[name]) : match,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,14 +14,21 @@ export function interpolate(template: string, vars?: Record<string, string | num
|
||||
* Перевод по точечному пути вида 'header.workspace' или 'errors.ERR_BAD_HEX'.
|
||||
* Сначала активная локаль, затем базовая; если ключа нет нигде — возвращается сам путь.
|
||||
*/
|
||||
export function t(path: string, vars?: Record<string, string | number>): string {
|
||||
export function t(
|
||||
path: string,
|
||||
vars?: Record<string, string | number>,
|
||||
): string {
|
||||
let node: unknown = getMergedDict();
|
||||
for (const part of path.split('.')) {
|
||||
if (node && typeof node === 'object' && part in (node as Record<string, unknown>)) {
|
||||
for (const part of path.split(".")) {
|
||||
if (
|
||||
node &&
|
||||
typeof node === "object" &&
|
||||
part in (node as Record<string, unknown>)
|
||||
) {
|
||||
node = (node as Record<string, unknown>)[part];
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
return typeof node === 'string' ? interpolate(node, vars) : path;
|
||||
return typeof node === "string" ? interpolate(node, vars) : path;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ParamDef, ToolEntry } from '$lib/registry';
|
||||
import type { SearchDoc } from './matching';
|
||||
import { getMergedDict } from './locale.svelte';
|
||||
import { ru } from './ru';
|
||||
import type { ParamDef, ToolEntry } from "$lib/registry";
|
||||
import type { SearchDoc } from "./matching";
|
||||
import { getMergedDict } from "./locale.svelte";
|
||||
import { ru } from "./ru";
|
||||
|
||||
/**
|
||||
* Строки инструмента на активной локали.
|
||||
@@ -23,10 +23,14 @@ export function paramLabel(tool: ToolEntry, param: ParamDef): string {
|
||||
return viaDef.label ?? param.id;
|
||||
}
|
||||
|
||||
export function optionLabel(tool: ToolEntry, param: ParamDef, value: string): string {
|
||||
export function optionLabel(
|
||||
tool: ToolEntry,
|
||||
param: ParamDef,
|
||||
value: string,
|
||||
): string {
|
||||
const viaDict = getMergedDict().tools[tool.id]?.options?.[param.id]?.[value];
|
||||
if (viaDict) return viaDict;
|
||||
if (param.type === 'select') {
|
||||
if (param.type === "select") {
|
||||
return param.options.find((o) => o.value === value)?.label ?? value;
|
||||
}
|
||||
return value;
|
||||
@@ -45,11 +49,15 @@ export function toolSearchDoc(tool: ToolEntry): SearchDoc {
|
||||
const active = getMergedDict().tools[tool.id];
|
||||
return {
|
||||
id: tool.id,
|
||||
titles: dedupe([active?.title ?? '', ru.tools[tool.id]?.title ?? '', tool.title]),
|
||||
titles: dedupe([
|
||||
active?.title ?? "",
|
||||
ru.tools[tool.id]?.title ?? "",
|
||||
tool.title,
|
||||
]),
|
||||
descriptions: dedupe([
|
||||
active?.description ?? '',
|
||||
ru.tools[tool.id]?.description ?? '',
|
||||
tool.description
|
||||
])
|
||||
active?.description ?? "",
|
||||
ru.tools[tool.id]?.description ?? "",
|
||||
tool.description,
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { TOOLS } from '../registry';
|
||||
import { en } from './en';
|
||||
import { ru } from './ru';
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { TOOLS } from "../registry";
|
||||
import { en } from "./en";
|
||||
import { ru } from "./ru";
|
||||
|
||||
describe('секция tools словарей покрывает реестр', () => {
|
||||
it('у каждого инструмента есть перевод ru с непустыми title/description', () => {
|
||||
describe("секция tools словарей покрывает реестр", () => {
|
||||
it("у каждого инструмента есть перевод ru с непустыми title/description", () => {
|
||||
for (const tool of TOOLS) {
|
||||
const strings = ru.tools[tool.id];
|
||||
expect(strings, `нет перевода для ${tool.id}`).toBeDefined();
|
||||
@@ -13,33 +13,39 @@ describe('секция tools словарей покрывает реестр',
|
||||
}
|
||||
});
|
||||
|
||||
it('переведены подписи всех параметров', () => {
|
||||
it("переведены подписи всех параметров", () => {
|
||||
for (const tool of TOOLS) {
|
||||
const params = ru.tools[tool.id]?.params ?? {};
|
||||
for (const param of tool.params) {
|
||||
const label = params[param.id];
|
||||
expect(label, `${tool.id}.${param.id}: нет перевода подписи`).toBeDefined();
|
||||
expect(
|
||||
label,
|
||||
`${tool.id}.${param.id}: нет перевода подписи`,
|
||||
).toBeDefined();
|
||||
expect(label?.length).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('переведены подписи всех опций select', () => {
|
||||
it("переведены подписи всех опций select", () => {
|
||||
for (const tool of TOOLS) {
|
||||
const options = ru.tools[tool.id]?.options ?? {};
|
||||
for (const param of tool.params) {
|
||||
if (param.type !== 'select') continue;
|
||||
if (param.type !== "select") continue;
|
||||
const labels = options[param.id] ?? {};
|
||||
for (const option of param.options) {
|
||||
const label = labels[option.value];
|
||||
expect(label, `${tool.id}.${param.id}[${option.value}]: нет перевода`).toBeDefined();
|
||||
expect(
|
||||
label,
|
||||
`${tool.id}.${param.id}[${option.value}]: нет перевода`,
|
||||
).toBeDefined();
|
||||
expect(label.length).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('в словарях нет лишних ключей инструментов и параметров', () => {
|
||||
it("в словарях нет лишних ключей инструментов и параметров", () => {
|
||||
const ids = new Set(TOOLS.map((tool) => tool.id));
|
||||
for (const dict of [ru.tools, en.tools]) {
|
||||
for (const id of Object.keys(dict)) {
|
||||
@@ -47,25 +53,32 @@ describe('секция tools словарей покрывает реестр',
|
||||
const tool = TOOLS.find((t) => t.id === id)!;
|
||||
const paramIds = new Set(tool.params.map((p) => p.id));
|
||||
for (const paramId of Object.keys(dict[id].params ?? {})) {
|
||||
expect(paramIds.has(paramId), `${id}: лишний параметр ${paramId}`).toBe(true);
|
||||
expect(
|
||||
paramIds.has(paramId),
|
||||
`${id}: лишний параметр ${paramId}`,
|
||||
).toBe(true);
|
||||
}
|
||||
const selectIds = new Set(
|
||||
tool.params.filter((p) => p.type === 'select').map((p) => p.id)
|
||||
tool.params.filter((p) => p.type === "select").map((p) => p.id),
|
||||
);
|
||||
for (const paramId of Object.keys(dict[id].options ?? {})) {
|
||||
expect(selectIds.has(paramId), `${id}: options у не-select параметра ${paramId}`).toBe(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
selectIds.has(paramId),
|
||||
`${id}: options у не-select параметра ${paramId}`,
|
||||
).toBe(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('тексты-результаты анализаторов переведены в обоих словарях', () => {
|
||||
it("тексты-результаты анализаторов переведены в обоих словарях", () => {
|
||||
const expectations: Array<[string, string[]]> = [
|
||||
['png-is-grayscale', ['grayscaleYes', 'grayscaleNo']],
|
||||
['png-is-transparent', ['transparentYes', 'transparentNo']],
|
||||
['png-orientation', ['orientationPortrait', 'orientationLandscape', 'orientationSquare']]
|
||||
["png-is-grayscale", ["grayscaleYes", "grayscaleNo"]],
|
||||
["png-is-transparent", ["transparentYes", "transparentNo"]],
|
||||
[
|
||||
"png-orientation",
|
||||
["orientationPortrait", "orientationLandscape", "orientationSquare"],
|
||||
],
|
||||
];
|
||||
for (const [id, keys] of expectations) {
|
||||
for (const key of keys) {
|
||||
|
||||
Reference in New Issue
Block a user