import { type InferLocaleFromConfig } from "@/vi18n-lib/config"; import { bundleToUncompiledLocaleRecord, loadFluentBundle, } from "@/vi18n-lib/setup"; import { localeConfig } from "./config"; import type { Result } from "@/utils/types"; import { useLocalStorage } from "@vueuse/core"; import { computed, type DeepReadonly } from "vue"; import { type } from "arktype"; import enUsFtlSrc from "@/assets/locale/en_US.ftl"; import vpVlFtlSrc from "@/assets/locale/vp_VL.ftl"; import wpVlFtlSrc from "@/assets/locale/wp_VL.ftl"; import type { FluentBundle } from "@fluent/bundle"; import { compileLocale } from "@/vi18n-lib/compile"; export const LOCALE_IDS = ["en-US", "vp-VL", "wp-VL"] as const; export type LocaleId = typeof LocaleId.infer; export const LocaleId = type.enumerated(...LOCALE_IDS); export const DEFAULT_LOCALE_ID = "en-US" satisfies LocaleId; // users could manually edit localStorage to make this value anything, so we need to validate it const localStorageLocaleId = useLocalStorage( "localeId", DEFAULT_LOCALE_ID, ); export const localeId = computed({ get: (): LocaleId => { const localeIdRes = LocaleId(localStorageLocaleId.value); if (localeIdRes instanceof type.errors) { // if invalid LocaleId, reset to default localStorageLocaleId.value = DEFAULT_LOCALE_ID; return DEFAULT_LOCALE_ID; } // else return user's selection const localeId = localeIdRes; return localeId; }, // custom setter to ensure it is only set to a valid LocaleId by our code // (since the localStorage ref is typed as `unknown`, it can be set to any value) set: (id: LocaleId) => { localStorageLocaleId.value = id; }, }); export interface Locale extends InferLocaleFromConfig {} async function loadLocale( localeId: LocaleId, localeFtlSrc: string, ): Promise> { const bundleRes = await loadFluentBundle(localeId, localeFtlSrc); if (bundleRes.type === "err") { return bundleRes; } const bundle = bundleRes.ok; return { type: "ok", ok: bundle }; } interface SetupLocaleFallback { bundle: FluentBundle; locale: Locale; } function setupLocale( localeId: LocaleId, localeBundle: FluentBundle, fallback: SetupLocaleFallback | undefined, ): Result { const fallbackBundle = fallback?.bundle; const fallbackLocale = fallback?.locale; const maybeFallbackedBundle = (() => { if (fallbackBundle === undefined) { return localeBundle; } const localeMessageIds = new Set(localeBundle._messages.keys()); const fallbackMessageIds = new Set(fallbackBundle._messages.keys()); const missingMessageIds = fallbackMessageIds.difference(localeMessageIds); for (const id of missingMessageIds) { const fallbackMessage = fallbackBundle._messages.get(id); if (fallbackMessage) { localeBundle._messages.set(id, fallbackMessage); } } return localeBundle; })(); const uncompiledLocaleRecordRes = bundleToUncompiledLocaleRecord( maybeFallbackedBundle, ); if (uncompiledLocaleRecordRes.type === "err") { return uncompiledLocaleRecordRes; } const uncompiledLocaleRecord = uncompiledLocaleRecordRes.ok; const localeRes = compileLocale({ config: localeConfig, bundle: maybeFallbackedBundle, uncompiled: uncompiledLocaleRecord, fallback: fallbackLocale, messageIdChain: [], }); const compilationErrorCount = localeRes.errors.length; if (compilationErrorCount === 0) { console.log(`[vi18n] Set up locale \`${localeId}\` with no errors!`); } else { console.error( `[vi18n] Set up locale \`${localeId}\` with ${compilationErrorCount} following errors:`, ); console.error(localeRes.errors); } const locale = localeRes.locale; return { type: "ok", ok: locale }; } function unwrap(result: Result): T { switch (result.type) { case "ok": { return result.ok; } case "err": { throw new Error(String(result.err)); } } } function deepReadonly(value: T): DeepReadonly { // SAFETY: we're just making an immutable view to the type, this isn't dangerous return value as DeepReadonly; } const DEFAULT_LOCALE_BUNDLE = unwrap(await loadLocale("en-US", enUsFtlSrc)); const DEFAULT_LOCALE = unwrap( setupLocale(DEFAULT_LOCALE_ID, DEFAULT_LOCALE_BUNDLE, undefined), ); const doItAllForLocale = async ( localeId: LocaleId, localeFtlSrc: string, ): Promise> => deepReadonly( unwrap( setupLocale( localeId, unwrap(await loadLocale(localeId, localeFtlSrc)), { bundle: DEFAULT_LOCALE_BUNDLE, locale: DEFAULT_LOCALE }, ), ), ); const [vpVl, wpVl] = await Promise.all([ doItAllForLocale("vp-VL", vpVlFtlSrc), doItAllForLocale("wp-VL", wpVlFtlSrc), ]); const localeIdToLocale = { "en-US": deepReadonly(DEFAULT_LOCALE), "vp-VL": vpVl, "wp-VL": wpVl, } as const satisfies Record>; export interface UseLocaleOptions { locale?: LocaleId; } export const useLocale = (opt: UseLocaleOptions = {}) => computed>(() => { const localLocaleId = opt.locale ?? localeId.value; return localeIdToLocale[localLocaleId]; });