feat: move all common messages to i18n (ru\en)

This commit is contained in:
2026-08-25 05:45:34 +05:00
parent 8255d33098
commit 2301b0e165
22 changed files with 100 additions and 87 deletions
+3 -2
View File
@@ -3,6 +3,7 @@
import { downloadBlob, encode } from '$lib/core/io'; import { downloadBlob, encode } from '$lib/core/io';
import type { PixelImage } from '$lib/core/types'; import type { PixelImage } from '$lib/core/types';
import type { OutputFormat } from '$lib/registry'; import type { OutputFormat } from '$lib/registry';
import { t } from '$lib/i18n/t';
interface Props { interface Props {
image: PixelImage | null; image: PixelImage | null;
@@ -38,8 +39,8 @@
fullWidth fullWidth
disabled={!image || !format} disabled={!image || !format}
{busy} {busy}
busyText="Готовим файл…" busyText={t('download.busy')}
onclick={download} onclick={download}
> >
Скачать .{format?.ext ?? 'png'} {t('download.file', { ext: format?.ext ?? 'png' })}
</Button> </Button>
+2 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import { isSupportedImage, unsupportedImageMessage } from '$lib/core/io'; import { isSupportedImage, unsupportedImageMessage } from '$lib/core/io';
import { t } from '$lib/i18n/t';
interface Props { interface Props {
onFile: (file: File) => void; onFile: (file: File) => void;
@@ -12,7 +13,7 @@
let { let {
onFile, onFile,
onError, onError,
label = 'Отпустите файл, чтобы заменить изображение', label = t('dropZone.overlayDefault'),
children children
}: Props = $props(); }: Props = $props();
+2 -2
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { ACCEPTED_IMAGE_TYPES, isSupportedImage, unsupportedImageMessage } from '$lib/core/io'; import { ACCEPTED_IMAGE_TYPES, isSupportedImage, unsupportedImageMessage } from '$lib/core/io';
import { t } from '$lib/i18n/t';
interface Props { interface Props {
onFile: (file: File) => void; onFile: (file: File) => void;
@@ -7,8 +8,7 @@
label?: string; label?: string;
} }
let { onFile, onError, label = 'Перетащите изображение сюда или нажмите, чтобы выбрать файл' }: Props = let { onFile, onError, label = t('dropZone.pickDefault') }: Props = $props();
$props();
let input = $state<HTMLInputElement | undefined>(); let input = $state<HTMLInputElement | undefined>();
let depth = $state(0); let depth = $state(0);
+8 -5
View File
@@ -1,5 +1,8 @@
<script lang="ts"> <script lang="ts">
import type { ImageInfo } from '$lib/core/analyze'; import type { ImageInfo } from '$lib/core/analyze';
import { LOCALE_TAGS } from '$lib/i18n/dict';
import { getLocale } from '$lib/i18n/locale.svelte';
import { t } from '$lib/i18n/t';
interface Props { interface Props {
info: ImageInfo | null; info: ImageInfo | null;
@@ -11,16 +14,16 @@
{#if info} {#if info}
<dl> <dl>
<div class="panel"> <div class="panel">
<dt>Размеры</dt> <dt>{t('infoPanel.dimensions')}</dt>
<dd>{info.width} × {info.height} px</dd> <dd>{info.width} × {info.height} px</dd>
</div> </div>
<div class="panel"> <div class="panel">
<dt>Альфа-канал</dt> <dt>{t('infoPanel.alpha')}</dt>
<dd>{info.hasAlpha ? 'есть — есть полупрозрачные пиксели' : 'нет'}</dd> <dd>{info.hasAlpha ? t('infoPanel.alphaYes') : t('infoPanel.alphaNo')}</dd>
</div> </div>
<div class="panel"> <div class="panel">
<dt>Уникальных цветов (RGBA)</dt> <dt>{t('infoPanel.colorCount')}</dt>
<dd>{info.colorCount.toLocaleString('ru-RU')}</dd> <dd>{info.colorCount.toLocaleString(LOCALE_TAGS[getLocale()])}</dd>
</div> </div>
</dl> </dl>
{/if} {/if}
+2 -1
View File
@@ -5,6 +5,7 @@
import SliderField from './ui/SliderField.svelte'; import SliderField from './ui/SliderField.svelte';
import TextField from './ui/TextField.svelte'; import TextField from './ui/TextField.svelte';
import type { ParamDef } from '$lib/registry'; import type { ParamDef } from '$lib/registry';
import { t } from '$lib/i18n/t';
interface Props { interface Props {
params: ParamDef[]; params: ParamDef[];
@@ -27,7 +28,7 @@
<div class="params-grid"> <div class="params-grid">
{#if hasMask} {#if hasMask}
<CheckboxField id="show-mask" label="Показать маску" bind:checked={showMask} /> <CheckboxField id="show-mask" label={t('ui.showMask')} bind:checked={showMask} />
{/if} {/if}
{#each params as param (param.id)} {#each params as param (param.id)}
<div class="field"> <div class="field">
+12 -8
View File
@@ -11,6 +11,7 @@
import ChainToolBlock from './chain/ChainToolBlock.svelte'; import ChainToolBlock from './chain/ChainToolBlock.svelte';
import { createAutoRunner } from '$lib/tools/auto-run'; import { createAutoRunner } from '$lib/tools/auto-run';
import { executeStep } from '$lib/tools/executor'; import { executeStep } from '$lib/tools/executor';
import { t } from '$lib/i18n/t';
import ParamsCard from './tool/ParamsCard.svelte'; import ParamsCard from './tool/ParamsCard.svelte';
import ResultCard from './tool/ResultCard.svelte'; import ResultCard from './tool/ResultCard.svelte';
import SourceCard from './tool/SourceCard.svelte'; import SourceCard from './tool/SourceCard.svelte';
@@ -175,8 +176,10 @@
try { try {
current = await executeStep(stepTool, current, sanitizeParams(stepTool, step.values)); current = await executeStep(stepTool, current, sanitizeParams(stepTool, step.values));
} catch (e) { } catch (e) {
const message = e instanceof Error ? e.message : String(e); const rawMsg = e instanceof Error ? e.message : String(e);
throw new Error(`Шаг ${i + 1} (${stepTool.title}): ${message}`); throw new Error(
t('toolPage.stepError', { n: i + 1, title: stepTool.title, msg: t(rawMsg) })
);
} }
if (!runner.isCurrent(token)) return; if (!runner.isCurrent(token)) return;
collected.push(current); collected.push(current);
@@ -213,7 +216,8 @@
function showError(e: unknown) { function showError(e: unknown) {
status = source ? 'loaded' : 'idle'; status = source ? 'loaded' : 'idle';
errorText = e instanceof Error ? e.message : String(e); const raw = e instanceof Error ? e.message : String(e);
errorText = t(raw);
} }
function reset() { function reset() {
@@ -261,7 +265,7 @@
<div class="panel tool-block"> <div class="panel tool-block">
<div class="tool-stage" class:single={isSourceless}> <div class="tool-stage" class:single={isSourceless}>
{#if !isSourceless} {#if !isSourceless}
<span class="edge-legend source-legend" aria-hidden="true">Исходник</span> <span class="edge-legend source-legend" aria-hidden="true">{t('toolPage.legendSource')}</span>
<div class="cell"> <div class="cell">
{#if isTextSource && !source} {#if isTextSource && !source}
<TextInputCard onSubmit={handleTextSubmit} /> <TextInputCard onSubmit={handleTextSubmit} />
@@ -278,7 +282,7 @@
</div> </div>
{/if} {/if}
<span class="edge-legend result-legend" aria-hidden="true"> <span class="edge-legend result-legend" aria-hidden="true">
{isInfo ? 'Сводка' : 'Результат'} {isInfo ? t('toolPage.legendSummary') : t('toolPage.legendResult')}
</span> </span>
<div class="cell"> <div class="cell">
<ResultCard <ResultCard
@@ -300,7 +304,7 @@
{#if (source || isSourceless) && !isInfo && (tool.params.length > 0 || hasMask)} {#if (source || isSourceless) && !isInfo && (tool.params.length > 0 || hasMask)}
<div class="params-sep"> <div class="params-sep">
<span class="edge-legend" aria-hidden="true">Параметры</span> <span class="edge-legend" aria-hidden="true">{t('toolPage.legendParams')}</span>
</div> </div>
<ParamsCard <ParamsCard
params={tool.params} params={tool.params}
@@ -318,11 +322,11 @@
{#if step.toolId === ''} {#if step.toolId === ''}
<div class="panel empty-slot"> <div class="panel empty-slot">
<header> <header>
<h3 class="heading-section">Шаг {index + 1}</h3> <h3 class="heading-section">{t('toolPage.stepHeading', { n: index + 1 })}</h3>
<button <button
type="button" type="button"
class="remove-step" class="remove-step"
aria-label="Убрать шаг" aria-label={t('toolPage.removeStepAria')}
onclick={() => removeChainStep(index)} onclick={() => removeChainStep(index)}
> >
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { outputOf, sanitizeParams, type ToolEntry } from '$lib/registry'; import { outputOf, sanitizeParams, type ToolEntry } from '$lib/registry';
import type { PixelImage } from '$lib/core/types'; import type { PixelImage } from '$lib/core/types';
import { t } from '$lib/i18n/t';
import DownloadButton from '../DownloadButton.svelte'; import DownloadButton from '../DownloadButton.svelte';
import Button from '../ui/Button.svelte'; import Button from '../ui/Button.svelte';
import EmptyState from '../ui/EmptyState.svelte'; import EmptyState from '../ui/EmptyState.svelte';
@@ -47,12 +48,12 @@
{#if StepIcon} {#if StepIcon}
<span class="step-icon" aria-hidden="true"><StepIcon size={14} strokeWidth={2} /></span> <span class="step-icon" aria-hidden="true"><StepIcon size={14} strokeWidth={2} /></span>
{/if} {/if}
Шаг {index + 1}: {tool.title} {t('chain.stepLabel', { n: index + 1, title: tool.title })}
<button <button
type="button" type="button"
class="remove" class="remove"
aria-label="Убрать шаг" aria-label={t('chain.removeStepAria')}
title="Убрать шаг" title={t('chain.removeStepAria')}
onclick={onRemove} onclick={onRemove}
> >
@@ -60,22 +61,22 @@
</span> </span>
<div class="cell"> <div class="cell">
<span class="edge-legend cell-legend" aria-hidden="true">Вход</span> <span class="edge-legend cell-legend" aria-hidden="true">{t('chain.inputLegend')}</span>
<div class="cell-media"> <div class="cell-media">
<Preview image={input} /> <Preview image={input} />
</div> </div>
</div> </div>
<div class="cell"> <div class="cell">
<span class="edge-legend cell-legend" aria-hidden="true">Результат</span> <span class="edge-legend cell-legend" aria-hidden="true">{t('chain.resultLegend')}</span>
{#if busy && !result} {#if busy && !result}
<div class="cell-media"> <div class="cell-media">
<EmptyState title="Обработка…" hint="Выполняется шаг цепочки" /> <EmptyState title={t('chain.busyTitle')} hint={t('chain.busyHint')} />
</div> </div>
{:else} {:else}
<div class="cell-media"> <div class="cell-media">
<Preview image={result} /> <Preview image={result} />
{#if busy} {#if busy}
<span class="recalc" aria-live="polite">Пересчёт…</span> <span class="recalc" aria-live="polite">{t('resultCard.recalc')}</span>
{/if} {/if}
</div> </div>
<div class="actions-row"> <div class="actions-row">
@@ -87,7 +88,7 @@
{onError} {onError}
/> />
<Button variant="secondary" fullWidth onclick={isLast ? onAddStep : onRemoveChain}> <Button variant="secondary" fullWidth onclick={isLast ? onAddStep : onRemoveChain}>
{isLast ? '⛓ Следующий инструмент' : '✂ Оборвать цепочку'} {isLast ? t('resultCard.nextTool') : t('resultCard.breakChain')}
</Button> </Button>
</div> </div>
{/if} {/if}
@@ -96,7 +97,7 @@
{#if tool.params.length > 0} {#if tool.params.length > 0}
<div class="params-sep"> <div class="params-sep">
<span class="edge-legend" aria-hidden="true">Параметры</span> <span class="edge-legend" aria-hidden="true">{t('chain.paramsLegend')}</span>
</div> </div>
<ParamsCard params={tool.params} bind:values /> <ParamsCard params={tool.params} bind:values />
{/if} {/if}
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { isChainable, TOOLS } from '$lib/registry'; import { isChainable, TOOLS } from '$lib/registry';
import { t } from '$lib/i18n/t';
import ToolCard from './ToolCard.svelte'; import ToolCard from './ToolCard.svelte';
interface Props { interface Props {
@@ -106,14 +107,14 @@
activeIndex = 0; activeIndex = 0;
}} }}
onkeydown={onKeydown} onkeydown={onKeydown}
placeholder="Найдите инструмент…" placeholder={t('search.placeholder')}
aria-label="Поиск инструмента" aria-label={t('search.aria')}
role="combobox" role="combobox"
aria-expanded={listOpen} aria-expanded={listOpen}
aria-controls="tool-search-list" aria-controls="tool-search-list"
/> />
{#if listOpen && query.trim().length > 0 && matches.length === 0} {#if listOpen && query.trim().length > 0 && matches.length === 0}
<p class="none text-muted">Ничего не найдено — попробуйте другое слово.</p> <p class="none text-muted">{t('search.nothingFound')}</p>
{:else if listOpen && matches.length > 0} {:else if listOpen && matches.length > 0}
<div id="tool-search-list" class="cards" role="listbox"> <div id="tool-search-list" class="cards" role="listbox">
{#each matches as match, index (match.id)} {#each matches as match, index (match.id)}
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import ParamForm from '../ParamForm.svelte'; import ParamForm from '../ParamForm.svelte';
import type { ParamDef } from '$lib/registry'; import type { ParamDef } from '$lib/registry';
import { t } from '$lib/i18n/t';
interface Props { interface Props {
params: ParamDef[]; params: ParamDef[];
@@ -32,9 +33,7 @@
bind:showMask bind:showMask
/> />
{:else} {:else}
<p class="hint text-caption text-muted"> <p class="hint text-caption text-muted">{t('paramsCard.noParams')}</p>
У этого инструмента нет параметров — результат уже готов.
</p>
{/if} {/if}
</div> </div>
@@ -8,6 +8,7 @@
import type { ImageInfo } from '$lib/core/analyze'; import type { ImageInfo } from '$lib/core/analyze';
import type { PixelImage } from '$lib/core/types'; import type { PixelImage } from '$lib/core/types';
import { outputOf, type ToolEntry } from '$lib/registry'; import { outputOf, type ToolEntry } from '$lib/registry';
import { t } from '$lib/i18n/t';
type Status = 'idle' | 'loaded' | 'processing' | 'error'; type Status = 'idle' | 'loaded' | 'processing' | 'error';
@@ -45,16 +46,13 @@
<div class="container"> <div class="container">
{#if !sourceLoaded} {#if !sourceLoaded}
<div class="media"> <div class="media">
<EmptyState <EmptyState title={t('resultCard.emptyTitle')} hint={t('resultCard.emptyHint')} />
title="Результат появится здесь"
hint="Сначала загрузите исходное изображение слева"
/>
</div> </div>
{:else if !isInfo && status === 'processing' && !result} {:else if !isInfo && status === 'processing' && !result}
<div class="media"> <div class="media">
<EmptyState <EmptyState
title="Обработка…" title={t('resultCard.processingTitle')}
hint="Изображение обрабатывается, это займёт немного времени" hint={t('resultCard.processingHint')}
/> />
</div> </div>
{:else if isInfo} {:else if isInfo}
@@ -73,7 +71,7 @@
<div class="media"> <div class="media">
<Preview image={displayImage} /> <Preview image={displayImage} />
{#if status === 'processing'} {#if status === 'processing'}
<span class="recalc" aria-live="polite">Пересчёт…</span> <span class="recalc" aria-live="polite">{t('resultCard.recalc')}</span>
{/if} {/if}
</div> </div>
<div class="actions-row"> <div class="actions-row">
@@ -86,7 +84,7 @@
/> />
{#if onChainToggle} {#if onChainToggle}
<Button variant="secondary" fullWidth onclick={onChainToggle}> <Button variant="secondary" fullWidth onclick={onChainToggle}>
{hasChain ? '✂ Оборвать цепочку' : '⛓ Следующий инструмент'} {hasChain ? t('resultCard.breakChain') : t('resultCard.nextTool')}
</Button> </Button>
{/if} {/if}
</div> </div>
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { PixelImage } from '$lib/core/types'; import type { PixelImage } from '$lib/core/types';
import { t } from '$lib/i18n/t';
import DropOverlay from '../DropOverlay.svelte'; import DropOverlay from '../DropOverlay.svelte';
import DropZone from '../DropZone.svelte'; import DropZone from '../DropZone.svelte';
import Preview from '../Preview.svelte'; import Preview from '../Preview.svelte';
@@ -25,7 +26,7 @@
<div class="media"> <div class="media">
<Preview image={source} pipetteActive={pipetteActive} onPickColor={onPickColor} /> <Preview image={source} pipetteActive={pipetteActive} onPickColor={onPickColor} />
</div> </div>
<Button variant="secondary" onclick={onReset}>Заменить изображение</Button> <Button variant="secondary" onclick={onReset}>{t('sourceCard.replaceImage')}</Button>
</DropOverlay> </DropOverlay>
{/if} {/if}
</div> </div>
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import Button from '../ui/Button.svelte'; import Button from '../ui/Button.svelte';
import { t } from '$lib/i18n/t';
interface Props { interface Props {
onSubmit: (text: string) => void; onSubmit: (text: string) => void;
@@ -16,16 +17,16 @@
</script> </script>
<div class="container"> <div class="container">
<h2 class="heading-section">Текст</h2> <h2 class="heading-section">{t('textInput.heading')}</h2>
<textarea <textarea
class="input" class="input"
rows="8" rows="8"
bind:value={text} bind:value={text}
placeholder="Вставьте данные сюда" placeholder={t('textInput.placeholder')}
aria-label="Текстовые данные" aria-label={t('textInput.aria')}
></textarea> ></textarea>
<Button variant="secondary" onclick={submit} disabled={text.trim().length === 0}> <Button variant="secondary" onclick={submit} disabled={text.trim().length === 0}>
Декодировать {t('textInput.decode')}
</Button> </Button>
</div> </div>
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { downloadBlob } from '$lib/core/io'; import { downloadBlob } from '$lib/core/io';
import { t } from '$lib/i18n/t';
interface Props { interface Props {
text: string; text: string;
@@ -22,12 +23,12 @@
</script> </script>
<div class="panel text-result"> <div class="panel text-result">
<textarea class="output" rows="10" readonly value={text} aria-label="Текстовый результат"></textarea> <textarea class="output" rows="10" readonly value={text} aria-label={t('textResult.outputAria')}></textarea>
<div class="actions"> <div class="actions">
<button type="button" class="secondary" onclick={copy}> <button type="button" class="secondary" onclick={copy}>
{copied ? 'Скопировано' : 'Копировать'} {copied ? t('textResult.copied') : t('textResult.copy')}
</button> </button>
<button type="button" class="primary" onclick={download}>Скачать .txt</button> <button type="button" class="primary" onclick={download}>{t('textResult.downloadTxt')}</button>
</div> </div>
</div> </div>
+3 -2
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import Field from './Field.svelte'; import Field from './Field.svelte';
import { t } from '$lib/i18n/t';
interface Props { interface Props {
id: string; id: string;
@@ -21,9 +22,9 @@
<button <button
type="button" type="button"
class="pipette" class="pipette"
aria-label="Пипетка" aria-label={t('ui.pipette')}
aria-pressed={pipetteActive} aria-pressed={pipetteActive}
title="Пипетка" title={t('ui.pipette')}
onclick={onPipetteToggle} onclick={onPipetteToggle}
> >
+4 -3
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import Field from './Field.svelte'; import Field from './Field.svelte';
import { t } from '$lib/i18n/t';
interface Props { interface Props {
id: string; id: string;
@@ -42,13 +43,13 @@
<Field {id} {label} {hint}> <Field {id} {label} {hint}>
<div class="row"> <div class="row">
<button type="button" class="step" aria-label="Уменьшить" onclick={decrement}></button> <button type="button" class="step" aria-label={t('ui.decrease')} onclick={decrement}></button>
<input id={id} type="range" min={min} max={max} step={step} bind:value /> <input id={id} type="range" min={min} max={max} step={step} bind:value />
<button type="button" class="step" aria-label="Увеличить" onclick={increment}>+</button> <button type="button" class="step" aria-label={t('ui.increase')} onclick={increment}>+</button>
<button <button
type="button" type="button"
class="step" class="step"
aria-label="Сбросить" aria-label={t('ui.reset')}
disabled={resetDisabled} disabled={resetDisabled}
onclick={reset} onclick={reset}
> >
+1 -1
View File
@@ -31,7 +31,7 @@ describe('executeStep: прямой путь (среда без Worker)', () =>
it('инструмент без run даёт понятную ошибку', async () => { it('инструмент без run даёт понятную ошибку', async () => {
await expect(executeStep({ id: 'stub' }, makeImage(1, 1, [[0, 0, 0, 255]]), {})).rejects.toThrow( await expect(executeStep({ id: 'stub' }, makeImage(1, 1, [[0, 0, 0, 255]]), {})).rejects.toThrow(
'не обрабатывает изображения' 'errors.noImageRun'
); );
}); });
}); });
+4 -4
View File
@@ -11,7 +11,7 @@ export async function executeStep(
params: Record<string, unknown> params: Record<string, unknown>
): Promise<PixelImage> { ): Promise<PixelImage> {
if (!tool.run) { if (!tool.run) {
throw new Error('Этот инструмент не обрабатывает изображения'); throw new Error('errors.noImageRun');
} }
if (typeof Worker === 'undefined') { if (typeof Worker === 'undefined') {
return await runDirect(tool, img, params); return await runDirect(tool, img, params);
@@ -35,7 +35,7 @@ async function runDirect(
params: Record<string, unknown> params: Record<string, unknown>
): Promise<PixelImage> { ): Promise<PixelImage> {
if (!tool.run) { if (!tool.run) {
throw new Error('Этот инструмент не обрабатывает изображения'); throw new Error('errors.noImageRun');
} }
return await tool.run(img, params); return await tool.run(img, params);
} }
@@ -74,13 +74,13 @@ function ensureWorker(): Worker | null {
data: new Uint8ClampedArray(payload.data) data: new Uint8ClampedArray(payload.data)
}); });
} else { } else {
entry.reject(new Error(payload.error ?? 'Ошибка исполнения в воркере')); entry.reject(new Error(payload.error ?? 'errors.workerFailed'));
} }
}; };
candidate.onerror = () => { candidate.onerror = () => {
worker = null; worker = null;
for (const entry of pending.values()) { for (const entry of pending.values()) {
entry.reject(new Error('Воркер недоступен')); entry.reject(new Error('errors.workerUnavailable'));
} }
pending.clear(); pending.clear();
}; };
+1 -1
View File
@@ -17,7 +17,7 @@ async function handle(request: WorkerRequest): Promise<void> {
try { try {
const tool = getTool(request.toolId); const tool = getTool(request.toolId);
if (!tool?.run) { if (!tool?.run) {
throw new Error('Этот инструмент не обрабатывает изображения'); throw new Error('errors.noImageRun');
} }
const image: PixelImage = { const image: PixelImage = {
width: request.image.width, width: request.image.width,
+6 -5
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { getTool } from '$lib/registry'; import { getTool } from '$lib/registry';
import { t } from '$lib/i18n/t';
import ToolPage from '$lib/components/ToolPage.svelte'; import ToolPage from '$lib/components/ToolPage.svelte';
import ToolSearch from '$lib/components/search/ToolSearch.svelte'; import ToolSearch from '$lib/components/search/ToolSearch.svelte';
import Button from '$lib/components/ui/Button.svelte'; import Button from '$lib/components/ui/Button.svelte';
@@ -42,20 +43,20 @@
<svelte:head> <svelte:head>
<title> <title>
{selected ? `${selected.title} easy-png-tools` : 'easy-png-tools — PNG-утилиты прямо в браузере'} {selected ? `${selected.title} easy-png-tools` : t('home.defaultTitle')}
</title> </title>
</svelte:head> </svelte:head>
{#if !selected} {#if !selected}
<section class="hero"> <section class="hero">
<h1>Что делаем с изображением?</h1> <h1>{t('home.heroTitle')}</h1>
<p class="lead text-muted">Найдите инструмент — все операции выполняются локально в браузере.</p> <p class="lead text-muted">{t('home.heroLead')}</p>
<ToolSearch onSelect={(id) => openTool(id)} /> <ToolSearch onSelect={(id) => openTool(id)} />
{#if lastToolId !== null && getTool(lastToolId)} {#if lastToolId !== null && getTool(lastToolId)}
{@const restoreId = lastToolId} {@const restoreId = lastToolId}
<div class="restore-row"> <div class="restore-row">
<Button variant="secondary" fullWidth onclick={() => openTool(restoreId, true)}> <Button variant="secondary" fullWidth onclick={() => openTool(restoreId, true)}>
↩ Вернуть последний: {getTool(restoreId)?.title} {t('home.restoreLast', { title: getTool(restoreId)?.title ?? '' })}
</Button> </Button>
</div> </div>
{/if} {/if}
@@ -63,7 +64,7 @@
{:else} {:else}
<section class="workbench"> <section class="workbench">
<button type="button" class="back text-caption text-muted" onclick={closeTool}> <button type="button" class="back text-caption text-muted" onclick={closeTool}>
← Сменить инструмент {t('home.changeTool')}
</button> </button>
{#key selectedId} {#key selectedId}
<ToolPage tool={selected} restoreChain={restoreOnOpen} /> <ToolPage tool={selected} restoreChain={restoreOnOpen} />
+6 -10
View File
@@ -1,27 +1,23 @@
<script lang="ts"> <script lang="ts">
import { CATEGORIES } from '$lib/categories'; import { CATEGORIES } from '$lib/categories';
import { t } from '$lib/i18n/t';
import ToolCard from '$lib/components/search/ToolCard.svelte'; import ToolCard from '$lib/components/search/ToolCard.svelte';
import { TOOLS } from '$lib/registry'; import { TOOLS } from '$lib/registry';
</script> </script>
<svelte:head> <svelte:head>
<title>Все инструменты — easy-png-tools</title> <title>{t('catalog.pageTitle')}</title>
<meta <meta name="description" content={t('catalog.metaDescription')} />
name="description"
content="Полный каталог PNG-утилит: конвертация, прозрачность, цвет, геометрия, анализ и генерация изображений."
/>
</svelte:head> </svelte:head>
<h1>Каталог инструментов</h1> <h1>{t('catalog.heading')}</h1>
<p class="lead text-muted"> <p class="lead text-muted">{t('catalog.lead', { count: TOOLS.length })}</p>
{TOOLS.length} утилит для работы с PNG. Все операции выполняются локально в браузере.
</p>
{#each CATEGORIES as category (category.id)} {#each CATEGORIES as category (category.id)}
{@const categoryTools = TOOLS.filter((tool) => tool.category === category.id)} {@const categoryTools = TOOLS.filter((tool) => tool.category === category.id)}
{#if categoryTools.length > 0} {#if categoryTools.length > 0}
<section id={category.id} class="category" aria-labelledby="{category.id}-heading"> <section id={category.id} class="category" aria-labelledby="{category.id}-heading">
<h2 id="{category.id}-heading" class="heading-section">{category.label}</h2> <h2 id="{category.id}-heading" class="heading-section">{t(`categories.${category.id}`)}</h2>
<div class="grid"> <div class="grid">
{#each categoryTools as tool (tool.id)} {#each categoryTools as tool (tool.id)}
<div class="panel"> <div class="panel">
+2 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import ToolPage from '$lib/components/ToolPage.svelte'; import ToolPage from '$lib/components/ToolPage.svelte';
import { getTool } from '$lib/registry'; import { getTool } from '$lib/registry';
import { t } from '$lib/i18n/t';
let { data } = $props(); let { data } = $props();
@@ -8,7 +9,7 @@
</script> </script>
<svelte:head> <svelte:head>
<title>{tool?.title ?? 'Инструмент'} — easy-png-tools</title> <title>{tool?.title ?? t('toolPage.fallbackTitle')} — easy-png-tools</title>
{#if tool} {#if tool}
<meta name="description" content={tool.description} /> <meta name="description" content={tool.description} />
{/if} {/if}
+2 -1
View File
@@ -1,5 +1,6 @@
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import { getTool, TOOLS } from '$lib/registry'; import { getTool, TOOLS } from '$lib/registry';
import { t } from '$lib/i18n/t';
import type { EntryGenerator, PageLoad } from './$types'; import type { EntryGenerator, PageLoad } from './$types';
export const entries: EntryGenerator = () => TOOLS.map((tool) => ({ id: tool.id })); export const entries: EntryGenerator = () => TOOLS.map((tool) => ({ id: tool.id }));
@@ -7,7 +8,7 @@ export const entries: EntryGenerator = () => TOOLS.map((tool) => ({ id: tool.id
export const load: PageLoad = ({ params }) => { export const load: PageLoad = ({ params }) => {
const tool = getTool(params.id); const tool = getTool(params.id);
if (!tool) { if (!tool) {
error(404, 'Инструмент не найден'); error(404, t('errors.notFound'));
} }
return { id: tool.id }; return { id: tool.id };
}; };