You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/web/routes/active/docs/lang/+page.svelte

645 lines
26 KiB

<script lang="ts">
import CodeBlock from '../../_components/CodeBlock.svelte';
import Callout from '../../_components/Callout.svelte';
import AiAgentsBox from '../../_components/AiAgentsBox.svelte';
import ModuleHeader from '../../_components/ModuleHeader.svelte';
import PageNav from '../../_components/PageNav.svelte';
const schemaExample = `import { p, type LangNode } from '$langs';
export const schema = {
common: {
ok: { en: 'OK', es: 'Aceptar' },
cancel: { en: 'Cancel', es: 'Cancelar' }
},
cart: {
// Plurals must use p(); raw plural objects are not LangRecord leaves.
items: p({
en: { one: '1 item', other: '{{count}} items' },
es: { one: '1 artículo', other: '{{count}} artículos' }
})
}
} satisfies LangNode;`;
const usage = `import { App } from '$lib/app';
App.langs.t('common.ok'); // 'Aceptar' (when locale = 'es')
App.langs.t('cart.items', { count: 3 }); // '3 artículos'
App.prefs.language.set('en'); // core prefs drives lang
App.langs.t('common.ok'); // 'OK'`;
const mentalModel = `// EngineLangs: pure resolver. Locale is passed explicitly.
const Engine = createEngineLangs(schema, 'es', ['en']);
Engine.t('common.ok', undefined, 'es-MX');
// ActiveLangs: Svelte wrapper. Locale is owned as $state.
const Lang = createActiveLangs(schema, 'es', ['en']);
Lang.t('common.ok'); // uses current locale
Lang.setLocale('en-GB'); // re-renders consumers that read t(), ts() or getLocale()
// App.langs: service declared through $active-app/services.
App.prefs.language.set('ar');
App.langs.t('home.title');`;
const fallback = `createActiveApp({
prefs: { capabilities, environment },
services: {
langs: defineActiveLangs({
schema,
defaultLocale: 'es',
fallbackChain: ['en'] // missing keys in 'es' fall back to 'en'
})
}
});`;
const appInjection = `// src/lib/i18n/schema.ts
import { p, type LangNode } from '$langs';
export const schema = {
common: {
ok: { es: 'Aceptar', en: 'OK' },
cancel: { es: 'Cancelar', en: 'Cancel' }
},
user: {
greeting: (params: { name: string }) => ({
es: 'Hola {{name}}',
en: 'Hello {{name}}'
}),
notifications: p({
es: { one: '{{count}} aviso', other: '{{count}} avisos' },
en: { one: '{{count}} notification', other: '{{count}} notifications' }
})
}
} satisfies LangNode;
export type AppLangSchema = typeof schema;
// src/lib/app.ts
import { createActiveApp } from '$active-app';
import { defineActiveLangs } from '$active-app/services';
import { schema } from '$lib/i18n/schema';
export const App = createActiveApp({
prefs: {
capabilities,
environment,
intent: { language: 'es' }
},
services: {
langs: defineActiveLangs({ schema, defaultLocale: 'es', fallbackChain: ['en'] })
}
});`;
const directCreation = `// Non-Svelte / server-safe pure engine.
import { createEngineLangs } from '$langs';
const Engine = createEngineLangs(schema, 'es', ['en']);
Engine.t('common.ok', undefined, 'es-MX');
// Svelte 5 active wrapper. Import from the .svelte.ts file, not from $langs.
import { createActiveLangs } from '$langs/active-langs.svelte';
const Lang = createActiveLangs(schema, 'es', ['en']);
Lang.t('common.ok');
Lang.setLocale('en');`;
const refs = `// LangRef syntax inside a translation: '#?other.key|literal fallback'
const schema = {
common: {
terms: { en: 'terms of use', es: 'condiciones de uso' },
privacy: { en: 'privacy policy', es: 'política de privacidad' }
},
legal: {
termsLabel: '#?common.terms|terms',
privacyLabel: '#?common.privacy|privacy',
missingLabel: '#?common.cookies|cookies'
}
} satisfies LangNode;
App.langs.t('legal.termsLabel'); // 'condiciones de uso'
App.langs.t('legal.missingLabel'); // 'cookies'`;
const pathFallback = `// t() resolves a schema path. The "|fallback" suffix belongs to the path string.
App.langs.t('actions.save|Save'); // schema hit → translated value
App.langs.t('actions.archive|Archive'); // missing key → 'Archive'
// Fallback text is interpolated with the same params object.
App.langs.t('user.greeting|Hello {{name}}', { name: 'Ada' });
// → 'Hello Ada' when user.greeting is missing`;
const tsRefs = `// ts() resolves a LangString directly.
App.langs.ts('Plain literal'); // 'Plain literal'
App.langs.ts({ es: 'Guardar', en: 'Save' }); // 'Guardar' when locale = es
// LangRef form: prefix "#?", then the schema path, then optional "|fallback".
App.langs.ts('#?actions.save|Save'); // resolves actions.save
App.langs.ts('#?actions.archive|Archive'); // missing ref → 'Archive'
// The real prefix is "#?", not "?#".
parseLangRef('#?terra.calendar.month|month');
// → { path: 'terra.calendar.month', fallback: 'month' }`;
const resolutionFlow = `App.langs.t('legal.termsLabel|Terms', { count: 2 });
// 1. parsePathFallback('legal.termsLabel|Terms')
// cleanPath = 'legal.termsLabel'
// pathFallback = 'Terms'
//
// 2. resolvePath(schema, 'legal.termsLabel')
// If missing: return interpolated pathFallback, or 'legal.termsLabel'.
//
// 3. If the value is a LangRef ('#?common.terms|terms'):
// parseLangRef() and resolve that target path recursively.
// Circular refs are capped by LANG_MAX_RESOLVE_DEPTH.
//
// 4. If the value is a function:
// call it with params and expect a LangRecord.
//
// 5. If the value is a LangRecord:
// resolve locale through exact -> base -> sibling -> fallbackChain -> defaultLocale.
//
// 6. Interpolate {{params}} in the final string.`;
const featureModule = `// src/routes/checkout/checkout.lang.ts
import { p, type LangNode } from '$langs';
export const checkoutLang = {
title: { es: 'Pago', en: 'Checkout' },
submit: { es: 'Pagar', en: 'Pay' },
items: p({
es: { one: '{{count}} producto', other: '{{count}} productos' },
en: { one: '{{count}} item', other: '{{count}} items' }
})
} satisfies LangNode;
export type CheckoutLangSchema = typeof checkoutLang;`;
const extendExample = `import { checkoutLang } from './checkout.lang';
// extend() mutates App.langs at runtime.
// Namespace "checkout" becomes "checkout.title", "checkout.submit", ...
App.langs.extend('checkout', checkoutLang);
App.langs.t('checkout.title'); // runtime OK through the string overload
// Existing consumers re-render because ActiveLangs tracks schema version.
const detach = App.langs.onSchemaChange(() => {
console.log('translations changed');
});`;
const registerExample = `import { checkoutLang } from './checkout.lang';
// register() returns a new typed child instance.
const CheckoutLang = App.langs.register('checkout', checkoutLang);
CheckoutLang.t('checkout.title'); // typed path on the child
CheckoutLang.t('checkout.items', { count: 3 });
// The child follows the parent locale until disposed.
App.prefs.language.set('en');
CheckoutLang.t('checkout.title'); // 'Checkout'
CheckoutLang.dispose();`;
const collisionExample = `App.langs.extend('common', {
ok: { es: 'Sí', en: 'Yes' }
});
// Runtime result: common.ok is replaced.
// DEV diagnostic: lang.extend_leaf_overwrite for "common.ok".
App.langs.extend('common.ok.extra', {
label: { es: 'Extra', en: 'Extra' }
});
// Throws: cannot extend below an existing translation leaf.`;
const lazyRoutePattern = `// +page.svelte
<script lang="ts">
import { App } from '$lib/app';
import { checkoutLang } from './checkout.lang';
// One-way lazy registration. There is no unload API.
App.langs.extend('checkout', checkoutLang);
<\/script>
<h1>{App.langs.t('checkout.title|Checkout')}</h1>`;
const typedFeaturePattern = `// Feature-local typed API.
import { checkoutLang } from './checkout.lang';
export function createCheckoutText(lang = App.langs) {
const CheckoutLang = lang.register('checkout', checkoutLang);
return {
title: () => CheckoutLang.t('checkout.title'),
submit: () => CheckoutLang.t('checkout.submit'),
items: (count: number) => CheckoutLang.t('checkout.items', { count }),
dispose: () => CheckoutLang.dispose()
};
}`;
const reactive = `<script lang="ts">
import { App } from '$lib/app';
// Reads track automatically inside templates and $derived.
<\/script>
<button onclick={() => App.prefs.language.set('en')}>EN</button>
<button onclick={() => App.prefs.language.set('es')}>ES</button>
<h1>{App.langs.t('home.title')}</h1>
<p>Locale: {App.langs.getLocale()}</p>`;
const monoLang = `// Explicit mono-lang fallback. Keys pass through verbatim.
import { createActiveMonoLangs } from '$langs/mono-langs.svelte';
const Lang = createActiveMonoLangs();
Lang.t('home.title'); // 'home.title'`;
const commonMistakes = [
{
name: 'Using ?# instead of #?',
why: 'The parser only recognizes LANG_ID_PREFIX = #?. ?# is just text and will never resolve as a reference.',
fix: 'Write #?schema.path|fallback and use parseLangRef() when documenting helpers.'
},
{
name: 'Calling t() for raw literals',
why: 't() treats the first argument as a schema path and may emit key_not_found diagnostics.',
fix: 'Use ts() for LangString values: plain strings, locale records or #? refs.'
},
{
name: 'Relying on regional sibling fallback',
why: 'A bare locale can match the first sibling in insertion order when no bare key exists; that is fragile and emits dev diagnostics when ambiguous.',
fix: 'Add a canonical bare locale entry such as es/en, then override only regional differences.'
},
{
name: 'Putting HTML in translations',
why: 'lang resolves strings; it does not sanitize HTML.',
fix: 'Keep translations as text. If HTML is unavoidable, sanitize upstream and render only trusted content.'
},
{
name: 'Using extend() as uncontrolled runtime overwrite',
why: 'Deep merges can replace leaves and change labels globally while the app is running.',
fix: 'Reserve extend() for lazy modules, namespace them clearly and watch extend_leaf_overwrite diagnostics in development.'
},
{
name: 'Expecting extend() to widen TypeScript',
why: 'extend() mutates the runtime schema but cannot change the static type of an existing variable.',
fix: 'Use register() for typed feature-local instances, or use dynamic paths with fallbacks after extend().'
},
{
name: 'Writing raw plural objects',
why: '{ en: { one, other } } is not a LangRecord because LangRecord values must be strings.',
fix: 'Use p({ en: { one, other }, es: { one, other } }) for plural leaves.'
},
{
name: 'Assuming route translations unload',
why: 'extend() has no removal API; injected namespaces remain until the Lang root is disposed.',
fix: 'Use unique stable namespaces for lazy modules, or create/dispose a register() child for feature-local text.'
},
{
name: 'Expecting mono-lang to validate keys',
why: 'Mono-lang has no schema, so it cannot type-check or warn about missing translation paths in the same way.',
fix: 'Use a real schema for applications that depend on i18n correctness.'
},
{
name: 'Expecting App.langs without declaring lang',
why: 'The current App only exposes services that are declared in the service schema.',
fix: 'Add langs: defineActiveLangs({ schema }) under services; configure core prefs with createActiveApp({ prefs }) when language intent needs app-specific capabilities.'
}
] as const;
const aiAgentRows = [
{
step: 'Public exports',
where: 'src/arts/langs/index.ts',
rule: 'Keep the barrel runes-free. Active factories live in active-langs.svelte.ts and mono-langs.svelte.ts.'
},
{
step: 'Reference syntax',
where: 'parseLangRef() and parsePathFallback()',
rule: 'The real LangRef prefix is #?. Do not document or implement ?#.'
},
{
step: 'Resolution rules',
where: 'resolveLocaleInRecord(), resolvePath(), interpolation helpers',
rule: 'Preserve BCP 47 fallback order, path fallback behavior and plural resolution.'
},
{
step: 'Diagnostics',
where: 'src/arts/langs/consts.ts and $libs/logger.Logger',
rule: 'Use the shared Logger contract and constants for diagnostics; no local logger shapes or hard-coded messages.'
},
{
step: 'Tests',
where: 'src/arts/langs/test/langs.test.ts and /test/lang',
rule: 'Cover schema lookup, locale fallbacks, plurals, LangRefs and mono-lang behavior.'
}
] as const;
</script>
<svelte:head>
<title>Lang ($langs) — Active</title>
</svelte:head>
<article class="article">
<ModuleHeader
section="I18n & Format"
title="Lang"
alias="$langs"
summary="Type-safe translations: BCP 47 resolution, plurals via CLDR rules, references between keys, and JSON round-trip for tooling."
factories={['createActiveLangs', 'createActiveMonoLangs', 'createEngineLangs']}
layer="ActiveLangs / EngineLangs"
/>
<h2>Overview</h2>
<p>
<code>lang</code> is the translation runtime. It exposes a typed <code>t()</code>
function over a tree of translations, resolves <strong>BCP 47</strong> tags through a
fallback chain, supports <strong>CLDR plural rules</strong> per locale, and lets
translation strings reference each other through a small <code>#?key|fallback</code>
syntax. Inside <code>$active-app</code>, it is an opt-in service declared with
<code>defineActiveLangs()</code>.
</p>
<h2>Mental model</h2>
<p>
<code>lang</code> has two layers. <code>EngineLangs</code> is the pure resolver: every
call receives the locale explicitly. <code>ActiveLangs</code> wraps that engine with a
Svelte <code>$state</code> locale, so reads inside templates, <code>$derived</code> and
<code>$effect</code> update when <code>setLocale()</code> changes the current locale.
<code>App.langs</code> is the application instance wired by <code>$active-app/services</code>.
The factory follows core <code>App.prefs.language.get()</code>; direct
<code>setLocale()</code> calls still work, but the next <code>App.prefs</code> change
becomes authoritative again.
</p>
<CodeBlock code={mentalModel} lang="ts" title="Engine vs Active vs App" />
<h2>Define the schema</h2>
<p>
Translations live in a literal object that the type system reads to derive the typed
<code>t()</code>:
</p>
<CodeBlock code={schemaExample} lang="ts" title="src/lib/i18n/schema.ts" />
<h2>Creation and injection</h2>
<p>
The normal application path is to declare <code>lang</code> in
<code>createActiveApp()</code> with <code>defineActiveLangs()</code>. User language intent
flows through core <code>App.prefs</code> into
<code>App.langs</code>. If you are outside App, create the pure engine with
<code>createEngineLangs()</code> or the Svelte wrapper with <code>createActiveLangs()</code>.
</p>
<CodeBlock code={appInjection} lang="ts" title="Initial schema through App" />
<CodeBlock code={directCreation} lang="ts" title="Direct creation" />
<h2>Use it</h2>
<CodeBlock code={usage} lang="ts" />
<p>
Reading <code>App.langs.t(...)</code> inside a template or <code>$derived</code> tracks
the current locale; switching locales triggers a re-render of every consumer.
</p>
<CodeBlock code={reactive} lang="svelte" />
<h2>Resolution dynamics</h2>
<p>
Most mistakes come from not knowing what <code>t()</code> actually does. It first parses
a path-level fallback, then looks up the schema path, then resolves references/functions/
records, then resolves locale, and only at the end interpolates params. Missing keys,
ambiguous locale siblings and reference fallbacks are diagnostic events in development.
</p>
<CodeBlock code={resolutionFlow} lang="ts" title="What t() does internally" />
<h2>API reference</h2>
<p>
The <code>$langs</code> barrel exports the engine and pure helpers. The active factories
live in their runes files: <code>$langs/active-langs.svelte</code> and
<code>$langs/mono-langs.svelte</code>, so the main barrel stays runes-free.
</p>
<table>
<thead>
<tr>
<th>Member</th>
<th>Layer</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr><td><code>createEngineLangs(schema, defaultLocale?, fallbackChain?)</code></td><td>engine</td><td>Creates the pure translation engine. Calls require an explicit locale.</td></tr>
<tr><td><code>createActiveLangs(schema, defaultLocale?, fallbackChain?)</code></td><td>active</td><td>Creates a reactive language root with internal locale state.</td></tr>
<tr><td><code>createActiveMonoLangs(options?)</code></td><td>active</td><td>Creates the mono-lang fallback used by App when no schema is configured.</td></tr>
<tr><td><code>t(path, params?, locale?)</code></td><td>both</td><td>Resolve a schema path. Engine requires locale; Active defaults to current locale.</td></tr>
<tr><td><code>ts(value, locale?)</code></td><td>both</td><td>Resolve a <code>LangString</code>: literal, record or <code>#?path|fallback</code> reference.</td></tr>
<tr><td><code>getLocale()</code></td><td>active</td><td>Read current locale.</td></tr>
<tr><td><code>setLocale(locale)</code></td><td>active</td><td>Change locale and notify active consumers.</td></tr>
<tr><td><code>onLocaleChange(fn)</code></td><td>active</td><td>Subscribe to locale changes.</td></tr>
<tr><td><code>extend(namespace, module)</code></td><td>both</td><td>Merge a module into the current schema namespace.</td></tr>
<tr><td><code>register(namespace, module)</code></td><td>both</td><td>Return a type-widened engine after registering a namespace.</td></tr>
<tr><td><code>onSchemaChange(fn)</code></td><td>both</td><td>Subscribe to schema extension/registration changes.</td></tr>
<tr><td><code>setLogger(logger)</code></td><td>both</td><td>Inject or replace the shared Logger contract.</td></tr>
<tr><td><code>getDefaultLocale()</code></td><td>both</td><td>Read configured default locale.</td></tr>
<tr><td><code>getFallbackChain()</code></td><td>both</td><td>Read configured fallback chain.</td></tr>
<tr><td><code>dispose()</code></td><td>active</td><td>Release active listeners.</td></tr>
</tbody>
</table>
<table>
<thead>
<tr>
<th>Helper</th>
<th>Purpose</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr><td><code>parseLangRef('#?path|fallback')</code></td><td>Parse a LangRef.</td><td>Prefix is <code>#?</code>.</td></tr>
<tr><td><code>parsePathFallback('path|fallback')</code></td><td>Parse path-level fallback for <code>t()</code>.</td><td>No <code>#?</code> prefix.</td></tr>
<tr><td><code>resolvePath(schema, path)</code></td><td>Read a raw node from a schema path.</td><td>Used internally and in tests.</td></tr>
<tr><td><code>resolveLocaleInRecord(record, locale)</code></td><td>Resolve BCP 47 fallback in one record.</td><td>Exact, base, regional sibling, fallback chain.</td></tr>
<tr><td><code>interpolateTemplate(text, params)</code></td><td>Apply template params.</td><td>Used by fallback literals too.</td></tr>
<tr><td><code>langNodeToJSON() / JSONToLangNode()</code></td><td>Translation tooling round-trip.</td><td>Useful for external translation pipelines.</td></tr>
</tbody>
</table>
<h2>Locale resolution</h2>
<p>
<code>setLocale('es-MX')</code> picks the most specific match available in the schema,
walking <code>es-MX → es → fallbackChain → defaultLocale</code>. Unknown tags resolve to
<code>defaultLocale</code> with a logger warning.
</p>
<CodeBlock code={fallback} lang="ts" />
<Callout variant="tip" title="Ambiguous siblings">
<p>
In dev, if <code>setLocale('en')</code> matches multiple sibling translations
(e.g. you accidentally have both <code>en</code> and <code>en-US</code> at the same
level), <code>lang</code> emits an <code>AMBIGUOUS_SIBLING_MATCH</code> diagnostic.
Production builds short-circuit silently to keep the path hot.
</p>
</Callout>
<h2>Plurals</h2>
<p>
Use a plural object instead of a string. <code>lang</code> selects the CLDR category for
the active locale (<code>zero</code>, <code>one</code>, <code>two</code>, <code>few</code>,
<code>many</code>, <code>other</code>):
</p>
<CodeBlock
lang="ts"
code={`schema.cart.items = {
en: { one: '1 item', other: '#? items' },
ar: {
zero: 'لا توجد عناصر',
one: 'عنصر واحد',
two: 'عنصران',
few: '#? عناصر',
many: '#? عنصراً',
other: '#? عنصر'
}
};
App.langs.t('cart.items', { count: 0 }); // 'لا توجد عناصر' (ar)`}
/>
<h2>References</h2>
<p>
The real reference prefix is <code>#?</code>. A <code>LangRef</code> looks like
<code>#?path.to.key|literal fallback</code>: first the schema path, then an optional
<code>|</code> fallback used only when the referenced key is missing.
</p>
<CodeBlock code={refs} lang="ts" />
<h2>Path fallback vs LangRef fallback</h2>
<p>
There are two related but different forms. <code>t()</code> receives a schema path, so
<code>path|fallback</code> means “try this path, otherwise render this literal”.
<code>ts()</code> receives a <code>LangString</code>, so <code>#?path|fallback</code>
means “resolve this reference, otherwise render this literal”.
</p>
<CodeBlock code={pathFallback} lang="ts" title="t(path|fallback)" />
<CodeBlock code={tsRefs} lang="ts" title="ts('#?path|fallback')" />
<h2>Translation modules</h2>
<p>
Large applications should not keep every route translation in one huge object. Create
feature-local modules that satisfy <code>LangNode</code>, then inject them into
<code>App.langs</code> with a namespace. A namespace is part of the final path:
<code>extend('checkout', checkoutLang)</code> creates <code>checkout.title</code>,
<code>checkout.submit</code> and so on.
</p>
<CodeBlock code={featureModule} lang="ts" title="Feature translation module" />
<h2>Injecting translations with extend()</h2>
<p>
<code>extend()</code> mutates the current engine schema and notifies active consumers.
Use it for lazy route/module translations when you want the existing
<code>App.langs</code> instance to start resolving new paths. Because it mutates an
existing value, TypeScript cannot widen the type of <code>App.langs</code> in place; use
path fallbacks or the dynamic string overload at call sites, or use <code>register()</code>
when you need a typed child instance.
</p>
<CodeBlock code={extendExample} lang="ts" title="extend() vs register()" />
<Callout variant="warn" title="extend() is one-way">
<p>
There is no unload/remove API today. Once a module is extended, its translations stay in
the engine until the Lang root is disposed. That is fine for route-level lazy loading in
a single-page app, but it means <code>extend()</code> should use unique namespaces and
should not be used for temporary per-component text.
</p>
</Callout>
<h2>Typed child instances with register()</h2>
<p>
<code>register()</code> returns a new Lang instance whose type includes the registered
namespace. In <code>ActiveLangs</code>, that child follows the parent locale until the child
is disposed. This is the best pattern for feature packages that want typed local paths
without mutating the static type of the global <code>App.langs</code> variable.
</p>
<CodeBlock code={registerExample} lang="ts" title="Typed feature lang" />
<CodeBlock code={typedFeaturePattern} lang="ts" title="Feature-local wrapper" />
<h2>Lazy route pattern</h2>
<p>
For a route or feature module, call <code>extend()</code> before rendering text from that
namespace. Existing template reads re-run because <code>ActiveLangs</code> increments an
internal schema version on every schema change.
</p>
<CodeBlock code={lazyRoutePattern} lang="svelte" />
<h2>Collisions and overwrite diagnostics</h2>
<p>
<code>extend()</code> deep-merges branches. Adding a new branch is silent. Replacing an
existing leaf is allowed but emits <code>lang.extend_leaf_overwrite</code> in development.
Extending below an existing leaf throws, because a translation leaf cannot also become a
namespace.
</p>
<CodeBlock code={collisionExample} lang="ts" />
<h2>Mono-lang fallback</h2>
<p>
<code>createActiveMonoLangs()</code> is still available as an explicit passthrough runtime
for apps or tests that want readable keys without a translation schema. App no longer
creates it implicitly: if <code>lang</code> is not declared in <code>services</code>,
<code>App.langs</code> is not part of the typed App surface.
</p>
<CodeBlock code={monoLang} lang="ts" />
<Callout variant="warn" title="Mono-lang is type-loose">
<p>
Do not rely on auto-completion for translation keys when you use mono-lang - there is
no schema to derive types from. For typed apps, declare <code>defineActiveLangs()</code>
with a real schema.
</p>
</Callout>
<h2>JSON round-trip</h2>
<p>
<code>$langs/json</code> exposes <code>toJson(schema)</code> and <code>fromJson(json)</code>
so you can extract / reimport translations into external tooling (Phrase, Locize, glossary
repos) without losing structure or plural objects.
</p>
<h2>Limits</h2>
<ul>
<li>No runtime parser for free-form MessageFormat 2 patterns yet. For 1.0, model text with plural objects + refs and keep any MF2 parser as a dedicated adapter above the core resolver.</li>
<li>No async loading by default — translations are part of the bundle. For lazy locales, build a custom <code>EngineLangs</code> with on-demand <code>setSchema()</code>.</li>
<li>HTML inside translations is not sanitised. Render with <code>{`{@html …}`}</code> only when you trust the source.</li>
</ul>
<h2>Common mistakes</h2>
<table>
<thead>
<tr>
<th>Mistake</th>
<th>Why it hurts</th>
<th>Correct pattern</th>
</tr>
</thead>
<tbody>
{#each commonMistakes as mistake (mistake.name)}
<tr>
<td><code>{mistake.name}</code></td>
<td>{mistake.why}</td>
<td>{mistake.fix}</td>
</tr>
{/each}
</tbody>
</table>
<h2>Testing</h2>
<p>
Suite at <code>src/arts/langs/test/langs.test.ts</code> covers schema lookup, fallback chain,
plural categories per locale, refs and mono-lang. Try the interactive page at
<a href="/test/lang">/test/lang</a> for live locale switching.
</p>
<AiAgentsBox
intro="Before editing $langs, verify the real parser and resolver helpers instead of inferring syntax from examples."
rows={aiAgentRows}
/>
<PageNav />
</article>

Powered by TurnKey Linux.