Surfaces `App.prefs` and routes lang/format/frontend through the prefs engine when it's declared in the schema. Apps that don't opt into prefs keep the existing lang-driven locale chain untouched. - `defineActivePrefs(options)` — service factory with `initMode: 'immediate'` (consumer factories ask for `prefs` synchronously at construction time; deferring would make the dependency graph order-sensitive). No core dependencies — the engine is pure data. - `defineActiveLang` — when `prefs` is in the schema, subscribes to `prefs.language` and drives `lang.setLocale()` for both the initial value and changes; the subscription is detached on `dispose()`. - `defineActiveFormat` — locale-source resolution is now `options.localeSource` → `prefs.locale` → `lang.locale` → Format default. `prefs.locale` (regional formatting) wins over `prefs.language` (i18n) when both are present. - `defineActiveFrontend` — same precedence chain but uses `prefs.language` (NOT `prefs.locale`) because Frontend's `dir = auto` follows the writing system, which is a property of the language. - `$prefs` alias added to `svelte.config.js`. Sium needs no factory change — it consumes `lang` for translation strings, so the prefs-driven language flows through transitively. Tests: 1 service-factory integration + 4 consumer-wiring integration scenarios covering lang.t() flip, format.getLocale flip, frontend.getLocale flip, and the prefs-less fallback. Full suite: 1674 / 1674 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>master
parent
620a471850
commit
1f8015f2fe
@ -0,0 +1,42 @@
|
||||
import { createActivePrefs } from '$prefs';
|
||||
import type { ActivePrefs, EnginePrefsOptions } from '$prefs';
|
||||
import type { AppServiceFactory } from '../services.ts';
|
||||
|
||||
/**
|
||||
* Options for `defineActivePrefs`. A thin alias of `EnginePrefsOptions`
|
||||
* — exported here so callers can `import { type DefineActivePrefsOptions }
|
||||
* from '$active-app'` without reaching into `$prefs`.
|
||||
*/
|
||||
export type DefineActivePrefsOptions = EnginePrefsOptions;
|
||||
|
||||
/**
|
||||
* `defineActivePrefs(options)` produces a service factory for the
|
||||
* `prefs` slot. Adds the runtime to the App schema as
|
||||
* `App.prefs: ActivePrefs`, where downstream service factories
|
||||
* (`defineActiveLang`, `defineActiveFormat`, `defineActiveFrontend`)
|
||||
* read capability sources via the helpers in `$prefs/sources`.
|
||||
*
|
||||
* No core dependencies: the engine is pure data — capabilities,
|
||||
* environment and intent are passed in by the caller. Environment
|
||||
* detection lives in `$prefs/adapters/*` and is invoked by the
|
||||
* application's bootstrap code, not by this factory.
|
||||
*
|
||||
* `initMode: 'immediate'` because consumer factories ask for `prefs`
|
||||
* via `serviceDependencies` at construction time; deferring the build
|
||||
* would make the dependency graph order-sensitive.
|
||||
*/
|
||||
export function defineActivePrefs(
|
||||
options: DefineActivePrefsOptions
|
||||
): AppServiceFactory<'prefs', readonly [], readonly [], ActivePrefs> {
|
||||
return {
|
||||
name: 'prefs',
|
||||
coreDependencies: [],
|
||||
initMode: 'immediate',
|
||||
create(): ActivePrefs {
|
||||
return createActivePrefs(options);
|
||||
},
|
||||
dispose(instance) {
|
||||
instance.dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Verifies the cross-cutting wiring done by the consumer factories
|
||||
* (`defineActiveLang`, `defineActiveFormat`, `defineActiveFrontend`):
|
||||
* when `prefs` is declared in the schema, those consumers must source
|
||||
* their locale/language from the prefs engine instead of from each
|
||||
* other or from their own defaults.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { PrefsCapabilities } from '$libs/prefs';
|
||||
import { createSvelteEngineBus } from '$bus';
|
||||
import { createEngineLogger } from '$logger/engine-logger';
|
||||
import { createEngineOrca } from '$orca';
|
||||
import { createActiveTimers } from '$timer/active-timers.svelte';
|
||||
import { buildServiceBuilders } from '../service-builder.ts';
|
||||
import {
|
||||
defineActiveDom,
|
||||
defineActiveFormat,
|
||||
defineActiveFrontend,
|
||||
defineActiveLang,
|
||||
defineActivePrefs
|
||||
} from '../service-factories/index.ts';
|
||||
import type { CoreServices } from '../services.ts';
|
||||
|
||||
function buildCore(): CoreServices {
|
||||
const logger = createEngineLogger({});
|
||||
const timers = createActiveTimers({ logger });
|
||||
const bus = createSvelteEngineBus({ logger, clock: timers.clock });
|
||||
const orca = createEngineOrca({ bus, timers, logger });
|
||||
return { logger, bus, timers, orca };
|
||||
}
|
||||
|
||||
const CAPS: PrefsCapabilities = {
|
||||
languages: ['es-ES', 'en-US'],
|
||||
locales: ['es-ES', 'en-US'],
|
||||
currencies: ['EUR', 'USD'],
|
||||
unitSystems: ['metric', 'imperial'],
|
||||
themes: ['light', 'dark', 'system'],
|
||||
densities: ['compact', 'comfortable', 'spacious'],
|
||||
motions: ['allow', 'reduce', 'system'],
|
||||
defaults: {
|
||||
language: 'es-ES',
|
||||
locale: 'es-ES',
|
||||
currency: 'EUR',
|
||||
timezone: 'Europe/Madrid',
|
||||
unitSystem: 'metric',
|
||||
theme: 'light',
|
||||
density: 'comfortable',
|
||||
motion: 'allow',
|
||||
direction: 'ltr'
|
||||
}
|
||||
};
|
||||
|
||||
const LANG_SCHEMA = {
|
||||
hello: { 'es-ES': 'Hola', 'en-US': 'Hello' }
|
||||
};
|
||||
|
||||
describe('prefs → consumer wiring', () => {
|
||||
it('lang.setLocale fires when prefs.language changes', () => {
|
||||
const core = buildCore();
|
||||
const builders = buildServiceBuilders(
|
||||
{
|
||||
prefs: defineActivePrefs({ capabilities: CAPS }),
|
||||
lang: defineActiveLang({ schema: LANG_SCHEMA, defaultLocale: 'es-ES' })
|
||||
},
|
||||
core
|
||||
);
|
||||
const { prefs, lang } = builders.proxies as {
|
||||
prefs: { setIntent: (k: 'language', v: string) => unknown };
|
||||
lang: { getLocale: () => string; t: (k: 'hello') => string };
|
||||
};
|
||||
|
||||
// Initial language flows from prefs (defaults.language = 'es-ES').
|
||||
expect(lang.getLocale()).toBe('es-ES');
|
||||
expect(lang.t('hello')).toBe('Hola');
|
||||
|
||||
// Switching prefs.language triggers lang.setLocale via the
|
||||
// factory's subscription.
|
||||
prefs.setIntent('language', 'en-US');
|
||||
expect(lang.getLocale()).toBe('en-US');
|
||||
expect(lang.t('hello')).toBe('Hello');
|
||||
|
||||
builders.disposeAll();
|
||||
});
|
||||
|
||||
it('format follows prefs.locale instead of lang when both are declared', () => {
|
||||
const core = buildCore();
|
||||
const builders = buildServiceBuilders(
|
||||
{
|
||||
prefs: defineActivePrefs({ capabilities: CAPS }),
|
||||
lang: defineActiveLang({ schema: LANG_SCHEMA, defaultLocale: 'es-ES' }),
|
||||
format: defineActiveFormat()
|
||||
},
|
||||
core
|
||||
);
|
||||
const { prefs, format } = builders.proxies as {
|
||||
prefs: { setIntent: (k: 'locale', v: string) => unknown };
|
||||
format: { getLocale: () => string };
|
||||
};
|
||||
|
||||
expect(format.getLocale()).toBe('es-ES');
|
||||
|
||||
prefs.setIntent('locale', 'en-US');
|
||||
expect(format.getLocale()).toBe('en-US');
|
||||
|
||||
builders.disposeAll();
|
||||
});
|
||||
|
||||
it('frontend follows prefs.language for direction derivation', () => {
|
||||
const core = buildCore();
|
||||
const builders = buildServiceBuilders(
|
||||
{
|
||||
prefs: defineActivePrefs({ capabilities: CAPS }),
|
||||
dom: defineActiveDom(),
|
||||
lang: defineActiveLang({ schema: LANG_SCHEMA, defaultLocale: 'es-ES' }),
|
||||
frontend: defineActiveFrontend({ applyDom: false })
|
||||
},
|
||||
core
|
||||
);
|
||||
const { prefs, frontend } = builders.proxies as {
|
||||
prefs: { setIntent: (k: 'language', v: string) => unknown };
|
||||
frontend: { getLocale: () => string };
|
||||
};
|
||||
|
||||
expect(frontend.getLocale()).toBe('es-ES');
|
||||
|
||||
prefs.setIntent('language', 'en-US');
|
||||
expect(frontend.getLocale()).toBe('en-US');
|
||||
|
||||
builders.disposeAll();
|
||||
});
|
||||
|
||||
it('format / frontend retain the lang fallback when prefs is not declared', () => {
|
||||
const core = buildCore();
|
||||
const builders = buildServiceBuilders(
|
||||
{
|
||||
lang: defineActiveLang({ schema: LANG_SCHEMA, defaultLocale: 'en-US' }),
|
||||
format: defineActiveFormat()
|
||||
},
|
||||
core
|
||||
);
|
||||
const { format } = builders.proxies as {
|
||||
format: { getLocale: () => string };
|
||||
};
|
||||
// No prefs in schema → format falls back to lang's locale.
|
||||
expect(format.getLocale()).toBe('en-US');
|
||||
builders.disposeAll();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in new issue