/** * Localization seam for procode-vs-template. * * The template's widgets need values that only the host app can resolve: the * signed-in user's date pattern, the reseller's currency symbol, a unit of * measure. Every one of those lives somewhere app-specific — an auth-token * claim, a cached user-defaults payload, a settings API — and the library must * not learn any of those shapes. * * So the app registers a **resolver function** and the library asks it for a * value by key. Two consequences worth having: * * - **Any key works.** A widget can ask for something this package has never * heard of, and the app can answer it, with no change here. Adding a unit of * measure or a number separator is an app-side edit only. * - **Values resolve at render, not at boot.** A symbol that arrives from an * async settings call is picked up the next time a widget asks; a fixed * object registered once would have frozen the first (possibly empty) read. * * Unregistered, every lookup returns the caller's fallback, which is how these * widgets behaved before this existed. * * @example * // custom-project / vsbackoffice, once at boot * registerLocalization((key) => { * switch (key) { * case LocalizationKey.DateFormat: return localization.getDateFormat(); * case LocalizationKey.DatePlaceholder: return localization.getDatePlaceholder(); * case LocalizationKey.CurrencySymbol: * return getSessionDataNestedObjValByKeys( * "UserDefaults", "resellerProfile", "currencysymbol", * ); * default: return undefined; // library falls back * } * }); */ /** * Keys this package asks for today. Plain strings, shared as constants only so * both sides spell them the same — a resolver may answer any key it likes, and * a widget may ask for any key. */ export const LocalizationKey = { /** Kendo date pattern, e.g. `"MM-dd-yyyy"`. */ DateFormat: "dateFormat", /** Idle placeholder for a date input, e.g. `"MM-DD-YYYY"`. */ DatePlaceholder: "datePlaceholder", /** * Per-section hints for a focused Kendo date input (`formatPlaceholder`), * e.g. `{ year: "YYYY", month: "MM", day: "DD" }`. Without it Kendo swaps the * idle placeholder for its CLDR long form the moment the box takes focus. */ DateFormatSections: "dateFormatSections", /** Symbol for money values, e.g. `"€"`. Per-org, not per-field. */ CurrencySymbol: "currencySymbol", /** Decimal places for money values. Defaults to 2. */ CurrencyDecimals: "currencyDecimals", } as const; export type LocalizationResolver = (key: string) => unknown; let resolve: LocalizationResolver | null = null; /** * Register the host's resolver. Passing `null`/`undefined` clears it, which * restores the library's own fallbacks. */ export const registerLocalization = ( resolver: LocalizationResolver | null | undefined, ): void => { resolve = resolver ?? null; }; /** * Ask the host for a value. * * A resolver that throws is treated as "no value" rather than being allowed to * take the render down with it — a formatting lookup must never be able to * blank a screen. */ export const getLocalizationValue = ( key: string, fallback?: T, ): T | undefined => { if (!resolve) return fallback; try { const value = resolve(key); return value === undefined || value === null ? fallback : (value as T); } catch { return fallback; } }; /** * A Kendo number pattern that renders a value with the org's currency symbol. * * A literal symbol rather than Kendo's `"c"` specifier: `c` resolves the symbol * from the Intl locale, which is the *browser's* idea of currency, not the * reseller's. Returns undefined when the host resolves no symbol, so the caller * falls through to the plain numeric pattern. */ export const getCurrencyFormat = (): string | undefined => { const symbol = getLocalizationValue(LocalizationKey.CurrencySymbol); if (!symbol) return undefined; const decimals = getLocalizationValue( LocalizationKey.CurrencyDecimals, 2, ) as number; const fraction = decimals > 0 ? `.${"0".repeat(decimals)}` : ""; // Quoted so a symbol that collides with a pattern character ("#", "0", ".") // is emitted literally. return `'${symbol}'#,##0${fraction}`; };