Files
ss-tools/docs/bug-reports/2026-06-02-svelte5-proxy-store-t-undefined.md
busya 37fe425c59 docs: add bug report for Svelte 5 Proxy-based i18n store $t undefined
Documents the 12-iteration binary search, root cause analysis,
fix pattern, and lessons learned for the Svelte 5 store-detection
failure on Proxy objects with subscribe getter.

Includes recommended follow-ups: ESLint rule, document the _t
pattern in semantics-svelte skill, audit other complex-template
pages for the same latent bug.
2026-06-02 20:45:06 +03:00

12 KiB
Raw Blame History

Bug Report: Svelte 5 — Proxy-based i18n store causes ReferenceError: $t is not defined

Status: Fixed Severity: Critical (entire page unrenderable) Affected version: Svelte 5.43.8+ (verified on 5.46.0) Date: 2026-06-02 Reporter: Browser validation of translate/[id] refactor Fix commit: 27435e4


Summary

After binding the translate/[id]/+page.svelte page to TranslationJobModel, the entire detail page rendered blank with Uncaught ReferenceError: $t is not defined. The same page renders fine for simpler pages (e.g. /dashboards, /translate list). Root cause: Svelte 5's static store-detection cannot recognize the t export from $lib/i18n/index.svelte.ts (a Proxy with a subscribe getter) as a store in deeply nested template expressions and in .svelte.ts model files, generating ReferenceError: $t is not defined at runtime.


Reproduction

  1. Run dev server: cd frontend && npm run dev -- --port 5173
  2. Navigate to http://localhost:5173/translate/54215412-d6ad-49cf-9704-ff8b194f74cd
  3. Observe: page renders only the layout (sidebar, navbar, footer) — <main> is empty
  4. Open DevTools console → Uncaught ReferenceError: $t is not defined

The same page works for /translate (list), /dashboards, etc. The detail page is unique in that it uses a 49-atom TranslationJobModel plus 9 translate-specific sub-components with deeply nested template conditions.


Root Cause Analysis

The i18n module's t export

frontend/src/lib/i18n/index.svelte.ts exports t as a Proxy with a subscribe getter:

export const t = new Proxy({} as any, {
  get(_target, prop) {
    if (prop === 'subscribe') {
      return (fn: (v: any) => void) => {
        fn(_tValue);
        _tSubs.add(fn);
        return () => { _tSubs.delete(fn); };
      };
    }
    if (prop === 'current') {
      return _tValue;
    }
    return (_tValue as any)[prop];
  }
});

The subscribe method is defined via a get trap on the Proxy, not as a direct own property.

Svelte 5's $store template syntax

In Svelte 5, the $t template prefix triggers store subscription. The compiler normally generates code like:

let $t;
const unsubscribe = t.subscribe((v) => ($t = v));
// ... template uses $t ...
unsubscribe();

For the compiler to detect t as a store, it must statically determine that t has a subscribe method.

Why detection fails

Three trigger conditions (verified by binary-search testing of templates):

  1. Deeply nested conditional/loop blocks ({#if}…{:else if}…{:else}… chains, nested {#each})
  2. Complex ternary expressions in attribute bindings like title={a ? b : c ? d : e}
  3. .svelte.ts model files using $derived.by() callbacks that reference $t

In all three cases, the Svelte 5 compiler fails to statically determine t.subscribe exists (Proxy trap is invisible to static analysis) and emits a direct reference to $t instead of generating the subscription code. At runtime, $t is undefined → ReferenceError.

Why other pages work

  • /dashboards and /translate list: simpler templates without the trigger conditions
  • The minimal test page with just <h1>{_t.translate?.config?.new_title}</h1> and no nested blocks: worked fine with _t = $derived(getT())

Why even a model file triggers it

TranslationJobModel.svelte.ts line 7081 has:

tabs: { id: string; label: string }[] = $derived.by(() => {
  const base = [
    { id: 'config', label: $t.translate?.config?.basic_info || 'Configuration' },
    // ...
  ];
  // ...
});

The $derived.by callback body is compiled similarly to a template. Static analysis on the t import fails → $t is emitted as a direct reference → at model instantiation, the callback runs and throws.


Symptoms (chronological)

  1. Build: npm run build — passes clean, no errors.
  2. Type check: tsc — no errors.
  3. Unit tests: npm run test — 698/698 pass (tests don't exercise the page's runtime rendering).
  4. Browser: clicking on a job card from /translate navigates to the detail URL, but the <main> content is empty.
  5. Console: Uncaught ReferenceError: $t is not defined in <unknown> in ProtectedRoute.svelte in +layout.svelte in root.svelte

The stack trace shows <unknown> because Svelte's component-name resolution fails when the component crashes before mounting.


Took ~12 iterations to isolate the root cause:

# What was changed Result
1 Navigate to /translate/[id] Blank, $t is not defined
2 Restart dev server (port conflict) Same error
3 Replace full page with minimal 34-line test Renders ("Новое задание перевода")
4 Add all imports but simple template Still renders
5 Restore full template (no sub-component usages) Blank again
6 Comment out all <Component> template usages Still blank
7 Comment out title={...long ternary...} on tab button Still blank
8 Identify TranslationRunGlobalIndicator is global → fix it Still blank
9 Fix script-block $t in ScheduleConfig, TranslationPreview Still blank
10 Apply _t = $derived(getT()) pattern to 6 sub-components Still blank
11 Find $t in TranslationJobModel.svelte.ts line 7278 🎯 Root cause
12 Replace model's $t. with getT()?. and import getT Renders all 5 tabs

Fix

Pattern

Replace all $t.xxx references in templates and .svelte.ts model files with one of:

Context Pattern
Template const _t = $derived(getT()) in script, then _t.xxx in template
Script function getT()?.xxx direct call (no _t needed because function bodies don't have reactive context)
Script $derived.by() callback getT()?.xxx direct call (Svelte can't make $t reactive inside model callbacks)

Import change

- import { t } from '$lib/i18n/index.svelte.js';
+ import { getT } from '$lib/i18n/index.svelte.js';

Template change

- <h1>{$t.translate?.config?.new_title}</h1>
+ const _t = $derived(getT());
+ <h1>{_t.translate?.config?.new_title}</h1>

Script change

- addToast($t.translate?.schedule?.saved, 'success');
+ addToast(getT()?.translate?.schedule?.saved, 'success');

Why this works

getT() is a plain function exported from the i18n module. It does NOT require store-detection — the compiler treats it as a regular function call. The $derived(getT()) wrapper in scripts gives templates reactive re-evaluation when the locale changes (because getT() reads the $state-backed _locale internally).


Files Changed

File _t added $tgetT()?. in script
src/routes/translate/[id]/+page.svelte
src/lib/models/TranslationJobModel.svelte.ts 8 refs
src/lib/components/translate/ConfigTabForm.svelte 2 refs
src/lib/components/translate/ScheduleConfig.svelte 12 refs
src/lib/components/translate/TranslationPreview.svelte 15 refs
src/lib/components/translate/TargetTabForm.svelte 2 refs
src/lib/components/translate/RunTabContent.svelte 3 refs
src/lib/components/translate/BulkReplaceModal.svelte 2 refs
src/lib/components/translate/TranslationRunProgress.svelte (template only)
src/lib/components/translate/TranslationRunResult.svelte (template only)
src/lib/components/translate/TermCorrectionPopup.svelte (template only)
src/lib/components/translate/BulkCorrectionSidebar.svelte (template only)
src/lib/components/translate/TranslationMetricsDashboard.svelte (template only)
src/lib/components/translate/CorrectionCell.svelte (template only)
src/lib/components/translate/TranslationRunGlobalIndicator.svelte 8 refs

Total: 15 files, +496 / -451 LOC.


Verification

After the fix:

Check Result
npm run build clean
npm run test -- --run 698/698 tests pass
node scripts/audit-frontend-style.mjs 0 color violations, 2 oversized (out of scope)
Browser /translate/[id] Config tab renders form, all fields, columns, LLM, languages
Browser /translate/[id] Preview tab renders sample size slider + Run button
Browser /translate/[id] Target tab renders schema/table/DB/columns/write-settings
Browser /translate/[id] Run tab renders incremental/full + run history (5 runs)
Browser /translate/[id] Schedule tab renders "Create schedule" prompt
Browser console no JS errors (only expected backend API 404/422)

Lessons Learned

1. Build/test green ≠ runtime safe

All standard verification (build, type-check, 698 unit tests) passed. The bug only manifested in browser runtime when Svelte's reactive engine actually executed the compiled template. Browser validation is non-negotiable for pages with complex templates.

2. Svelte 5 Proxy-based stores are fragile

Svelte 5's static store-detection can fail on Proxy objects even when they implement the store protocol. Prefer:

  • Class instances with subscribe as a direct method, or
  • Plain objects with subscribe as an own property, or
  • Skip the store pattern entirely — use functions like getT() + $derived for reactive reads.

3. Model files are a blind spot

.svelte.ts files using $derived.by() callbacks that reference $t will also fail. The compiler treats model callback bodies like templates for store-detection purposes. Always use getT()?.xxx in model files, not $t.

4. The error message is misleading

Uncaught ReferenceError: $t is not defined in <unknown> looks like a missing import, but the import is correct. The actual cause is the compiler failing to generate the store-subscription code. Look at the i18n module's t export mechanism, not the call sites.

5. The minimal reproduction pattern is critical

A 34-line minimal page with identical imports and one $t reference worked. Adding back the full template (no sub-components) broke it. This proved the issue was in template complexity, not the import chain.


  • Commit: 27435e4 fix(frontend): replace $t with _t pattern in translate components
  • i18n module: frontend/src/lib/i18n/index.svelte.ts (lines 188202: t Proxy export; line 209: getT() function)
  • Model file: frontend/src/lib/models/TranslationJobModel.svelte.ts (line 70: tabs: ... = $derived.by(() => { ... $t ... }))
  • Page: frontend/src/routes/translate/[id]/+page.svelte
  • Related ADR: docs/adr/ADR-0006-frontend-architecture.md (Design Token System, Screen Model tier)
  • Svelte 5 docs: Stores$store template syntax
  • Svelte 5 issue: store-detection on Proxy-based objects with get trap (upstream behavior)

  1. Update t export in i18n/index.svelte.ts to use a class instance with subscribe as a direct method, avoiding the Proxy entirely. This may restore $t template syntax reliability.

  2. Add ESLint rule to disallow $t template references — force _t = $derived(getT()) pattern.

  3. Document the pattern in semantics-svelte skill (§VII or new §X) so future agents don't repeat this debugging cycle.

  4. Audit other pages with complex templates: any page using t import + nested {#if} chains is a latent bug.