feat: move routes to slugs

This commit is contained in:
2026-09-27 16:35:27 +05:00
parent 9776e1d325
commit d09c954694
8 changed files with 25 additions and 26 deletions
+4 -4
View File
@@ -31,14 +31,14 @@ export async function useEnglish(page: Page): Promise<void> {
}); });
} }
export async function openTool(page: Page, id: string): Promise<void> { export async function openTool(page: Page, slug: string): Promise<void> {
await useEnglish(page); await useEnglish(page);
await page.goto(`/tools/${id}`); await page.goto(`/tools/${slug}`);
await expect(page.getByRole("heading", { level: 1 })).toBeVisible(); await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
} }
export function toolLink(page: Page, id: string): Locator { export function toolLink(page: Page, slug: string): Locator {
return page.locator(`a[href="/tools/${id}"]`); return page.locator(`a[href="/tools/${slug}"]`);
} }
export function resultImage(page: Page): Locator { export function resultImage(page: Page): Locator {
+1 -1
View File
@@ -14,7 +14,7 @@
let { title, slug, description, index, icon }: Props = $props(); let { title, slug, description, index, icon }: Props = $props();
</script> </script>
<a class="tool-card" href={resolve("/tools/[id]", { id: slug })}> <a class="tool-card" href={resolve("/tools/[slug]", { slug })}>
{#if icon}<span class="tool-icon"><Icon {icon} size={19} /></span>{/if} {#if icon}<span class="tool-icon"><Icon {icon} size={19} /></span>{/if}
<span class="tool-copy"> <span class="tool-copy">
<strong>{title}</strong> <strong>{title}</strong>
+2 -3
View File
@@ -166,7 +166,7 @@ export const en: Dict = {
textRequired: "Enter text first", textRequired: "Enter text first",
workerFailed: "Worker execution failed", workerFailed: "Worker execution failed",
workerUnavailable: "Worker is unavailable", workerUnavailable: "Worker is unavailable",
notFound: "Tool not found", notFound: "Page not found",
noWatermark: "Pick a watermark image first", noWatermark: "Pick a watermark image first",
badTransform: "Degenerate transformation matrix", badTransform: "Degenerate transformation matrix",
skewAngle: "Skew angles cannot be 90° or -90°", skewAngle: "Skew angles cannot be 90° or -90°",
@@ -189,7 +189,6 @@ export const en: Dict = {
"Each pixel must be 8 hex characters RRGGBBAA, separated by spaces", "Each pixel must be 8 hex characters RRGGBBAA, separated by spaces",
pixelCountMismatch: pixelCountMismatch:
"Pixel count ({count}) is not divisible by width {width} without a remainder", "Pixel count ({count}) is not divisible by width {width} without a remainder",
toolNotFound: 'Tool "{id}" not found',
badJson: "The file is not valid JSON", badJson: "The file is not valid JSON",
badPipelineShape: "The file structure does not look like a chain of steps", badPipelineShape: "The file structure does not look like a chain of steps",
pipelineVersion: "Unsupported chain version: {version}", pipelineVersion: "Unsupported chain version: {version}",
@@ -201,7 +200,7 @@ export const en: Dict = {
cropSize: "Crop width and height must be positive", cropSize: "Crop width and height must be positive",
sizePositive: "Dimensions must be positive and finite", sizePositive: "Dimensions must be positive and finite",
tooManyParts: "Too many parts ({count}). Maximum is 1000.", tooManyParts: "Too many parts ({count}). Maximum is 1000.",
toolUnknown: 'No tool is registered under "{id}".', pageUnknown: 'No page is registered under "{slug}".',
}, },
fields: { fields: {
alphaThreshold: "Alpha threshold", alphaThreshold: "Alpha threshold",
+2 -3
View File
@@ -167,7 +167,7 @@ export const ru: Dict = {
textRequired: "Сначала введите текст", textRequired: "Сначала введите текст",
workerFailed: "Ошибка исполнения в воркере", workerFailed: "Ошибка исполнения в воркере",
workerUnavailable: "Воркер недоступен", workerUnavailable: "Воркер недоступен",
notFound: "Инструмент не найден", notFound: "Страница не найдена",
noWatermark: "Сначала выберите картинку-знак", noWatermark: "Сначала выберите картинку-знак",
badTransform: "Вырожденная матрица трансформации", badTransform: "Вырожденная матрица трансформации",
skewAngle: "Углы наклона не могут быть 90° или -90°", skewAngle: "Углы наклона не могут быть 90° или -90°",
@@ -190,7 +190,6 @@ export const ru: Dict = {
"Каждый пиксель — 8 hex-символов RRGGBBAA, значения через пробел", "Каждый пиксель — 8 hex-символов RRGGBBAA, значения через пробел",
pixelCountMismatch: pixelCountMismatch:
"Число пикселей ({count}) не делится на ширину {width} без остатка", "Число пикселей ({count}) не делится на ширину {width} без остатка",
toolNotFound: 'Инструмент "{id}" не найден',
badJson: "Файл не является корректным JSON", badJson: "Файл не является корректным JSON",
badPipelineShape: "Структура файла не похожа на цепочку шагов", badPipelineShape: "Структура файла не похожа на цепочку шагов",
pipelineVersion: "Неподдерживаемая версия цепочки: {version}", pipelineVersion: "Неподдерживаемая версия цепочки: {version}",
@@ -202,7 +201,7 @@ export const ru: Dict = {
cropSize: "Ширина и высота области обрезки должны быть положительными", cropSize: "Ширина и высота области обрезки должны быть положительными",
sizePositive: "Размеры должны быть положительными и конечными", sizePositive: "Размеры должны быть положительными и конечными",
tooManyParts: "Слишком много частей ({count}). Ограничение — 1000.", tooManyParts: "Слишком много частей ({count}). Ограничение — 1000.",
toolUnknown: "Инструмента с id «{id}» нет в реестре.", pageUnknown: "Страницы «{slug}» нет в каталоге.",
}, },
fields: { fields: {
alphaThreshold: "Порог альфы", alphaThreshold: "Порог альфы",
+2 -1
View File
@@ -64,11 +64,12 @@ describe("Смоук §6 i18n (новый UI)", () => {
expect(t("errors.badHex", { value: "#zz" })).toBe( expect(t("errors.badHex", { value: "#zz" })).toBe(
'Некорректный HEX-цвет: "#zz"', 'Некорректный HEX-цвет: "#zz"',
); );
expect(t("errors.toolNotFound", { id: "x" })).toContain("не найден"); expect(t("errors.pageUnknown", { slug: "x" })).toContain("каталоге");
setLocale("en"); setLocale("en");
expect(t("errors.badHex", { value: "#zz" })).toBe( expect(t("errors.badHex", { value: "#zz" })).toBe(
'Invalid HEX color: "#zz"', 'Invalid HEX color: "#zz"',
); );
expect(t("errors.pageUnknown", { slug: "x" })).toContain("No page");
}); });
it("6. Ё не мешает нормализации поискового запроса", () => { it("6. Ё не мешает нормализации поискового запроса", () => {
-11
View File
@@ -1,11 +0,0 @@
import { PAGES } from "$lib/registry";
export const prerender = true;
export function entries() {
return PAGES.map((page) => ({ id: page.slug }));
}
export function load({ params }: { params: { id: string } }) {
return { id: params.id };
}
@@ -6,11 +6,11 @@
import { SlidersHorizontal as ToolIcon } from "@lucide/svelte"; import { SlidersHorizontal as ToolIcon } from "@lucide/svelte";
interface Props { interface Props {
data: { id: string }; data: { slug: string };
} }
let { data }: Props = $props(); let { data }: Props = $props();
const page = $derived(getPageBySlug(data.id)); const page = $derived(getPageBySlug(data.slug));
const tool = $derived(page ? getTool(page.steps[0].id) : undefined); const tool = $derived(page ? getTool(page.steps[0].id) : undefined);
</script> </script>
@@ -23,7 +23,7 @@
<div class="notfound"> <div class="notfound">
<EmptyState <EmptyState
title={t("errors.notFound")} title={t("errors.notFound")}
description={t("errors.toolUnknown", { id: data.id })} description={t("errors.pageUnknown", { slug: data.slug })}
icon={ToolIcon} icon={ToolIcon}
/> />
</div> </div>
+11
View File
@@ -0,0 +1,11 @@
import { PAGES } from "$lib/registry";
export const prerender = true;
export function entries() {
return PAGES.map((page) => ({ slug: page.slug }));
}
export function load({ params }: { params: { slug: string } }) {
return { slug: params.slug };
}