diff --git a/web/eslint-plugins/README.md b/web/eslint-plugins/README.md index 5e76d57..b4c7247 100644 --- a/web/eslint-plugins/README.md +++ b/web/eslint-plugins/README.md @@ -96,9 +96,10 @@ - Опция `allowPaths: string[]` — dot-path ключи, исключаемые из всех проверок (осознанные отклонения до достижения zero-warn). - `baseLocaleFallback: string[]` — паттерны ключей, которые допустимо - отсутствовать только в базовой локали; в текущей конфигурации это registry - metadata `tools.*.title` и `tools.*.description`. Пустые значения и - placeholders для присутствующих ключей всё равно проверяются. + отсутствовать только в базовой локали; в текущей конфигурации это тексты + страниц `pages.*.title` и `pages.*.description` (EN-строка берётся из + реестра). Пустые значения и placeholders для присутствующих ключей всё равно + проверяются. - `ignoreMissingPatterns: string[]` — паттерны ключей, которые временно не проверяются на missing; в текущей конфигурации это неиспользуемые `tools.*.params`. Эта опция не отключает empty/placeholder checks. diff --git a/web/eslint-plugins/__fixtures__/src/lib/i18n/de.ts b/web/eslint-plugins/__fixtures__/src/lib/i18n/de.ts index 9d1927d..b5b0b6b 100644 --- a/web/eslint-plugins/__fixtures__/src/lib/i18n/de.ts +++ b/web/eslint-plugins/__fixtures__/src/lib/i18n/de.ts @@ -12,10 +12,14 @@ export const de: Dict = { errors: { sourceRequired: "Quelle erforderlich", }, - tools: { + pages: { addBorder: { title: "Rahmen hinzufügen", description: "Zeichnet einen {kind}-Rahmen.", + }, + }, + tools: { + addBorder: { results: { done: "Rahmen {what}", }, diff --git a/web/eslint-plugins/__fixtures__/src/lib/i18n/dict.ts b/web/eslint-plugins/__fixtures__/src/lib/i18n/dict.ts index 358ebed..fd06a7e 100644 --- a/web/eslint-plugins/__fixtures__/src/lib/i18n/dict.ts +++ b/web/eslint-plugins/__fixtures__/src/lib/i18n/dict.ts @@ -6,11 +6,15 @@ export type Dict = { header: Record; home: Record; errors: Record; + pages?: Record; tools: Record; }; -export type ToolStrings = { +export type PageStrings = { title?: string; description?: string; +}; + +export type ToolStrings = { results?: Record; }; diff --git a/web/eslint-plugins/__fixtures__/src/lib/i18n/en.ts b/web/eslint-plugins/__fixtures__/src/lib/i18n/en.ts index e827a95..8de7296 100644 --- a/web/eslint-plugins/__fixtures__/src/lib/i18n/en.ts +++ b/web/eslint-plugins/__fixtures__/src/lib/i18n/en.ts @@ -12,10 +12,10 @@ export const en: Dict = { errors: { sourceRequired: "Source image required", }, + // Базовая локаль: тексты страниц берутся из EN-строк реестра. + pages: {}, tools: { addBorder: { - title: "Add border", - description: "Draws a {kind} frame.", results: { done: "Border {what}", }, diff --git a/web/eslint-plugins/__fixtures__/src/lib/i18n/ru.ts b/web/eslint-plugins/__fixtures__/src/lib/i18n/ru.ts index 05cbf2b..e8302d6 100644 --- a/web/eslint-plugins/__fixtures__/src/lib/i18n/ru.ts +++ b/web/eslint-plugins/__fixtures__/src/lib/i18n/ru.ts @@ -12,10 +12,14 @@ export const ru: Dict = { errors: { sourceRequired: "Нужен исходник", }, - tools: { + pages: { addBorder: { title: "Добавить рамку", description: "Рисует рамку {kind}.", + }, + }, + tools: { + addBorder: { results: { done: "Рамка {what}", }, diff --git a/web/eslint-plugins/__tests__/dict-consistency.test.ts b/web/eslint-plugins/__tests__/dict-consistency.test.ts index 655d580..71f1ccd 100644 --- a/web/eslint-plugins/__tests__/dict-consistency.test.ts +++ b/web/eslint-plugins/__tests__/dict-consistency.test.ts @@ -51,24 +51,28 @@ function messageIds(messages: { messageId?: string }[]) { return messages.map((m) => m.messageId); } +const registryPolicy = { + baseLocaleFallback: ["pages.*.title", "pages.*.description"], + ignoreMissingPatterns: ["tools.*.params"], +}; + describe("i18n/dict-consistency", () => { it("stays clean on the consistent fixture dicts", () => { - expect(messageIds(lint(fixture("en.ts"), "lib/i18n/en.ts"))).toEqual([]); - expect(messageIds(lint(fixture("ru.ts"), "lib/i18n/ru.ts"))).toEqual([]); - expect(messageIds(lint(fixture("de.ts"), "lib/i18n/de.ts"))).toEqual([]); + expect( + messageIds(lint(fixture("en.ts"), "lib/i18n/en.ts", registryPolicy)), + ).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 = { - baseLocaleFallback: ["tools.*.title", "tools.*.description"], - 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', ""); + it("allows page texts only in the base locale", () => { + // EN не переводит заголовки страниц: их берёт EN-строка реестра. expect( - messageIds(lint(enMissing, "lib/i18n/en.ts", registryPolicy)), + messageIds(lint(fixture("en.ts"), "lib/i18n/en.ts", registryPolicy)), ).toEqual([]); const ruMissing = fixture("ru.ts").replace( @@ -80,7 +84,7 @@ describe("i18n/dict-consistency", () => { ).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( /\t\taddBorder: \{[\s\S]*?\n\t\t\},\n/, "", @@ -106,18 +110,19 @@ describe("i18n/dict-consistency", () => { expect( messageIds( lint(enMissingResults, "lib/i18n/en.ts", { + ...registryPolicy, ignoreMissingPatterns: ["tools.*.results"], }), ), ).toEqual([]); }); - it("still reports empty fallback metadata", () => { - const enEmpty = fixture("en.ts").replace( - 'title: "Add border",', + it("still reports empty page text", () => { + const ruEmpty = fixture("ru.ts").replace( + 'title: "Добавить рамку",', 'title: "",', ); - expect(messageIds(lint(enEmpty, "lib/i18n/en.ts", registryPolicy))).toEqual( + expect(messageIds(lint(ruEmpty, "lib/i18n/ru.ts", registryPolicy))).toEqual( ["emptyValue"], ); }); @@ -127,7 +132,7 @@ describe("i18n/dict-consistency", () => { '\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(messageIds(messages)).toEqual(["missingKey"]); 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", () => { 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(messages[0].message).toContain('"header"'); expect(messages[0].message).not.toContain("header.workspace"); @@ -147,7 +152,7 @@ describe("i18n/dict-consistency", () => { '\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(messageIds(messages)).toEqual(["missingKey"]); expect(messages[0].message).toContain('"errors.sourceRequired"'); @@ -158,7 +163,7 @@ describe("i18n/dict-consistency", () => { const enEmpty = fixture("en.ts") .replace('workspace: "Workspace",', 'workspace: "",') .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(messages[0].message).toContain('"header.workspace"'); expect(messages[0].message).toContain(" in en"); @@ -178,14 +183,14 @@ describe("i18n/dict-consistency", () => { }); it("flags an extra placeholder introduced in one locale", () => { - const enExtraPlaceholder = fixture("en.ts").replace( - 'description: "Draws a {kind} frame.",', - 'description: "Draws a {kind} frame by {author}.",', + const ruExtraPlaceholder = fixture("ru.ts").replace( + 'description: "Рисует рамку {kind}.",', + 'description: "Рисует рамку {kind} от {author}.",', ); - const messages = lint(enExtraPlaceholder, "lib/i18n/en.ts"); + const messages = lint(ruExtraPlaceholder, "lib/i18n/ru.ts"); expect(messages).toHaveLength(1); 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}"); }); @@ -195,6 +200,7 @@ describe("i18n/dict-consistency", () => { "", ); const messages = lint(enMissing, "lib/i18n/en.ts", { + ...registryPolicy, allowPaths: ["errors.sourceRequired"], }); expect(messageIds(messages)).toEqual([]); diff --git a/web/eslint.config.js b/web/eslint.config.js index c4369f7..bd27a46 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -175,7 +175,7 @@ export default tseslint.config( "i18n/dict-consistency": [ "warn", { - baseLocaleFallback: ["tools.*.title", "tools.*.description"], + baseLocaleFallback: ["pages.*.title", "pages.*.description"], ignoreMissingPatterns: ["tools.*.params"], }, ], diff --git a/web/src/lib/i18n/dict.ts b/web/src/lib/i18n/dict.ts index 206177b..10e3d39 100644 --- a/web/src/lib/i18n/dict.ts +++ b/web/src/lib/i18n/dict.ts @@ -16,9 +16,14 @@ export function isLocale(value: unknown): value is Locale { ); } -export type ToolStrings = { +/** Тексты страницы (`pages.`): адрес, имя файла и иконка — по slug. */ +export type PageStrings = { title?: string; description?: string; +}; + +/** Тексты инструмента (`tools.`): подписи полей, verdict-ключи. */ +export type ToolStrings = { /** Подписи параметров по их id. */ params?: Record; /** Подписи опций select: paramId -> value -> label. */ @@ -54,6 +59,8 @@ export type Dict = { search: Record; ui: Record; errors: Record; + /** Заголовки и описания страниц; в базовой локали их нет — берётся EN из реестра. */ + pages?: Record; tools: Record; /** Действия панели результата/генерации (Generate, Open image…). */ actions?: Record; diff --git a/web/src/lib/i18n/en.ts b/web/src/lib/i18n/en.ts index 7d76869..e61362a 100644 --- a/web/src/lib/i18n/en.ts +++ b/web/src/lib/i18n/en.ts @@ -334,6 +334,9 @@ export const en: Dict = { tile: "Tile", watermark: "Watermark", }, + // Заголовки и описания страниц в базовой локали не дублируются: их берёт + // EN-строка из реестра страниц (`web/src/lib/registry/pages/`). + pages: {}, tools: { "png-file-size": { results: { diff --git a/web/src/lib/i18n/ru.ts b/web/src/lib/i18n/ru.ts index 4ff18af..9d4b184 100644 --- a/web/src/lib/i18n/ru.ts +++ b/web/src/lib/i18n/ru.ts @@ -335,7 +335,7 @@ export const ru: Dict = { tile: "Плитка", watermark: "Водяной знак", }, - tools: { + pages: { "png-to-bmp": { title: "Конвертировать PNG в BMP", description: @@ -369,85 +369,45 @@ export const ru: Dict = { title: "HEX-пиксели в PNG", description: "Собирает картинку из hex-значений rrggbbaa (через пробел). Укажите ширину — высота рассчитается сама.", - params: { width: "Ширина изображения" }, }, "resize-png": { title: "Изменить размер PNG", description: "Масштабирование изображения с билинейной интерполяцией. При сохранении пропорций одна сторона задаёт масштаб, а если указаны обе — изображение вписывается в эти размеры.", - params: { - width: "Ширина (0 — авто)", - height: "Высота (0 — авто)", - keepAspect: "Сохранять пропорции", - }, }, "crop-png": { title: "Обрезать PNG", description: "Вырезает прямоугольную область. Координаты и размеры выходят за границы изображения — область усекается до пересечения с картинкой.", - params: { - x: "X (слева)", - y: "Y (сверху)", - width: "Ширина области", - height: "Высота области", - }, }, "rotate-png": { title: "Повернуть PNG", description: "Поворот на 90°, 180° или 270° по часовой стрелке без потери качества.", - params: { angle: "Угол поворота" }, - options: { - angle: { - "90": "90° по часовой", - "180": "180°", - "270": "270° по часовой", - }, - }, }, "flip-png": { title: "Отразить PNG", description: "Зеркальное отражение по горизонтали или вертикали без потери качества.", - params: { axis: "Ось отражения" }, - options: { - axis: { - horizontal: "По горизонтали (слева направо)", - vertical: "По вертикали (сверху вниз)", - }, - }, }, "add-padding-png": { title: "Добавить поля PNG", description: "Расширяет холст во все стороны на выбранное число пикселей.", - params: { - padding: "Поля, px", - transparent: "Прозрачные поля", - color: "Цвет полей", - }, }, "add-border-png": { title: "Добавить рамку PNG", description: "Рисует цветную рамку вокруг изображения выбранной толщины.", - params: { thickness: "Толщина рамки, px", color: "Цвет рамки" }, }, "fit-on-background-png": { title: "Вписать PNG на фон", description: "Помещает изображение по центру полотна заданного размера с прозрачным или цветным фоном.", - params: { - width: "Ширина полотна", - height: "Высота полотна", - transparent: "Прозрачный фон", - color: "Цвет фона", - }, }, "tile-png": { title: "Замостить PNG", description: "Повторяет изображение сеткой из выбранного числа столбцов и строк.", - params: { columns: "Столбцов", rows: "Строк" }, }, "split-into-parts-png": { title: "Разрезать PNG на части", @@ -463,13 +423,11 @@ export const ru: Dict = { title: "Размытие PNG", description: "Гауссово размытие: три прохода разделяемого бокса — быстро при любом радиусе. Прозрачные края не темнеют.", - params: { radius: "Радиус, px" }, }, "sharpen-png": { title: "Резкость PNG", description: "Подчёркивает края ядром резкости; сила задаёт смесь с оригиналом. 0% — без изменений.", - params: { strength: "Сила, %" }, }, "grayscale-png": { title: "Чёрно-белый PNG", @@ -485,13 +443,11 @@ export const ru: Dict = { title: "Яркость и контраст PNG", description: "Изменяет яркость и контраст в диапазоне от −100 до +100. Значение 0 — без изменений.", - params: { brightness: "Яркость", contrast: "Контраст" }, }, "change-png-opacity": { title: "Изменить прозрачность PNG", description: "Умножает альфа-канал на процент: 0% — полностью прозрачный, 100% — без изменений.", - params: { percent: "Прозрачность, %" }, }, "sepia-png": { title: "Эффект сепии", @@ -502,108 +458,70 @@ export const ru: Dict = { title: "Сменить оттенок PNG", description: "Сдвиг цветового тона по кругу. Насыщенность и яркость сохраняются.", - params: { degrees: "Сдвиг тона, °" }, }, "extract-channel-png": { title: "Извлечь канал PNG", description: "Оставляет выбранный канал — красный, зелёный или синий — в оттенках серого.", - params: { channel: "Канал" }, - options: { channel: { red: "Красный", green: "Зелёный", blue: "Синий" } }, }, "swap-channels-png": { title: "Переставить каналы PNG", description: "Меняет местами два цветовых канала — быстрый способ получить необычный окрас.", - params: { pair: "Пара каналов" }, - options: { - pair: { - "r-g": "Красный ↔ Зелёный", - "r-b": "Красный ↔ Синий", - "g-b": "Зелёный ↔ Синий", - }, - }, }, "png-to-hsl": { title: "Разбить PNG на HSL", description: "Раскладывает изображение на компоненты тона, насыщенности и светлоты.", - options: { - display: { gray: "Оттенки серого", color: "Пространство как RGB" }, - }, }, "png-to-hsv": { title: "Разбить PNG на HSV", description: "Раскладывает изображение на компоненты тона, насыщенности и значения (яркости).", - options: { - display: { gray: "Оттенки серого", color: "Пространство как RGB" }, - }, }, "png-to-hsi": { title: "Разбить PNG на HSI", description: "Раскладывает изображение на компоненты тона, насыщенности и интенсивности.", - options: { - display: { gray: "Оттенки серого", color: "Пространство как RGB" }, - }, }, "png-to-cmyk": { title: "Конвертировать PNG в цвета CMYK", description: "Раскладывает изображение на печатные компоненты Cyan, Magenta, Yellow и Key (чёрный).", - options: { - display: { gray: "Оттенки серого", color: "Пространство как RGB" }, - }, }, "png-to-ycbcr": { title: "Конвертировать PNG в цвета YCbCr", description: "Раскладывает изображение на яркость (Y) и цветоразностные компоненты Cb / Cr.", - options: { - display: { gray: "Оттенки серого", color: "Пространство как RGB" }, - }, }, "png-to-lab": { title: "Конвертировать PNG в цвета LAB", description: "Раскладывает изображение на перцепционную светлоту и оппонентные пары зелёный–пурпур / синий–жёлтый.", - options: { - display: { gray: "Оттенки серого", color: "Пространство как RGB" }, - }, }, "black-and-white-png": { title: "Чёрно-белый PNG по порогу", description: "Жёсткая бинаризация по яркости: каждый пиксель становится чёрным или белым.", - params: { threshold: "Порог яркости, %" }, }, "posterize-png": { title: "Постеризация PNG", description: "Уменьшает число уровней каждого канала — плакатный эффект.", - params: { levels: "Уровней на канал" }, }, "two-colors-png": { title: "Два цвета PNG", description: "Перекрашивает изображение в два выбранных цвета по порогу яркости.", - params: { - lightColor: "Цвет светлых участков", - darkColor: "Цвет тёмных участков", - threshold: "Порог яркости, %", - }, }, "convert-png-to-jpg": { title: "Конвертировать PNG в JPG", description: "Прозрачность накладывается на выбранный цвет подложки (по умолчанию белый), результат сохраняется в JPEG.", - params: { background: "Цвет подложки", quality: "Качество JPEG" }, }, "convert-png-to-webp": { title: "Конвертировать PNG в WebP", description: "Перекодирует изображение в WebP с настраиваемым качеством. Прозрачность сохраняется.", - params: { quality: "Качество WebP" }, }, "remove-alpha-channel-png": { title: "Убрать альфа-канал PNG", @@ -614,7 +532,6 @@ export const ru: Dict = { title: "Задать альфа-канал PNG", description: "Присваивает всем пикселям одинаковую прозрачность, цвета не меняются.", - params: { percent: "Прозрачность, %" }, }, "extract-alpha-mask-png": { title: "Извлечь маску альфы PNG", @@ -624,7 +541,6 @@ export const ru: Dict = { title: "Скруглить углы PNG", description: "Обрезает углы по радиусу, заданному в процентах от половины меньшей стороны.", - params: { radius: "Радиус скругления, %" }, }, "invert-alpha-png": { title: "Инвертировать альфа-канал PNG", @@ -634,6 +550,532 @@ export const ru: Dict = { title: "Удалить фон PNG (умно)", description: "Убирает однотонный фон: по цвету с допуском, только внешние области от краёв или весь совпадающий цвет. Умеет сглаживать границу.", + }, + "add-stroke-png": { + title: "Обвести PNG", + description: + "Добавляет цветную обводку-кольцо вокруг непрозрачного содержимого заданной толщины.", + }, + "find-contour-png": { + title: "Найти контур PNG", + description: + "Оставляет только линию по границе непрозрачных областей выбранного цвета и толщины.", + }, + "make-thicker-png": { + title: "Утолщить PNG", + description: "Расширяет непрозрачные области на заданное число пикселей.", + }, + "make-thinner-png": { + title: "Утончить PNG", + description: + "Сужает непрозрачные области — утоньшает штрихи надписей и деталей.", + }, + "harden-alpha-png": { + title: "Жёсткие края PNG", + description: + "Бинаризует альфа-канал по порогу: полупрозрачные пиксели становятся либо полностью прозрачными, либо непрозрачными.", + }, + "despeckle-alpha-png": { + title: "Убрать мусор PNG", + description: + "Открытие: убирает одиночные полупрозрачные пиксели и мелкие крапинки.", + }, + "close-holes-png": { + title: "Закрыть дыры PNG", + description: + "Закрытие: заполняет одиночные прозрачные точки внутри объекта.", + }, + "remove-color-from-png": { + title: "Удалить цвет из PNG (прозрачность)", + description: + "Делает прозрачными все пиксели, близкие к выбранному цвету. Порог задаёт допустимое отклонение в процентах от максимального цветового расстояния.", + }, + "create-empty-png": { + title: "Создать пустой PNG", + description: + "Генерирует холст выбранного размера — прозрачный или залитый цветом.", + }, + "single-color-png": { + title: "Создать одноцветный PNG", + description: "Генерирует прямоугольник заданного размера и цвета.", + }, + "random-noise-png": { + title: "Создать случайный шум PNG", + description: + "Генерирует картинку со случайными пикселями. Зерно фиксирует результат: одно зерно — одна картинка.", + }, + "linear-gradient-png": { + title: "Создать градиент PNG", + description: + "Генерирует плавный переход между двумя цветами по горизонтали или вертикали.", + }, + "color-wheel-png": { + title: "Цветовой круг PNG", + description: + "Круг HSL: оттенок по окружности, насыщенность от центра к краю, выбранная светлота.", + }, + "complementary-png": { + title: "Комплементарная палитра PNG", + description: + "Два противоположных цвета круга — базовый и его дополнение.", + }, + "triadic-png": { + title: "Триадная палитра PNG", + description: "Три цвета через 120° друг от друга на цветовом круге.", + }, + "tetradic-png": { + title: "Тетрадная палитра PNG", + description: "Четыре цвета — две комплементарные пары, шаг 90° по кругу.", + }, + "analogous-png": { + title: "Аналоговая палитра PNG", + description: + "Соседние оттенки вокруг базового — спокойная родственная гамма.", + }, + "monochromatic-png": { + title: "Монохромная палитра PNG", + description: + "Тоны одного оттенка: меняется светлота в выбранном диапазоне, тон и насыщенность фиксированы.", + }, + "shades-png": { + title: "Градация оттенка PNG", + description: "Ступени базового цвета от исходного к более тёмному.", + }, + "mix-colors-png": { + title: "Смешать цвета PNG", + description: + "Усредняет несколько hex-цветов в один свотч. Введите значения через запятую; неверные токены пропускаются.", + }, + "blend-two-png": { + title: "Перелив двух цветов PNG", + description: "Непрерывный горизонтальный градиент между двумя цветами.", + }, + "step-colors-png": { + title: "Ступени между цветами PNG", + description: + "Дискретный набор равномерно распределённых ступеней между двумя цветами.", + }, + "sort-colors-png": { + title: "Отсортировать цвета PNG", + description: + "Рисует ваш hex-список свотчами, отсортированными по тону, яркости или насыщенности. Неверные токены пропускаются.", + }, + "png-is-grayscale": { + title: "Проверить: PNG монохромный?", + description: + "Сообщает, состоит ли изображение только из оттенков серого.", + }, + "circle-mask-png": { + title: "Круглая маска PNG", + description: + "Вырезает из изображения круг. Диаметр задаётся в процентах от меньшей стороны.", + }, + "square-mask-png": { + title: "Прямоугольная маска PNG", + description: + "Вырезает прямоугольник со сторонами в процентах от меньшей стороны изображения.", + }, + "star-mask-png": { + title: "Маска-звезда PNG", + description: + "Вырезает звезду с настраиваемым числом лучей, глубиной впадин и поворотом.", + }, + "wavy-mask-png": { + title: "Волнистая маска PNG", + description: + "Вырезает круг с волнистым краем: радиус модулируется синусом с заданной амплитудой и частотой.", + }, + "trim-empty-space-png": { + title: "Обрезать пустые поля PNG", + description: + "Обрезает прозрачные рамки вокруг содержимого. Пиксели с альфой выше порога считаются содержимым.", + }, + "change-canvas-size-png": { + title: "Изменить размер холста PNG", + description: + "Задаёт точный размер холста: лишнее обрезается, недостающее дополняется прозрачностью. Якорь выбирает, какая часть изображения остаётся.", + }, + "change-aspect-ratio-png": { + title: "Изменить соотношение сторон PNG", + description: + "Вписывает изображение в целевое соотношение сторон: обрезать центр до заполнения или дополнить прозрачностью.", + }, + "swap-orientation-png": { + title: "Поменять ориентацию PNG", + description: + "Поворачивает изображение на 90°, если ориентация отличается от целевой — ландшафт становится портретом и наоборот. Квадрат не трогается.", + }, + "symmetric-copy-png": { + title: "Симметричная копия PNG", + description: + "Удваивает холст, зеркалируя сохранённую половину на пустую — мгновенный симметричный узор.", + }, + "feather-edges-png": { + title: "Растушевать края PNG", + description: + "Размывает только альфа-канал: жёсткие края выреза становятся мягкими, цвета не трогаются.", + }, + "clean-edges-png": { + title: "Почистить края PNG (defringe)", + description: + "Заменяет цветную кайму полупрозрачных пикселей цветом ближайшего полностью непрозрачного соседа. Альфа остаётся как была.", + }, + "pixelate-png": { + title: "Пикселизация PNG", + description: + "Усредняет каждый блок blockSize×blockSize в один цвет — классическая мозаика.", + }, + "randomize-pixels-png": { + title: "Перемешать пиксели PNG", + description: + "Переставляет блоки изображения между собой. Одинаковый seed даёт одинаковую раскладку.", + }, + "add-noise-png": { + title: "Добавить шум PNG", + description: + "Добавляет зерно в стиле плёнки. Детерминировано по seed; монохромный сохраняет баланс оттенков.", + }, + "silhouette-png": { + title: "Силуэт PNG", + description: + "Заливает все видимые пиксели одним цветом, сохраняя их прозрачность — мгновенный силуэт.", + }, + "png-to-bytes": { + title: "PNG в байты", + description: + "Выводит каждый пиксель четырьмя десятичными байтами (R G B A), по строке изображения на строку текста.", + }, + "bytes-to-png": { + title: "Байты в PNG", + description: + "Собирает изображение из десятичных RGBA-байтов (разделители любые). Укажите ширину — высота рассчитается сама.", + }, + "png-to-rgb-values": { + title: "PNG в RGB-значения", + description: + "Выводит каждый пиксель как rgba(r, g, b, a), ряды через перевод строки.", + }, + "rgb-values-to-png": { + title: "RGB-значения в PNG", + description: + "Собирает изображение из чисел rgba(r, g, b, a). Укажите ширину — высота рассчитается сама.", + }, + "verify-is-png": { + title: "Проверить: это PNG?", + description: + "Проверяет сигнатуру вставленного base64 / data-uri и сообщает, настоящий ли это PNG.", + }, + "text-to-png": { + title: "Текст в PNG", + description: + "Создаёт PNG из текста: холст подгоняется под размер надписи с полями.", + }, + "emoji-to-png": { + title: "Эмодзи в PNG", + description: + "Рисует эмодзи или любой Unicode-символ как прозрачный PNG выбранного размера.", + }, + "placeholder-png": { + title: "Заглушка PNG", + description: + "Генерирует прямоугольник-заглушку с напечатанными размерами в центре.", + }, + "color-spectrum-png": { + title: "Спектр цветов PNG", + description: + "Полный радужный переход 0–360° вдоль выбранной оси с настройкой насыщенности и светлоты.", + }, + "random-colors-png": { + title: "Случайные цветные блоки PNG", + description: + "Заполняет холст случайными яркими блоками. Детерминировано по seed.", + }, + "draw-grid-png": { + title: "Нарисовать сетку PNG", + description: + "Рисует сетку с заданными столбцами, строками и толщиной линий на прозрачном или белом фоне.", + }, + "quantize-png": { + title: "Квантовать PNG", + description: + "Уменьшает изображение до k цветов через median-cut палитру. Прозрачные пиксели сохраняются.", + }, + "decrease-color-count-png": { + title: "Уменьшить число цветов PNG", + description: + "Тот же движок median-cut с фиксированными пресетами степеней двойки — быстрый спуск до 2–256 цветов.", + }, + "custom-palette-png": { + title: "Своя палитра PNG", + description: + "Сопоставляет каждый пиксель с ближайшим цветом из вашего списка hex через запятую.", + }, + "dithering-png": { + title: "Дизеринг PNG", + description: + "Применяет распространение ошибки Флойда–Стейнберга или упорядоченный Байер при сведении к k цветам.", + }, + "png-file-size": { + title: "Размер файла PNG", + description: + "Кодирует изображение в PNG и показывает итоговый размер файла.", + }, + "show-transparent-png": { + title: "Показать прозрачные области PNG", + description: + "Подсвечивает выбранным цветом каждый прозрачный или полупрозрачный пиксель — дыры становятся заметными.", + }, + "show-grayscale-pixels-png": { + title: "Показать серые пиксели PNG", + description: + "Находит пиксели с почти равными каналами и рисует их маску. Допуск — в единицах канала.", + }, + "show-color-pixels-png": { + title: "Показать цветные пиксели PNG", + description: + "Находит цветные (не серые) пиксели за пределами допуска и рисует их маску.", + }, + "light-pixel-mask-png": { + title: "Маска светлых пикселей PNG", + description: "Выбирает пиксели ярче порога яркости.", + }, + "dark-pixel-mask-png": { + title: "Маска тёмных пикселей PNG", + description: "Выбирает пиксели темнее порога яркости.", + }, + "unique-color-mask-png": { + title: "Маска уникальных цветов PNG", + description: + "Выбирает цвета, которые встречаются не чаще заданного числа раз, — редкие и одиночные пиксели.", + }, + "extract-color-from-png": { + title: "Извлечь цвет из PNG", + description: + "Оставляет только пиксели, близкие к выбранному цвету, остальное делает прозрачным — обратное «Удалить цвет».", + }, + "watermark-tile-png": { + title: "Плитка-водяной знак PNG", + description: + "Покрывает изображение повторяющейся диагональной полупрозрачной плиткой текста — защитный водяной знак.", + }, + "add-text-png": { + title: "Надпись на PNG", + description: + "Рисует текст на изображении: шрифт, размер, цвет, жирность, позиция на сетке 3×3 и опциональная подложка.", + }, + "date-stamp-png": { + title: "Дата-штамп PNG", + description: + "Ставит текущую дату и время по строке формата (токены YYYY MM DD hh mm ss). Оформление — как у надписи.", + }, + "skew-png": { + title: "Наклонить PNG", + description: + "Сдвигает содержимое по горизонтали и вертикали — эффект перспективы.", + }, + "rotate-free-png": { + title: "Повернуть на произвольный угол", + description: + "Поворот на любой угол. Холст расширяется под новые габариты, углы остаются прозрачными.", + }, + "zoom-png": { + title: "Приблизить PNG", + description: + "Увеличивает содержимое к центру. Холст прежнего размера — края обрезаются.", + }, + "shift-png": { + title: "Сдвинуть PNG", + description: "Перемещает содержимое на заданное смещение по X и Y.", + }, + "vignette-png": { + title: "Виньетка PNG", + description: "Плавно затемняет края изображения, центр не затрагивает.", + }, + "jpeg-artifacts-png": { + title: "Артефакты JPEG", + description: + "Имитирует пережатие в JPEG с низким качеством — видимые квадраты и размытие цветов.", + }, + "gamma-png": { + title: "Гамма-коррекция PNG", + description: + "Корректирует яркость средних тонов. <1 темнее, >1 светлее, 1 — без изменений.", + }, + "auto-contrast-png": { + title: "Автоконтраст PNG", + description: + "Растягивает диапазон каждого канала на весь доступный диапазон яркости.", + }, + "temperature-png": { + title: "Температура PNG", + description: + "Положительные значения делают изображение теплее (оранжевее), отрицательные — холоднее (синеватее).", + }, + "tint-png": { + title: "Тонировать PNG", + description: + "Умножает цветовые каналы на выбранный оттенок с заданной силой.", + }, + "svg-to-png": { + title: "SVG в PNG", + description: + "Декодирует SVG-разметку в растровое изображение. Вставьте SVG-код слева.", + }, + "png-is-transparent": { + title: "Проверить: PNG прозрачный?", + description: + "Сообщает, есть ли в изображении прозрачные или полупрозрачные пиксели.", + }, + "png-orientation": { + title: "Ориентация PNG", + description: "Сообщает, портрет это, ландшафт или квадрат.", + }, + }, + + tools: { + "hex-to-png": { + params: { width: "Ширина изображения" }, + }, + "resize-png": { + params: { + width: "Ширина (0 — авто)", + height: "Высота (0 — авто)", + keepAspect: "Сохранять пропорции", + }, + }, + "crop-png": { + params: { + x: "X (слева)", + y: "Y (сверху)", + width: "Ширина области", + height: "Высота области", + }, + }, + "rotate-png": { + params: { angle: "Угол поворота" }, + options: { + angle: { + "90": "90° по часовой", + "180": "180°", + "270": "270° по часовой", + }, + }, + }, + "flip-png": { + params: { axis: "Ось отражения" }, + options: { + axis: { + horizontal: "По горизонтали (слева направо)", + vertical: "По вертикали (сверху вниз)", + }, + }, + }, + "add-padding-png": { + params: { + padding: "Поля, px", + transparent: "Прозрачные поля", + color: "Цвет полей", + }, + }, + "add-border-png": { + params: { thickness: "Толщина рамки, px", color: "Цвет рамки" }, + }, + "fit-on-background-png": { + params: { + width: "Ширина полотна", + height: "Высота полотна", + transparent: "Прозрачный фон", + color: "Цвет фона", + }, + }, + "tile-png": { + params: { columns: "Столбцов", rows: "Строк" }, + }, + "blur-png": { + params: { radius: "Радиус, px" }, + }, + "sharpen-png": { + params: { strength: "Сила, %" }, + }, + "adjust-brightness-contrast-png": { + params: { brightness: "Яркость", contrast: "Контраст" }, + }, + "change-png-opacity": { + params: { percent: "Прозрачность, %" }, + }, + "change-png-hue": { + params: { degrees: "Сдвиг тона, °" }, + }, + "extract-channel-png": { + params: { channel: "Канал" }, + options: { channel: { red: "Красный", green: "Зелёный", blue: "Синий" } }, + }, + "swap-channels-png": { + params: { pair: "Пара каналов" }, + options: { + pair: { + "r-g": "Красный ↔ Зелёный", + "r-b": "Красный ↔ Синий", + "g-b": "Зелёный ↔ Синий", + }, + }, + }, + "png-to-hsl": { + options: { + display: { gray: "Оттенки серого", color: "Пространство как RGB" }, + }, + }, + "png-to-hsv": { + options: { + display: { gray: "Оттенки серого", color: "Пространство как RGB" }, + }, + }, + "png-to-hsi": { + options: { + display: { gray: "Оттенки серого", color: "Пространство как RGB" }, + }, + }, + "png-to-cmyk": { + options: { + display: { gray: "Оттенки серого", color: "Пространство как RGB" }, + }, + }, + "png-to-ycbcr": { + options: { + display: { gray: "Оттенки серого", color: "Пространство как RGB" }, + }, + }, + "png-to-lab": { + options: { + display: { gray: "Оттенки серого", color: "Пространство как RGB" }, + }, + }, + "black-and-white-png": { + params: { threshold: "Порог яркости, %" }, + }, + "posterize-png": { + params: { levels: "Уровней на канал" }, + }, + "two-colors-png": { + params: { + lightColor: "Цвет светлых участков", + darkColor: "Цвет тёмных участков", + threshold: "Порог яркости, %", + }, + }, + "convert-png-to-jpg": { + params: { background: "Цвет подложки", quality: "Качество JPEG" }, + }, + "convert-png-to-webp": { + params: { quality: "Качество WebP" }, + }, + "set-alpha-channel-png": { + params: { percent: "Прозрачность, %" }, + }, + "round-corners-png": { + params: { radius: "Радиус скругления, %" }, + }, + "remove-background-png": { params: { color: "Цвет фона", tolerance: "Допуск похожести, %", @@ -642,59 +1084,33 @@ export const ru: Dict = { }, }, "add-stroke-png": { - title: "Обвести PNG", - description: - "Добавляет цветную обводку-кольцо вокруг непрозрачного содержимого заданной толщины.", params: { color: "Цвет обводки", thickness: "Толщина, px" }, }, "find-contour-png": { - title: "Найти контур PNG", - description: - "Оставляет только линию по границе непрозрачных областей выбранного цвета и толщины.", params: { color: "Цвет линии", thickness: "Толщина линии, px" }, }, "make-thicker-png": { - title: "Утолщить PNG", - description: "Расширяет непрозрачные области на заданное число пикселей.", params: { radius: "На сколько px" }, }, "make-thinner-png": { - title: "Утончить PNG", - description: - "Сужает непрозрачные области — утоньшает штрихи надписей и деталей.", params: { radius: "На сколько px" }, }, "harden-alpha-png": { - title: "Жёсткие края PNG", - description: - "Бинаризует альфа-канал по порогу: полупрозрачные пиксели становятся либо полностью прозрачными, либо непрозрачными.", params: { threshold: "Порог альфы, %" }, }, "despeckle-alpha-png": { - title: "Убрать мусор PNG", - description: - "Открытие: убирает одиночные полупрозрачные пиксели и мелкие крапинки.", params: { radius: "Радиус очистки, px" }, }, "close-holes-png": { - title: "Закрыть дыры PNG", - description: - "Закрытие: заполняет одиночные прозрачные точки внутри объекта.", params: { radius: "Радиус закрытия, px" }, }, "remove-color-from-png": { - title: "Удалить цвет из PNG (прозрачность)", - description: - "Делает прозрачными все пиксели, близкие к выбранному цвету. Порог задаёт допустимое отклонение в процентах от максимального цветового расстояния.", params: { targetColor: "Цвет для удаления", tolerance: "Порог похожести, %", }, }, "create-empty-png": { - title: "Создать пустой PNG", - description: - "Генерирует холст выбранного размера — прозрачный или залитый цветом.", params: { width: "Ширина", height: "Высота", @@ -703,20 +1119,12 @@ export const ru: Dict = { }, }, "single-color-png": { - title: "Создать одноцветный PNG", - description: "Генерирует прямоугольник заданного размера и цвета.", params: { width: "Ширина", height: "Высота", color: "Цвет" }, }, "random-noise-png": { - title: "Создать случайный шум PNG", - description: - "Генерирует картинку со случайными пикселями. Зерно фиксирует результат: одно зерно — одна картинка.", params: { width: "Ширина", height: "Высота", seed: "Зерно" }, }, "linear-gradient-png": { - title: "Создать градиент PNG", - description: - "Генерирует плавный переход между двумя цветами по горизонтали или вертикали.", params: { width: "Ширина", height: "Высота", @@ -726,15 +1134,9 @@ export const ru: Dict = { }, }, "color-wheel-png": { - title: "Цветовой круг PNG", - description: - "Круг HSL: оттенок по окружности, насыщенность от центра к краю, выбранная светлота.", params: { width: "Размер", lightness: "Светлота, %" }, }, "complementary-png": { - title: "Комплементарная палитра PNG", - description: - "Два противоположных цвета круга — базовый и его дополнение.", params: { baseColor: "Базовый цвет", width: "Ширина", @@ -743,8 +1145,6 @@ export const ru: Dict = { options: { layout: { grid: "Сетка", strip: "Полоса" } }, }, "triadic-png": { - title: "Триадная палитра PNG", - description: "Три цвета через 120° друг от друга на цветовом круге.", params: { baseColor: "Базовый цвет", width: "Ширина", @@ -753,8 +1153,6 @@ export const ru: Dict = { options: { layout: { grid: "Сетка", strip: "Полоса" } }, }, "tetradic-png": { - title: "Тетрадная палитра PNG", - description: "Четыре цвета — две комплементарные пары, шаг 90° по кругу.", params: { baseColor: "Базовый цвет", width: "Ширина", @@ -763,9 +1161,6 @@ export const ru: Dict = { options: { layout: { grid: "Сетка", strip: "Полоса" } }, }, "analogous-png": { - title: "Аналоговая палитра PNG", - description: - "Соседние оттенки вокруг базового — спокойная родственная гамма.", params: { baseColor: "Базовый цвет", width: "Ширина", @@ -776,9 +1171,6 @@ export const ru: Dict = { options: { layout: { grid: "Сетка", strip: "Полоса" } }, }, "monochromatic-png": { - title: "Монохромная палитра PNG", - description: - "Тоны одного оттенка: меняется светлота в выбранном диапазоне, тон и насыщенность фиксированы.", params: { baseColor: "Базовый цвет", width: "Ширина", @@ -789,8 +1181,6 @@ export const ru: Dict = { options: { layout: { grid: "Сетка", strip: "Полоса" } }, }, "shades-png": { - title: "Градация оттенка PNG", - description: "Ступени базового цвета от исходного к более тёмному.", params: { baseColor: "Базовый цвет", width: "Ширина", @@ -801,20 +1191,12 @@ export const ru: Dict = { options: { layout: { grid: "Сетка", strip: "Полоса" } }, }, "mix-colors-png": { - title: "Смешать цвета PNG", - description: - "Усредняет несколько hex-цветов в один свотч. Введите значения через запятую; неверные токены пропускаются.", params: { colors: "Цвета (hex через запятую)", width: "Ширина" }, }, "blend-two-png": { - title: "Перелив двух цветов PNG", - description: "Непрерывный горизонтальный градиент между двумя цветами.", params: { colorA: "Цвет A", colorB: "Цвет B", width: "Ширина" }, }, "step-colors-png": { - title: "Ступени между цветами PNG", - description: - "Дискретный набор равномерно распределённых ступеней между двумя цветами.", params: { colorA: "Цвет A", colorB: "Цвет B", @@ -825,9 +1207,6 @@ export const ru: Dict = { options: { layout: { grid: "Сетка", strip: "Полоса" } }, }, "sort-colors-png": { - title: "Отсортировать цвета PNG", - description: - "Рисует ваш hex-список свотчами, отсортированными по тону, яркости или насыщенности. Неверные токены пропускаются.", params: { colors: "Цвета (hex через запятую)", order: "Сортировка", @@ -840,18 +1219,12 @@ export const ru: Dict = { }, }, "png-is-grayscale": { - title: "Проверить: PNG монохромный?", - description: - "Сообщает, состоит ли изображение только из оттенков серого.", results: { grayscaleYes: "Да — все пиксели являются оттенками серого.", grayscaleNo: "Нет — найдены цветные пиксели.", }, }, "circle-mask-png": { - title: "Круглая маска PNG", - description: - "Вырезает из изображения круг. Диаметр задаётся в процентах от меньшей стороны.", params: { size: "Диаметр, % меньшей стороны", offsetX: "Смещение X, %", @@ -859,9 +1232,6 @@ export const ru: Dict = { }, }, "square-mask-png": { - title: "Прямоугольная маска PNG", - description: - "Вырезает прямоугольник со сторонами в процентах от меньшей стороны изображения.", params: { widthPct: "Ширина, % меньшей стороны", heightPct: "Высота, % меньшей стороны", @@ -870,9 +1240,6 @@ export const ru: Dict = { }, }, "star-mask-png": { - title: "Маска-звезда PNG", - description: - "Вырезает звезду с настраиваемым числом лучей, глубиной впадин и поворотом.", params: { points: "Лучи", innerRadius: "Радиус впадин, %", @@ -883,9 +1250,6 @@ export const ru: Dict = { }, }, "wavy-mask-png": { - title: "Волнистая маска PNG", - description: - "Вырезает круг с волнистым краем: радиус модулируется синусом с заданной амплитудой и частотой.", params: { size: "Базовый радиус, % меньшей стороны", amplitude: "Амплитуда волн, %", @@ -896,15 +1260,9 @@ export const ru: Dict = { }, }, "trim-empty-space-png": { - title: "Обрезать пустые поля PNG", - description: - "Обрезает прозрачные рамки вокруг содержимого. Пиксели с альфой выше порога считаются содержимым.", params: { threshold: "Порог альфы" }, }, "change-canvas-size-png": { - title: "Изменить размер холста PNG", - description: - "Задаёт точный размер холста: лишнее обрезается, недостающее дополняется прозрачностью. Якорь выбирает, какая часть изображения остаётся.", params: { width: "Ширина", height: "Высота", anchor: "Якорь" }, options: { anchor: { @@ -921,9 +1279,6 @@ export const ru: Dict = { }, }, "change-aspect-ratio-png": { - title: "Изменить соотношение сторон PNG", - description: - "Вписывает изображение в целевое соотношение сторон: обрезать центр до заполнения или дополнить прозрачностью.", params: { ratio: "Целевое отношение", mode: "Режим" }, options: { ratio: { @@ -942,16 +1297,10 @@ export const ru: Dict = { }, }, "swap-orientation-png": { - title: "Поменять ориентацию PNG", - description: - "Поворачивает изображение на 90°, если ориентация отличается от целевой — ландшафт становится портретом и наоборот. Квадрат не трогается.", params: { target: "Целевая ориентация" }, options: { target: { portrait: "Портрет", landscape: "Ландшафт" } }, }, "symmetric-copy-png": { - title: "Симметричная копия PNG", - description: - "Удваивает холст, зеркалируя сохранённую половину на пустую — мгновенный симметричный узор.", params: { axis: "Линия зеркала", keepSide: "Какая сторона остаётся" }, options: { axis: { @@ -967,77 +1316,37 @@ export const ru: Dict = { }, }, "feather-edges-png": { - title: "Растушевать края PNG", - description: - "Размывает только альфа-канал: жёсткие края выреза становятся мягкими, цвета не трогаются.", params: { radius: "Радиус растушёвки, px" }, }, "clean-edges-png": { - title: "Почистить края PNG (defringe)", - description: - "Заменяет цветную кайму полупрозрачных пикселей цветом ближайшего полностью непрозрачного соседа. Альфа остаётся как была.", params: { radius: "Радиус поиска, px" }, }, "pixelate-png": { - title: "Пикселизация PNG", - description: - "Усредняет каждый блок blockSize×blockSize в один цвет — классическая мозаика.", params: { blockSize: "Размер блока, px" }, }, "randomize-pixels-png": { - title: "Перемешать пиксели PNG", - description: - "Переставляет блоки изображения между собой. Одинаковый seed даёт одинаковую раскладку.", params: { blockSize: "Размер блока, px", seed: "Seed" }, }, "add-noise-png": { - title: "Добавить шум PNG", - description: - "Добавляет зерно в стиле плёнки. Детерминировано по seed; монохромный сохраняет баланс оттенков.", params: { amount: "Сила, %", mode: "Тип шума", seed: "Seed" }, options: { mode: { mono: "Монохромное зерно", color: "Цветной шум" } }, }, "silhouette-png": { - title: "Силуэт PNG", - description: - "Заливает все видимые пиксели одним цветом, сохраняя их прозрачность — мгновенный силуэт.", params: { color: "Цвет силуэта", threshold: "Порог видимости, %" }, }, - "png-to-bytes": { - title: "PNG в байты", - description: - "Выводит каждый пиксель четырьмя десятичными байтами (R G B A), по строке изображения на строку текста.", - }, "bytes-to-png": { - title: "Байты в PNG", - description: - "Собирает изображение из десятичных RGBA-байтов (разделители любые). Укажите ширину — высота рассчитается сама.", params: { width: "Ширина изображения" }, }, - "png-to-rgb-values": { - title: "PNG в RGB-значения", - description: - "Выводит каждый пиксель как rgba(r, g, b, a), ряды через перевод строки.", - }, "rgb-values-to-png": { - title: "RGB-значения в PNG", - description: - "Собирает изображение из чисел rgba(r, g, b, a). Укажите ширину — высота рассчитается сама.", params: { width: "Ширина изображения" }, }, "verify-is-png": { - title: "Проверить: это PNG?", - description: - "Проверяет сигнатуру вставленного base64 / data-uri и сообщает, настоящий ли это PNG.", results: { verifyYes: "Да — сигнатура настоящего PNG.", verifyNo: "Нет — сигнатура не совпадает с PNG-файлом.", }, }, "text-to-png": { - title: "Текст в PNG", - description: - "Создаёт PNG из текста: холст подгоняется под размер надписи с полями.", params: { text: "Текст", fontSize: "Размер шрифта, px", @@ -1050,15 +1359,9 @@ export const ru: Dict = { }, }, "emoji-to-png": { - title: "Эмодзи в PNG", - description: - "Рисует эмодзи или любой Unicode-символ как прозрачный PNG выбранного размера.", params: { emoji: "Эмодзи / символ", size: "Размер" }, }, "placeholder-png": { - title: "Заглушка PNG", - description: - "Генерирует прямоугольник-заглушку с напечатанными размерами в центре.", params: { width: "Ширина", height: "Высота", @@ -1068,9 +1371,6 @@ export const ru: Dict = { }, }, "color-spectrum-png": { - title: "Спектр цветов PNG", - description: - "Полный радужный переход 0–360° вдоль выбранной оси с настройкой насыщенности и светлоты.", params: { width: "Ширина", height: "Высота", @@ -1083,9 +1383,6 @@ export const ru: Dict = { }, }, "random-colors-png": { - title: "Случайные цветные блоки PNG", - description: - "Заполняет холст случайными яркими блоками. Детерминировано по seed.", params: { width: "Ширина", height: "Высота", @@ -1094,9 +1391,6 @@ export const ru: Dict = { }, }, "draw-grid-png": { - title: "Нарисовать сетку PNG", - description: - "Рисует сетку с заданными столбцами, строками и толщиной линий на прозрачном или белом фоне.", params: { width: "Ширина", height: "Высота", @@ -1108,15 +1402,9 @@ export const ru: Dict = { }, }, "quantize-png": { - title: "Квантовать PNG", - description: - "Уменьшает изображение до k цветов через median-cut палитру. Прозрачные пиксели сохраняются.", params: { colors: "Цветов (k)" }, }, "decrease-color-count-png": { - title: "Уменьшить число цветов PNG", - description: - "Тот же движок median-cut с фиксированными пресетами степеней двойки — быстрый спуск до 2–256 цветов.", params: { maxColors: "Максимум цветов" }, options: { maxColors: { @@ -1135,32 +1423,20 @@ export const ru: Dict = { }, }, "custom-palette-png": { - title: "Своя палитра PNG", - description: - "Сопоставляет каждый пиксель с ближайшим цветом из вашего списка hex через запятую.", params: { colors: "Палитра (hex через запятую)" }, }, "dithering-png": { - title: "Дизеринг PNG", - description: - "Применяет распространение ошибки Флойда–Стейнберга или упорядоченный Байер при сведении к k цветам.", params: { colors: "Цветов (k)", pattern: "Узор" }, options: { pattern: { "floyd-steinberg": "Флойд–Стейнберг", bayer: "Байер 4×4" }, }, }, "png-file-size": { - title: "Размер файла PNG", - description: - "Кодирует изображение в PNG и показывает итоговый размер файла.", results: { line: "Размер PNG: {kb} КБ", }, }, "show-transparent-png": { - title: "Показать прозрачные области PNG", - description: - "Подсвечивает выбранным цветом каждый прозрачный или полупрозрачный пиксель — дыры становятся заметными.", params: { mode: "Режим маски", color: "Цвет подсветки", @@ -1171,9 +1447,6 @@ export const ru: Dict = { }, }, "show-grayscale-pixels-png": { - title: "Показать серые пиксели PNG", - description: - "Находит пиксели с почти равными каналами и рисует их маску. Допуск — в единицах канала.", params: { tolerance: "Допуск по каналам", mode: "Режим маски", @@ -1185,9 +1458,6 @@ export const ru: Dict = { }, }, "show-color-pixels-png": { - title: "Показать цветные пиксели PNG", - description: - "Находит цветные (не серые) пиксели за пределами допуска и рисует их маску.", params: { tolerance: "Допуск по каналам", mode: "Режим маски", @@ -1199,8 +1469,6 @@ export const ru: Dict = { }, }, "light-pixel-mask-png": { - title: "Маска светлых пикселей PNG", - description: "Выбирает пиксели ярче порога яркости.", params: { threshold: "Порог яркости, %", mode: "Режим маски", @@ -1212,8 +1480,6 @@ export const ru: Dict = { }, }, "dark-pixel-mask-png": { - title: "Маска тёмных пикселей PNG", - description: "Выбирает пиксели темнее порога яркости.", params: { threshold: "Порог яркости, %", mode: "Режим маски", @@ -1225,9 +1491,6 @@ export const ru: Dict = { }, }, "unique-color-mask-png": { - title: "Маска уникальных цветов PNG", - description: - "Выбирает цвета, которые встречаются не чаще заданного числа раз, — редкие и одиночные пиксели.", params: { rarity: "Максимум повторов", mode: "Режим маски", @@ -1239,18 +1502,12 @@ export const ru: Dict = { }, }, "extract-color-from-png": { - title: "Извлечь цвет из PNG", - description: - "Оставляет только пиксели, близкие к выбранному цвету, остальное делает прозрачным — обратное «Удалить цвет».", params: { color: "Какой цвет оставить", tolerance: "Допуск похожести, %", }, }, "watermark-tile-png": { - title: "Плитка-водяной знак PNG", - description: - "Покрывает изображение повторяющейся диагональной полупрозрачной плиткой текста — защитный водяной знак.", params: { text: "Текст", fontSize: "Размер шрифта, px", @@ -1264,9 +1521,6 @@ export const ru: Dict = { }, }, "add-text-png": { - title: "Надпись на PNG", - description: - "Рисует текст на изображении: шрифт, размер, цвет, жирность, позиция на сетке 3×3 и опциональная подложка.", params: { text: "Текст", fontSize: "Размер шрифта, px", @@ -1281,9 +1535,6 @@ export const ru: Dict = { }, }, "date-stamp-png": { - title: "Дата-штамп PNG", - description: - "Ставит текущую дату и время по строке формата (токены YYYY MM DD hh mm ss). Оформление — как у надписи.", params: { format: "Формат", fontSize: "Размер шрифта, px", @@ -1298,26 +1549,15 @@ export const ru: Dict = { }, }, "skew-png": { - title: "Наклонить PNG", - description: - "Сдвигает содержимое по горизонтали и вертикали — эффект перспективы.", params: { degX: "Наклон по X, °", degY: "Наклон по Y, °" }, }, "rotate-free-png": { - title: "Повернуть на произвольный угол", - description: - "Поворот на любой угол. Холст расширяется под новые габариты, углы остаются прозрачными.", params: { angle: "Угол, °" }, }, "zoom-png": { - title: "Приблизить PNG", - description: - "Увеличивает содержимое к центру. Холст прежнего размера — края обрезаются.", params: { scale: "Масштаб, %" }, }, "shift-png": { - title: "Сдвинуть PNG", - description: "Перемещает содержимое на заданное смещение по X и Y.", params: { offsetX: "Смещение X, px", offsetY: "Смещение Y, px", @@ -1325,57 +1565,30 @@ export const ru: Dict = { }, }, "vignette-png": { - title: "Виньетка PNG", - description: "Плавно затемняет края изображения, центр не затрагивает.", params: { strength: "Сила затемнения, %" }, }, "jpeg-artifacts-png": { - title: "Артефакты JPEG", - description: - "Имитирует пережатие в JPEG с низким качеством — видимые квадраты и размытие цветов.", params: { quality: "Качество JPEG" }, }, "gamma-png": { - title: "Гамма-коррекция PNG", - description: - "Корректирует яркость средних тонов. <1 темнее, >1 светлее, 1 — без изменений.", params: { value: "Гамма" }, }, - "auto-contrast-png": { - title: "Автоконтраст PNG", - description: - "Растягивает диапазон каждого канала на весь доступный диапазон яркости.", - }, "temperature-png": { - title: "Температура PNG", - description: - "Положительные значения делают изображение теплее (оранжевее), отрицательные — холоднее (синеватее).", params: { percent: "Температура" }, }, "tint-png": { - title: "Тонировать PNG", - description: - "Умножает цветовые каналы на выбранный оттенок с заданной силой.", params: { color: "Цвет тонирования", strength: "Сила, %" }, }, "svg-to-png": { - title: "SVG в PNG", - description: - "Декодирует SVG-разметку в растровое изображение. Вставьте SVG-код слева.", params: { width: "Ширина результата, px" }, }, "png-is-transparent": { - title: "Проверить: PNG прозрачный?", - description: - "Сообщает, есть ли в изображении прозрачные или полупрозрачные пиксели.", results: { transparentYes: "Да — есть прозрачные или полупрозрачные пиксели.", transparentNo: "Нет — все пиксели полностью непрозрачны.", }, }, "png-orientation": { - title: "Ориентация PNG", - description: "Сообщает, портрет это, ландшафт или квадрат.", results: { orientationPortrait: "Портрет — высота больше ширины.", orientationLandscape: "Ландшафт — ширина больше высоты.", diff --git a/web/src/lib/i18n/schema-tool-strings.ts b/web/src/lib/i18n/schema-tool-strings.ts index 3ae81c1..19b3e06 100644 --- a/web/src/lib/i18n/schema-tool-strings.ts +++ b/web/src/lib/i18n/schema-tool-strings.ts @@ -13,11 +13,11 @@ function labelOf(id: string): 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 { - return getMergedDict().tools[page.slug]?.description ?? page.description; + return getMergedDict().pages?.[page.slug]?.description ?? page.description; } export function fieldLabel(field: Field, id: string): string { @@ -65,17 +65,17 @@ function dedupe(values: string[]): string[] { } export function pageSearchDoc(page: Page): SearchDoc { - const active = getMergedDict().tools[page.slug]; + const active = getMergedDict().pages?.[page.slug]; return { id: page.slug, titles: dedupe([ active?.title ?? "", - ru.tools[page.slug]?.title ?? "", + ru.pages?.[page.slug]?.title ?? "", page.title, ]), descriptions: dedupe([ active?.description ?? "", - ru.tools[page.slug]?.description ?? "", + ru.pages?.[page.slug]?.description ?? "", page.description, ]), }; diff --git a/web/src/lib/i18n/tools-i18n.test.ts b/web/src/lib/i18n/tools-i18n.test.ts index d3e0904..609bee2 100644 --- a/web/src/lib/i18n/tools-i18n.test.ts +++ b/web/src/lib/i18n/tools-i18n.test.ts @@ -34,7 +34,7 @@ function selectOptions( describe("полнота словарей для нового registry", () => { it("у каждой страницы есть перевод ru с непустыми title/description", () => { 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?.title, `${page.slug}: title`).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("в словарях нет ключей без записи в реестре", () => { const known = new Set([ ...TOOLS.map((tool) => tool.id),