import { LANG_ID_FALLBACK_SEPARATOR, parseLangRef, type LangParams, type SupportedLocale } from '$libs/langs'; import type { EngineLangs } from '$langs'; import { SIUM_DIAGNOSTIC_EVENTS } from './consts'; import { emitSiumDiagnostic, type SiumDiagnostics } from './diagnostics'; import { resolve as resolveFallbackTemplate } from './langs'; import type { Issue } from './core'; export interface SiumResolver { readonly resolve: (message: string, params?: LangParams, locale?: SupportedLocale) => string; readonly resolveIssue: (issue: Issue, locale?: SupportedLocale) => string; readonly resolveIssues: (issues: ReadonlyArray, locale?: SupportedLocale) => string[]; } /** * Duck-typed shape: when Sium is wired to `ActiveLangs` (which exposes a * runtime `getLocale()`), the resolver follows the active locale lazily. * `EngineLangs` does not expose this getter, so consumers wired to the * pure engine still use the captured construction-time default. */ interface ActiveLocaleSource { readonly getLocale: () => SupportedLocale; } interface SiumResolverOptions { readonly langs?: EngineLangs; /** * Locale used when neither the caller passes one nor `langs.getLocale` * exists. When `langs` is wired through `ActiveLangs`, the resolver * reads `langs.getLocale()` on every call so a runtime * `Lang.setLocale(...)` takes effect immediately. */ readonly defaultLocale: SupportedLocale; readonly diagnostics: SiumDiagnostics; } function hasActiveLocaleSource( langs: EngineLangs | undefined ): langs is EngineLangs & ActiveLocaleSource { return ( langs !== undefined && typeof (langs as Partial).getLocale === 'function' ); } function composeLangPath(message: string): string | undefined { const parsed = parseLangRef(message); if (parsed === null) return undefined; if (parsed.fallback === undefined) return parsed.path; return `${parsed.path}${LANG_ID_FALLBACK_SEPARATOR}${parsed.fallback}`; } function fallbackMessage(message: string, params?: LangParams): string { const parsed = parseLangRef(message); return resolveFallbackTemplate(parsed?.fallback ?? parsed?.path ?? message, params); } export function createSiumResolver(options: SiumResolverOptions): SiumResolver { const { langs, defaultLocale, diagnostics } = options; function effectiveLocale(explicit: SupportedLocale | undefined): SupportedLocale { if (explicit !== undefined) return explicit; // Read langs' current locale lazily when available so a runtime // `setLocale()` is honored. EngineLangs has no `getLocale()`; for // that path we fall back to the construction-time default. if (hasActiveLocaleSource(langs)) return langs.getLocale(); return defaultLocale; } const resolve: SiumResolver['resolve'] = (message, params, locale) => { const langPath = composeLangPath(message); if (langs === undefined || langPath === undefined) { return fallbackMessage(message, params); } try { return langs.t(langPath, params, effectiveLocale(locale)); } catch (error) { emitSiumDiagnostic(diagnostics, SIUM_DIAGNOSTIC_EVENTS.RESOLVE_FALLBACK, { message, error }); return fallbackMessage(message, params); } }; const resolveIssue: SiumResolver['resolveIssue'] = (entry, locale) => { return resolve(entry.message, entry.params, locale); }; const resolveIssues: SiumResolver['resolveIssues'] = (issues, locale) => { return issues.map((entry) => resolveIssue(entry, locale)); }; return { resolve, resolveIssue, resolveIssues }; }