diff --git a/src/arts/langs/README.md b/src/arts/langs/README.md index 001a2f7d7..f11a5310b 100644 --- a/src/arts/langs/README.md +++ b/src/arts/langs/README.md @@ -1,4 +1,4 @@ -# lang +# langs Type-safe i18n library for SvelteKit. **Zero external dependencies.** Reactive translation resolution with fallback chain, pluralization via @@ -6,7 +6,7 @@ translation resolution with fallback chain, pluralization via ## Handoff 2026-05-12 -`Lang` no es `locale` y no debe decidir formatos. En la arquitectura a cerrar, +`Langs` no es `locale` y no debe decidir formatos. En la arquitectura a cerrar, `ActiveLangs` consume la preferencia de idioma (`prefs.language`) que le pasa la composicion (`ActiveApp` o `ActiveUix` standalone). El locale de formato vive en `prefs.locale` y pertenece a `Format`. @@ -16,7 +16,7 @@ en `prefs.locale` y pertenece a `Format`. ## Architecture ``` -lang/ +langs/ ├── index.ts Barrel exports ├── types.ts LangBase, SupportedLocale, LangRecord, LangNode, ... ├── engine-langs.ts Pure factory: createEngineLangs() @@ -172,15 +172,15 @@ export type TranslationSchema = typeof translations; import { createEngineLangs } from '$langs'; import { translations } from './translations'; -const lang = createEngineLangs(translations, 'es'); +const langs = createEngineLangs(translations, 'es'); // `t()` and `ts()` require an explicit locale on the pure engine. -lang.t('common.ok', undefined, 'es'); // 'Aceptar' -lang.t('greet', { name: 'Ana' }, 'en'); // 'Hello, Ana' -lang.t('messages.unread', { count: 5 }, 'en'); // '5 unread messages' +langs.t('common.ok', undefined, 'es'); // 'Aceptar' +langs.t('greet', { name: 'Ana' }, 'en'); // 'Hello, Ana' +langs.t('messages.unread', { count: 5 }, 'en'); // '5 unread messages' -lang.ts({ es: 'Hola', en: 'Hello' }, 'en'); // 'Hello' -lang.ts('#?common.ok', 'es'); // 'Aceptar' +langs.ts({ es: 'Hola', en: 'Hello' }, 'en'); // 'Hello' +langs.ts('#?common.ok', 'es'); // 'Aceptar' ``` ### `ts()` and the `LangString` type @@ -195,19 +195,19 @@ type LangString = string | LangRecord | LangRef; This is the idiomatic way to accept "maybe translatable" text through component boundaries: a prop typed `message: LangString` accepts all three -shapes transparently, and components call `lang.ts(message)` to render. +shapes transparently, and components call `langs.ts(message)` to render. ```ts // 1) Plain string — passthrough, no lookup -lang.ts('Static text', 'en'); // 'Static text' +langs.ts('Static text', 'en'); // 'Static text' // 2) LangRecord — inline per-locale values -lang.ts({ es: 'Guardar', en: 'Save', ar: 'حفظ' }, 'en'); // 'Save' -lang.ts({ es: 'Solo español' }, 'en'); // 'Solo español' (single-locale fallback) +langs.ts({ es: 'Guardar', en: 'Save', ar: 'حفظ' }, 'en'); // 'Save' +langs.ts({ es: 'Solo español' }, 'en'); // 'Solo español' (single-locale fallback) // 3) LangRef — alias to a schema key (optional literal fallback) -lang.ts('#?common.ok', 'es'); // resolves to 'Aceptar' -lang.ts('#?missing.key|Default', 'es'); // fallback literal wins +langs.ts('#?common.ok', 'es'); // resolves to 'Aceptar' +langs.ts('#?missing.key|Default', 'es'); // fallback literal wins ``` Practical pattern — a generic button component that accepts a translatable label: @@ -220,10 +220,10 @@ Practical pattern — a generic button component that accepts a translatable lab import type { ActiveLangs } from '$langs'; let { label, onclick }: { label: LangString; onclick: () => void } = $props(); - const lang = getContext('lang'); + const langs = getContext('langs'); - + ``` Callers pass whichever form fits: @@ -251,12 +251,12 @@ if (isLangString(value)) { /* value is one of the three */ } import { createActiveLangs } from '$langs/active-langs.svelte'; import { translations } from './translations'; - const lang = createActiveLangs(translations, 'es'); + const langs = createActiveLangs(translations, 'es'); -

{lang.t('common.ok')}

- - +

{langs.t('common.ok')}

+ + ``` `createActiveLangs` holds `locale` in `$state`. When you call @@ -267,18 +267,18 @@ Signatures on the reactive wrapper make `locale` **optional** — it defaults to the active reactive locale: ```ts -lang.t('common.ok'); // uses active locale -lang.t('common.ok', undefined, 'en'); // explicit locale override +langs.t('common.ok'); // uses active locale +langs.t('common.ok', undefined, 'en'); // explicit locale override ``` ### Fallback chain ```ts // Resolution order: currentLocale → en → es (default) -const lang = createActiveLangs(translations, 'es', ['en']); +const langs = createActiveLangs(translations, 'es', ['en']); -lang.setLocale('fr'); -lang.t('common.ok'); // fr missing → tries en → 'OK' +langs.setLocale('fr'); +langs.t('common.ok'); // fr missing -> tries en -> 'OK' ``` ### Dynamic modules — `extend()` @@ -287,19 +287,19 @@ Adds translations to the schema at runtime. Supports dotted namespaces and deep-merges existing keys: ```ts -lang.extend('shop', { +langs.extend('shop', { product: { es: 'Producto', en: 'Product' } }); -lang.t('shop.product'); // 'Producto' +langs.t('shop.product'); // 'Producto' // Dotted namespace — creates nested structure -lang.extend('app.settings', { +langs.extend('app.settings', { title: { es: 'Ajustes', en: 'Settings' } }); // Subscribe to schema changes (triggers re-render in Svelte automatically) -const unsub = lang.onSchemaChange(() => { +const unsub = langs.onSchemaChange(() => { // schema changed }); ``` @@ -319,7 +319,7 @@ const unsub = lang.onSchemaChange(() => { registering the same key by mistake. ```ts -lang.extend('common', { ok: { es: 'Sí', en: 'Yes' } }); +langs.extend('common', { ok: { es: 'Sí', en: 'Yes' } }); // DEV: [lang] extend(): leaf at "common.ok" was overwritten silently. // Last write wins — register namespaces uniquely or check for // collisions before extending. @@ -335,24 +335,24 @@ Creates a new instance with the module merged. In the reactive wrapper the child shares locale state with the parent: ```ts -const child = lang.register('admin', { +const child = langs.register('admin', { dashboard: { es: 'Panel', en: 'Dashboard' } }); child.t('admin.dashboard'); // 'Panel' child.t('common.ok'); // 'Aceptar' (inherits parent schema) -lang.setLocale('en'); +langs.setLocale('en'); child.getLocale(); // 'en' (synced) child.dispose(); // detach from parent -lang.setLocale('es'); +langs.setLocale('es'); child.getLocale(); // 'en' (stopped syncing) ``` --- -## Mono lang — single-language passthrough +## Mono langs — single-language passthrough When the app does not need translation, use `createActiveMonoLangs()` to get an `ActiveLangs`-shaped instance whose `t()` / `ts()` methods are passthrough: @@ -360,48 +360,48 @@ When the app does not need translation, use `createActiveMonoLangs()` to get an ```ts import { createActiveMonoLangs } from '$langs/mono-langs.svelte'; -const lang = createActiveMonoLangs(); +const langs = createActiveMonoLangs(); -lang.t('common.ok'); // 'common.ok' -lang.t('common.ok|Aceptar'); // 'Aceptar' -lang.t('greet|Hello {{name}}', { name: 'Ada' }); // 'Hello Ada' -lang.ts('plain text'); // 'plain text' -lang.ts({ es: 'Hola', en: 'Hello' }); // 'Hola' (first non-empty entry) +langs.t('common.ok'); // 'common.ok' +langs.t('common.ok|Aceptar'); // 'Aceptar' +langs.t('greet|Hello {{name}}', { name: 'Ada' }); // 'Hello Ada' +langs.ts('plain text'); // 'plain text' +langs.ts({ es: 'Hola', en: 'Hello' }); // 'Hola' (first non-empty entry) ``` The locale is held as `$state` so consumers that subscribe via -`onLocaleChange` still react to `setLocale`. In `ActiveApp`, the lang service +`onLocaleChange` still react to `setLocale`. In `ActiveApp`, the langs service is normally synchronized from `prefs.language`; `Format` uses `prefs.locale` instead. -In DEV the mono lang warns once per unresolved path via the supplied logger +In DEV the mono langs adapter warns once per unresolved path via the supplied logger under category `lang.mono` — strong signal that the caller forgot to -configure a real lang or to add `|fallback` text. Calls that include +configure real translations or to add `|fallback` text. Calls that include `|fallback` are silent. ```ts -const lang = createActiveMonoLangs({ logger: app.Logger }); -lang.t('users.profile.name'); // warns once -lang.t('users.profile.name'); // dedup, no second warn -lang.t('users.profile.email'); // different path → warns once +const langs = createActiveMonoLangs({ logger: app.logger }); +langs.t('users.profile.name'); // warns once +langs.t('users.profile.name'); // dedup, no second warn +langs.t('users.profile.email'); // different path -> warns once ``` -Mono lang is consumed automatically by `createActiveApp({ /* no lang */ })` +`createActiveMonoLangs()` is used automatically by `createActiveApp({ /* no langs */ })` so monolingual apps never have to wire it themselves. -### Mono lang type contract +### Mono langs type contract `createActiveMonoLangs()` is public, but intentionally **type-loose**: there is no schema, so it cannot provide typed path autocomplete or static missing-key checks. `createActiveApp()` casts the mono implementation to -`ActiveLangs` when the app is created without `lang`, only to keep the root +`ActiveLangs` when the app is created without `langs`, only to keep the root surface uniform (`App.langs.t(...)` always exists). Treat that cast as a null-object convenience, not as a type guarantee: - Use `createActiveApp({ langs: { schema, defaultLocale } })` when translation key correctness matters. -- Use mono-lang for monolingual apps, prototypes or apps that deliberately want +- Use mono langs for monolingual apps, prototypes or apps that deliberately want literal/fallback strings. - Add `|fallback` text for user-facing strings so missing i18n configuration does not paint internal paths in the UI. diff --git a/src/arts/langs/active-langs.svelte.ts b/src/arts/langs/active-langs.svelte.ts index 88a6cb1db..ca0164d9c 100644 --- a/src/arts/langs/active-langs.svelte.ts +++ b/src/arts/langs/active-langs.svelte.ts @@ -6,7 +6,7 @@ import type { Logger } from '$libs/logger'; export type { LangNode, LangParams, LangString, SupportedLocale, ActiveLangs }; /** - * Reactive wrapper around the pure lang engine for Svelte 5. + * Reactive wrapper around the pure langs engine for Svelte 5. * * The engine is stateless regarding locale — this wrapper owns the * `_locale` `$state` and injects it into every `t()`/`ts()` call. diff --git a/src/arts/langs/index.ts b/src/arts/langs/index.ts index 7273971f6..b59ea9776 100644 --- a/src/arts/langs/index.ts +++ b/src/arts/langs/index.ts @@ -1,4 +1,4 @@ -// Public surface of the lang artifact. The pure contracts (types, +// Public surface of the langs artifact. The pure contracts (types, // constants, helpers, plural rules, JSON serialization, diagnostics // builders, error catalog) live in `$libs/langs`. This barrel mirrors // them for back-compat — existing `import … from '$langs'` imports diff --git a/src/arts/langs/mono-langs.svelte.ts b/src/arts/langs/mono-langs.svelte.ts index 351a4e042..a005626ab 100644 --- a/src/arts/langs/mono-langs.svelte.ts +++ b/src/arts/langs/mono-langs.svelte.ts @@ -21,7 +21,7 @@ import type { Logger } from '$libs/logger'; export { LANG_MONO_LANG_CATEGORY } from '$libs/langs'; /** - * Default initial locale for `createActiveMonoLangs()`. The mono lang does not + * Default initial locale for `createActiveMonoLangs()`. The mono langs adapter does not * translate, so the value is mostly cosmetic — it determines what * `getLocale()` returns until `setLocale()` is called and what consumers * see when they subscribe to `onLocaleChange`. @@ -31,9 +31,9 @@ export const DEFAULT_MONO_LOCALE: SupportedLocale = 'en'; /** * Single-language `ActiveLangs` for monolingual apps. * - * Used by `createActiveApp()` when the caller does not configure `lang`. Also + * Used by `createActiveApp()` when the caller does not configure `langs`. Also * usable standalone for apps that have no i18n needs at all but still want a - * uniform `lang.t(...)` / `lang.ts(...)` call site. + * uniform `langs.t(...)` / `langs.ts(...)` call site. * * Behavior: * @@ -46,9 +46,9 @@ export const DEFAULT_MONO_LOCALE: SupportedLocale = 'en'; * The locale is held as `$state` so subscribers still see updates from * `setLocale`. * - * In DEV the mono lang warns (deduplicated by path, once per path) when + * In DEV the mono langs adapter warns (deduplicated by path, once per path) when * `t()` / `ts()` returns a path-shaped value verbatim — strong signal that - * the caller forgot to configure `lang` or to add `|fallback` text. Warnings + * the caller forgot to configure `langs` or to add `|fallback` text. Warnings * flow through the supplied logger under category `lang.mono`. Calls that * include `|fallback` are silent because the caller intentionally provided * fallback text. diff --git a/src/libs/langs/errors.ts b/src/libs/langs/errors.ts index 4e87df41f..f0b954ff2 100644 --- a/src/libs/langs/errors.ts +++ b/src/libs/langs/errors.ts @@ -43,7 +43,7 @@ export function isLangCircularReferenceError( // ── Legacy diagnostic / log message strings ──────────────────────────── // // These are not thrown errors — they are diagnostic/log messages built -// at the call site by the lang resolver. Kept here next to the typed +// at the call site by the langs resolver. Kept here next to the typed // errors so the module's user-facing strings live together. export const LANG_ERRORS = { @@ -84,5 +84,5 @@ export const LANG_ERRORS = { `${LANG_ERRORS.KEY_NOT_FOUND(path)}. Using fallback "${fallback}".`, MONO_PATH_RETURNED: (path: string, kind: 't' | 'ts'): string => - `Mono langs: ${kind}("${path}") returned the path verbatim. Configure a real lang schema or use the |fallback suffix to provide literal text.` + `Mono langs: ${kind}("${path}") returned the path verbatim. Configure real translations or use the |fallback suffix to provide literal text.` } as const; diff --git a/src/libs/langs/index.ts b/src/libs/langs/index.ts index e3dc81eb0..4059262fb 100644 --- a/src/libs/langs/index.ts +++ b/src/libs/langs/index.ts @@ -1,5 +1,5 @@ /** - * `libs/langs` — pure contracts and helpers for the lang artifact. + * `libs/langs` — pure contracts and helpers for the langs artifact. * * Mirrors the `libs/bus` / `libs/logger` / `libs/dom` pattern: this module * exports interfaces, generic constants, error catalogs, pure helpers, @@ -8,7 +8,7 @@ * `arts/langs/engine-langs.ts` and the active wrappers in * `arts/langs/active-langs.svelte.ts` / `arts/langs/mono-langs.svelte.ts`. * - * Modules that need types or pure helpers from lang import from + * Modules that need types or pure helpers from langs import from * `$libs/langs`. Only the composition root (`aapp`) and tests touch * `$langs` to reach the engine factories. */