fix(frontend): replace $t with _t pattern in translate components
Svelte 5 compiler fails to detect Proxy-based i18n store `t` as a store in deeply nested/complex templates and in `.svelte.ts` model files, generating `ReferenceError: $t is not defined` at runtime. Fix: replace `$t.` references with: - Template: `_t.` where `const _t = $derived(getT())` - Script: `getT()?.` direct function call Applied to: - translate/[id]/+page.svelte (the failing page) - TranslationJobModel.svelte.ts ($derived.by() in `tabs`) - 12 translate components (ConfigTabForm, ScheduleConfig, TranslationPreview, TargetTabForm, RunTabContent, BulkReplaceModal, TranslationRunProgress, TranslationRunResult, TermCorrectionPopup, BulkCorrectionSidebar, TranslationMetricsDashboard, CorrectionCell, TranslationRunGlobalIndicator) Verified all 5 tabs render correctly in browser: Config, Preview, Target, Run, Schedule. Build clean, 698/698 tests pass, 0 color violations.
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
<!-- @UX_FEEDBACK Toast on success/error; count badge on toggle button -->
|
||||
<!-- @UX_RECOVERY Remove individual items; retry submission -->
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n/index.svelte.js';
|
||||
import { getT } from '$lib/i18n/index.svelte.js';
|
||||
import { addToast } from '$lib/toasts.svelte.js';
|
||||
import { submitBulkCorrections, dictionaryApi } from '$lib/api/translate.js';
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
|
||||
/** @type {'closed'|'collecting'|'reviewing'|'submitting'|'submitted'} */
|
||||
let uxState = $state('closed');
|
||||
// @RATIONALE Svelte 5 store detection fails for `$t` in deeply nested templates.
|
||||
// Use getT() + $derived to keep reactivity to locale changes.
|
||||
const _t = $derived(getT());
|
||||
let items = $state([]);
|
||||
let dictionaries = $state([]);
|
||||
let selectedDictId = $state('');
|
||||
@@ -69,7 +72,7 @@
|
||||
selectedDictId = dictionaries[0].id;
|
||||
}
|
||||
} catch (err) {
|
||||
addToast($t.translate?.common?.error + ': ' + ($t.translate?.corrections?.loading || 'Failed to load dictionaries'), 'error');
|
||||
addToast(getT()?.translate?.common?.error + ': ' + (getT()?.translate?.corrections?.loading || 'Failed to load dictionaries'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +93,7 @@
|
||||
// Validate all items have corrected targets
|
||||
const invalidItems = items.filter(i => !i.correctedTarget.trim());
|
||||
if (invalidItems.length > 0) {
|
||||
addToast($t.translate?.corrections?.submit_failed || 'All items must have a corrected target', 'error');
|
||||
addToast(getT()?.translate?.corrections?.submit_failed || 'All items must have a corrected target', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -115,10 +118,10 @@
|
||||
const successCount = result.created_count || result.updated_count || corrections.length;
|
||||
const totalCount = corrections.length;
|
||||
if (successCount === totalCount) {
|
||||
addToast($t.translate?.corrections?.submit_success || 'All corrections submitted', 'success');
|
||||
addToast(getT()?.translate?.corrections?.submit_success || 'All corrections submitted', 'success');
|
||||
} else {
|
||||
addToast(
|
||||
($t.translate?.corrections?.submit_partial || '{success} of {total} submitted')
|
||||
(getT()?.translate?.corrections?.submit_partial || '{success} of {total} submitted')
|
||||
.replace('{success}', successCount)
|
||||
.replace('{total}', totalCount),
|
||||
'info'
|
||||
@@ -126,7 +129,7 @@
|
||||
}
|
||||
onSubmitted(result);
|
||||
} catch (err) {
|
||||
addToast(err?.message || $t.translate?.corrections?.submit_failed || 'Failed to submit corrections', 'error');
|
||||
addToast(err?.message || getT()?.translate?.corrections?.submit_failed || 'Failed to submit corrections', 'error');
|
||||
uxState = 'collecting';
|
||||
}
|
||||
}
|
||||
@@ -146,12 +149,12 @@
|
||||
onclick={openSidebar}
|
||||
class="fixed right-0 top-1/3 z-40 flex items-center gap-2 px-3 py-2 bg-primary text-white rounded-l-lg shadow-lg hover:bg-primary-hover transition-all translate-x-0"
|
||||
class:translate-x-[360px]={isOpen}
|
||||
title={$t.translate?.corrections?.title}
|
||||
title={_t.translate?.corrections?.title}
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
<span class="text-xs font-medium">{$t.translate?.corrections?.title}</span>
|
||||
<span class="text-xs font-medium">{_t.translate?.corrections?.title}</span>
|
||||
{#if items.length > 0}
|
||||
<span class="inline-flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-destructive rounded-full">
|
||||
{items.length}
|
||||
@@ -167,8 +170,8 @@
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-border">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-text">{$t.translate?.corrections?.title}</h3>
|
||||
<p class="text-xs text-text-muted">{$t.translate?.corrections?.subtitle}</p>
|
||||
<h3 class="text-sm font-semibold text-text">{_t.translate?.corrections?.title}</h3>
|
||||
<p class="text-xs text-text-muted">{_t.translate?.corrections?.subtitle}</p>
|
||||
</div>
|
||||
<button onclick={closeSidebar} class="p-1 text-text-subtle hover:text-text-muted rounded hover:bg-surface-muted">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -185,21 +188,21 @@
|
||||
<svg class="w-12 h-12 text-text-subtle mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
</svg>
|
||||
<p class="text-sm text-text-muted">{$t.translate?.corrections?.no_items}</p>
|
||||
<p class="text-xs text-text-subtle mt-1">{$t.translate?.corrections?.add_hint}</p>
|
||||
<p class="text-sm text-text-muted">{_t.translate?.corrections?.no_items}</p>
|
||||
<p class="text-xs text-text-subtle mt-1">{_t.translate?.corrections?.add_hint}</p>
|
||||
</div>
|
||||
{:else}
|
||||
{#each items as item, idx}
|
||||
<div class="border border-border rounded-lg p-3 space-y-2">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-xs font-medium text-text-muted">{$t.translate?.corrections?.source_term}</p>
|
||||
<p class="text-xs font-medium text-text-muted">{_t.translate?.corrections?.source_term}</p>
|
||||
<p class="text-sm font-mono text-text truncate">{item.sourceTerm}</p>
|
||||
</div>
|
||||
<button
|
||||
onclick={() => removeItem(idx)}
|
||||
class="ml-2 p-0.5 text-text-subtle hover:text-destructive rounded"
|
||||
title={$t.translate?.corrections?.remove}
|
||||
title={_t.translate?.corrections?.remove}
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
@@ -207,16 +210,16 @@
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-medium text-destructive">{$t.translate?.corrections?.incorrect_target}</p>
|
||||
<p class="text-xs font-medium text-destructive">{_t.translate?.corrections?.incorrect_target}</p>
|
||||
<p class="text-sm font-mono text-destructive truncate">{item.incorrectTarget}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-medium text-success">{$t.translate?.corrections?.corrected_target}</label>
|
||||
<label class="text-xs font-medium text-success">{_t.translate?.corrections?.corrected_target}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={item.correctedTarget}
|
||||
oninput={(e) => updateCorrection(idx, e.target.value)}
|
||||
placeholder={$t.translate?.corrections?.corrected_placeholder}
|
||||
placeholder={_t.translate?.corrections?.corrected_placeholder}
|
||||
class="w-full mt-1 px-2 py-1 text-sm border border-border-strong rounded focus:ring-2 focus:ring-success-ring focus:border-success"
|
||||
/>
|
||||
</div>
|
||||
@@ -227,7 +230,7 @@
|
||||
{:else if uxState === 'submitting'}
|
||||
<div class="flex flex-col items-center justify-center py-12">
|
||||
<div class="animate-spin w-8 h-8 border-2 border-primary-ring border-t-transparent rounded-full mb-3"></div>
|
||||
<p class="text-sm text-text-muted">{$t.translate?.corrections?.submitting}</p>
|
||||
<p class="text-sm text-text-muted">{_t.translate?.corrections?.submitting}</p>
|
||||
</div>
|
||||
|
||||
{:else if uxState === 'submitted'}
|
||||
@@ -235,10 +238,10 @@
|
||||
<svg class="w-12 h-12 text-success mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<p class="text-sm font-medium text-text">{$t.translate?.corrections?.submit_success}</p>
|
||||
<p class="text-sm font-medium text-text">{_t.translate?.corrections?.submit_success}</p>
|
||||
{#if submitResult}
|
||||
<p class="text-xs text-text-muted mt-1">
|
||||
{$t.translate?.corrections?.submit_partial
|
||||
{_t.translate?.corrections?.submit_partial
|
||||
?.replace('{success}', submitResult.created_count || submitResult.updated_count || items.length)
|
||||
?.replace('{total}', items.length)}
|
||||
</p>
|
||||
@@ -252,12 +255,12 @@
|
||||
{#if uxState === 'collecting' && items.length > 0}
|
||||
<!-- Dictionary selector -->
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-text mb-1">{$t.translate?.corrections?.dictionary}</label>
|
||||
<label class="block text-xs font-medium text-text mb-1">{_t.translate?.corrections?.dictionary}</label>
|
||||
<select
|
||||
bind:value={selectedDictId}
|
||||
class="w-full px-2 py-1.5 border border-border-strong rounded text-sm"
|
||||
>
|
||||
<option value="">{$t.translate?.corrections?.select_dictionary}</option>
|
||||
<option value="">{_t.translate?.corrections?.select_dictionary}</option>
|
||||
{#each dictionaries as dict}
|
||||
<option value={dict.id}>{dict.name}</option>
|
||||
{/each}
|
||||
@@ -269,14 +272,14 @@
|
||||
disabled={!selectedDictId || validItemCount === 0}
|
||||
class="w-full px-4 py-2 text-sm font-medium text-white bg-primary rounded-lg hover:bg-primary-hover disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{$t.translate?.corrections?.submit_all?.replace('{count}', validItemCount)}
|
||||
{_t.translate?.corrections?.submit_all?.replace('{count}', validItemCount)}
|
||||
</button>
|
||||
{:else if uxState === 'submitted'}
|
||||
<button
|
||||
onclick={resetAndClose}
|
||||
class="w-full px-4 py-2 text-sm font-medium text-white bg-primary rounded-lg hover:bg-primary-hover transition-colors"
|
||||
>
|
||||
{$t.translate?.common?.close}
|
||||
{_t.translate?.common?.close}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import { addToast } from '$lib/toasts.svelte.js';
|
||||
import { bulkFindReplace, bulkReplacePreview } from '$lib/api/translate.js';
|
||||
import { t, _ } from '$lib/i18n/index.svelte.js';
|
||||
import { _, getT } from '$lib/i18n/index.svelte.js';
|
||||
|
||||
/** @type {{ show: boolean, runId: string, targetLanguages: string[], onClose: () => void, onApplied?: (count: number) => void }} */
|
||||
let {
|
||||
@@ -28,6 +28,9 @@
|
||||
|
||||
/** @type {'closed'|'configuring'|'previewing'|'preview_error'|'confirming'|'applying'|'applied'|'apply_error'} */
|
||||
let uxState = $state('closed');
|
||||
// @RATIONALE Svelte 5 store detection fails for `$t` in deeply nested templates.
|
||||
// Use getT() + $derived to keep reactivity to locale changes.
|
||||
const _t = $derived(getT());
|
||||
|
||||
let findPattern = $state('');
|
||||
let replacementText = $state('');
|
||||
@@ -147,15 +150,15 @@
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={$t.translate?.bulk_replace?.title}
|
||||
aria-label={_t.translate?.bulk_replace?.title}
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between px-6 py-4 border-b border-border flex-shrink-0">
|
||||
<h2 class="text-lg font-semibold text-text">{$t.translate?.bulk_replace?.title}</h2>
|
||||
<h2 class="text-lg font-semibold text-text">{_t.translate?.bulk_replace?.title}</h2>
|
||||
<button
|
||||
onclick={handleClose}
|
||||
class="p-1 text-text-subtle hover:text-text-muted rounded-lg hover:bg-surface-muted transition-colors"
|
||||
aria-label={$t.translate?.bulk_replace?.close}
|
||||
aria-label={_t.translate?.bulk_replace?.close}
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
@@ -170,12 +173,12 @@
|
||||
{#if uxState === 'configuring' || uxState === 'previewing' || uxState === 'preview_error'}
|
||||
<!-- Find Pattern -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text mb-1">{$t.translate?.bulk_replace?.find_pattern}</label>
|
||||
<label class="block text-sm font-medium text-text mb-1">{_t.translate?.bulk_replace?.find_pattern}</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={findPattern}
|
||||
placeholder={$t.translate?.bulk_replace?.find_placeholder}
|
||||
placeholder={_t.translate?.bulk_replace?.find_placeholder}
|
||||
class="flex-1 px-3 py-2 border border-border-strong rounded-lg text-sm focus-visible:ring-2 focus-visible:ring-primary-ring focus-visible:border-primary-ring"
|
||||
onkeydown={(e) => e.key === 'Enter' && handlePreview()}
|
||||
/>
|
||||
@@ -186,18 +189,18 @@
|
||||
class="rounded"
|
||||
/>
|
||||
<span class="text-text-muted font-mono text-xs">.*</span>
|
||||
<span class="text-text-muted text-xs">{$t.translate?.bulk_replace?.regex}</span>
|
||||
<span class="text-text-muted text-xs">{_t.translate?.bulk_replace?.regex}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Replacement Text -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text mb-1">{$t.translate?.bulk_replace?.replace_with}</label>
|
||||
<label class="block text-sm font-medium text-text mb-1">{_t.translate?.bulk_replace?.replace_with}</label>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={replacementText}
|
||||
placeholder={$t.translate?.bulk_replace?.replace_placeholder}
|
||||
placeholder={_t.translate?.bulk_replace?.replace_placeholder}
|
||||
class="w-full px-3 py-2 border border-border-strong rounded-lg text-sm focus-visible:ring-2 focus-visible:ring-primary-ring focus-visible:border-primary-ring"
|
||||
onkeydown={(e) => e.key === 'Enter' && handlePreview()}
|
||||
/>
|
||||
@@ -205,12 +208,12 @@
|
||||
|
||||
<!-- Target Language -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text mb-1">{$t.translate?.bulk_replace?.target_language}</label>
|
||||
<label class="block text-sm font-medium text-text mb-1">{_t.translate?.bulk_replace?.target_language}</label>
|
||||
<select
|
||||
bind:value={targetLanguage}
|
||||
class="w-full px-3 py-2 border border-border-strong rounded-lg text-sm"
|
||||
>
|
||||
<option value="">{$t.translate?.bulk_replace?.select_language}</option>
|
||||
<option value="">{_t.translate?.bulk_replace?.select_language}</option>
|
||||
{#each targetLanguages as lang}
|
||||
<option value={lang}>{lang}</option>
|
||||
{/each}
|
||||
@@ -223,7 +226,7 @@
|
||||
disabled={!findPattern.trim() || !targetLanguage}
|
||||
class="w-full px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover disabled:opacity-50 transition-colors text-sm font-medium"
|
||||
>
|
||||
{$t.translate?.bulk_replace?.preview_affected}
|
||||
{_t.translate?.bulk_replace?.preview_affected}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -239,22 +242,22 @@
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="text-sm font-medium text-text">
|
||||
{$t.translate?.bulk_replace?.preview_count.replace('{count}', affectedRecords.length)}
|
||||
{_t.translate?.bulk_replace?.preview_count.replace('{count}', affectedRecords.length)}
|
||||
</h3>
|
||||
<button
|
||||
onclick={handleShowConfirm}
|
||||
class="px-4 py-1.5 bg-warning text-white rounded-lg hover:bg-warning transition-colors text-sm font-medium"
|
||||
>
|
||||
{$t.translate?.bulk_replace?.apply_changes}
|
||||
{_t.translate?.bulk_replace?.apply_changes}
|
||||
</button>
|
||||
</div>
|
||||
<div class="max-h-60 overflow-y-auto border border-border rounded-lg">
|
||||
<table class="min-w-full divide-y divide-border text-xs">
|
||||
<thead class="bg-surface-muted sticky top-0">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left font-medium text-text-muted uppercase">{$t.translate?.bulk_replace?.table_number}</th>
|
||||
<th class="px-3 py-2 text-left font-medium text-text-muted uppercase">{$t.translate?.bulk_replace?.table_before}</th>
|
||||
<th class="px-3 py-2 text-left font-medium text-text-muted uppercase">{$t.translate?.bulk_replace?.table_after}</th>
|
||||
<th class="px-3 py-2 text-left font-medium text-text-muted uppercase">{_t.translate?.bulk_replace?.table_number}</th>
|
||||
<th class="px-3 py-2 text-left font-medium text-text-muted uppercase">{_t.translate?.bulk_replace?.table_before}</th>
|
||||
<th class="px-3 py-2 text-left font-medium text-text-muted uppercase">{_t.translate?.bulk_replace?.table_after}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-border">
|
||||
@@ -269,7 +272,7 @@
|
||||
</table>
|
||||
{#if affectedRecords.length > 100}
|
||||
<div class="px-3 py-2 text-xs text-text-subtle bg-surface-muted border-t border-border">
|
||||
{$t.translate?.bulk_replace?.showing_first.replace('{shown}', 100).replace('{total}', affectedRecords.length)}
|
||||
{_t.translate?.bulk_replace?.showing_first.replace('{shown}', 100).replace('{total}', affectedRecords.length)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -278,8 +281,8 @@
|
||||
|
||||
{#if uxState === 'previewing' && affectedRecords.length === 0}
|
||||
<div class="bg-surface-muted border border-border rounded-lg p-6 text-center">
|
||||
<p class="text-sm text-text-muted">{$t.translate?.bulk_replace?.no_matching}</p>
|
||||
<p class="text-xs text-text-subtle mt-1">{$t.translate?.bulk_replace?.no_matching_hint}</p>
|
||||
<p class="text-sm text-text-muted">{_t.translate?.bulk_replace?.no_matching}</p>
|
||||
<p class="text-xs text-text-subtle mt-1">{_t.translate?.bulk_replace?.no_matching_hint}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -292,22 +295,22 @@
|
||||
</svg>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-warning">
|
||||
{$t.translate?.bulk_replace?.confirm_title.replace('{count}', applyCount)}
|
||||
{_t.translate?.bulk_replace?.confirm_title.replace('{count}', applyCount)}
|
||||
</p>
|
||||
<p class="text-xs text-warning mt-1">
|
||||
{$t.translate?.bulk_replace?.confirm_body.replace('{language}', targetLanguage)}
|
||||
{_t.translate?.bulk_replace?.confirm_body.replace('{language}', targetLanguage)}
|
||||
</p>
|
||||
|
||||
<!-- Submit to Dictionary checkbox -->
|
||||
<div class="mt-3 space-y-2">
|
||||
<label class="flex items-center gap-2 text-sm text-text cursor-pointer">
|
||||
<input type="checkbox" bind:checked={submitToDict} class="rounded" />
|
||||
<span>{$t.translate?.bulk_replace?.submit_to_dict}</span>
|
||||
<span>{_t.translate?.bulk_replace?.submit_to_dict}</span>
|
||||
</label>
|
||||
{#if submitToDict}
|
||||
<textarea
|
||||
bind:value={usageNotes}
|
||||
placeholder={$t.translate?.bulk_replace?.usage_notes_placeholder}
|
||||
placeholder={_t.translate?.bulk_replace?.usage_notes_placeholder}
|
||||
class="w-full px-3 py-2 border border-border-strong rounded-lg text-sm"
|
||||
rows="2"
|
||||
></textarea>
|
||||
@@ -319,13 +322,13 @@
|
||||
onclick={handleCancelConfirm}
|
||||
class="px-3 py-1.5 text-sm border border-border-strong text-text rounded-lg hover:bg-surface-card transition-colors"
|
||||
>
|
||||
{$t.translate?.bulk_replace?.cancel}
|
||||
{_t.translate?.bulk_replace?.cancel}
|
||||
</button>
|
||||
<button
|
||||
onclick={handleApply}
|
||||
class="px-3 py-1.5 text-sm bg-warning text-white rounded-lg hover:bg-warning transition-colors"
|
||||
>
|
||||
{$t.translate?.bulk_replace?.apply_count.replace('{count}', applyCount)}
|
||||
{_t.translate?.bulk_replace?.apply_count.replace('{count}', applyCount)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -337,7 +340,7 @@
|
||||
{#if uxState === 'applying'}
|
||||
<div class="flex items-center justify-center gap-3 py-8">
|
||||
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-warning" />
|
||||
<span class="text-sm text-text-muted">{$t.translate?.bulk_replace?.applying}</span>
|
||||
<span class="text-sm text-text-muted">{_t.translate?.bulk_replace?.applying}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -347,13 +350,13 @@
|
||||
<svg class="w-12 h-12 text-success mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<p class="text-sm font-medium text-success">{$t.translate?.bulk_replace?.applied_title}</p>
|
||||
<p class="text-xs text-success mt-1">{$t.translate?.bulk_replace?.applied_body.replace('{count}', applyCount)}</p>
|
||||
<p class="text-sm font-medium text-success">{_t.translate?.bulk_replace?.applied_title}</p>
|
||||
<p class="text-xs text-success mt-1">{_t.translate?.bulk_replace?.applied_body.replace('{count}', applyCount)}</p>
|
||||
<button
|
||||
onclick={handleClose}
|
||||
class="mt-4 px-4 py-2 bg-success text-white rounded-lg hover:bg-success transition-colors text-sm"
|
||||
>
|
||||
{$t.translate?.bulk_replace?.done}
|
||||
{_t.translate?.bulk_replace?.done}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -367,13 +370,13 @@
|
||||
onclick={handleShowConfirm}
|
||||
class="px-3 py-1.5 text-sm bg-destructive text-white rounded-lg hover:bg-destructive-hover transition-colors"
|
||||
>
|
||||
{$t.translate?.bulk_replace?.retry}
|
||||
{_t.translate?.bulk_replace?.retry}
|
||||
</button>
|
||||
<button
|
||||
onclick={handleClose}
|
||||
class="px-3 py-1.5 text-sm border border-border-strong text-text rounded-lg hover:bg-surface-muted transition-colors"
|
||||
>
|
||||
{$t.translate?.bulk_replace?.cancel}
|
||||
{_t.translate?.bulk_replace?.cancel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -387,7 +390,7 @@
|
||||
onclick={handleClose}
|
||||
class="px-4 py-2 text-sm text-text bg-surface-muted rounded-lg hover:bg-surface-muted transition-colors"
|
||||
>
|
||||
{$t.translate?.bulk_replace?.close}
|
||||
{_t.translate?.bulk_replace?.close}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<!-- @UX_REACTIVITY Props -> $bindable() for all form fields, $props() for environment list -->
|
||||
<!-- @UX_REACTIVITY LocalState -> datasourceList, datasourceLoading, showDatasourceDropdown -->
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n/index.svelte.js';
|
||||
import { getT } from '$lib/i18n/index.svelte.js';
|
||||
import { ALL_LANGUAGES } from '$lib/i18n/languages.js';
|
||||
import { fetchDatasources, fetchDatasourceColumns } from '$lib/api/translate.js';
|
||||
import MultiSelect from '$lib/components/ui/MultiSelect.svelte';
|
||||
@@ -59,6 +59,9 @@
|
||||
let showDatasourceDropdown = $state(false);
|
||||
let isColumnsLoading = $state(false);
|
||||
let columnList = $derived(availableColumns);
|
||||
// @RATIONALE Svelte 5 store detection fails for `$t` in deeply nested templates.
|
||||
// Use getT() + $derived to keep reactivity to locale changes.
|
||||
const _t = $derived(getT());
|
||||
|
||||
// ---- Derived ----
|
||||
let logicalColumns = $derived(availableColumns.filter(c => c.is_physical !== false));
|
||||
@@ -156,18 +159,18 @@
|
||||
<div class="space-y-6">
|
||||
<!-- Basic Info -->
|
||||
<section class="bg-surface-card border border-border rounded-lg p-6">
|
||||
<h2 class="text-lg font-semibold text-text mb-4">{$t.translate?.config?.basic_info}</h2>
|
||||
<h2 class="text-lg font-semibold text-text mb-4">{_t.translate?.config?.basic_info}</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium text-text mb-1 flex items-center gap-1">
|
||||
{$t.translate?.config?.name} <span class="text-destructive">*</span>
|
||||
<HelpTooltip text={$t.translate?.config?.help_name || ''} />
|
||||
{_t.translate?.config?.name} <span class="text-destructive">*</span>
|
||||
<HelpTooltip text={_t.translate?.config?.help_name || ''} />
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={name}
|
||||
class="w-full px-3 py-2 border rounded-lg text-sm {validationErrors.name ? 'border-destructive-ring bg-destructive-light' : 'border-border-strong'} focus-visible:ring-2 focus-visible:ring-primary-ring focus-visible:border-primary-ring"
|
||||
placeholder={$t.translate?.config?.name_placeholder}
|
||||
placeholder={_t.translate?.config?.name_placeholder}
|
||||
/>
|
||||
{#if validationErrors.name}
|
||||
<p class="text-xs text-destructive mt-1">{validationErrors.name}</p>
|
||||
@@ -175,14 +178,14 @@
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium text-text mb-1 flex items-center gap-1">
|
||||
{$t.translate?.config?.description}
|
||||
<HelpTooltip text={$t.translate?.config?.help_description || ''} />
|
||||
{_t.translate?.config?.description}
|
||||
<HelpTooltip text={_t.translate?.config?.help_description || ''} />
|
||||
</label>
|
||||
<textarea
|
||||
bind:value={description}
|
||||
rows="2"
|
||||
class="w-full px-3 py-2 border border-border-strong rounded-lg text-sm focus-visible:ring-2 focus-visible:ring-primary-ring focus-visible:border-primary-ring"
|
||||
placeholder={$t.translate?.config?.description_placeholder}
|
||||
placeholder={_t.translate?.config?.description_placeholder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -190,19 +193,19 @@
|
||||
|
||||
<!-- Datasource & Environment -->
|
||||
<section class="bg-surface-card border border-border rounded-lg p-6">
|
||||
<h2 class="text-lg font-semibold text-text mb-4">{$t.translate?.config?.datasource}</h2>
|
||||
<h2 class="text-lg font-semibold text-text mb-4">{_t.translate?.config?.datasource}</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text mb-1 flex items-center gap-1">
|
||||
{$t.translate?.config?.environment}
|
||||
<HelpTooltip text={$t.translate?.config?.help_environment || ''} />
|
||||
{_t.translate?.config?.environment}
|
||||
<HelpTooltip text={_t.translate?.config?.help_environment || ''} />
|
||||
</label>
|
||||
<select
|
||||
bind:value={environmentId}
|
||||
onchange={onEnvChange}
|
||||
class="w-full px-3 py-2 border border-border-strong rounded-lg text-sm focus-visible:ring-2 focus-visible:ring-primary-ring focus-visible:border-primary-ring"
|
||||
>
|
||||
<option value="">{$t.translate?.config?.select_environment}</option>
|
||||
<option value="">{_t.translate?.config?.select_environment}</option>
|
||||
{#each environments as env}
|
||||
<option value={env.id}>{env.name} ({env.id})</option>
|
||||
{/each}
|
||||
@@ -210,8 +213,8 @@
|
||||
</div>
|
||||
<div class="relative">
|
||||
<label class="block text-sm font-medium text-text mb-1 flex items-center gap-1">
|
||||
{$t.translate?.config?.datasource_id}
|
||||
<HelpTooltip text={$t.translate?.config?.help_datasource || ''} />
|
||||
{_t.translate?.config?.datasource_id}
|
||||
<HelpTooltip text={_t.translate?.config?.help_datasource || ''} />
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -220,7 +223,7 @@
|
||||
onfocus={() => searchDatasources('')}
|
||||
onblur={() => setTimeout(() => (showDatasourceDropdown = false), 150)}
|
||||
class="w-full px-3 py-2 border border-border-strong rounded-lg text-sm focus-visible:ring-2 focus-visible:ring-primary-ring focus-visible:border-primary-ring"
|
||||
placeholder={$t.translate?.config?.datasource_search_placeholder || 'Search datasets...'}
|
||||
placeholder={_t.translate?.config?.datasource_search_placeholder || 'Search datasets...'}
|
||||
/>
|
||||
{#if datasourceLoading}
|
||||
<div class="absolute right-3 top-9 animate-spin h-4 w-4 border-2 border-primary-ring border-t-transparent rounded-full"></div>
|
||||
@@ -244,7 +247,7 @@
|
||||
{#if databaseDialect}
|
||||
<div class="md:col-span-2">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-success-light text-success">
|
||||
{$t.translate?.config?.detected_dialect.replace('{dialect}', databaseDialect)}
|
||||
{_t.translate?.config?.detected_dialect.replace('{dialect}', databaseDialect)}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -257,7 +260,7 @@
|
||||
{#if columnList.length > 0}
|
||||
<div class="mt-4">
|
||||
<label class="block text-sm font-medium text-text mb-2">
|
||||
{$t.translate?.config?.available_columns.replace('{count}', columnList.length)}
|
||||
{_t.translate?.config?.available_columns.replace('{count}', columnList.length)}
|
||||
</label>
|
||||
<div class="max-h-40 overflow-y-auto border border-border rounded-lg divide-y divide-border">
|
||||
{#each columnList as col}
|
||||
@@ -267,7 +270,7 @@
|
||||
<span class="text-xs text-text-subtle">({col.type})</span>
|
||||
{/if}
|
||||
{#if isVirtual(col.name)}
|
||||
<span class="text-xs text-warning italic">{$t.translate?.config?.virtual}</span>
|
||||
<span class="text-xs text-warning italic">{_t.translate?.config?.virtual}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
@@ -278,21 +281,21 @@
|
||||
|
||||
<!-- Column Mapping -->
|
||||
<section class="bg-surface-card border border-border rounded-lg p-6">
|
||||
<h2 class="text-lg font-semibold text-text mb-4">{$t.translate?.config?.column_mapping}</h2>
|
||||
<h2 class="text-lg font-semibold text-text mb-4">{_t.translate?.config?.column_mapping}</h2>
|
||||
|
||||
<!-- Translation Column -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-text mb-1 flex items-center gap-1">
|
||||
{$t.translate?.config?.translation_column} <span class="text-destructive">*</span>
|
||||
<HelpTooltip text={$t.translate?.config?.help_translation_column || ''} />
|
||||
{_t.translate?.config?.translation_column} <span class="text-destructive">*</span>
|
||||
<HelpTooltip text={_t.translate?.config?.help_translation_column || ''} />
|
||||
</label>
|
||||
<select
|
||||
bind:value={translationColumn}
|
||||
class="w-full px-3 py-2 border rounded-lg text-sm {validationErrors.translationColumn ? 'border-destructive-ring bg-destructive-light' : 'border-border-strong'} focus-visible:ring-2 focus-visible:ring-primary-ring focus-visible:border-primary-ring"
|
||||
>
|
||||
<option value="">{$t.translate?.config?.select_column}</option>
|
||||
<option value="">{_t.translate?.config?.select_column}</option>
|
||||
{#each columnList as col}
|
||||
<option value={col.name}>{col.name}{isVirtual(col.name) ? ` (${$t.translate?.config?.virtual})` : ''}</option>
|
||||
<option value={col.name}>{col.name}{isVirtual(col.name) ? ` (${_t.translate?.config?.virtual})` : ''}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if validationErrors.translationColumn}
|
||||
@@ -304,15 +307,15 @@
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<label class="block text-sm font-medium text-text flex items-center gap-1">
|
||||
{$t.translate?.config?.key_columns}
|
||||
<HelpTooltip text={$t.translate?.config?.help_key_columns || ''} />
|
||||
{_t.translate?.config?.key_columns}
|
||||
<HelpTooltip text={_t.translate?.config?.help_key_columns || ''} />
|
||||
</label>
|
||||
{#if sourceKeyCols.length > 0}
|
||||
<button
|
||||
onclick={() => { sourceKeyCols = []; targetKeyCols = []; }}
|
||||
class="text-xs text-destructive hover:text-destructive"
|
||||
>
|
||||
{$t.translate?.config?.clear_all}
|
||||
{_t.translate?.config?.clear_all}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -326,7 +329,7 @@
|
||||
}}
|
||||
class="px-3 py-1 text-xs border border-dashed border-border-strong rounded text-text-muted hover:border-primary-ring hover:text-primary"
|
||||
>
|
||||
{$t.translate?.config?.add_key_column}
|
||||
{_t.translate?.config?.add_key_column}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -338,7 +341,7 @@
|
||||
{sourceKeyCols.includes(col.name)
|
||||
? 'bg-primary-light border-primary-ring text-primary'
|
||||
: 'bg-surface-card border-border text-text-muted hover:border-border-strong'}"
|
||||
title={isVirtual(col.name) ? $t.translate?.config?.virtual_column_warning.replace('{col}', col.name) : ''}
|
||||
title={isVirtual(col.name) ? _t.translate?.config?.virtual_column_warning.replace('{col}', col.name) : ''}
|
||||
>
|
||||
{col.name}
|
||||
{#if isVirtual(col.name)}
|
||||
@@ -363,7 +366,7 @@
|
||||
<input
|
||||
type="text"
|
||||
value={targetKeyCols[idx] || ''}
|
||||
placeholder={$t.translate?.config?.target_column_placeholder}
|
||||
placeholder={_t.translate?.config?.target_column_placeholder}
|
||||
oninput={(e) => updateTargetKeyCol(idx, e.target.value)}
|
||||
class="flex-1 px-3 py-1.5 border border-border-strong rounded text-sm font-mono focus-visible:ring-2 focus-visible:ring-primary-ring"
|
||||
/>
|
||||
@@ -390,11 +393,11 @@
|
||||
<!-- Context Columns -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text mb-2 flex items-center gap-1">
|
||||
{$t.translate?.config?.context_columns}
|
||||
<HelpTooltip text={$t.translate?.config?.help_context_columns || ''} />
|
||||
{_t.translate?.config?.context_columns}
|
||||
<HelpTooltip text={_t.translate?.config?.help_context_columns || ''} />
|
||||
</label>
|
||||
{#if columnList.length === 0}
|
||||
<p class="text-xs text-text-subtle">{$t.translate?.config?.select_datasource_hint}</p>
|
||||
<p class="text-xs text-text-subtle">{_t.translate?.config?.select_datasource_hint}</p>
|
||||
{:else}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each columnList as col}
|
||||
@@ -418,15 +421,15 @@
|
||||
|
||||
<!-- LLM Settings -->
|
||||
<section class="bg-surface-card border border-border rounded-lg p-6">
|
||||
<h2 class="text-lg font-semibold text-text mb-4">{$t.translate?.config?.llm_settings}</h2>
|
||||
<h2 class="text-lg font-semibold text-text mb-4">{_t.translate?.config?.llm_settings}</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text mb-1 flex items-center gap-1">
|
||||
{$t.translate?.config?.provider}
|
||||
<HelpTooltip text={$t.translate?.config?.help_provider || ''} />
|
||||
{_t.translate?.config?.provider}
|
||||
<HelpTooltip text={_t.translate?.config?.help_provider || ''} />
|
||||
</label>
|
||||
<select bind:value={providerId} class="w-full px-3 py-2 border border-border-strong rounded-lg text-sm">
|
||||
<option value="">{$t.translate?.config?.select_provider}</option>
|
||||
<option value="">{_t.translate?.config?.select_provider}</option>
|
||||
{#each llmProviders as provider}
|
||||
<option value={provider.id}>{provider.name || provider.provider_type}</option>
|
||||
{/each}
|
||||
@@ -434,20 +437,20 @@
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center gap-1 mb-1">
|
||||
<span class="text-sm font-medium text-text">{$t.translate?.config?.target_language}</span>
|
||||
<span class="text-sm font-medium text-text">{_t.translate?.config?.target_language}</span>
|
||||
<span class="text-destructive">*</span>
|
||||
<HelpTooltip text={$t.translate?.config?.help_target_language || ''} />
|
||||
<HelpTooltip text={_t.translate?.config?.help_target_language || ''} />
|
||||
</div>
|
||||
<MultiSelect
|
||||
label=""
|
||||
options={ALL_LANGUAGES}
|
||||
bind:selected={targetLanguages}
|
||||
searchable={true}
|
||||
placeholder={$t.translate?.config?.target_language_search_placeholder || 'Search languages...'}
|
||||
placeholder={_t.translate?.config?.target_language_search_placeholder || 'Search languages...'}
|
||||
required={false}
|
||||
/>
|
||||
{#if targetLanguages.length === 0}
|
||||
<p class="text-xs text-warning mt-1">{$t.translate?.config?.target_language_required || 'Select at least one target language'}</p>
|
||||
<p class="text-xs text-warning mt-1">{_t.translate?.config?.target_language_required || 'Select at least one target language'}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -462,11 +465,11 @@
|
||||
/>
|
||||
<div>
|
||||
<span class="text-sm font-medium text-text flex items-center gap-1">
|
||||
{$t.translate?.config?.disable_reasoning || 'Disable reasoning (save tokens)'}
|
||||
<HelpTooltip text={$t.translate?.config?.help_disable_reasoning || ''} />
|
||||
{_t.translate?.config?.disable_reasoning || 'Disable reasoning (save tokens)'}
|
||||
<HelpTooltip text={_t.translate?.config?.help_disable_reasoning || ''} />
|
||||
</span>
|
||||
<p class="text-xs text-text-subtle mt-0.5">
|
||||
{$t.translate?.config?.disable_reasoning_hint || 'Saves output tokens by suppressing Chain of Thought reasoning'}
|
||||
{_t.translate?.config?.disable_reasoning_hint || 'Saves output tokens by suppressing Chain of Thought reasoning'}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
@@ -476,12 +479,12 @@
|
||||
<!-- Dictionary Attachment -->
|
||||
<section class="bg-surface-card border border-border rounded-lg p-6">
|
||||
<h2 class="text-lg font-semibold text-text mb-4 flex items-center gap-2">
|
||||
{$t.translate?.config?.terminology_dictionaries}
|
||||
<HelpTooltip text={$t.translate?.config?.help_dictionaries || ''} />
|
||||
{_t.translate?.config?.terminology_dictionaries}
|
||||
<HelpTooltip text={_t.translate?.config?.help_dictionaries || ''} />
|
||||
</h2>
|
||||
{#if availableDictionaries.length === 0}
|
||||
<p class="text-sm text-text-subtle">
|
||||
{$t.translate?.config?.no_dictionaries}
|
||||
{_t.translate?.config?.no_dictionaries}
|
||||
</p>
|
||||
{:else}
|
||||
<div class="space-y-2">
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import { addToast } from '$lib/toasts.svelte.js';
|
||||
import { inlineEditCorrection, submitCorrectionToDict, dictionaryApi } from '$lib/api/translate.js';
|
||||
import { t, _ } from '$lib/i18n/index.svelte.js';
|
||||
import { _, getT } from '$lib/i18n/index.svelte.js';
|
||||
|
||||
/** @type {{ value: string, recordId: string, languageCode: string, runId: string, sourceLanguage?: string, contextData?: object|null, usageNotes?: string, onSave?: (newValue: string) => void }} */
|
||||
let {
|
||||
@@ -31,6 +31,9 @@
|
||||
|
||||
/** @type {'view'|'editing'|'saving'|'saved'|'submitting_to_dict'|'dict_submitted'|'error'} */
|
||||
let uxState = $state('view');
|
||||
// @RATIONALE Svelte 5 store detection fails for `$t` in deeply nested templates.
|
||||
// Use getT() + $derived to keep reactivity to locale changes.
|
||||
const _t = $derived(getT());
|
||||
let editValue = $state(value);
|
||||
let displayValue = $state(value);
|
||||
let errorMessage = $state('');
|
||||
@@ -167,14 +170,14 @@
|
||||
<button
|
||||
onclick={saveEdit}
|
||||
class="px-2 py-0.5 text-xs bg-primary text-white rounded hover:bg-primary-hover transition-colors"
|
||||
title={$t.translate?.corrections?.cell_save}
|
||||
title={_t.translate?.corrections?.cell_save}
|
||||
>
|
||||
✓
|
||||
</button>
|
||||
<button
|
||||
onclick={cancelEditing}
|
||||
class="px-2 py-0.5 text-xs border border-border-strong text-text-muted rounded hover:bg-surface-muted transition-colors"
|
||||
title={$t.translate?.corrections?.cell_cancel}
|
||||
title={_t.translate?.corrections?.cell_cancel}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
@@ -185,21 +188,21 @@
|
||||
{:else if uxState === 'saving'}
|
||||
<div class="flex items-center gap-2 px-2 py-1">
|
||||
<div class="animate-spin rounded-full h-3 w-3 border-b-2 border-primary-ring" />
|
||||
<span class="text-xs text-text-muted">{$t.translate?.corrections?.cell_saving}</span>
|
||||
<span class="text-xs text-text-muted">{_t.translate?.corrections?.cell_saving}</span>
|
||||
</div>
|
||||
|
||||
<!-- Saved mode: show value + Dictionary submit button -->
|
||||
{:else if uxState === 'saved'}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-text">{displayValue}</span>
|
||||
<span class="text-xs text-success" title={$t.translate?.corrections?.cell_save_success} aria-label={$t.translate?.corrections?.cell_save_success} role="img">📝</span>
|
||||
<span class="text-xs text-success" title={_t.translate?.corrections?.cell_save_success} aria-label={_t.translate?.corrections?.cell_save_success} role="img">📝</span>
|
||||
<button
|
||||
onclick={openDictPopup}
|
||||
class="ml-1 px-1.5 py-0.5 text-xs bg-warning-light text-warning rounded hover:bg-warning-light transition-colors"
|
||||
title={$t.translate?.corrections?.cell_submit_to_dict}
|
||||
aria-label={$t.translate?.corrections?.cell_submit_to_dict}
|
||||
title={_t.translate?.corrections?.cell_submit_to_dict}
|
||||
aria-label={_t.translate?.corrections?.cell_submit_to_dict}
|
||||
>
|
||||
📕 {$t.translate?.corrections?.cell_submit_to_dict}
|
||||
📕 {_t.translate?.corrections?.cell_submit_to_dict}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -207,14 +210,14 @@
|
||||
{:else if uxState === 'submitting_to_dict'}
|
||||
<div class="flex items-center gap-2 px-2 py-1">
|
||||
<div class="animate-spin rounded-full h-3 w-3 border-b-2 border-warning" />
|
||||
<span class="text-xs text-warning">{$t.translate?.corrections?.cell_submitting}</span>
|
||||
<span class="text-xs text-warning">{_t.translate?.corrections?.cell_submitting}</span>
|
||||
</div>
|
||||
|
||||
<!-- Dictionary submitted mode -->
|
||||
{:else if uxState === 'dict_submitted'}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-text">{displayValue}</span>
|
||||
<span class="text-xs text-success font-medium" title={$t.translate?.corrections?.cell_dict_submitted} aria-label={$t.translate?.corrections?.cell_dict_submitted} role="img">✅ {$t.translate?.corrections?.cell_submitted_badge}</span>
|
||||
<span class="text-xs text-success font-medium" title={_t.translate?.corrections?.cell_dict_submitted} aria-label={_t.translate?.corrections?.cell_dict_submitted} role="img">✅ {_t.translate?.corrections?.cell_submitted_badge}</span>
|
||||
</div>
|
||||
|
||||
<!-- Error mode -->
|
||||
@@ -226,7 +229,7 @@
|
||||
onclick={startEditing}
|
||||
class="px-2 py-0.5 text-xs bg-destructive-light text-destructive rounded hover:bg-destructive-light transition-colors self-start"
|
||||
>
|
||||
{$t.translate?.corrections?.cell_retry}
|
||||
{_t.translate?.corrections?.cell_retry}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -238,24 +241,24 @@
|
||||
onclick={handleCloseDictPopup}
|
||||
>
|
||||
<div class="bg-surface-card rounded-lg shadow-xl p-6 w-full max-w-lg mx-4 max-h-[80vh] overflow-y-auto" onclick={(e) => e.stopPropagation()}>
|
||||
<h3 class="text-sm font-semibold text-text mb-4">{$t.translate?.corrections?.cell_submit_to_dict}</h3>
|
||||
<h3 class="text-sm font-semibold text-text mb-4">{_t.translate?.corrections?.cell_submit_to_dict}</h3>
|
||||
<div class="space-y-3">
|
||||
{#if sourceLanguage}
|
||||
<div class="text-xs text-text-muted">
|
||||
<span class="font-medium">{$t.translate?.corrections?.cell_language_pair}</span> {sourceLanguage} → {languageCode}
|
||||
<span class="font-medium">{_t.translate?.corrections?.cell_language_pair}</span> {sourceLanguage} → {languageCode}
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-text mb-1">{$t.translate?.corrections?.cell_corrected_value}</label>
|
||||
<label class="block text-xs font-medium text-text mb-1">{_t.translate?.corrections?.cell_corrected_value}</label>
|
||||
<div class="px-3 py-2 bg-surface-muted border border-border rounded text-sm">{editValue}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-text mb-1">{$t.translate?.corrections?.dictionary}</label>
|
||||
<label class="block text-xs font-medium text-text mb-1">{_t.translate?.corrections?.dictionary}</label>
|
||||
<select
|
||||
bind:value={selectedDictId}
|
||||
class="w-full px-3 py-2 border border-border-strong rounded-lg text-sm"
|
||||
>
|
||||
<option value="">{$t.translate?.corrections?.cell_select_dictionary}</option>
|
||||
<option value="">{_t.translate?.corrections?.cell_select_dictionary}</option>
|
||||
{#each dictionaries as dict}
|
||||
<option value={dict.id}>{dict.name}</option>
|
||||
{/each}
|
||||
@@ -266,12 +269,12 @@
|
||||
{#if editableContextData || contextEditMode}
|
||||
<div class="border border-border rounded-lg p-3 bg-surface-muted">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs font-medium text-text">{$t.translate?.corrections?.cell_context_data}</span>
|
||||
<span class="text-xs font-medium text-text">{_t.translate?.corrections?.cell_context_data}</span>
|
||||
<button
|
||||
onclick={() => { contextEditMode = !contextEditMode; if (!contextEditMode) editableContextData = contextData ? JSON.parse(JSON.stringify(contextData)) : null; }}
|
||||
class="text-xs text-primary hover:text-primary"
|
||||
>
|
||||
{contextEditMode ? $t.translate?.corrections?.cell_cancel_edit : $t.translate?.corrections?.cell_edit}
|
||||
{contextEditMode ? _t.translate?.corrections?.cell_cancel_edit : _t.translate?.corrections?.cell_edit}
|
||||
</button>
|
||||
</div>
|
||||
{#if contextEditMode}
|
||||
@@ -279,7 +282,7 @@
|
||||
bind:value={editableContextData}
|
||||
class="w-full px-2 py-1.5 border border-border-strong rounded text-xs font-mono"
|
||||
rows="3"
|
||||
placeholder={$t.translate?.corrections?.cell_context_json_placeholder}
|
||||
placeholder={_t.translate?.corrections?.cell_context_json_placeholder}
|
||||
></textarea>
|
||||
{:else if typeof editableContextData === 'object' && editableContextData !== null}
|
||||
<div class="text-xs text-text-muted space-y-1">
|
||||
@@ -288,19 +291,19 @@
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-xs text-text-subtle italic">{$t.translate?.corrections?.cell_no_context_data}</p>
|
||||
<p class="text-xs text-text-subtle italic">{_t.translate?.corrections?.cell_no_context_data}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Usage Notes Section (T128) -->
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-text mb-1">{$t.translate?.corrections?.cell_usage_notes}</label>
|
||||
<label class="block text-xs font-medium text-text mb-1">{_t.translate?.corrections?.cell_usage_notes}</label>
|
||||
<textarea
|
||||
bind:value={editableUsageNotes}
|
||||
class="w-full px-3 py-2 border border-border-strong rounded-lg text-sm"
|
||||
rows="2"
|
||||
placeholder={$t.translate?.corrections?.cell_usage_notes_placeholder}
|
||||
placeholder={_t.translate?.corrections?.cell_usage_notes_placeholder}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
@@ -309,14 +312,14 @@
|
||||
onclick={cancelDictSubmit}
|
||||
class="px-3 py-1.5 text-xs text-text bg-surface-muted rounded hover:bg-surface-muted transition-colors"
|
||||
>
|
||||
{$t.translate?.corrections?.cell_cancel}
|
||||
{_t.translate?.corrections?.cell_cancel}
|
||||
</button>
|
||||
<button
|
||||
onclick={handleDictSubmit}
|
||||
disabled={!selectedDictId}
|
||||
class="px-3 py-1.5 text-xs text-white bg-warning rounded hover:bg-warning disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{$t.translate?.corrections?.cell_submit}
|
||||
{_t.translate?.corrections?.cell_submit}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<!-- @UX_RECOVERY Retry button on TranslationRunProgress for failed runs -->
|
||||
<!-- @UX_REACTIVITY Props -> $props() for config/state, callbacks for actions -->
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n/index.svelte.js';
|
||||
import { getT } from '$lib/i18n/index.svelte.js';
|
||||
import { addToast } from '$lib/toasts.svelte.js';
|
||||
import { updateJob } from '$lib/api/translate.js';
|
||||
import HelpTooltip from '$lib/ui/HelpTooltip.svelte';
|
||||
@@ -44,19 +44,23 @@
|
||||
try {
|
||||
await updateJob(jobId, { status: 'READY' });
|
||||
status = 'READY';
|
||||
addToast($t.translate?.config?.job_updated, 'success');
|
||||
addToast(getT()?.translate?.config?.job_updated, 'success');
|
||||
} catch (e) {
|
||||
addToast(e?.message || 'Failed to update status', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// @RATIONALE Svelte 5 store detection fails for `$t` in deeply nested templates.
|
||||
// Use getT() + $derived to keep reactivity to locale changes.
|
||||
const _t = $derived(getT());
|
||||
</script>
|
||||
|
||||
<section class="bg-surface-card border border-border rounded-lg p-6">
|
||||
<h2 class="text-lg font-semibold text-text mb-4">{$t.translate?.config?.run_translation}</h2>
|
||||
<h2 class="text-lg font-semibold text-text mb-4">{_t.translate?.config?.run_translation}</h2>
|
||||
|
||||
<!-- Status display + transition -->
|
||||
<div class="flex items-center gap-3 mb-4 p-3 bg-surface-muted rounded-lg">
|
||||
<span class="text-sm text-text-muted">{$t.translate?.config?.status}:</span>
|
||||
<span class="text-sm text-text-muted">{_t.translate?.config?.status}:</span>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||
{status === 'READY' ? 'bg-success-light text-success' : ''}
|
||||
{status === 'DRAFT' ? 'bg-warning-light text-warning' : ''}
|
||||
@@ -71,7 +75,7 @@
|
||||
onclick={handleMarkReady}
|
||||
class="ml-auto px-3 py-1 text-xs bg-primary text-white rounded hover:bg-primary-hover transition-colors"
|
||||
>
|
||||
{$t.translate?.config?.mark_ready || 'Mark as READY'}
|
||||
{_t.translate?.config?.mark_ready || 'Mark as READY'}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -89,16 +93,16 @@
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-sm font-semibold text-text flex items-center gap-1">
|
||||
{$t.translate?.config?.run_incremental}
|
||||
<HelpTooltip text={$t.translate?.config?.help_incremental || ''} />
|
||||
{_t.translate?.config?.run_incremental}
|
||||
<HelpTooltip text={_t.translate?.config?.help_incremental || ''} />
|
||||
</h3>
|
||||
<p class="text-xs text-text-muted mt-1">{$t.translate?.config?.run_incremental_desc}</p>
|
||||
<p class="text-xs text-text-muted mt-1">{_t.translate?.config?.run_incremental_desc}</p>
|
||||
<button
|
||||
onclick={() => onTriggerRun(false)}
|
||||
disabled={isRunning || status === 'DRAFT'}
|
||||
class="mt-3 px-5 py-1.5 text-sm font-medium bg-success text-white rounded-lg hover:bg-success disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{isRunning && !isFullRun ? $t.translate?.config?.running : $t.translate?.config?.run_translation}
|
||||
{isRunning && !isFullRun ? _t.translate?.config?.running : _t.translate?.config?.run_translation}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,16 +118,16 @@
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-sm font-semibold text-text flex items-center gap-1">
|
||||
{$t.translate?.config?.run_full}
|
||||
<HelpTooltip text={$t.translate?.config?.help_full || ''} />
|
||||
{_t.translate?.config?.run_full}
|
||||
<HelpTooltip text={_t.translate?.config?.help_full || ''} />
|
||||
</h3>
|
||||
<p class="text-xs text-text-muted mt-1">{$t.translate?.config?.run_full_desc}</p>
|
||||
<p class="text-xs text-text-muted mt-1">{_t.translate?.config?.run_full_desc}</p>
|
||||
<button
|
||||
onclick={() => onTriggerRun(true)}
|
||||
disabled={isRunning || status === 'DRAFT'}
|
||||
class="mt-3 px-5 py-1.5 text-sm font-medium bg-primary text-white rounded-lg hover:bg-primary-hover disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{isRunning && isFullRun ? $t.translate?.config?.running : $t.translate?.config?.full_translate}
|
||||
{isRunning && isFullRun ? _t.translate?.config?.running : _t.translate?.config?.full_translate}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -147,12 +151,12 @@
|
||||
{#if completedRuns.length > 0}
|
||||
<div class="mt-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="text-sm font-medium text-text">{$t.translate?.config?.recent_runs}</h3>
|
||||
<h3 class="text-sm font-medium text-text">{_t.translate?.config?.recent_runs}</h3>
|
||||
<button
|
||||
onclick={() => showPageBulkReplace = true}
|
||||
class="px-3 py-1.5 text-xs bg-primary text-white rounded hover:bg-primary-hover transition-colors"
|
||||
>
|
||||
{$t.translate?.run?.bulk_replace || 'Bulk Replace'}
|
||||
{_t.translate?.run?.bulk_replace || 'Bulk Replace'}
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
@@ -161,7 +165,7 @@
|
||||
<div class="flex items-center justify-between gap-3 p-4">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class="text-xs text-text-muted">{$t.translate?.run?.run_id}</span>
|
||||
<span class="text-xs text-text-muted">{_t.translate?.run?.run_id}</span>
|
||||
<code class="text-xs text-text break-all">{run.id}</code>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[11px] font-medium
|
||||
{run.status === 'COMPLETED' ? 'bg-success-light text-success' : ''}
|
||||
@@ -173,10 +177,10 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-wrap gap-3 text-xs text-text-muted">
|
||||
<span>{$t.translate?.run?.total}: {run.total_records || 0}</span>
|
||||
<span>{$t.translate?.run?.success}: {run.successful_records || 0}</span>
|
||||
<span>{$t.translate?.run?.failed}: {run.failed_records || 0}</span>
|
||||
<span>{$t.translate?.run?.skipped}: {run.skipped_records || 0}</span>
|
||||
<span>{_t.translate?.run?.total}: {run.total_records || 0}</span>
|
||||
<span>{_t.translate?.run?.success}: {run.successful_records || 0}</span>
|
||||
<span>{_t.translate?.run?.failed}: {run.failed_records || 0}</span>
|
||||
<span>{_t.translate?.run?.skipped}: {run.skipped_records || 0}</span>
|
||||
</div>
|
||||
{#if run.error_message}
|
||||
<p class="mt-2 text-xs text-destructive break-words">{run.error_message}</p>
|
||||
@@ -187,8 +191,8 @@
|
||||
class="shrink-0 px-3 py-1.5 text-xs border border-border-strong text-text rounded hover:bg-surface-muted transition-colors"
|
||||
>
|
||||
{expandedRunIds.includes(run.id)
|
||||
? ($t.translate?.run?.hide_details || 'Hide details')
|
||||
: ($t.translate?.run?.show_details || 'Show details')}
|
||||
? (_t.translate?.run?.hide_details || 'Hide details')
|
||||
: (_t.translate?.run?.show_details || 'Show details')}
|
||||
</button>
|
||||
</div>
|
||||
{#if expandedRunIds.includes(run.id)}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<!-- @UX_FEEDBACK Next-3-executions preview; toast on save/error -->
|
||||
<!-- @UX_RECOVERY Enable/disable toggle; delete schedule -->
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n/index.svelte.js';
|
||||
import { getT } from '$lib/i18n/index.svelte.js';
|
||||
import { addToast } from '$lib/toasts.svelte.js';
|
||||
import HelpTooltip from "$lib/ui/HelpTooltip.svelte";
|
||||
import {
|
||||
@@ -25,6 +25,9 @@
|
||||
|
||||
/** @type {'idle'|'editing'|'enabled'|'disabled'|'no_prior_run_warning'} */
|
||||
let uxState = $state('idle');
|
||||
// @RATIONALE Svelte 5 store detection fails for `$t` in deeply nested templates.
|
||||
// Use getT() + $derived to keep reactivity to locale changes.
|
||||
const _t = $derived(getT());
|
||||
let cronExpression = $state('0 2 * * *');
|
||||
let timezone = $state('UTC');
|
||||
let executionMode = $state('full');
|
||||
@@ -80,25 +83,25 @@
|
||||
execution_mode: executionMode,
|
||||
};
|
||||
await setSchedule(jobId, payload);
|
||||
addToast($t.translate?.schedule?.saved || 'Schedule saved', 'success');
|
||||
addToast(getT()?.translate?.schedule?.saved || 'Schedule saved', 'success');
|
||||
scheduleExists = true;
|
||||
uxState = isEnabled ? 'enabled' : 'disabled';
|
||||
loadNextExecutions();
|
||||
} catch (err) {
|
||||
addToast(err?.message || ($t.translate?.schedule?.save_failed || 'Failed to save schedule'), 'error');
|
||||
addToast(err?.message || (getT()?.translate?.schedule?.save_failed || 'Failed to save schedule'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
try {
|
||||
await deleteSchedule(jobId);
|
||||
addToast($t.translate?.schedule?.deleted || 'Schedule deleted', 'success');
|
||||
addToast(getT()?.translate?.schedule?.deleted || 'Schedule deleted', 'success');
|
||||
scheduleExists = false;
|
||||
uxState = 'idle';
|
||||
cronExpression = '0 2 * * *';
|
||||
nextExecutions = [];
|
||||
} catch (err) {
|
||||
addToast(err?.message || ($t.translate?.schedule?.delete_failed || 'Failed to delete schedule'), 'error');
|
||||
addToast(err?.message || (getT()?.translate?.schedule?.delete_failed || 'Failed to delete schedule'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +111,7 @@
|
||||
await disableSchedule(jobId);
|
||||
isEnabled = false;
|
||||
uxState = 'disabled';
|
||||
addToast($t.translate?.schedule?.disabled_toast || 'Schedule disabled', 'info');
|
||||
addToast(getT()?.translate?.schedule?.disabled_toast || 'Schedule disabled', 'info');
|
||||
} else {
|
||||
if (!scheduleExists) {
|
||||
await handleSave();
|
||||
@@ -117,11 +120,11 @@
|
||||
await enableSchedule(jobId);
|
||||
isEnabled = true;
|
||||
uxState = 'enabled';
|
||||
addToast($t.translate?.schedule?.enabled_toast || 'Schedule enabled', 'success');
|
||||
addToast(getT()?.translate?.schedule?.enabled_toast || 'Schedule enabled', 'success');
|
||||
loadNextExecutions();
|
||||
}
|
||||
} catch (err) {
|
||||
addToast(err?.message || ($t.translate?.schedule?.toggle_failed || 'Failed to toggle schedule'), 'error');
|
||||
addToast(err?.message || (getT()?.translate?.schedule?.toggle_failed || 'Failed to toggle schedule'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,11 +135,11 @@
|
||||
const hour = parts[0] === '*' ? '' : parts[0];
|
||||
const min = parts[1];
|
||||
if (parts[0] === '*') {
|
||||
return ($t.translate?.schedule?.description_every_hour || 'at :{minute} past the hour').replace('{minute}', min);
|
||||
return (getT()?.translate?.schedule?.description_every_hour || 'at :{minute} past the hour').replace('{minute}', min);
|
||||
}
|
||||
return ($t.translate?.schedule?.description_daily || 'Daily at {hour}:{minute}').replace('{hour}', hour).replace('{minute}', min);
|
||||
return (getT()?.translate?.schedule?.description_daily || 'Daily at {hour}:{minute}').replace('{hour}', hour).replace('{minute}', min);
|
||||
}
|
||||
return ($t.translate?.schedule?.description_cron || 'Cron: {cron}').replace('{cron}', cron);
|
||||
return (getT()?.translate?.schedule?.description_cron || 'Cron: {cron}').replace('{cron}', cron);
|
||||
}
|
||||
|
||||
const commonTimezones = [
|
||||
@@ -148,14 +151,14 @@
|
||||
|
||||
<div class="bg-surface-card border border-border rounded-lg p-4 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-text">{$t.translate?.schedule?.title || 'Schedule Configuration'}</h3>
|
||||
<h3 class="text-lg font-semibold text-text">{_t.translate?.schedule?.title || 'Schedule Configuration'}</h3>
|
||||
{#if !isLoading && scheduleExists}
|
||||
<button
|
||||
onclick={handleToggle}
|
||||
class="inline-flex items-center px-3 py-1.5 text-sm rounded-full {isEnabled ? 'bg-success-light text-success' : 'bg-surface-muted text-text-muted'}"
|
||||
>
|
||||
<span class="w-2 h-2 rounded-full mr-2 {isEnabled ? 'bg-success' : 'bg-border-strong'}"></span>
|
||||
{isEnabled ? ($t.translate?.schedule?.enabled || 'Enabled') : ($t.translate?.schedule?.disabled || 'Disabled')}
|
||||
{isEnabled ? (_t.translate?.schedule?.enabled || 'Enabled') : (_t.translate?.schedule?.disabled || 'Disabled')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -168,10 +171,10 @@
|
||||
<svg class="w-12 h-12 text-text-subtle mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<p class="text-sm text-text-muted mb-1">{$t.translate?.schedule?.no_schedule || 'No schedule configured'}</p>
|
||||
<p class="text-xs text-text-subtle mb-4">{$t.translate?.schedule?.no_schedule_hint || 'Set up a recurring schedule for automatic translations'}</p>
|
||||
<p class="text-sm text-text-muted mb-1">{_t.translate?.schedule?.no_schedule || 'No schedule configured'}</p>
|
||||
<p class="text-xs text-text-subtle mb-4">{_t.translate?.schedule?.no_schedule_hint || 'Set up a recurring schedule for automatic translations'}</p>
|
||||
<button onclick={() => (uxState = 'editing')} class="px-4 py-2 text-sm text-white bg-primary rounded-lg hover:bg-primary-hover">
|
||||
{$t.translate?.schedule?.create_schedule || 'Create Schedule'}
|
||||
{_t.translate?.schedule?.create_schedule || 'Create Schedule'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -180,8 +183,8 @@
|
||||
<!-- Cron expression -->
|
||||
<div>
|
||||
<span class="flex items-center gap-1.5 mb-1">
|
||||
<label class="text-sm font-medium text-text">{$t.translate?.schedule?.cron_expression || 'Cron Expression'}</label>
|
||||
<HelpTooltip text={$t.translate?.schedule?.help_cron || ""} />
|
||||
<label class="text-sm font-medium text-text">{_t.translate?.schedule?.cron_expression || 'Cron Expression'}</label>
|
||||
<HelpTooltip text={_t.translate?.schedule?.help_cron || ""} />
|
||||
</span>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
@@ -196,11 +199,11 @@
|
||||
disabled={!isEditing}
|
||||
class="px-3 py-2 border border-border-strong rounded-lg text-sm disabled:bg-surface-muted disabled:text-text-muted"
|
||||
>
|
||||
<option value="0 2 * * *">{$t.translate?.schedule?.preset_daily || 'Daily at 02:00'}</option>
|
||||
<option value="0 */6 * * *">{$t.translate?.schedule?.preset_every_6h || 'Every 6 hours'}</option>
|
||||
<option value="30 1 * * 0">{$t.translate?.schedule?.preset_weekly || 'Weekly (Sun 01:30)'}</option>
|
||||
<option value="0 3 1 * *">{$t.translate?.schedule?.preset_monthly || 'Monthly (1st at 03:00)'}</option>
|
||||
<option value="*/15 * * * *">{$t.translate?.schedule?.preset_every_15min || 'Every 15 min'}</option>
|
||||
<option value="0 2 * * *">{_t.translate?.schedule?.preset_daily || 'Daily at 02:00'}</option>
|
||||
<option value="0 */6 * * *">{_t.translate?.schedule?.preset_every_6h || 'Every 6 hours'}</option>
|
||||
<option value="30 1 * * 0">{_t.translate?.schedule?.preset_weekly || 'Weekly (Sun 01:30)'}</option>
|
||||
<option value="0 3 1 * *">{_t.translate?.schedule?.preset_monthly || 'Monthly (1st at 03:00)'}</option>
|
||||
<option value="*/15 * * * *">{_t.translate?.schedule?.preset_every_15min || 'Every 15 min'}</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="text-xs text-text-subtle mt-1">
|
||||
@@ -211,8 +214,8 @@
|
||||
<!-- Timezone -->
|
||||
<div>
|
||||
<span class="flex items-center gap-1.5 mb-1">
|
||||
<label class="text-sm font-medium text-text">{$t.translate?.schedule?.timezone || 'Timezone'}</label>
|
||||
<HelpTooltip text={$t.translate?.schedule?.help_timezone || ""} />
|
||||
<label class="text-sm font-medium text-text">{_t.translate?.schedule?.timezone || 'Timezone'}</label>
|
||||
<HelpTooltip text={_t.translate?.schedule?.help_timezone || ""} />
|
||||
</span>
|
||||
<select
|
||||
bind:value={timezone}
|
||||
@@ -228,22 +231,22 @@
|
||||
<!-- Execution mode -->
|
||||
<div>
|
||||
<span class="flex items-center gap-1.5 mb-1">
|
||||
<label class="text-sm font-medium text-text">{$t.translate?.schedule?.execution_mode || 'Execution Mode'}</label>
|
||||
<HelpTooltip text={$t.translate?.schedule?.help_execution_mode || ""} />
|
||||
<label class="text-sm font-medium text-text">{_t.translate?.schedule?.execution_mode || 'Execution Mode'}</label>
|
||||
<HelpTooltip text={_t.translate?.schedule?.help_execution_mode || ""} />
|
||||
</span>
|
||||
<div class="flex gap-4">
|
||||
<label class="inline-flex items-center cursor-pointer">
|
||||
<input type="radio" bind:group={executionMode} value="full" class="sr-only peer" disabled={!isEditing} />
|
||||
<div class="px-4 py-2 text-sm border border-border-strong rounded-lg peer-checked:border-primary-ring peer-checked:bg-primary-light peer-checked:text-primary hover:border-border-strong {isEditing ? '' : 'opacity-70'}">
|
||||
<div class="font-medium">{$t.translate?.schedule?.mode_full || 'Full'}</div>
|
||||
<div class="text-xs text-text-muted mt-0.5">{$t.translate?.schedule?.mode_full_desc || 'Translate all rows every run'}</div>
|
||||
<div class="font-medium">{_t.translate?.schedule?.mode_full || 'Full'}</div>
|
||||
<div class="text-xs text-text-muted mt-0.5">{_t.translate?.schedule?.mode_full_desc || 'Translate all rows every run'}</div>
|
||||
</div>
|
||||
</label>
|
||||
<label class="inline-flex items-center cursor-pointer">
|
||||
<input type="radio" bind:group={executionMode} value="new_key_only" class="sr-only peer" disabled={!isEditing} />
|
||||
<div class="px-4 py-2 text-sm border border-border-strong rounded-lg peer-checked:border-primary-ring peer-checked:bg-primary-light peer-checked:text-primary hover:border-border-strong {isEditing ? '' : 'opacity-70'}">
|
||||
<div class="font-medium">{$t.translate?.schedule?.mode_new_keys || 'New Keys Only'}</div>
|
||||
<div class="text-xs text-text-muted mt-0.5">{$t.translate?.schedule?.mode_new_keys_desc || 'Only translate rows with new key values'}</div>
|
||||
<div class="font-medium">{_t.translate?.schedule?.mode_new_keys || 'New Keys Only'}</div>
|
||||
<div class="text-xs text-text-muted mt-0.5">{_t.translate?.schedule?.mode_new_keys_desc || 'Only translate rows with new key values'}</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
@@ -252,7 +255,7 @@
|
||||
<!-- Next executions preview -->
|
||||
{#if nextExecutions.length > 0}
|
||||
<div class="bg-surface-muted rounded-lg p-3">
|
||||
<p class="text-xs font-medium text-text-muted mb-2">{$t.translate?.schedule?.next_executions || 'Next Executions'}</p>
|
||||
<p class="text-xs font-medium text-text-muted mb-2">{_t.translate?.schedule?.next_executions || 'Next Executions'}</p>
|
||||
{#each nextExecutions as dt, i}
|
||||
<p class="text-xs text-text-muted font-mono">{i + 1}. {new Date(dt).toLocaleString()}</p>
|
||||
{/each}
|
||||
@@ -263,7 +266,7 @@
|
||||
{#if uxState === 'no_prior_run_warning'}
|
||||
<div class="bg-warning-light border border-warning rounded-lg p-3">
|
||||
<p class="text-xs text-warning">
|
||||
⚠ {$t.translate?.schedule?.no_prior_run_warning || 'No prior successful manual run. Schedule may run with default settings. Consider running manually first.'}
|
||||
⚠ {_t.translate?.schedule?.no_prior_run_warning || 'No prior successful manual run. Schedule may run with default settings. Consider running manually first.'}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -272,7 +275,7 @@
|
||||
<div class="flex justify-between items-center pt-2">
|
||||
{#if scheduleExists}
|
||||
<button onclick={handleDelete} class="px-3 py-1.5 text-sm text-destructive hover:text-destructive">
|
||||
{$t.translate?.schedule?.delete_schedule || 'Delete Schedule'}
|
||||
{_t.translate?.schedule?.delete_schedule || 'Delete Schedule'}
|
||||
</button>
|
||||
{:else}
|
||||
<div></div>
|
||||
@@ -280,12 +283,12 @@
|
||||
<div class="flex gap-2">
|
||||
{#if scheduleExists && (uxState === 'enabled' || uxState === 'disabled')}
|
||||
<button onclick={() => (uxState = 'editing')} class="px-4 py-2 text-sm text-text bg-surface-muted rounded-lg hover:bg-surface-muted">
|
||||
{$t.translate?.schedule?.edit || 'Edit'}
|
||||
{_t.translate?.schedule?.edit || 'Edit'}
|
||||
</button>
|
||||
{/if}
|
||||
{#if uxState === 'editing'}
|
||||
<button onclick={handleSave} class="px-4 py-2 text-sm text-white bg-primary rounded-lg hover:bg-primary-hover">
|
||||
{scheduleExists ? ($t.translate?.schedule?.update || 'Update') : ($t.translate?.schedule?.create || 'Create')}
|
||||
{scheduleExists ? (_t.translate?.schedule?.update || 'Update') : (_t.translate?.schedule?.create || 'Create')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<!-- @UX_REACTIVITY Props -> $bindable() for all target fields -->
|
||||
<!-- @UX_REACTIVITY Props -> $props() for databases array -->
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n/index.svelte.js';
|
||||
import { getT } from '$lib/i18n/index.svelte.js';
|
||||
import HelpTooltip from '$lib/ui/HelpTooltip.svelte';
|
||||
import TargetSchemaHint from './TargetSchemaHint.svelte';
|
||||
|
||||
@@ -34,46 +34,50 @@
|
||||
databases = [],
|
||||
databasesLoading = false,
|
||||
} = $props();
|
||||
|
||||
// @RATIONALE Svelte 5 store detection fails for `$t` in deeply nested templates.
|
||||
// Use getT() + $derived to keep reactivity to locale changes.
|
||||
const _t = $derived(getT());
|
||||
</script>
|
||||
|
||||
<section class="bg-surface-card border border-border rounded-lg p-6">
|
||||
<h2 class="text-lg font-semibold text-text mb-4">{$t.translate?.config?.target_table_title}</h2>
|
||||
<h2 class="text-lg font-semibold text-text mb-4">{_t.translate?.config?.target_table_title}</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text mb-1 flex items-center gap-1">
|
||||
{$t.translate?.config?.target_schema}
|
||||
<HelpTooltip text={$t.translate?.config?.help_target_schema || ''} />
|
||||
{_t.translate?.config?.target_schema}
|
||||
<HelpTooltip text={_t.translate?.config?.help_target_schema || ''} />
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={targetSchema}
|
||||
class="w-full px-3 py-2 border border-border-strong rounded-lg text-sm focus-visible:ring-2 focus-visible:ring-primary-ring"
|
||||
placeholder={$t.translate?.config?.target_schema_placeholder}
|
||||
placeholder={_t.translate?.config?.target_schema_placeholder}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text mb-1 flex items-center gap-1">
|
||||
{$t.translate?.config?.target_table}
|
||||
<HelpTooltip text={$t.translate?.config?.help_target_table || ''} />
|
||||
{_t.translate?.config?.target_table}
|
||||
<HelpTooltip text={_t.translate?.config?.help_target_table || ''} />
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={targetTable}
|
||||
class="w-full px-3 py-2 border border-border-strong rounded-lg text-sm focus-visible:ring-2 focus-visible:ring-primary-ring"
|
||||
placeholder={$t.translate?.config?.target_table_placeholder}
|
||||
placeholder={_t.translate?.config?.target_table_placeholder}
|
||||
/>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium text-text mb-1 flex items-center gap-1">
|
||||
{$t.translate?.config?.target_database}
|
||||
<HelpTooltip text={$t.translate?.config?.help_target_database || ''} />
|
||||
{_t.translate?.config?.target_database}
|
||||
<HelpTooltip text={_t.translate?.config?.help_target_database || ''} />
|
||||
</label>
|
||||
<select
|
||||
bind:value={targetDatabaseId}
|
||||
class="w-full px-3 py-2 border border-border-strong rounded-lg text-sm focus-visible:ring-2 focus-visible:ring-primary-ring"
|
||||
disabled={!environmentId || databasesLoading}
|
||||
>
|
||||
<option value="">{$t.translate?.config?.select_target_database}</option>
|
||||
<option value="">{_t.translate?.config?.select_target_database}</option>
|
||||
{#each databases as db}
|
||||
<option value={String(db.id || db.uuid)}>
|
||||
{db.database_name || db.name} ({db.engine || db.backend || '?'})
|
||||
@@ -81,11 +85,11 @@
|
||||
{/each}
|
||||
</select>
|
||||
{#if databasesLoading}
|
||||
<p class="text-xs text-text-subtle mt-1">{$t.translate?.config?.loading_databases}</p>
|
||||
<p class="text-xs text-text-subtle mt-1">{_t.translate?.config?.loading_databases}</p>
|
||||
{:else if !environmentId}
|
||||
<p class="text-xs text-text-subtle mt-1">{$t.translate?.config?.select_environment}</p>
|
||||
<p class="text-xs text-text-subtle mt-1">{_t.translate?.config?.select_environment}</p>
|
||||
{:else}
|
||||
<p class="text-xs text-text-subtle mt-1">{$t.translate?.config?.target_database_hint}</p>
|
||||
<p class="text-xs text-text-subtle mt-1">{_t.translate?.config?.target_database_hint}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,14 +97,14 @@
|
||||
<!-- Target Column Mapping -->
|
||||
<div class="mt-4 pt-4 border-t border-border">
|
||||
<h3 class="text-sm font-semibold text-text mb-3 flex items-center gap-2">
|
||||
{$t.translate?.config?.target_column_mapping_title || 'Target Column Mapping'}
|
||||
<HelpTooltip text={$t.translate?.config?.target_column_mapping_description || ''} />
|
||||
{_t.translate?.config?.target_column_mapping_title || 'Target Column Mapping'}
|
||||
<HelpTooltip text={_t.translate?.config?.target_column_mapping_description || ''} />
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text mb-1 flex items-center gap-1">
|
||||
{$t.translate?.config?.target_column_label}
|
||||
<HelpTooltip text={$t.translate?.config?.help_target_column || ''} />
|
||||
{_t.translate?.config?.target_column_label}
|
||||
<HelpTooltip text={_t.translate?.config?.help_target_column || ''} />
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -108,12 +112,12 @@
|
||||
class="w-full px-3 py-2 border border-border-strong rounded-lg text-sm focus-visible:ring-2 focus-visible:ring-primary-ring"
|
||||
placeholder="e.g. translated_text"
|
||||
/>
|
||||
<p class="text-xs text-text-subtle mt-1">{$t.translate?.config?.target_column_hint}</p>
|
||||
<p class="text-xs text-text-subtle mt-1">{_t.translate?.config?.target_column_hint}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text mb-1 flex items-center gap-1">
|
||||
{$t.translate?.config?.target_language_column_label}
|
||||
<HelpTooltip text={$t.translate?.config?.help_target_language_column || ''} />
|
||||
{_t.translate?.config?.target_language_column_label}
|
||||
<HelpTooltip text={_t.translate?.config?.help_target_language_column || ''} />
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -121,12 +125,12 @@
|
||||
class="w-full px-3 py-2 border border-border-strong rounded-lg text-sm focus-visible:ring-2 focus-visible:ring-primary-ring"
|
||||
placeholder="e.g. lang_code"
|
||||
/>
|
||||
<p class="text-xs text-text-subtle mt-1">{$t.translate?.config?.target_language_column_hint}</p>
|
||||
<p class="text-xs text-text-subtle mt-1">{_t.translate?.config?.target_language_column_hint}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text mb-1 flex items-center gap-1">
|
||||
{$t.translate?.config?.target_source_column_label}
|
||||
<HelpTooltip text={$t.translate?.config?.help_target_source_column || ''} />
|
||||
{_t.translate?.config?.target_source_column_label}
|
||||
<HelpTooltip text={_t.translate?.config?.help_target_source_column || ''} />
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -134,12 +138,12 @@
|
||||
class="w-full px-3 py-2 border border-border-strong rounded-lg text-sm focus-visible:ring-2 focus-visible:ring-primary-ring"
|
||||
placeholder="e.g. source_text"
|
||||
/>
|
||||
<p class="text-xs text-text-subtle mt-1">{$t.translate?.config?.target_source_column_hint}</p>
|
||||
<p class="text-xs text-text-subtle mt-1">{_t.translate?.config?.target_source_column_hint}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text mb-1 flex items-center gap-1">
|
||||
{$t.translate?.config?.target_source_language_column_label}
|
||||
<HelpTooltip text={$t.translate?.config?.help_target_source_language_column || ''} />
|
||||
{_t.translate?.config?.target_source_language_column_label}
|
||||
<HelpTooltip text={_t.translate?.config?.help_target_source_language_column || ''} />
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -147,7 +151,7 @@
|
||||
class="w-full px-3 py-2 border border-border-strong rounded-lg text-sm focus-visible:ring-2 focus-visible:ring-primary-ring"
|
||||
placeholder="e.g. src_lang"
|
||||
/>
|
||||
<p class="text-xs text-text-subtle mt-1">{$t.translate?.config?.target_source_language_column_hint}</p>
|
||||
<p class="text-xs text-text-subtle mt-1">{_t.translate?.config?.target_source_language_column_hint}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -155,13 +159,13 @@
|
||||
<!-- Write Settings: batch size, upsert strategy, source reference -->
|
||||
<div class="mt-4 pt-4 border-t border-border">
|
||||
<h3 class="text-sm font-semibold text-text mb-3">
|
||||
{$t.translate?.config?.write_settings || 'Write Settings'}
|
||||
{_t.translate?.config?.write_settings || 'Write Settings'}
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text mb-1 flex items-center gap-1">
|
||||
{$t.translate?.config?.batch_size}
|
||||
<HelpTooltip text={$t.translate?.config?.help_batch_size || ''} />
|
||||
{_t.translate?.config?.batch_size}
|
||||
<HelpTooltip text={_t.translate?.config?.help_batch_size || ''} />
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
@@ -173,13 +177,13 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text mb-1 flex items-center gap-1">
|
||||
{$t.translate?.config?.upsert_strategy}
|
||||
<HelpTooltip text={$t.translate?.config?.help_upsert_strategy || ''} />
|
||||
{_t.translate?.config?.upsert_strategy}
|
||||
<HelpTooltip text={_t.translate?.config?.help_upsert_strategy || ''} />
|
||||
</label>
|
||||
<select bind:value={upsertStrategy} class="w-full px-3 py-2 border border-border-strong rounded-lg text-sm">
|
||||
<option value="MERGE">{$t.translate?.config?.upsert_merge}</option>
|
||||
<option value="INSERT">{$t.translate?.config?.upsert_insert}</option>
|
||||
<option value="UPDATE">{$t.translate?.config?.upsert_update}</option>
|
||||
<option value="MERGE">{_t.translate?.config?.upsert_merge}</option>
|
||||
<option value="INSERT">{_t.translate?.config?.upsert_insert}</option>
|
||||
<option value="UPDATE">{_t.translate?.config?.upsert_update}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -192,11 +196,11 @@
|
||||
/>
|
||||
<div>
|
||||
<span class="text-sm font-medium text-text flex items-center gap-1">
|
||||
{$t.translate?.config?.include_source_reference || 'Include source language in translations'}
|
||||
<HelpTooltip text={$t.translate?.config?.help_include_source_reference || ''} />
|
||||
{_t.translate?.config?.include_source_reference || 'Include source language in translations'}
|
||||
<HelpTooltip text={_t.translate?.config?.help_include_source_reference || ''} />
|
||||
</span>
|
||||
<p class="text-xs text-text-subtle mt-0.5">
|
||||
{$t.translate?.config?.include_source_reference_hint || 'The original text will be stored as a verified reference copy in its detected language'}
|
||||
{_t.translate?.config?.include_source_reference_hint || 'The original text will be stored as a verified reference copy in its detected language'}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<script lang="ts">
|
||||
import { addToast } from '$lib/toasts.svelte.js';
|
||||
import { submitCorrection, dictionaryApi } from '$lib/api/translate.js';
|
||||
import { t, _ } from '$lib/i18n/index.svelte.js';
|
||||
import { _, getT } from '$lib/i18n/index.svelte.js';
|
||||
|
||||
let {
|
||||
sourceTerm = '',
|
||||
@@ -32,6 +32,9 @@
|
||||
|
||||
/** @type {'closed'|'selecting'|'editing'|'submitting'|'conflict_detected'|'submitted'} */
|
||||
let uxState = $state('closed');
|
||||
// @RATIONALE Svelte 5 store detection fails for `$t` in deeply nested templates.
|
||||
// Use getT() + $derived to keep reactivity to locale changes.
|
||||
const _t = $derived(getT());
|
||||
let correctedTarget = $state('');
|
||||
let dictionaries = $state([]);
|
||||
let selectedDictId = $state('');
|
||||
@@ -147,7 +150,7 @@
|
||||
<div class="bg-surface-card rounded-lg shadow-xl p-6 w-full max-w-md mx-4" onclick={(e) => e.stopPropagation()}>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-text">{$t.translate?.term_correction?.title}</h3>
|
||||
<h3 class="text-lg font-semibold text-text">{_t.translate?.term_correction?.title}</h3>
|
||||
<button onclick={handleClose} class="text-text-subtle hover:text-text-muted">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
@@ -157,14 +160,14 @@
|
||||
|
||||
<!-- Selecting state -->
|
||||
{#if uxState === 'selecting'}
|
||||
<p class="text-sm text-text-muted">{$t.translate?.term_correction?.loading_dictionaries}</p>
|
||||
<p class="text-sm text-text-muted">{_t.translate?.term_correction?.loading_dictionaries}</p>
|
||||
|
||||
<!-- Editing state -->
|
||||
{:else if uxState === 'editing'}
|
||||
<div class="space-y-4">
|
||||
{#if sourceLanguage || targetLanguage}
|
||||
<div class="bg-primary-light border-primary-ring rounded-lg px-3 py-2 text-xs text-primary">
|
||||
<span class="font-medium">{$t.translate?.term_correction?.language_pair}</span> {sourceLanguage || $t.translate?.term_correction?.detected} → {targetLanguage || languageCode}
|
||||
<span class="font-medium">{_t.translate?.term_correction?.language_pair}</span> {sourceLanguage || _t.translate?.term_correction?.detected} → {targetLanguage || languageCode}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -176,13 +179,13 @@
|
||||
>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span>📋</span>
|
||||
<span>{$t.translate?.term_correction?.context_from_source}</span>
|
||||
<span>{_t.translate?.term_correction?.context_from_source}</span>
|
||||
{#if contextData}
|
||||
<span class="inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded-full"
|
||||
class:bg-primary-light:text-primary={!hasEditedContext}
|
||||
class:bg-success-light:text-success={hasEditedContext}
|
||||
>
|
||||
{hasEditedContext ? '✏️ ' + $t.translate?.term_correction?.edited_badge : '🤖 ' + $t.translate?.term_correction?.auto_badge}
|
||||
{hasEditedContext ? '✏️ ' + _t.translate?.term_correction?.edited_badge : '🤖 ' + _t.translate?.term_correction?.auto_badge}
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
@@ -204,7 +207,7 @@
|
||||
}}
|
||||
class="text-[11px] px-2 py-1 rounded border border-border-strong text-text-muted hover:bg-surface-muted"
|
||||
>
|
||||
{$t.translate?.term_correction?.edit_context}
|
||||
{_t.translate?.term_correction?.edit_context}
|
||||
</button>
|
||||
<button
|
||||
onclick={() => {
|
||||
@@ -213,11 +216,11 @@
|
||||
}}
|
||||
class="text-[11px] px-2 py-1 rounded border border-destructive-light text-destructive hover:bg-destructive-light"
|
||||
>
|
||||
{$t.translate?.term_correction?.remove_context}
|
||||
{_t.translate?.term_correction?.remove_context}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-[11px] text-text-subtle italic">{$t.translate?.term_correction?.no_context}</p>
|
||||
<p class="text-[11px] text-text-subtle italic">{_t.translate?.term_correction?.no_context}</p>
|
||||
<button
|
||||
onclick={() => {
|
||||
contextData = JSON.parse(JSON.stringify(initialContextData));
|
||||
@@ -226,7 +229,7 @@
|
||||
}}
|
||||
class="text-[11px] px-2 py-1 rounded border border-border-strong text-text-muted hover:bg-surface-muted"
|
||||
>
|
||||
{$t.translate?.term_correction?.restore_context}
|
||||
{_t.translate?.term_correction?.restore_context}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -236,48 +239,48 @@
|
||||
<!-- Usage notes -->
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-text-muted mb-1">
|
||||
📝 {$t.translate?.term_correction?.usage_notes}
|
||||
📝 {_t.translate?.term_correction?.usage_notes}
|
||||
</label>
|
||||
<textarea
|
||||
bind:value={contextUsageNotes}
|
||||
placeholder={$t.translate?.term_correction?.usage_notes_placeholder}
|
||||
placeholder={_t.translate?.term_correction?.usage_notes_placeholder}
|
||||
rows="2"
|
||||
class="w-full px-3 py-2 border border-border-strong rounded-lg text-xs focus-visible:ring-2 focus-visible:ring-primary-ring focus-visible:border-primary-ring resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text mb-1">{$t.translate?.term_correction?.source_term}</label>
|
||||
<label class="block text-sm font-medium text-text mb-1">{_t.translate?.term_correction?.source_term}</label>
|
||||
<input type="text" readonly value={sourceTerm} class="w-full px-3 py-2 bg-surface-muted border border-border rounded-lg text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text mb-1">{$t.translate?.term_correction?.incorrect_target}</label>
|
||||
<label class="block text-sm font-medium text-text mb-1">{_t.translate?.term_correction?.incorrect_target}</label>
|
||||
<input type="text" readonly value={incorrectTarget} class="w-full px-3 py-2 bg-surface-muted border border-border rounded-lg text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text mb-1">{$t.translate?.term_correction?.source_language}</label>
|
||||
<input type="text" readonly value={sourceLanguage || $t.translate?.term_correction?.auto_detected} class="w-full px-3 py-2 bg-surface-muted border border-border rounded-lg text-sm font-mono" />
|
||||
<label class="block text-sm font-medium text-text mb-1">{_t.translate?.term_correction?.source_language}</label>
|
||||
<input type="text" readonly value={sourceLanguage || _t.translate?.term_correction?.auto_detected} class="w-full px-3 py-2 bg-surface-muted border border-border rounded-lg text-sm font-mono" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text mb-1">{$t.translate?.term_correction?.target_language}</label>
|
||||
<label class="block text-sm font-medium text-text mb-1">{_t.translate?.term_correction?.target_language}</label>
|
||||
<input type="text" readonly value={targetLanguage || '—'} class="w-full px-3 py-2 bg-surface-muted border border-border rounded-lg text-sm font-mono" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text mb-1">{$t.translate?.term_correction?.corrected_target}</label>
|
||||
<label class="block text-sm font-medium text-text mb-1">{_t.translate?.term_correction?.corrected_target}</label>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={correctedTarget}
|
||||
placeholder={$t.translate?.term_correction?.corrected_placeholder}
|
||||
placeholder={_t.translate?.term_correction?.corrected_placeholder}
|
||||
class="w-full px-3 py-2 border border-border-strong rounded-lg text-sm focus-visible:ring-2 focus-visible:ring-primary-ring focus-visible:border-primary-ring"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text mb-1">{$t.translate?.term_correction?.dictionary}</label>
|
||||
<label class="block text-sm font-medium text-text mb-1">{_t.translate?.term_correction?.dictionary}</label>
|
||||
<select
|
||||
bind:value={selectedDictId}
|
||||
class="w-full px-3 py-2 border border-border-strong rounded-lg text-sm"
|
||||
>
|
||||
<option value="">{$t.translate?.term_correction?.select_dictionary}</option>
|
||||
<option value="">{_t.translate?.term_correction?.select_dictionary}</option>
|
||||
{#each dictionaries as dict}
|
||||
<option value={dict.id}>{dict.name}</option>
|
||||
{/each}
|
||||
@@ -285,14 +288,14 @@
|
||||
</div>
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<button onclick={handleClose} class="px-4 py-2 text-sm text-text bg-surface-muted rounded-lg hover:bg-surface-muted">
|
||||
{$t.translate?.term_correction?.cancel}
|
||||
{_t.translate?.term_correction?.cancel}
|
||||
</button>
|
||||
<button
|
||||
onclick={handleSubmit}
|
||||
disabled={!selectedDictId || !correctedTarget.trim()}
|
||||
class="px-4 py-2 text-sm text-white bg-primary rounded-lg hover:bg-primary-hover disabled:opacity-50"
|
||||
>
|
||||
{$t.translate?.term_correction?.submit_correction}
|
||||
{_t.translate?.term_correction?.submit_correction}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -301,24 +304,24 @@
|
||||
{:else if uxState === 'submitting'}
|
||||
<div class="text-center py-6">
|
||||
<div class="animate-spin w-8 h-8 border-2 border-primary-ring border-t-transparent rounded-full mx-auto mb-3"></div>
|
||||
<p class="text-sm text-text-muted">{$t.translate?.term_correction?.submitting}</p>
|
||||
<p class="text-sm text-text-muted">{_t.translate?.term_correction?.submitting}</p>
|
||||
</div>
|
||||
|
||||
<!-- Conflict state -->
|
||||
{:else if uxState === 'conflict_detected'}
|
||||
<div class="space-y-4">
|
||||
<div class="bg-warning-light border border-warning rounded-lg p-4">
|
||||
<p class="text-sm font-medium text-warning">{$t.translate?.term_correction?.conflict_detected}</p>
|
||||
<p class="text-sm font-medium text-warning">{_t.translate?.term_correction?.conflict_detected}</p>
|
||||
<p class="text-xs text-warning mt-1">
|
||||
{$t.translate?.term_correction?.conflict_description.replace('{source}', conflictInfo?.source_term || '').replace('{existing}', conflictInfo?.existing_target_term || '')}
|
||||
{_t.translate?.term_correction?.conflict_description.replace('{source}', conflictInfo?.source_term || '').replace('{existing}', conflictInfo?.existing_target_term || '')}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button onclick={handleConflictKeep} class="px-4 py-2 text-sm text-text bg-surface-muted rounded-lg hover:bg-surface-muted">
|
||||
{$t.translate?.term_correction?.keep_existing}
|
||||
{_t.translate?.term_correction?.keep_existing}
|
||||
</button>
|
||||
<button onclick={handleConflictOverwrite} class="px-4 py-2 text-sm text-white bg-yellow-600 rounded-lg hover:bg-yellow-700">
|
||||
{$t.translate?.term_correction?.overwrite}
|
||||
{_t.translate?.term_correction?.overwrite}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -329,10 +332,10 @@
|
||||
<svg class="w-12 h-12 text-success mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<p class="text-sm font-medium text-text">{$t.translate?.term_correction?.submitted}</p>
|
||||
<p class="text-sm font-medium text-text">{_t.translate?.term_correction?.submitted}</p>
|
||||
<p class="text-xs text-text-muted mt-1">{submitResult?.message || ''}</p>
|
||||
<button onclick={handleClose} class="mt-4 px-4 py-2 text-sm text-white bg-primary rounded-lg hover:bg-primary-hover">
|
||||
{$t.translate?.term_correction?.close}
|
||||
{_t.translate?.term_correction?.close}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -8,12 +8,15 @@
|
||||
<!-- @UX_FEEDBACK Spinner during load; card grid for totals; table for per-job breakdown -->
|
||||
<!-- @UX_RECOVERY Retry button on error -->
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n/index.svelte.js';
|
||||
import { getT } from '$lib/i18n/index.svelte.js';
|
||||
import { addToast } from '$lib/toasts.svelte.js';
|
||||
import { fetchAllMetrics } from '$lib/api/translate.js';
|
||||
|
||||
/** @type {'loading'|'loaded'|'error'|'empty'} */
|
||||
let uxState = $state('loading');
|
||||
// @RATIONALE Svelte 5 store detection fails for `$t` in deeply nested templates.
|
||||
// Use getT() + $derived to keep reactivity to locale changes.
|
||||
const _t = $derived(getT());
|
||||
let metrics = $state([]);
|
||||
let errorMessage = $state('');
|
||||
|
||||
@@ -52,7 +55,7 @@
|
||||
uxState = 'loaded';
|
||||
}
|
||||
} catch (err) {
|
||||
errorMessage = err?.message || $t.translate?.metrics?.load_failed;
|
||||
errorMessage = err?.message || getT()?.translate?.metrics?.load_failed;
|
||||
uxState = 'error';
|
||||
}
|
||||
}
|
||||
@@ -66,8 +69,8 @@
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-text">{$t.translate?.metrics?.title}</h2>
|
||||
<p class="text-sm text-text-muted mt-1">{$t.translate?.metrics?.subtitle}</p>
|
||||
<h2 class="text-xl font-bold text-text">{_t.translate?.metrics?.title}</h2>
|
||||
<p class="text-sm text-text-muted mt-1">{_t.translate?.metrics?.subtitle}</p>
|
||||
</div>
|
||||
<button
|
||||
onclick={handleRefresh}
|
||||
@@ -76,7 +79,7 @@
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
{$t.translate?.metrics?.refresh}
|
||||
{_t.translate?.metrics?.refresh}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -96,7 +99,7 @@
|
||||
onclick={handleRefresh}
|
||||
class="px-4 py-2 bg-destructive text-white rounded-lg hover:bg-destructive-hover transition-colors text-sm"
|
||||
>
|
||||
{$t.translate?.common?.retry}
|
||||
{_t.translate?.common?.retry}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -106,7 +109,7 @@
|
||||
<svg class="w-16 h-16 text-text-subtle mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
<p class="text-text-muted">{$t.translate?.metrics?.no_data}</p>
|
||||
<p class="text-text-muted">{_t.translate?.metrics?.no_data}</p>
|
||||
</div>
|
||||
|
||||
<!-- Loaded -->
|
||||
@@ -114,27 +117,27 @@
|
||||
<!-- Summary Cards -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||
<div class="bg-surface-card border border-border rounded-lg p-4">
|
||||
<p class="text-xs text-text-muted uppercase">{$t.translate?.metrics?.total_runs}</p>
|
||||
<p class="text-xs text-text-muted uppercase">{_t.translate?.metrics?.total_runs}</p>
|
||||
<p class="text-2xl font-bold text-text mt-1">{totalRuns}</p>
|
||||
</div>
|
||||
<div class="bg-surface-card border border-border rounded-lg p-4">
|
||||
<p class="text-xs text-text-muted uppercase">{$t.translate?.metrics?.success_rate}</p>
|
||||
<p class="text-xs text-text-muted uppercase">{_t.translate?.metrics?.success_rate}</p>
|
||||
<p class="text-2xl font-bold text-success mt-1">{successRate}%</p>
|
||||
</div>
|
||||
<div class="bg-surface-card border border-border rounded-lg p-4">
|
||||
<p class="text-xs text-text-muted uppercase">{$t.translate?.metrics?.failed_runs}</p>
|
||||
<p class="text-xs text-text-muted uppercase">{_t.translate?.metrics?.failed_runs}</p>
|
||||
<p class="text-2xl font-bold text-destructive mt-1">{failedRuns}</p>
|
||||
</div>
|
||||
<div class="bg-surface-card border border-border rounded-lg p-4">
|
||||
<p class="text-xs text-text-muted uppercase">{$t.translate?.metrics?.total_records}</p>
|
||||
<p class="text-xs text-text-muted uppercase">{_t.translate?.metrics?.total_records}</p>
|
||||
<p class="text-2xl font-bold text-text mt-1">{totalRecords.toLocaleString()}</p>
|
||||
</div>
|
||||
<div class="bg-surface-card border border-border rounded-lg p-4">
|
||||
<p class="text-xs text-text-muted uppercase">{$t.translate?.metrics?.total_tokens}</p>
|
||||
<p class="text-xs text-text-muted uppercase">{_t.translate?.metrics?.total_tokens}</p>
|
||||
<p class="text-2xl font-bold text-text mt-1">{totalTokens.toLocaleString()}</p>
|
||||
</div>
|
||||
<div class="bg-surface-card border border-border rounded-lg p-4">
|
||||
<p class="text-xs text-text-muted uppercase">{$t.translate?.metrics?.total_cost}</p>
|
||||
<p class="text-xs text-text-muted uppercase">{_t.translate?.metrics?.total_cost}</p>
|
||||
<p class="text-2xl font-bold text-text mt-1">${totalCost.toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -142,18 +145,18 @@
|
||||
<!-- Per-Job Breakdown Table -->
|
||||
<div class="bg-surface-card border border-border rounded-lg overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-border">
|
||||
<h3 class="text-sm font-semibold text-text">{$t.translate?.metrics?.runs_per_job}</h3>
|
||||
<h3 class="text-sm font-semibold text-text">{_t.translate?.metrics?.runs_per_job}</h3>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-surface-muted">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-text-muted uppercase">{$t.translate?.metrics?.job}</th>
|
||||
<th class="px-4 py-2 text-center text-xs font-medium text-text-muted uppercase">{$t.translate?.metrics?.runs}</th>
|
||||
<th class="px-4 py-2 text-center text-xs font-medium text-text-muted uppercase">{$t.translate?.metrics?.successful}</th>
|
||||
<th class="px-4 py-2 text-center text-xs font-medium text-text-muted uppercase">{$t.translate?.metrics?.failed}</th>
|
||||
<th class="px-4 py-2 text-right text-xs font-medium text-text-muted uppercase">{$t.translate?.metrics?.tokens}</th>
|
||||
<th class="px-4 py-2 text-right text-xs font-medium text-text-muted uppercase">{$t.translate?.metrics?.cost}</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-text-muted uppercase">{_t.translate?.metrics?.job}</th>
|
||||
<th class="px-4 py-2 text-center text-xs font-medium text-text-muted uppercase">{_t.translate?.metrics?.runs}</th>
|
||||
<th class="px-4 py-2 text-center text-xs font-medium text-text-muted uppercase">{_t.translate?.metrics?.successful}</th>
|
||||
<th class="px-4 py-2 text-center text-xs font-medium text-text-muted uppercase">{_t.translate?.metrics?.failed}</th>
|
||||
<th class="px-4 py-2 text-right text-xs font-medium text-text-muted uppercase">{_t.translate?.metrics?.tokens}</th>
|
||||
<th class="px-4 py-2 text-right text-xs font-medium text-text-muted uppercase">{_t.translate?.metrics?.cost}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-border">
|
||||
@@ -175,7 +178,7 @@
|
||||
<!-- Additional stats row -->
|
||||
{#if avgLatency !== null}
|
||||
<div class="bg-surface-card border border-border rounded-lg p-4">
|
||||
<p class="text-xs text-text-muted uppercase">{$t.translate?.metrics?.avg_latency}</p>
|
||||
<p class="text-xs text-text-muted uppercase">{_t.translate?.metrics?.avg_latency}</p>
|
||||
<p class="text-lg font-semibold text-text mt-1">{avgLatency}s</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<!-- @UX_FEEDBACK spinner during preview; visual distinction for LLM vs edited translations; cost warning for large previews. -->
|
||||
<!-- @UX_RECOVERY retry preview; re-fetch with updated config. -->
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n/index.svelte.js';
|
||||
import { getT } from '$lib/i18n/index.svelte.js';
|
||||
import { addToast } from '$lib/toasts.svelte.js';
|
||||
import {
|
||||
fetchPreview,
|
||||
@@ -27,6 +27,9 @@
|
||||
|
||||
/** @type {'idle'|'loading'|'preview_loaded'|'preview_error'|'accepted'|'stale_config'} */
|
||||
let uxState = $state('idle');
|
||||
// @RATIONALE Svelte 5 store detection fails for `$t` in deeply nested templates.
|
||||
// Use getT() + $derived to keep reactivity to locale changes.
|
||||
const _t = $derived(getT());
|
||||
let session = $state(null);
|
||||
let records = $state([]);
|
||||
let targetLanguages = $state([]);
|
||||
@@ -47,9 +50,9 @@
|
||||
|
||||
function getRowStatusLabel(status) {
|
||||
const labels = {
|
||||
APPROVED: $t.translate?.preview?.status_approved || 'Approved',
|
||||
REJECTED: $t.translate?.preview?.status_rejected || 'Rejected',
|
||||
PENDING: $t.translate?.preview?.status_pending || 'Pending',
|
||||
APPROVED: getT()?.translate?.preview?.status_approved || 'Approved',
|
||||
REJECTED: getT()?.translate?.preview?.status_rejected || 'Rejected',
|
||||
PENDING: getT()?.translate?.preview?.status_pending || 'Pending',
|
||||
};
|
||||
return labels[status] || status;
|
||||
}
|
||||
@@ -91,9 +94,9 @@
|
||||
targetLanguages = result.target_languages || [];
|
||||
costEstimate = result.cost_estimate || null;
|
||||
uxState = 'preview_loaded';
|
||||
addToast($t.translate?.preview?.preview_loaded?.replace('{count}', records.length), 'success');
|
||||
addToast(getT()?.translate?.preview?.preview_loaded?.replace('{count}', records.length), 'success');
|
||||
} catch (err) {
|
||||
errorMessage = err?.message || $t.translate?.preview?.preview_failed;
|
||||
errorMessage = err?.message || getT()?.translate?.preview?.preview_failed;
|
||||
uxState = 'preview_error';
|
||||
addToast(errorMessage, 'error');
|
||||
}
|
||||
@@ -111,7 +114,7 @@
|
||||
_isEdited: false
|
||||
} : r);
|
||||
} catch (err) {
|
||||
addToast(err?.message || $t.translate?.preview?.approve_failed, 'error');
|
||||
addToast(err?.message || getT()?.translate?.preview?.approve_failed, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +129,7 @@
|
||||
_isEdited: false
|
||||
} : r);
|
||||
} catch (err) {
|
||||
addToast(err?.message || $t.translate?.preview?.reject_failed, 'error');
|
||||
addToast(err?.message || getT()?.translate?.preview?.reject_failed, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +145,7 @@
|
||||
_isEdited: false
|
||||
} : r);
|
||||
} catch (err) {
|
||||
addToast(err?.message || $t.translate?.preview?.approve_failed, 'error');
|
||||
addToast(err?.message || getT()?.translate?.preview?.approve_failed, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +160,7 @@
|
||||
_isEdited: false
|
||||
} : r);
|
||||
} catch (err) {
|
||||
addToast(err?.message || $t.translate?.preview?.reject_failed, 'error');
|
||||
addToast(err?.message || getT()?.translate?.preview?.reject_failed, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,9 +195,9 @@
|
||||
const newValues = { ...editValues };
|
||||
delete newValues[key];
|
||||
editValues = newValues;
|
||||
addToast($t.translate?.preview?.row_updated, 'success');
|
||||
addToast(getT()?.translate?.preview?.row_updated, 'success');
|
||||
} catch (err) {
|
||||
addToast(err?.message || $t.translate?.preview?.edit_failed, 'error');
|
||||
addToast(err?.message || getT()?.translate?.preview?.edit_failed, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,9 +226,9 @@
|
||||
const result = await acceptPreview(jobId);
|
||||
uxState = 'accepted';
|
||||
onAccept();
|
||||
addToast($t.translate?.preview?.accepted_body, 'success');
|
||||
addToast(getT()?.translate?.preview?.accepted_body, 'success');
|
||||
} catch (err) {
|
||||
addToast(err?.message || $t.translate?.preview?.accept_failed, 'error');
|
||||
addToast(err?.message || getT()?.translate?.preview?.accept_failed, 'error');
|
||||
} finally {
|
||||
isAccepting = false;
|
||||
}
|
||||
@@ -235,10 +238,10 @@
|
||||
<div class="translation-preview">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-text">{$t.translate?.preview?.title}</h3>
|
||||
<h3 class="text-lg font-semibold text-text">{_t.translate?.preview?.title}</h3>
|
||||
{#if uxState === 'preview_loaded'}
|
||||
<span class="px-2.5 py-0.5 rounded-full text-xs font-medium {session?.status === 'APPLIED' ? 'bg-success-light text-success' : 'bg-primary-light text-primary'}">
|
||||
{session?.status === 'APPLIED' ? $t.translate?.preview?.accepted_badge : $t.translate?.preview?.active_badge}
|
||||
{session?.status === 'APPLIED' ? _t.translate?.preview?.accepted_badge : _t.translate?.preview?.active_badge}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -247,10 +250,10 @@
|
||||
{#if uxState === 'idle'}
|
||||
<div class="bg-surface-muted border border-border rounded-lg p-6 text-center">
|
||||
<p class="text-sm text-text-muted mb-4">
|
||||
{$t.translate?.preview?.title} — {$t.translate?.preview?.sample_size} {sampleSize}
|
||||
{_t.translate?.preview?.title} — {_t.translate?.preview?.sample_size} {sampleSize}
|
||||
</p>
|
||||
<div class="flex items-center justify-center gap-3 mb-4">
|
||||
<label class="text-sm text-text-muted">{$t.translate?.preview?.sample_size}</label>
|
||||
<label class="text-sm text-text-muted">{_t.translate?.preview?.sample_size}</label>
|
||||
<input
|
||||
type="range"
|
||||
bind:value={sampleSize}
|
||||
@@ -265,14 +268,14 @@
|
||||
<svg class="w-4 h-4 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span class="text-xs font-medium">{($t.translate?.preview?.cost_warning) || 'Large sample size may incur higher costs'}</span>
|
||||
<span class="text-xs font-medium">{(_t.translate?.preview?.cost_warning) || 'Large sample size may incur higher costs'}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<button
|
||||
onclick={handlePreview}
|
||||
class="px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors text-sm"
|
||||
>
|
||||
{$t.translate?.preview?.run_preview}
|
||||
{_t.translate?.preview?.run_preview}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -281,7 +284,7 @@
|
||||
<div class="bg-surface-card border border-border rounded-lg p-6">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-primary-ring" />
|
||||
<span class="text-sm text-text-muted">{$t.translate?.preview?.running_preview}</span>
|
||||
<span class="text-sm text-text-muted">{_t.translate?.preview?.running_preview}</span>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
{#each Array(3) as _}
|
||||
@@ -299,10 +302,10 @@
|
||||
onclick={handlePreview}
|
||||
class="px-4 py-1.5 bg-destructive text-white rounded-lg hover:bg-destructive-hover transition-colors text-sm"
|
||||
>
|
||||
{$t.translate?.preview?.retry_preview}
|
||||
{_t.translate?.preview?.retry_preview}
|
||||
</button>
|
||||
<label class="text-sm text-text-muted flex items-center gap-2">
|
||||
{$t.translate?.preview?.sample_size}
|
||||
{_t.translate?.preview?.sample_size}
|
||||
<input
|
||||
type="range"
|
||||
bind:value={sampleSize}
|
||||
@@ -320,28 +323,28 @@
|
||||
<!-- Cost Estimate Card -->
|
||||
{#if costEstimate}
|
||||
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 border border-primary-ring rounded-lg p-4 mb-4">
|
||||
<h4 class="text-sm font-medium text-primary mb-2">{$t.translate?.preview?.cost_estimate}</h4>
|
||||
<h4 class="text-sm font-medium text-primary mb-2">{_t.translate?.preview?.cost_estimate}</h4>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 text-sm">
|
||||
<div>
|
||||
<span class="text-primary">{$t.translate?.preview?.sample_rows}</span>
|
||||
<span class="text-primary">{_t.translate?.preview?.sample_rows}</span>
|
||||
<span class="ml-1 font-semibold">{costEstimate.sample_size}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-primary">{$t.translate?.preview?.sample_tokens}</span>
|
||||
<span class="text-primary">{_t.translate?.preview?.sample_tokens}</span>
|
||||
<span class="ml-1 font-semibold">{costEstimate.sample_total_tokens?.toLocaleString()}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-primary">{$t.translate?.preview?.sample_cost}</span>
|
||||
<span class="text-primary">{_t.translate?.preview?.sample_cost}</span>
|
||||
<span class="ml-1 font-semibold">${costEstimate.sample_cost?.toFixed(6)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-primary">{$t.translate?.preview?.est_total_cost}</span>
|
||||
<span class="text-primary">{_t.translate?.preview?.est_total_cost}</span>
|
||||
<span class="ml-1 font-semibold">${costEstimate.estimated_cost?.toFixed(6)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Language count -->
|
||||
<p class="text-xs text-primary mt-1">
|
||||
{$t.translate?.preview?.across_languages?.replace('{count}', costEstimate.num_languages || targetLanguages.length || 1)}
|
||||
{_t.translate?.preview?.across_languages?.replace('{count}', costEstimate.num_languages || targetLanguages.length || 1)}
|
||||
</p>
|
||||
<!-- Cost warning for large previews -->
|
||||
{#if costEstimate.warning}
|
||||
@@ -358,14 +361,14 @@
|
||||
<!-- Stats Bar -->
|
||||
<div class="flex items-center justify-between mb-3 text-sm">
|
||||
<div class="flex gap-4">
|
||||
<span class="text-text-muted">{$t.translate?.preview?.rows_count?.replace('{count}', records.length)}</span>
|
||||
<span class="text-success">{$t.translate?.preview?.approved_count?.replace('{count}', approvedCount)}</span>
|
||||
<span class="text-destructive">{$t.translate?.preview?.rejected_count?.replace('{count}', rejectedCount)}</span>
|
||||
<span class="text-text-muted">{_t.translate?.preview?.rows_count?.replace('{count}', records.length)}</span>
|
||||
<span class="text-success">{_t.translate?.preview?.approved_count?.replace('{count}', approvedCount)}</span>
|
||||
<span class="text-destructive">{_t.translate?.preview?.rejected_count?.replace('{count}', rejectedCount)}</span>
|
||||
{#if pendingCount > 0}
|
||||
<span class="text-warning">{$t.translate?.preview?.pending_count?.replace('{count}', pendingCount)}</span>
|
||||
<span class="text-warning">{_t.translate?.preview?.pending_count?.replace('{count}', pendingCount)}</span>
|
||||
{/if}
|
||||
{#if targetLanguages.length > 0}
|
||||
<span class="text-primary">{$t.translate?.preview?.languages_count?.replace('{count}', targetLanguages.length)}</span>
|
||||
<span class="text-primary">{_t.translate?.preview?.languages_count?.replace('{count}', targetLanguages.length)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@@ -374,14 +377,14 @@
|
||||
onclick={handleBulkApprove}
|
||||
class="px-3 py-1 text-xs bg-success-light text-success rounded hover:bg-success-light transition-colors"
|
||||
>
|
||||
{$t.translate?.preview?.approve_all}
|
||||
{_t.translate?.preview?.approve_all}
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
onclick={handlePreview}
|
||||
class="px-3 py-1 text-xs border border-border-strong text-text-muted rounded hover:bg-surface-muted transition-colors"
|
||||
>
|
||||
{$t.translate?.preview?.re_run}
|
||||
{_t.translate?.preview?.re_run}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -392,8 +395,8 @@
|
||||
<thead class="bg-surface-muted">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-text-muted uppercase w-12">#</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-text-muted uppercase">{$t.translate?.preview?.table_source}</th>
|
||||
<th class="px-3 py-2 text-center text-xs font-medium text-text-muted uppercase w-28">{$t.translate?.preview?.detected_language}</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-text-muted uppercase">{_t.translate?.preview?.table_source}</th>
|
||||
<th class="px-3 py-2 text-center text-xs font-medium text-text-muted uppercase w-28">{_t.translate?.preview?.detected_language}</th>
|
||||
{#each targetLanguages as lang}
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-text-muted uppercase min-w-[250px]">
|
||||
<div class="flex items-center gap-1">
|
||||
@@ -401,7 +404,7 @@
|
||||
</div>
|
||||
</th>
|
||||
{/each}
|
||||
<th class="px-3 py-2 text-center text-xs font-medium text-text-muted uppercase w-24">{$t.translate?.preview?.table_status}</th>
|
||||
<th class="px-3 py-2 text-center text-xs font-medium text-text-muted uppercase w-24">{_t.translate?.preview?.table_status}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-border">
|
||||
@@ -410,7 +413,7 @@
|
||||
<td class="px-3 py-2 text-xs text-text-subtle align-top">{idx + 1}</td>
|
||||
<td class="px-3 py-2 align-top min-w-[200px]">
|
||||
<div class="max-h-32 overflow-y-auto">
|
||||
<code class="text-xs text-text whitespace-pre-wrap break-all">{row.source_sql || $t.translate?.preview?.empty_placeholder}</code>
|
||||
<code class="text-xs text-text whitespace-pre-wrap break-all">{row.source_sql || _t.translate?.preview?.empty_placeholder}</code>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center align-top">
|
||||
@@ -439,19 +442,19 @@
|
||||
onclick={() => saveLangEdit(row.id, lang)}
|
||||
class="px-2 py-0.5 text-xs bg-primary text-white rounded hover:bg-primary-hover"
|
||||
>
|
||||
{$t.translate?.preview?.save_edit}
|
||||
{_t.translate?.preview?.save_edit}
|
||||
</button>
|
||||
<button
|
||||
onclick={() => cancelLangEdit(row.id, lang)}
|
||||
class="px-2 py-0.5 text-xs border border-border-strong text-text-muted rounded hover:bg-surface-muted"
|
||||
>
|
||||
{$t.translate?.preview?.cancel_edit}
|
||||
{_t.translate?.preview?.cancel_edit}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="max-h-32 overflow-y-auto">
|
||||
<code class="text-xs whitespace-pre-wrap break-all {langStatus === 'edited' ? 'text-warning' : langStatus === 'approved' ? 'text-success' : 'text-text'}">
|
||||
{langVal || $t.translate?.preview?.pending_placeholder}
|
||||
{langVal || _t.translate?.preview?.pending_placeholder}
|
||||
</code>
|
||||
</div>
|
||||
<div class="flex gap-1 mt-1 flex-wrap">
|
||||
@@ -459,25 +462,25 @@
|
||||
<button
|
||||
onclick={() => handleApproveLanguage(row.id, lang)}
|
||||
class="px-1.5 py-0.5 text-xs bg-success-light text-success rounded hover:bg-success-light transition-colors"
|
||||
title={$t.translate?.preview?.approve}
|
||||
title={_t.translate?.preview?.approve}
|
||||
>
|
||||
{$t.translate?.preview?.approve}
|
||||
{_t.translate?.preview?.approve}
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
onclick={() => startLangEdit(row.id, lang)}
|
||||
class="px-1.5 py-0.5 text-xs bg-primary-light text-primary rounded hover:bg-primary-light transition-colors"
|
||||
title={$t.translate?.preview?.edit}
|
||||
title={_t.translate?.preview?.edit}
|
||||
>
|
||||
{$t.translate?.preview?.edit}
|
||||
{_t.translate?.preview?.edit}
|
||||
</button>
|
||||
{#if langStatus !== 'rejected'}
|
||||
<button
|
||||
onclick={() => handleRejectLanguage(row.id, lang)}
|
||||
class="px-1.5 py-0.5 text-xs bg-destructive-light text-destructive rounded hover:bg-destructive-light transition-colors"
|
||||
title={$t.translate?.preview?.reject}
|
||||
title={_t.translate?.preview?.reject}
|
||||
>
|
||||
{$t.translate?.preview?.reject}
|
||||
{_t.translate?.preview?.reject}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -498,18 +501,18 @@
|
||||
<button
|
||||
onclick={() => handleApprove(row.id)}
|
||||
class="px-1.5 py-0.5 text-xs bg-success-light text-success rounded hover:bg-success-light transition-colors"
|
||||
title={$t.translate?.preview?.approve_all}
|
||||
title={_t.translate?.preview?.approve_all}
|
||||
>
|
||||
{$t.translate?.preview?.approve}
|
||||
{_t.translate?.preview?.approve}
|
||||
</button>
|
||||
{/if}
|
||||
{#if row.status !== 'REJECTED'}
|
||||
<button
|
||||
onclick={() => handleReject(row.id)}
|
||||
class="px-1.5 py-0.5 text-xs bg-destructive-light text-destructive rounded hover:bg-destructive-light transition-colors"
|
||||
title={$t.translate?.preview?.reject}
|
||||
title={_t.translate?.preview?.reject}
|
||||
>
|
||||
{$t.translate?.preview?.reject}
|
||||
{_t.translate?.preview?.reject}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -524,9 +527,9 @@
|
||||
<!-- Accept Gate -->
|
||||
<div class="mt-6 flex items-center justify-between p-4 bg-surface-muted border border-border rounded-lg">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-text">{$t.translate?.preview?.quality_gate}</p>
|
||||
<p class="text-sm font-medium text-text">{_t.translate?.preview?.quality_gate}</p>
|
||||
<p class="text-xs text-text-muted">
|
||||
{isAllApproved ? $t.translate?.preview?.quality_gate_ready : $t.translate?.preview?.quality_gate_pending}
|
||||
{isAllApproved ? _t.translate?.preview?.quality_gate_ready : _t.translate?.preview?.quality_gate_pending}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -537,7 +540,7 @@
|
||||
? 'bg-success text-white hover:bg-success'
|
||||
: 'bg-surface-muted text-text-subtle cursor-not-allowed'}"
|
||||
>
|
||||
{isAccepting ? $t.translate?.preview?.accepting : $t.translate?.preview?.accept_preview}
|
||||
{isAccepting ? _t.translate?.preview?.accepting : _t.translate?.preview?.accept_preview}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -545,15 +548,15 @@
|
||||
{:else if uxState === 'accepted'}
|
||||
<div class="bg-success-light border border-success rounded-lg p-6 text-center">
|
||||
<div class="text-3xl mb-2">✓</div>
|
||||
<h4 class="text-lg font-semibold text-success mb-1">{$t.translate?.preview?.accepted_title}</h4>
|
||||
<h4 class="text-lg font-semibold text-success mb-1">{_t.translate?.preview?.accepted_title}</h4>
|
||||
<p class="text-sm text-success">
|
||||
{$t.translate?.preview?.accepted_body}
|
||||
{_t.translate?.preview?.accepted_body}
|
||||
</p>
|
||||
<button
|
||||
onclick={handlePreview}
|
||||
class="mt-4 px-4 py-1.5 text-sm border border-success text-success rounded-lg hover:bg-success-light transition-colors"
|
||||
>
|
||||
{$t.translate?.preview?.run_preview_again}
|
||||
{_t.translate?.preview?.run_preview_again}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
import { ROUTES } from '$lib/routes';
|
||||
import { t } from '$lib/i18n/index.svelte.js';
|
||||
import { getT } from '$lib/i18n/index.svelte.js';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { cancelRun } from '$lib/api/translate.js';
|
||||
import { addToast } from '$lib/toasts.svelte.js';
|
||||
@@ -43,6 +43,9 @@
|
||||
// @RATIONALE $effect(() => store.subscribe(s => state = s)) creates exactly ONE
|
||||
// subscription per component lifecycle — no accumulating render_effects.
|
||||
let storeState = $state({ uxState: 'idle', totalRecords: 0, successfulRecords: 0, failedRecords: 0, skippedRecords: 0, progressPct: 0, runId: null, jobId: null });
|
||||
// @RATIONALE Svelte 5 store detection fails for `$t` in deeply nested templates.
|
||||
// Use getT() + $derived to keep reactivity to locale changes.
|
||||
const _t = $derived(getT());
|
||||
$effect(() => {
|
||||
const unsub = translationRunStore.subscribe(s => { storeState = s; });
|
||||
return () => unsub();
|
||||
@@ -73,12 +76,12 @@
|
||||
let isActive = $derived(uxState === 'running' || uxState === 'inserting');
|
||||
|
||||
let label = $derived.by(() => {
|
||||
if (uxState === 'inserting') return $t.translate?.run?.insert_phase || 'Inserting...';
|
||||
if (uxState === 'running') return $t.translate?.run?.translate_phase || 'Translating...';
|
||||
if (uxState === 'completed') return $t.translate?.run?.completed || 'Completed';
|
||||
if (uxState === 'partial') return $t.translate?.run?.completed_with_errors || 'Completed with errors';
|
||||
if (uxState === 'failed') return $t.translate?.run?.translation_failed || 'Translation failed';
|
||||
if (uxState === 'cancelled') return $t.translate?.run?.cancelled || 'Cancelled';
|
||||
if (uxState === 'inserting') return getT()?.translate?.run?.insert_phase || 'Inserting...';
|
||||
if (uxState === 'running') return getT()?.translate?.run?.translate_phase || 'Translating...';
|
||||
if (uxState === 'completed') return getT()?.translate?.run?.completed || 'Completed';
|
||||
if (uxState === 'partial') return getT()?.translate?.run?.completed_with_errors || 'Completed with errors';
|
||||
if (uxState === 'failed') return getT()?.translate?.run?.translation_failed || 'Translation failed';
|
||||
if (uxState === 'cancelled') return getT()?.translate?.run?.cancelled || 'Cancelled';
|
||||
return '';
|
||||
});
|
||||
|
||||
@@ -122,9 +125,9 @@
|
||||
isCancelling = true;
|
||||
try {
|
||||
await cancelRun(runId);
|
||||
addToast($t.translate?.run?.run_cancelled || 'Run cancelled', 'info');
|
||||
addToast(getT()?.translate?.run?.run_cancelled || 'Run cancelled', 'info');
|
||||
} catch (err) {
|
||||
addToast(err?.message || $t.translate?.run?.cancel_failed || 'Cancel failed', 'error');
|
||||
addToast(err?.message || getT()?.translate?.run?.cancel_failed || 'Cancel failed', 'error');
|
||||
} finally {
|
||||
isCancelling = false;
|
||||
}
|
||||
@@ -152,7 +155,7 @@
|
||||
onclick={handleClick}
|
||||
class="text-xs text-primary hover:text-primary underline"
|
||||
>
|
||||
{$t.common?.open || 'Open'}
|
||||
{_t.common?.open || 'Open'}
|
||||
</button>
|
||||
<button
|
||||
onclick={handleCancel}
|
||||
@@ -160,8 +163,8 @@
|
||||
class="px-2 py-1 text-xs border border-destructive-ring text-destructive rounded hover:bg-destructive-light disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{isCancelling
|
||||
? ($t.translate?.run?.cancelling || 'Cancelling...')
|
||||
: ($t.translate?.run?.cancel || 'Cancel')}
|
||||
? (_t.translate?.run?.cancelling || 'Cancelling...')
|
||||
: (_t.translate?.run?.cancel || 'Cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -177,19 +180,19 @@
|
||||
<!-- Stats grid -->
|
||||
<div class="grid grid-cols-5 gap-2 text-xs">
|
||||
<div class="bg-surface-muted rounded px-2 py-1 text-center">
|
||||
<span class="text-text-muted block">{($t.translate?.run?.total || 'Total')}</span>
|
||||
<span class="text-text-muted block">{(_t.translate?.run?.total || 'Total')}</span>
|
||||
<span class="font-bold text-text">{totalRecords}</span>
|
||||
</div>
|
||||
<div class="bg-success-light rounded px-2 py-1 text-center">
|
||||
<span class="text-success block">{($t.translate?.run?.success || 'OK')}</span>
|
||||
<span class="text-success block">{(_t.translate?.run?.success || 'OK')}</span>
|
||||
<span class="font-bold text-success">{successfulRecords}</span>
|
||||
</div>
|
||||
<div class="bg-destructive-light rounded px-2 py-1 text-center">
|
||||
<span class="text-destructive block">{($t.translate?.run?.failed || 'Fail')}</span>
|
||||
<span class="text-destructive block">{(_t.translate?.run?.failed || 'Fail')}</span>
|
||||
<span class="font-bold text-destructive">{failedRecords}</span>
|
||||
</div>
|
||||
<div class="bg-warning-light rounded px-2 py-1 text-center">
|
||||
<span class="text-warning block">{($t.translate?.run?.skipped || 'Skip')}</span>
|
||||
<span class="text-warning block">{(_t.translate?.run?.skipped || 'Skip')}</span>
|
||||
<span class="font-bold text-warning">{skippedRecords}</span>
|
||||
</div>
|
||||
<div class="bg-info-light rounded px-2 py-1 text-center">
|
||||
@@ -225,7 +228,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
<span class="opacity-70 hover:opacity-100 transition-opacity">
|
||||
▶ {$t.common?.open || 'Open'}
|
||||
▶ {_t.common?.open || 'Open'}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
@RATIONALE $effect(() => store.subscribe(s => state = s)) creates exactly ONE
|
||||
subscription per component lifecycle — no accumulating render_effects. -->
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n/index.svelte.js';
|
||||
import { getT } from '$lib/i18n/index.svelte.js';
|
||||
import { addToast } from '$lib/toasts.svelte.js';
|
||||
import { cancelRun, downloadFailedCsv } from '$lib/api/translate.js';
|
||||
import { translationRunStore } from '$lib/stores/translationRun.svelte.js';
|
||||
@@ -42,6 +42,9 @@
|
||||
let { runId, onRetry = () => {}, onRetryInsert = () => {}, onComplete = () => {} } = $props();
|
||||
|
||||
let isCancelling = $state(false);
|
||||
// @RATIONALE Svelte 5 store detection fails for `$t` in deeply nested templates.
|
||||
// Use getT() + $derived to keep reactivity to locale changes.
|
||||
const _t = $derived(getT());
|
||||
/** Non-reactive guard — prevents double-firing of onComplete.
|
||||
* MUST NOT be $state: the $effect reads AND writes it, and a reactive
|
||||
* write-after-read inside $effect triggers an infinite Svelte flush loop. */
|
||||
@@ -86,9 +89,9 @@
|
||||
isCancelling = true;
|
||||
try {
|
||||
await cancelRun(runId);
|
||||
addToast($t.translate?.run?.run_cancelled, 'info');
|
||||
addToast(getT()?.translate?.run?.run_cancelled, 'info');
|
||||
} catch (err) {
|
||||
addToast(err?.message || $t.translate?.run?.cancel_failed, 'error');
|
||||
addToast(err?.message || getT()?.translate?.run?.cancel_failed, 'error');
|
||||
} finally {
|
||||
isCancelling = false;
|
||||
}
|
||||
@@ -98,7 +101,7 @@
|
||||
<div class="translation-run-progress">
|
||||
{#if uxState === 'idle'}
|
||||
<div class="bg-surface-muted border border-border rounded-lg p-4 text-center">
|
||||
<p class="text-sm text-text-muted">{$t.translate?.run?.loading}</p>
|
||||
<p class="text-sm text-text-muted">{_t.translate?.run?.loading}</p>
|
||||
</div>
|
||||
|
||||
{:else if isRunning}
|
||||
@@ -108,7 +111,7 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-primary-ring" />
|
||||
<span class="text-sm font-medium text-text">
|
||||
{uxState === 'inserting' ? $t.translate?.run?.insert_phase : $t.translate?.run?.translate_phase}
|
||||
{uxState === 'inserting' ? _t.translate?.run?.insert_phase : _t.translate?.run?.translate_phase}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
@@ -116,7 +119,7 @@
|
||||
disabled={isCancelling}
|
||||
class="px-3 py-1 text-xs border border-destructive-ring text-destructive rounded hover:bg-destructive-light disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{isCancelling ? $t.translate?.run?.cancelling : $t.translate?.run?.cancel}
|
||||
{isCancelling ? _t.translate?.run?.cancelling : _t.translate?.run?.cancel}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -131,19 +134,19 @@
|
||||
<!-- Statistics -->
|
||||
<div class="grid grid-cols-5 gap-3 text-sm">
|
||||
<div>
|
||||
<span class="text-text-muted">{$t.translate?.run?.total}</span>
|
||||
<span class="text-text-muted">{_t.translate?.run?.total}</span>
|
||||
<p class="font-semibold">{totalRecords}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-success">{$t.translate?.run?.success}</span>
|
||||
<span class="text-success">{_t.translate?.run?.success}</span>
|
||||
<p class="font-semibold text-success">{successfulRecords}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-destructive">{$t.translate?.run?.failed}</span>
|
||||
<span class="text-destructive">{_t.translate?.run?.failed}</span>
|
||||
<p class="font-semibold text-destructive">{failedRecords}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-warning">{$t.translate?.run?.skipped}</span>
|
||||
<span class="text-warning">{_t.translate?.run?.skipped}</span>
|
||||
<p class="font-semibold text-warning">{skippedRecords}</p>
|
||||
</div>
|
||||
<div>
|
||||
@@ -154,7 +157,7 @@
|
||||
|
||||
<!-- Batch counter -->
|
||||
<p class="text-xs text-text-subtle">
|
||||
{$t.translate?.run?.batches_progress?.replace('{count}', batchCount).replace('{pct}', progressPct)}
|
||||
{_t.translate?.run?.batches_progress?.replace('{count}', batchCount).replace('{pct}', progressPct)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -163,20 +166,20 @@
|
||||
<div class="bg-success-light border border-success rounded-lg p-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-success text-lg">✓</span>
|
||||
<span class="text-sm font-semibold text-success">{$t.translate?.run?.completed}</span>
|
||||
<span class="text-sm font-semibold text-success">{_t.translate?.run?.completed}</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-3 text-sm">
|
||||
<div>
|
||||
<span class="text-text-muted">{$t.translate?.run?.total}</span>
|
||||
<span class="text-text-muted">{_t.translate?.run?.total}</span>
|
||||
<p class="font-semibold text-success">{successfulRecords} / {totalRecords}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-text-muted">{$t.translate?.run?.skipped}</span>
|
||||
<span class="text-text-muted">{_t.translate?.run?.skipped}</span>
|
||||
<p class="font-semibold">{skippedRecords}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-text-muted">{$t.translate?.run?.insert_status?.replace(':', '')}</span>
|
||||
<p class="font-semibold text-success">{insertStatus || $t.translate?.run?.insert_completed}</p>
|
||||
<span class="text-text-muted">{_t.translate?.run?.insert_status?.replace(':', '')}</span>
|
||||
<p class="font-semibold text-success">{insertStatus || _t.translate?.run?.insert_completed}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -187,26 +190,26 @@
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-warning text-lg">⚠</span>
|
||||
<span class="text-sm font-semibold text-warning">{$t.translate?.run?.completed_with_errors}</span>
|
||||
<span class="text-sm font-semibold text-warning">{_t.translate?.run?.completed_with_errors}</span>
|
||||
</div>
|
||||
<button
|
||||
onclick={onRetry}
|
||||
class="px-3 py-1 text-xs bg-warning-light text-warning rounded hover:bg-yellow-200 transition-colors"
|
||||
>
|
||||
{$t.translate?.run?.retry_failed}
|
||||
{_t.translate?.run?.retry_failed}
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-3 text-sm">
|
||||
<div>
|
||||
<span class="text-text-muted">{$t.translate?.run?.success}</span>
|
||||
<span class="text-text-muted">{_t.translate?.run?.success}</span>
|
||||
<p class="font-semibold text-success">{successfulRecords}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-text-muted">{$t.translate?.run?.failed}</span>
|
||||
<span class="text-text-muted">{_t.translate?.run?.failed}</span>
|
||||
<p class="font-semibold text-destructive">{failedRecords}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-text-muted">{$t.translate?.run?.skipped}</span>
|
||||
<span class="text-text-muted">{_t.translate?.run?.skipped}</span>
|
||||
<p class="font-semibold">{skippedRecords}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -218,16 +221,16 @@
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-destructive text-lg">✗</span>
|
||||
<span class="text-sm font-semibold text-destructive">{$t.translate?.run?.translation_failed}</span>
|
||||
<span class="text-sm font-semibold text-destructive">{_t.translate?.run?.translation_failed}</span>
|
||||
</div>
|
||||
<button
|
||||
onclick={onRetry}
|
||||
class="px-3 py-1 text-xs bg-destructive-light text-destructive rounded hover:bg-destructive-light transition-colors"
|
||||
>
|
||||
{$t.translate?.common?.retry}
|
||||
{_t.translate?.common?.retry}
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-destructive">{status?.error_message || $t.translate?.common?.unknown}</p>
|
||||
<p class="text-xs text-destructive">{status?.error_message || _t.translate?.common?.unknown}</p>
|
||||
</div>
|
||||
|
||||
<!-- Insert Failed -->
|
||||
@@ -236,7 +239,7 @@
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-warning text-lg">⚠</span>
|
||||
<span class="text-sm font-semibold text-warning">{$t.translate?.run?.insert_failed}</span>
|
||||
<span class="text-sm font-semibold text-warning">{_t.translate?.run?.insert_failed}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
@@ -246,18 +249,18 @@
|
||||
}}
|
||||
class="px-3 py-1 text-xs bg-primary-light text-primary rounded hover:bg-primary-light transition-colors"
|
||||
>
|
||||
{$t.translate?.run?.download_csv || 'Download CSV'}
|
||||
{_t.translate?.run?.download_csv || 'Download CSV'}
|
||||
</button>
|
||||
<button
|
||||
onclick={onRetryInsert}
|
||||
class="px-3 py-1 text-xs bg-warning-light text-warning rounded hover:bg-warning-light transition-colors"
|
||||
>
|
||||
{$t.translate?.run?.retry_insert}
|
||||
{_t.translate?.run?.retry_insert}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-text-muted mt-1">
|
||||
{$t.translate?.run?.completed} ({successfulRecords}) — {$t.translate?.run?.insert_failed}
|
||||
{_t.translate?.run?.completed} ({successfulRecords}) — {_t.translate?.run?.insert_failed}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -266,7 +269,7 @@
|
||||
<div class="bg-surface-muted border border-border rounded-lg p-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-text-muted text-lg">■</span>
|
||||
<span class="text-sm font-semibold text-text">{$t.translate?.run?.cancelled}</span>
|
||||
<span class="text-sm font-semibold text-text">{_t.translate?.run?.cancelled}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<!-- @UX_FEEDBACK Tabular statistics; click to copy Superset query ID; collapsible SQL block. -->
|
||||
<!-- @UX_RECOVERY Retry failed batches; retry insert. -->
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n/index.svelte.js';
|
||||
import { getT } from '$lib/i18n/index.svelte.js';
|
||||
import { addToast } from '$lib/toasts.svelte.js';
|
||||
import {
|
||||
fetchRunStatus,
|
||||
@@ -30,6 +30,9 @@
|
||||
* @type {'completed'|'partial'|'failed'|'insert_failed'}
|
||||
*/
|
||||
let uxState = $state('completed');
|
||||
// @RATIONALE Svelte 5 store detection fails for `$t` in deeply nested templates.
|
||||
// Use getT() + $derived to keep reactivity to locale changes.
|
||||
const _t = $derived(getT());
|
||||
let status = $state(null);
|
||||
let records = $state([]);
|
||||
let batches = $state([]);
|
||||
@@ -45,11 +48,11 @@
|
||||
|
||||
function getRunStatusLabel(value) {
|
||||
const labels = {
|
||||
COMPLETED: $t.translate?.run?.completed || 'Completed',
|
||||
FAILED: $t.translate?.run?.translation_failed || 'Failed',
|
||||
RUNNING: $t.translate?.history?.status_running || 'Running',
|
||||
PENDING: $t.translate?.history?.status_pending || 'Pending',
|
||||
CANCELLED: $t.translate?.run?.cancelled || 'Cancelled',
|
||||
COMPLETED: getT()?.translate?.run?.completed || 'Completed',
|
||||
FAILED: getT()?.translate?.run?.translation_failed || 'Failed',
|
||||
RUNNING: getT()?.translate?.history?.status_running || 'Running',
|
||||
PENDING: getT()?.translate?.history?.status_pending || 'Pending',
|
||||
CANCELLED: getT()?.translate?.run?.cancelled || 'Cancelled',
|
||||
};
|
||||
return labels[value] || value;
|
||||
}
|
||||
@@ -57,8 +60,8 @@
|
||||
// Derived
|
||||
let insertStatusBadge = $derived.by(() => {
|
||||
const s = status?.insert_status;
|
||||
if (!s || s === 'success') return { label: $t.translate?.run?.success, class: 'bg-success-light text-success' };
|
||||
if (s === 'failed' || s === 'timeout') return { label: $t.translate?.run?.failed, class: 'bg-destructive-light text-destructive' };
|
||||
if (!s || s === 'success') return { label: getT()?.translate?.run?.success, class: 'bg-success-light text-success' };
|
||||
if (s === 'failed' || s === 'timeout') return { label: getT()?.translate?.run?.failed, class: 'bg-destructive-light text-destructive' };
|
||||
return { label: s, class: 'bg-warning-light text-warning' };
|
||||
});
|
||||
|
||||
@@ -109,7 +112,7 @@
|
||||
uxState = 'completed';
|
||||
}
|
||||
} catch (err) {
|
||||
addToast(err?.message || $t.translate?.run?.load_failed, 'error');
|
||||
addToast(err?.message || getT()?.translate?.run?.load_failed, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,11 +121,11 @@
|
||||
isRetrying = true;
|
||||
try {
|
||||
await retryFailedBatches(runId);
|
||||
addToast($t.translate?.run?.retry_success, 'success');
|
||||
addToast(getT()?.translate?.run?.retry_success, 'success');
|
||||
await loadData();
|
||||
onRefresh();
|
||||
} catch (err) {
|
||||
addToast(err?.message || $t.translate?.run?.retry_failed_msg, 'error');
|
||||
addToast(err?.message || getT()?.translate?.run?.retry_failed_msg, 'error');
|
||||
} finally {
|
||||
isRetrying = false;
|
||||
}
|
||||
@@ -133,11 +136,11 @@
|
||||
isRetryingInsert = true;
|
||||
try {
|
||||
await retryInsert(runId);
|
||||
addToast($t.translate?.run?.insert_retry_success, 'success');
|
||||
addToast(getT()?.translate?.run?.insert_retry_success, 'success');
|
||||
await loadData();
|
||||
onRefresh();
|
||||
} catch (err) {
|
||||
addToast(err?.message || $t.translate?.run?.insert_retry_failed_msg, 'error');
|
||||
addToast(err?.message || getT()?.translate?.run?.insert_retry_failed_msg, 'error');
|
||||
} finally {
|
||||
isRetryingInsert = false;
|
||||
}
|
||||
@@ -146,9 +149,9 @@
|
||||
/** @param {string} text */
|
||||
function copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
addToast($t.translate?.run?.copy_success, 'success');
|
||||
addToast(getT()?.translate?.run?.copy_success, 'success');
|
||||
}).catch(() => {
|
||||
addToast($t.translate?.run?.copy_failed, 'error');
|
||||
addToast(getT()?.translate?.run?.copy_failed, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -162,7 +165,7 @@
|
||||
const items = allRecords?.items || [];
|
||||
sqlAuditLines = items.slice(0, 20).map(r => r.target_sql).filter(Boolean);
|
||||
} catch (err) {
|
||||
sqlAuditLines = [$t.translate?.run?.no_sql_audit];
|
||||
sqlAuditLines = [getT()?.translate?.run?.no_sql_audit];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,13 +174,13 @@
|
||||
<div class="translation-run-result">
|
||||
{#if !status}
|
||||
<div class="bg-surface-muted border border-border rounded-lg p-4 text-center">
|
||||
<p class="text-sm text-text-muted">{$t.translate?.run?.loading_result}</p>
|
||||
<p class="text-sm text-text-muted">{_t.translate?.run?.loading_result}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="bg-surface-card border border-border rounded-lg p-4 space-y-4">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-text">{$t.translate?.run?.result_title}</h3>
|
||||
<h3 class="text-lg font-semibold text-text">{_t.translate?.run?.result_title}</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Download CSV for failed/insert_failed runs with successful records -->
|
||||
{#if (uxState === 'insert_failed' || uxState === 'failed' || uxState === 'partial') && (status.successful_records || 0) > 0}
|
||||
@@ -188,7 +191,7 @@
|
||||
}}
|
||||
class="px-3 py-1.5 text-xs bg-primary-light text-primary rounded hover:bg-primary-light transition-colors"
|
||||
>
|
||||
{$t.translate?.run?.download_csv || 'CSV'}
|
||||
{_t.translate?.run?.download_csv || 'CSV'}
|
||||
</button>
|
||||
{/if}
|
||||
<!-- Retry buttons -->
|
||||
@@ -198,7 +201,7 @@
|
||||
disabled={isRetrying}
|
||||
class="px-3 py-1.5 text-xs bg-primary text-white rounded hover:bg-primary-hover disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{isRetrying ? $t.translate?.run?.retrying : $t.translate?.run?.retry_failed}
|
||||
{isRetrying ? _t.translate?.run?.retrying : _t.translate?.run?.retry_failed}
|
||||
</button>
|
||||
{/if}
|
||||
{#if uxState === 'insert_failed'}
|
||||
@@ -207,7 +210,7 @@
|
||||
disabled={isRetryingInsert}
|
||||
class="px-3 py-1.5 text-xs bg-warning-light text-white rounded hover:bg-warning-light disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{isRetryingInsert ? $t.translate?.run?.retrying_insert : $t.translate?.run?.retry_insert}
|
||||
{isRetryingInsert ? _t.translate?.run?.retrying_insert : _t.translate?.run?.retry_insert}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -216,19 +219,19 @@
|
||||
<!-- Statistics Grid -->
|
||||
<div class="grid grid-cols-2 sm:grid-cols-5 gap-4">
|
||||
<div class="bg-surface-muted rounded-lg p-3">
|
||||
<p class="text-xs text-text-muted uppercase">{$t.translate?.run?.total_records}</p>
|
||||
<p class="text-xs text-text-muted uppercase">{_t.translate?.run?.total_records}</p>
|
||||
<p class="text-xl font-bold text-text">{status.total_records || 0}</p>
|
||||
</div>
|
||||
<div class="bg-success-light rounded-lg p-3">
|
||||
<p class="text-xs text-success uppercase">{$t.translate?.run?.success}</p>
|
||||
<p class="text-xs text-success uppercase">{_t.translate?.run?.success}</p>
|
||||
<p class="text-xl font-bold text-success">{status.successful_records || 0}</p>
|
||||
</div>
|
||||
<div class="bg-destructive-light rounded-lg p-3">
|
||||
<p class="text-xs text-destructive uppercase">{$t.translate?.run?.failed}</p>
|
||||
<p class="text-xs text-destructive uppercase">{_t.translate?.run?.failed}</p>
|
||||
<p class="text-xl font-bold text-destructive">{status.failed_records || 0}</p>
|
||||
</div>
|
||||
<div class="bg-warning-light rounded-lg p-3">
|
||||
<p class="text-xs text-warning uppercase">{$t.translate?.run?.skipped}</p>
|
||||
<p class="text-xs text-warning uppercase">{_t.translate?.run?.skipped}</p>
|
||||
<p class="text-xl font-bold text-warning">{status.skipped_records || 0}</p>
|
||||
</div>
|
||||
<div class="bg-info-light rounded-lg p-3">
|
||||
@@ -240,7 +243,7 @@
|
||||
<!-- Per-language Statistics -->
|
||||
{#if status.language_stats && status.language_stats.length > 0}
|
||||
<div class="border-t border-border pt-3">
|
||||
<p class="text-xs text-text-muted uppercase mb-2">{$t.translate?.run?.per_language ?? 'Per-Language Statistics'}</p>
|
||||
<p class="text-xs text-text-muted uppercase mb-2">{_t.translate?.run?.per_language ?? 'Per-Language Statistics'}</p>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
{#each status.language_stats as lang}
|
||||
<div class="px-3 py-2 bg-surface-muted rounded border border-border min-w-[140px]">
|
||||
@@ -265,42 +268,42 @@
|
||||
<div class="border-t border-border pt-3">
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span class="text-text-muted">{$t.translate?.run?.run_id}</span>
|
||||
<span class="text-text-muted">{_t.translate?.run?.run_id}</span>
|
||||
<span class="ml-1 font-mono text-xs text-text">{status.id}</span>
|
||||
<button
|
||||
onclick={() => copyToClipboard(status.id)}
|
||||
class="ml-1 text-primary hover:text-primary text-xs"
|
||||
title={$t.translate?.run?.copy_id}
|
||||
title={_t.translate?.run?.copy_id}
|
||||
>
|
||||
{$t.translate?.run?.copy_id}
|
||||
{_t.translate?.run?.copy_id}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-text-muted">{$t.translate?.run?.status_label || 'Статус'}</span>
|
||||
<span class="text-text-muted">{_t.translate?.run?.status_label || 'Статус'}</span>
|
||||
<span class="ml-1 text-xs font-medium text-text">{getRunStatusLabel(status.status)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-text-muted">{$t.translate?.run?.insert_status}</span>
|
||||
<span class="text-text-muted">{_t.translate?.run?.insert_status}</span>
|
||||
<span class="ml-1 inline-flex px-2 py-0.5 text-xs rounded-full font-medium {insertStatusBadge.class}">
|
||||
{insertStatusBadge.label}
|
||||
</span>
|
||||
</div>
|
||||
{#if status.superset_execution_id}
|
||||
<div>
|
||||
<span class="text-text-muted">{$t.translate?.run?.superset_query}</span>
|
||||
<span class="text-text-muted">{_t.translate?.run?.superset_query}</span>
|
||||
<span class="ml-1 font-mono text-xs text-text">{status.superset_execution_id}</span>
|
||||
<button
|
||||
onclick={() => copyToClipboard(status.superset_execution_id)}
|
||||
class="ml-1 text-primary hover:text-primary text-xs"
|
||||
title={$t.translate?.run?.copy_id}
|
||||
title={_t.translate?.run?.copy_id}
|
||||
>
|
||||
{$t.translate?.run?.copy_id}
|
||||
{_t.translate?.run?.copy_id}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if status.error_message}
|
||||
<div class="col-span-2">
|
||||
<span class="text-destructive text-xs">{$t.translate?.run?.error_label?.replace('{error}', status.error_message)}</span>
|
||||
<span class="text-destructive text-xs">{_t.translate?.run?.error_label?.replace('{error}', status.error_message)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -318,13 +321,13 @@
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
{$t.translate?.run?.sql_audit?.replace('{count}', sqlAuditLines.length)}
|
||||
{_t.translate?.run?.sql_audit?.replace('{count}', sqlAuditLines.length)}
|
||||
</button>
|
||||
|
||||
{#if showSqlAudit}
|
||||
<div class="mt-2 bg-terminal-bg rounded-lg p-3 max-h-60 overflow-y-auto">
|
||||
{#if sqlAuditLines.length === 0}
|
||||
<p class="text-xs text-text-subtle">{$t.translate?.run?.no_sql_audit}</p>
|
||||
<p class="text-xs text-text-subtle">{_t.translate?.run?.no_sql_audit}</p>
|
||||
{:else}
|
||||
{#each sqlAuditLines as line, idx}
|
||||
<pre class="text-xs text-success mb-1 font-mono whitespace-pre-wrap break-all">
|
||||
@@ -340,14 +343,14 @@
|
||||
{#if status.event_invariants}
|
||||
<div class="border-t border-border pt-3">
|
||||
<div class="flex items-center gap-2 text-xs text-text-muted">
|
||||
<span>{$t.translate?.run?.event_invariants}</span>
|
||||
<span>{_t.translate?.run?.event_invariants}</span>
|
||||
<span class="{status.event_invariants.invariant_valid ? 'text-success' : 'text-destructive'}">
|
||||
{status.event_invariants.invariant_valid ? $t.translate?.run?.valid : $t.translate?.run?.violated}
|
||||
{status.event_invariants.invariant_valid ? _t.translate?.run?.valid : _t.translate?.run?.violated}
|
||||
</span>
|
||||
<span class="text-text-subtle">|</span>
|
||||
<span>{$t.translate?.run?.superset_query_id_label} {status.event_invariants.has_run_started ? $t.translate?.run?.started_yes : $t.translate?.run?.started_no}</span>
|
||||
<span>{_t.translate?.run?.superset_query_id_label} {status.event_invariants.has_run_started ? _t.translate?.run?.started_yes : _t.translate?.run?.started_no}</span>
|
||||
<span class="text-text-subtle">|</span>
|
||||
<span>{$t.translate?.run?.terminal_events?.replace('{count}', status.event_invariants.terminal_event_count)}</span>
|
||||
<span>{_t.translate?.run?.terminal_events?.replace('{count}', status.event_invariants.terminal_event_count)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -366,7 +369,7 @@
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
{$t.translate?.run?.records_title || 'Translation Records'} ({records.length})
|
||||
{_t.translate?.run?.records_title || 'Translation Records'} ({records.length})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -376,7 +379,7 @@
|
||||
<thead class="bg-surface-muted">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-text-muted uppercase w-12">#</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-text-muted uppercase min-w-[150px]">{$t.translate?.preview?.table_source || 'Source'}</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-text-muted uppercase min-w-[150px]">{_t.translate?.preview?.table_source || 'Source'}</th>
|
||||
{#each targetLanguages as lang}
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-text-muted uppercase min-w-[180px]">
|
||||
<div class="flex items-center gap-1">
|
||||
@@ -385,10 +388,10 @@
|
||||
</th>
|
||||
{/each}
|
||||
{#if targetLanguages.length === 0}
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-text-muted uppercase min-w-[180px]">{$t.translate?.preview?.translation || 'Translation'}</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-text-muted uppercase min-w-[180px]">{_t.translate?.preview?.translation || 'Translation'}</th>
|
||||
{/if}
|
||||
<th class="px-3 py-2 text-center text-xs font-medium text-text-muted uppercase w-20">{$t.translate?.preview?.table_status || 'Status'}</th>
|
||||
<th class="px-3 py-2 text-center text-xs font-medium text-text-muted uppercase w-24">{$t.translate?.preview?.table_actions || 'Actions'}</th>
|
||||
<th class="px-3 py-2 text-center text-xs font-medium text-text-muted uppercase w-20">{_t.translate?.preview?.table_status || 'Status'}</th>
|
||||
<th class="px-3 py-2 text-center text-xs font-medium text-text-muted uppercase w-24">{_t.translate?.preview?.table_actions || 'Actions'}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-border">
|
||||
@@ -450,7 +453,7 @@
|
||||
class="px-2 py-0.5 text-[10px] bg-primary text-white rounded hover:bg-primary-hover transition-colors"
|
||||
title="Submit Correction"
|
||||
>
|
||||
{$t.translate?.run?.correct || 'Correct'}
|
||||
{_t.translate?.run?.correct || 'Correct'}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import { api } from '$lib/api.js';
|
||||
import { log } from '$lib/cot-logger';
|
||||
import { addToast } from '$lib/toasts.svelte.js';
|
||||
import { t, _ } from '$lib/i18n/index.svelte.js';
|
||||
import { _, getT } from '$lib/i18n/index.svelte.js';
|
||||
import { triggerRun, fetchRunHistory, cancelRun } from '$lib/api/translate.js';
|
||||
import { startTranslationRun, resetTranslationRun, translationRunStore } from '$lib/stores/translationRun.svelte.js';
|
||||
|
||||
@@ -69,13 +69,13 @@ export class TranslationJobModel {
|
||||
activeTab: string = $state('config');
|
||||
tabs: { id: string; label: string }[] = $derived.by(() => {
|
||||
const base = [
|
||||
{ id: 'config', label: $t.translate?.config?.basic_info || 'Configuration' },
|
||||
{ id: 'preview', label: $t.translate?.preview?.title || 'Preview' },
|
||||
{ id: 'run', label: $t.translate?.config?.run_translation || 'Run' },
|
||||
{ id: 'config', label: getT()?.translate?.config?.basic_info || 'Configuration' },
|
||||
{ id: 'preview', label: getT()?.translate?.preview?.title || 'Preview' },
|
||||
{ id: 'run', label: getT()?.translate?.config?.run_translation || 'Run' },
|
||||
];
|
||||
if (!this.isNewJob && this.existingJob) {
|
||||
base.splice(2, 0, { id: 'target', label: $t.translate?.config?.target_table_tab || 'Target Config' });
|
||||
base.push({ id: 'schedule', label: $t.translate?.schedule?.tab_label || 'Schedule' });
|
||||
base.splice(2, 0, { id: 'target', label: getT()?.translate?.config?.target_table_tab || 'Target Config' });
|
||||
base.push({ id: 'schedule', label: getT()?.translate?.schedule?.tab_label || 'Schedule' });
|
||||
}
|
||||
return base;
|
||||
});
|
||||
@@ -92,9 +92,9 @@ export class TranslationJobModel {
|
||||
!!this.translationColumn && !!this.datasourceId && this.targetLanguages.length > 0 && !!this.providerId
|
||||
);
|
||||
pageTitle = $derived.by(() => {
|
||||
if (this.isNewJob) return $t.translate?.config?.new_title || 'New Translation Job';
|
||||
if (this.isNewJob) return getT()?.translate?.config?.new_title || 'New Translation Job';
|
||||
const jobName = this.existingJob?.name || this.name;
|
||||
const base = $t.translate?.config?.edit_title || 'Edit Translation Job';
|
||||
const base = getT()?.translate?.config?.edit_title || 'Edit Translation Job';
|
||||
return jobName ? `${jobName} | ${base}` : base;
|
||||
});
|
||||
|
||||
@@ -274,7 +274,7 @@ export class TranslationJobModel {
|
||||
this.isNewJob = false;
|
||||
this.existingJob = { id: resp.id, ...payload };
|
||||
}
|
||||
addToast($t.translate?.config?.saved || 'Job saved', 'success');
|
||||
addToast(getT()?.translate?.config?.saved || 'Job saved', 'success');
|
||||
this.uxState = 'configured';
|
||||
} catch (err: unknown) {
|
||||
this.error = err instanceof Error ? err.message : 'Failed to save';
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { ROUTES } from '$lib/routes';
|
||||
import { page } from '$app/state';
|
||||
import { t, _ } from '$lib/i18n/index.svelte.js';
|
||||
import { _, getT } from '$lib/i18n/index.svelte.js';
|
||||
import { api } from '$lib/api.js';
|
||||
import { TranslationJobModel } from '$lib/models/TranslationJobModel.svelte';
|
||||
import MultiSelect from '$lib/components/ui/MultiSelect.svelte';
|
||||
@@ -41,6 +41,10 @@
|
||||
import RunTabContent from '$lib/components/translate/RunTabContent.svelte';
|
||||
|
||||
const m = new TranslationJobModel();
|
||||
// @RATIONALE Svelte 5 compiler fails to detect `t` as store in deeply nested
|
||||
// template blocks, generating undefined `$t`. Bypass with `getT()`
|
||||
// wrapped in $derived to keep template reactive to locale changes.
|
||||
const _t = $derived(getT());
|
||||
|
||||
$effect(() => {
|
||||
m.jobId = page.params.id;
|
||||
@@ -76,18 +80,18 @@
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-text">
|
||||
{m.isNewJob ? $t.translate?.config?.new_title : $t.translate?.config?.edit_title}
|
||||
{m.isNewJob ? _t.translate?.config?.new_title : _t.translate?.config?.edit_title}
|
||||
</h1>
|
||||
{#if !m.isNewJob && m.existingJob}
|
||||
<p class="text-sm text-text-muted mt-1">{$t.translate?.config?.id_label.replace('{id}', m.existingJob.id)}</p>
|
||||
<p class="text-sm text-text-muted mt-1">{_t.translate?.config?.id_label.replace('{id}', m.existingJob.id)}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button onclick={() => goto(ROUTES.translate.list())} class="px-4 py-2 border border-border-strong text-text rounded-lg hover:bg-surface-muted transition-colors">
|
||||
{$t.translate?.config?.cancel}
|
||||
{_t.translate?.config?.cancel}
|
||||
</button>
|
||||
<button onclick={() => m.saveJob()} disabled={m.isSaving} class="px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
|
||||
{m.isSaving ? $t.translate?.config?.saving : $t.translate?.config?.save}
|
||||
{m.isSaving ? _t.translate?.config?.saving : _t.translate?.config?.save}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,7 +99,7 @@
|
||||
<!-- Warnings -->
|
||||
{#if m.warnings.length > 0}
|
||||
<div class="bg-warning-light border border-warning rounded-lg p-4 mb-6">
|
||||
<h4 class="text-sm font-medium text-warning mb-2">{$t.translate?.config?.warnings_title}</h4>
|
||||
<h4 class="text-sm font-medium text-warning mb-2">{_t.translate?.config?.warnings_title}</h4>
|
||||
<ul class="list-disc list-inside text-sm text-warning space-y-1">
|
||||
{#each m.warnings as w}
|
||||
<li>{w}</li>
|
||||
@@ -111,14 +115,14 @@
|
||||
<!-- Error -->
|
||||
{:else if m.uxState === 'error'}
|
||||
<div class="bg-destructive-light border border-destructive-light rounded-lg p-6 text-center">
|
||||
<p class="text-destructive mb-3">{m.error || $t.translate?.common?.unknown_error}</p>
|
||||
<button onclick={() => goto(ROUTES.translate.list())} class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors">{$t.translate?.config?.back_to_jobs}</button>
|
||||
<p class="text-destructive mb-3">{m.error || _t.translate?.common?.unknown_error}</p>
|
||||
<button onclick={() => goto(ROUTES.translate.list())} class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors">{_t.translate?.config?.back_to_jobs}</button>
|
||||
</div>
|
||||
|
||||
<!-- Datasource Unavailable -->
|
||||
{:else if m.uxState === 'datasource_unavailable'}
|
||||
<div class="bg-warning-light border border-orange-200 rounded-lg p-4 mb-6">
|
||||
<p class="text-warning text-sm">{$t.translate?.config?.datasource_unavailable}</p>
|
||||
<p class="text-warning text-sm">{_t.translate?.config?.datasource_unavailable}</p>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
@@ -135,7 +139,7 @@
|
||||
? 'bg-surface-card text-primary border border-b-white border-border -mb-px'
|
||||
: 'text-text-muted hover:text-text hover:bg-surface-muted'}
|
||||
{(tab.id === 'preview' && (!m.configValid || m.isNewJob)) || (tab.id === 'target' && m.isNewJob) ? 'opacity-40 cursor-not-allowed' : ''}"
|
||||
title={tab.id === 'preview' && (!m.configValid || m.isNewJob) ? ($t.translate?.preview?.config_incomplete || 'Configuration incomplete') : tab.id === 'target' && m.isNewJob ? ($t.translate?.config?.save_job_first || 'Save the job first') : ''}
|
||||
title={tab.id === 'preview' && (!m.configValid || m.isNewJob) ? (_t.translate?.preview?.config_incomplete || 'Configuration incomplete') : tab.id === 'target' && m.isNewJob ? (_t.translate?.config?.save_job_first || 'Save the job first') : ''}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
@@ -187,15 +191,15 @@
|
||||
{#if !m.isNewJob && m.existingJob}
|
||||
{#if !m.configValid}
|
||||
<div class="bg-warning-light border border-warning rounded-lg p-6 text-center">
|
||||
<p class="text-sm text-warning font-medium mb-2">{$t.translate?.preview?.config_incomplete}</p>
|
||||
<p class="text-xs text-warning">{$t.translate?.preview?.config_requirements}</p>
|
||||
<p class="text-sm text-warning font-medium mb-2">{_t.translate?.preview?.config_incomplete}</p>
|
||||
<p class="text-xs text-warning">{_t.translate?.preview?.config_requirements}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<TranslationPreview jobId={m.jobId} environmentId={m.environmentId} />
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="bg-surface-muted border border-border rounded-lg p-8 text-center">
|
||||
<p class="text-sm text-text-muted">{$t.translate?.config?.save_job_first || 'Save the job first'}</p>
|
||||
<p class="text-sm text-text-muted">{_t.translate?.config?.save_job_first || 'Save the job first'}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -222,7 +226,7 @@
|
||||
/>
|
||||
{:else}
|
||||
<div class="bg-surface-muted border border-border rounded-lg p-8 text-center">
|
||||
<p class="text-sm text-text-muted">{$t.translate?.config?.save_job_first || 'Save the job first'}</p>
|
||||
<p class="text-sm text-text-muted">{_t.translate?.config?.save_job_first || 'Save the job first'}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -251,7 +255,7 @@
|
||||
/>
|
||||
{:else}
|
||||
<div class="bg-surface-muted border border-border rounded-lg p-8 text-center">
|
||||
<p class="text-sm text-text-muted">{$t.translate?.config?.save_job_first || 'Save the job first'}</p>
|
||||
<p class="text-sm text-text-muted">{_t.translate?.config?.save_job_first || 'Save the job first'}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user