wip: fallback behavior for missing/malformed messages instead of panicking

This commit is contained in:
Benjamin Singleton 2026-03-01 16:30:11 -06:00
parent e48b6c8708
commit 44ab34133c
6 changed files with 548 additions and 405 deletions

View file

@ -0,0 +1,437 @@
import type { Result, Value } from "@/utils/types";
import {
computeAllVariants,
selectionChainToString,
type L10nRecord,
type UncompiledLocale,
} from "./setup";
import type { FluentBundle, FluentVariable } from "@fluent/bundle";
import { parseMarkdown, type Markdown } from "./markdown";
import {
configMessageSymbol,
type ConfigMessage,
type InferLocale,
type LocaleConfig,
} from "./config";
export interface CompileLocaleCtx<Config extends LocaleConfig> {
bundle: FluentBundle;
record: L10nRecord | undefined;
config: Config;
fallback?: InferLocale<Config>;
}
export interface CompileLocaleRes<Locale> {
locale: Locale;
errors: string[];
}
type GenericLocale = { [id: string]: GenericLocale | GenericMessageFn };
type GenericMessageFn =
| ((args?: Record<string, FluentVariable>) => string)
| ((args?: Record<string, FluentVariable>) => Markdown);
export function compileLocale<Config extends LocaleConfig>(
ctx: CompileLocaleCtx<Config>,
): CompileLocaleRes<InferLocale<Config>> {
const { bundle, record, config, fallback } = ctx;
const errors: string[] = [];
const recordKeys = new Set(Object.keys(record ?? {}));
const configKeys = new Set(Object.keys(config));
const excessKeys = recordKeys.difference(configKeys);
if (excessKeys.size > 0) {
errors.push(`Excess keys in record: ${[...excessKeys].join(", ")}`);
}
const locale: GenericLocale = {};
for (const [messageId, configValue] of Object.entries(config)) {
const recordValue = record?.[messageId];
const fallbackValue = fallback?.[messageId];
const compiledValue = ((): Value<GenericLocale> => {
if (configMessageSymbol in configValue) {
const compiledMessageRes = compileMessage({
bundle,
messageId,
configValue,
recordValue,
});
const compiledMessage = ((): GenericMessageFn => {
if (compiledMessageRes.type === "ok") {
return compiledMessageRes.ok;
}
errors.push(compiledMessageRes.err);
if (
fallbackValue !== undefined
&& typeof fallbackValue === "function"
) {
return fallbackValue as GenericMessageFn;
}
return () => `[PLACEHOLDER]${messageId}`;
})();
return compiledMessage;
} else {
const subrecord = (() => {
if (recordValue?.type !== "subrecord") {
errors.push(
`Expected subrecord for key \`${messageId}\`, found: ${typeof recordValue}`,
);
return undefined;
}
return recordValue.subrecord;
})();
return compileSublocale({
subconfig: configValue,
recordSublocale: subrecord,
fallbackSublocale:
typeof fallbackValue === "function" ? undefined : (
fallbackValue
),
errors,
bundle,
messageId,
});
}
})();
locale[messageId] = compiledValue;
}
// SAFETY: validated above that all keys exist and are the correct type
return { locale: locale as InferLocale<Config>, errors };
}
interface CompileMessageCtx {
bundle: FluentBundle;
messageId: string;
configValue: ConfigMessage;
recordValue: Value<UncompiledLocale["record"]> | undefined;
}
function compileMessage(
ctx: CompileMessageCtx,
): Result<GenericMessageFn, string> {
const { bundle, messageId, configValue, recordValue } = ctx;
if (recordValue?.type !== "message") {
return {
type: "err",
err: `Expected message for key \`${messageId}\`, found: ${typeof recordValue}`,
};
}
const message = recordValue.message;
const pattern = message.value;
if (pattern === null) {
return {
type: "err",
err: `Pattern is null for message with ID: ${messageId}`,
};
}
// validate placeables
if (typeof pattern !== "string") {
for (const element of pattern) {
if (typeof element === "string") {
continue;
}
switch (element.type) {
case "select": {
const { selector } = element;
if (selector.type !== "var") {
return {
type: "err",
err: `Expected selector to be a var expression for key: ${messageId}; Found: ${selector.type}`,
};
}
if (
!Object.keys(configValue.placeables).includes(
selector.name,
)
) {
return {
type: "err",
err: `Found unexpected placeable name \`${selector.name}\` for key: ${messageId}`,
};
}
break;
}
case "var": {
if (
!Object.keys(configValue.placeables).includes(
element.name,
)
) {
return {
type: "err",
err: `Found unexpected placeable name \`${element.name}\` for key: ${messageId}`,
};
}
break;
}
case "term":
case "mesg":
case "func":
case "str":
case "num": {
break; // ignore
}
}
}
}
const allVariantsRes = computeAllVariants(pattern);
if (allVariantsRes.type === "err") {
return {
type: "err",
err: `Failed to compute variants for key \`${messageId}\`:\n${allVariantsRes.err}`,
};
}
const allVariants = allVariantsRes.ok;
// create function
const markdown = configValue.markdown;
if (markdown !== null) {
// typecheck markdown
const markdownSlots = markdown.slots ?? [];
// check if all variants are valid markdown
for (const variant of allVariants) {
const markdownLiteralRes = parseMessageLiteral(
"md",
variant.string,
);
if (markdownLiteralRes.type === "err") {
return {
type: "err",
err: `Invalid literal for variant \`${selectionChainToString(variant.selectionChain)}\` of key \`${messageId}\`:\n${markdownLiteralRes.err}`,
};
}
const markdownLiteral = markdownLiteralRes.ok;
const markdownRes = parseMarkdown(markdownLiteral, markdownSlots);
if (markdownRes.type === "err") {
return {
type: "err",
err: `Invalid markdown for variant \`${selectionChainToString(variant.selectionChain)}\` of key \`${messageId}\`:\n${markdownRes.err}`,
};
}
}
// TODO: will need to make sure markdown/slots are escapes when inserting variable values
return {
type: "ok",
ok: (args: Record<string, FluentVariable> = {}) => {
const markdownLiteralRes = parseMessageLiteral(
"md",
bundle.formatPattern(pattern, args),
);
if (markdownLiteralRes.type === "err") {
// This should hopefully never happen since we've already
// verified all message variants parse as valid markdown above
throw new Error(
`Failed to parse markdown literal after compilation!\n${markdownLiteralRes.err}`,
);
}
const markdownLiteral = markdownLiteralRes.ok;
const res = parseMarkdown(markdownLiteral, markdownSlots);
if (res.type === "err") {
// This should hopefully never happen since we've already
// verified all message variants parse as valid markdown above
throw new Error(
`Failed to parse markdown after compilation!\n${res.err}`,
);
}
return res.ok;
},
};
} else {
// typecheck string
// check if all variants are valid markdown
for (const variant of allVariants) {
const stringLiteralRes = parseMessageLiteral(
"string",
variant.string,
);
if (stringLiteralRes.type === "err") {
return {
type: "err",
err: `Invalid literal for variant \`${selectionChainToString(variant.selectionChain)}\` of key \`${messageId}\`:\n${stringLiteralRes.err}`,
};
}
const stringLiteral = stringLiteralRes.ok;
const stringRes = parseString(stringLiteral);
if (stringRes.type === "err") {
return {
type: "err",
err: `Invalid string for variant \`${selectionChainToString(variant.selectionChain)}\` of key \`${messageId}\`:\n${stringRes.err}`,
};
}
}
// TODO: will need to make sure markdown/slots are escapes when inserting variable values
return {
type: "ok",
ok: (args: Record<string, FluentVariable> = {}) => {
const stringLiteralRes = parseMessageLiteral(
"string",
bundle.formatPattern(pattern, args),
);
if (stringLiteralRes.type === "err") {
// This should hopefully never happen since we've already
// verified all message variants parse as valid strings above
throw new Error(
`Failed to parse string literal after compilation!\n${stringLiteralRes.err}`,
);
}
const stringLiteral = stringLiteralRes.ok;
const res = parseString(stringLiteral);
if (res.type === "err") {
// This should hopefully never happen since we've already
// verified all message variants parse as valid strings above
// TODO: no we dont, do that
throw new Error(
`Failed to parse string after compilation!\n${res.err}`,
);
}
return res.ok;
},
};
}
}
interface CompileSublocaleCtx<Subconfig extends LocaleConfig> {
subconfig: Subconfig;
recordSublocale: L10nRecord | undefined;
fallbackSublocale: InferLocale<Subconfig> | undefined;
errors: string[];
bundle: FluentBundle;
messageId: string;
}
function compileSublocale<Subconfig extends LocaleConfig>(
ctx: CompileSublocaleCtx<Subconfig>,
): InferLocale<Subconfig> {
const {
subconfig: configValue,
recordSublocale: recordValue,
fallbackSublocale: fallbackValue,
errors,
bundle,
messageId,
} = ctx;
const subrecord = recordValue;
const compiledSubrecordRes = compileLocale({
bundle,
record: subrecord,
config: configValue,
fallback: fallbackValue,
});
errors.push(
...compiledSubrecordRes.errors.map(
(err) =>
`Error when compiling subrecord with key: \`${messageId}\`:\n${err}`,
),
);
return compiledSubrecordRes.locale;
}
function parseMessageLiteral(
type: "string" | "md",
message: string,
): Result<string, string> {
const trimmedMessage = message.trim();
const maybeStartIndexes: number[] = [];
const firstQuoteIndex = trimmedMessage.indexOf('"');
if (firstQuoteIndex !== -1) {
maybeStartIndexes.push(firstQuoteIndex);
}
const firstDashIndex = trimmedMessage.indexOf("-");
if (firstDashIndex !== -1) {
maybeStartIndexes.push(firstDashIndex);
}
const stringStartIndex = Math.min(...maybeStartIndexes);
const actualPrefix = trimmedMessage.substring(0, stringStartIndex).trim();
const expectedPrefix = (() => {
switch (type) {
case "string": {
return "";
}
case "md": {
return "md";
}
}
})();
if (actualPrefix !== expectedPrefix) {
return {
type: "err",
err: `Expected prefix "${expectedPrefix}" for message with type \`${type}\`; Found: "${actualPrefix}"`,
};
}
return {
type: "ok",
ok: trimmedMessage.substring(actualPrefix.length).trim(),
};
}
function parseString(message: string): Result<string, string> {
const AFFIX = '"';
if (!message.startsWith(AFFIX)) {
return {
type: "err",
err: `String message expected to start with \`${AFFIX}\``,
};
}
if (!message.endsWith(AFFIX)) {
return {
type: "err",
err: `String message expected to end with \`${AFFIX}\``,
};
}
const deprefixed = message.substring(AFFIX.length);
const dequoted = deprefixed.substring(0, deprefixed.length - AFFIX.length);
return { type: "ok", ok: dequoted };
}

View file

@ -1,18 +1,8 @@
import type { Result } from "@/utils/types";
import {
computeAllVariants,
selectionChainToString,
type UncompiledLocale,
} from "./setup";
import type { FluentVariable } from "@fluent/bundle";
import { parseMarkdown, type Markdown } from "./markdown";
import { ignore } from "@/utils/ignore";
import { type Markdown } from "./markdown";
export const configMessageSymbol: unique symbol = Symbol("configMessage");
export interface ConfigMessage<
Placeables extends { [name in string]?: ConfigPlaceableInfo } = {
[name in string]: ConfigPlaceableInfo;
},
Placeables extends { [name in string]?: ConfigPlaceableInfo } = object,
Markdown extends
ConfigMarkdown<string> | null = ConfigMarkdown<string> | null,
> {
@ -109,364 +99,3 @@ export function record<const Key extends PropertyKey, T>(
// SAFETY: we set all properties from keys array above
return obj as Record<Key, T>;
}
export interface CompileLocaleCtx<Config extends LocaleConfig> {
uncompiled: UncompiledLocale;
config: Config;
}
export function compileLocale<Config extends LocaleConfig>(
ctx: CompileLocaleCtx<Config>,
): Result<InferLocale<Config>, string> {
const { uncompiled, config } = ctx;
const { record, bundle } = uncompiled;
const recordKeys = new Set(Object.keys(record));
const configKeys = new Set(Object.keys(config));
const excessKeys = recordKeys.difference(configKeys);
console.log(recordKeys, configKeys, excessKeys);
if (excessKeys.size > 0) {
return {
type: "err",
err: `Excess keys in record: ${[...excessKeys].join(", ")}`,
};
}
type GenericLocale = {
[id: string]:
| GenericLocale
| ((args: Record<string, FluentVariable>) => string)
| ((args: Record<string, FluentVariable>) => Markdown);
};
const locale: GenericLocale = {};
for (const [id, configValue] of Object.entries(config)) {
const recordValue = record[id];
if (configMessageSymbol in configValue) {
if (recordValue?.type !== "message") {
return {
type: "err",
err: `Expected message for key \`${id}\`, found: ${typeof recordValue}`,
};
}
const message = recordValue.message;
console.log(message);
const pattern = message.value;
if (pattern === null) {
return {
type: "err",
err: `Pattern is null for message with ID: ${id}`,
};
}
// validate placeables
console.log(pattern);
if (typeof pattern !== "string") {
for (const element of pattern) {
if (typeof element === "string") {
continue;
}
switch (element.type) {
case "select": {
const { selector } = element;
if (selector.type !== "var") {
return {
type: "err",
err: `Expected selector to be a var expression for key: ${id}; Found: ${selector.type}`,
};
}
if (
!Object.keys(configValue.placeables).includes(
selector.name,
)
) {
return {
type: "err",
err: `Found unexpected placeable name \`${selector.name}\` for key: ${id}`,
};
}
break;
}
case "var": {
if (
!Object.keys(configValue.placeables).includes(
element.name,
)
) {
return {
type: "err",
err: `Found unexpected placeable name \`${element.name}\` for key: ${id}`,
};
}
break;
}
case "term":
case "mesg":
case "func":
case "str":
case "num": {
break; // ignore
}
}
}
}
const allVariantsRes = computeAllVariants(pattern);
if (allVariantsRes.type === "err") {
return {
type: "err",
err: `Failed to compute variants for key \`${id}\`:\n${allVariantsRes.err}`,
};
}
const allVariants = allVariantsRes.ok;
console.log(allVariants);
// create function
const markdown = configValue.markdown;
const compiledFnRes = ((): Result<
| ((args?: Record<string, FluentVariable>) => string)
| ((args?: Record<string, FluentVariable>) => Markdown),
string
> => {
if (markdown !== null) {
// typecheck markdown
const markdownSlots = markdown.slots ?? [];
// check if all variants are valid markdown
for (const variant of allVariants) {
const markdownLiteralRes = parseMessageLiteral(
"md",
variant.string,
);
if (markdownLiteralRes.type === "err") {
return {
type: "err",
err: `Invalid literal for variant \`${selectionChainToString(variant.selectionChain)}\` of key \`${id}\`:\n${markdownLiteralRes.err}`,
};
}
const markdownLiteral = markdownLiteralRes.ok;
const markdownRes = parseMarkdown(
markdownLiteral,
markdownSlots,
);
console.log(markdownRes);
if (markdownRes.type === "err") {
return {
type: "err",
err: `Invalid markdown for variant \`${selectionChainToString(variant.selectionChain)}\` of key \`${id}\`:\n${markdownRes.err}`,
};
}
}
// TODO: will need to make sure markdown/slots are escapes when inserting variable values
return {
type: "ok",
ok: (args: Record<string, FluentVariable> = {}) => {
const markdownLiteralRes = parseMessageLiteral(
"md",
bundle.formatPattern(pattern, args),
);
if (markdownLiteralRes.type === "err") {
// This should hopefully never happen since we've already
// verified all message variants parse as valid markdown above
throw new Error(
`Failed to parse markdown literal after compilation!\n${markdownLiteralRes.err}`,
);
}
const markdownLiteral = markdownLiteralRes.ok;
const res = parseMarkdown(
markdownLiteral,
markdownSlots,
);
if (res.type === "err") {
// This should hopefully never happen since we've already
// verified all message variants parse as valid markdown above
throw new Error(
`Failed to parse markdown after compilation!\n${res.err}`,
);
}
return res.ok;
},
};
} else {
// typecheck string
// check if all variants are valid markdown
for (const variant of allVariants) {
const stringLiteralRes = parseMessageLiteral(
"string",
variant.string,
);
if (stringLiteralRes.type === "err") {
return {
type: "err",
err: `Invalid literal for variant \`${selectionChainToString(variant.selectionChain)}\` of key \`${id}\`:\n${stringLiteralRes.err}`,
};
}
const stringLiteral = stringLiteralRes.ok;
const stringRes = parseString(stringLiteral);
console.log(stringRes);
if (stringRes.type === "err") {
return {
type: "err",
err: `Invalid string for variant \`${selectionChainToString(variant.selectionChain)}\` of key \`${id}\`:\n${stringRes.err}`,
};
}
}
// TODO: will need to make sure markdown/slots are escapes when inserting variable values
return {
type: "ok",
ok: (args: Record<string, FluentVariable> = {}) => {
const stringLiteralRes = parseMessageLiteral(
"string",
bundle.formatPattern(pattern, args),
);
if (stringLiteralRes.type === "err") {
// This should hopefully never happen since we've already
// verified all message variants parse as valid strings above
throw new Error(
`Failed to parse string literal after compilation!\n${stringLiteralRes.err}`,
);
}
const stringLiteral = stringLiteralRes.ok;
const res = parseString(stringLiteral);
if (res.type === "err") {
// This should hopefully never happen since we've already
// verified all message variants parse as valid strings above
// TODO: no we dont, do that
throw new Error(
`Failed to parse string after compilation!\n${res.err}`,
);
}
return res.ok;
},
};
}
})();
if (compiledFnRes.type === "err") {
return compiledFnRes;
}
const compiledFn = compiledFnRes.ok;
locale[id] = compiledFn;
} else {
if (recordValue?.type !== "subrecord") {
return {
type: "err",
err: `Expected subrecord for key \`${id}\`, found: ${typeof recordValue}`,
};
}
const subrecord = recordValue.subrecord;
const compiledSubrecordRes = compileLocale({
uncompiled: { bundle, record: subrecord },
config: configValue,
});
if (compiledSubrecordRes.type === "err") {
return {
type: "err",
err: `Error when compiling subrecord with key: \`${id}\`:\n${compiledSubrecordRes.err}`,
};
}
const compiledSubrecord = compiledSubrecordRes.ok;
locale[id] = compiledSubrecord;
}
}
// SAFETY: validated above that all keys exist and are the correct type
return { type: "ok", ok: locale as InferLocale<Config> };
}
function parseMessageLiteral(
type: "string" | "md",
message: string,
): Result<string, string> {
const trimmedMessage = message.trim();
console.log(trimmedMessage);
const maybeStartIndexes: number[] = [];
const firstQuoteIndex = trimmedMessage.indexOf('"');
if (firstQuoteIndex !== -1) {
maybeStartIndexes.push(firstQuoteIndex);
}
const firstDashIndex = trimmedMessage.indexOf("-");
if (firstDashIndex !== -1) {
maybeStartIndexes.push(firstDashIndex);
}
const stringStartIndex = Math.min(...maybeStartIndexes);
console.log(stringStartIndex);
const actualPrefix = trimmedMessage.substring(0, stringStartIndex).trim();
const expectedPrefix = (() => {
switch (type) {
case "string": {
return "";
}
case "md": {
return "md";
}
}
})();
if (actualPrefix !== expectedPrefix) {
return {
type: "err",
err: `Expected prefix "${expectedPrefix}" for message with type \`${type}\`; Found: "${actualPrefix}"`,
};
}
return {
type: "ok",
ok: trimmedMessage.substring(actualPrefix.length).trim(),
};
}
function parseString(message: string): Result<string, string> {
const AFFIX = '"';
if (!message.startsWith(AFFIX)) {
return {
type: "err",
err: `String message expected to start with \`${AFFIX}\``,
};
}
if (!message.endsWith(AFFIX)) {
return {
type: "err",
err: `String message expected to end with \`${AFFIX}\``,
};
}
const deprefixed = message.substring(AFFIX.length);
const dequoted = deprefixed.substring(0, deprefixed.length - AFFIX.length);
return { type: "ok", ok: dequoted };
}

View file

@ -1,9 +1,5 @@
import type { Result } from "@/utils/types";
import {
FluentBundle,
FluentResource,
type FluentVariable,
} from "@fluent/bundle";
import { FluentBundle, FluentResource } from "@fluent/bundle";
import { unsafeAsync } from "@/utils/unsafe";
import type { Literal, Message, Pattern } from "@fluent/bundle/esm/ast";
@ -26,11 +22,8 @@ export async function loadFluentBundle(
}
const ftlFileText = ftlFileTextRes.ok;
console.log(ftlFileText);
const resource = new FluentResource(ftlFileText);
console.log(resource.body);
const bundle = new FluentBundle(localeId);
const errors = bundle.addResource(resource);
if (errors.length > 0) {
@ -129,7 +122,6 @@ export function computeAllVariants(
}
let variants: PatternVariant[] = [{ selectionChain: [], string: "" }];
console.log(pattern);
for (const element of pattern) {
if (typeof element === "string") {
variants = variants.map((variant) => ({

View file

@ -0,0 +1,79 @@
# Viossa I18n Message Spec
## Types & Literals
There are two types of messages, `string` & `markdown`. Each type is specified by a prefix:
- `string` literal (no prefix): `"Hello world!"`
- `markdown` literal (`md` prefix): `md "Hello world!"`
Message literals are made up of lines. Each line is surrounded by quotes.
## String Literals
String literals take exactly one line. They have no special formatting or behavior, exactly what is in the string will be what is displayed:
- `"Hello world!"` => Hello world!
- `"123 *456* **789**"` => 123 \*456\* \*\*789\*\*
## Markdown Literals
Markdown literals can take any number of lines. Lines are separated by newline characters.
```
example-markdownMessage = md
"Line 1"
"Line 2"
"Line 3"
```
They can also consist of a single line:
```
example-markdownMessage = md "Line 1"
```
A special sigil exists for denoting that no lines exist:
```
example-markdownMessage = md --
```
### Line Types
- Paragraph: `Example`
- Header: `# Example` (subheaders are not supported)
- Unordered List Item: `- Example`
### Line Features
- Italic: `*Example*` => *Example*
- Bold: `**Example**` => **Example**
- Bold + Italic: `***Example***` => ***Example***
- Links: `[Example](external.new:https://example.com/)` => [Example](https://example.com/)
- Slots: `<example>`
Characters used for line feature syntax can be escaped to remove their effect and place the raw character in the string: `\*Example\*` => \*Example\*
### Links
Links are made up of 4 components:
```
[Example](external.new:https://example.com/)
^^^^^^^ ^^^^^^^^ ^^^ ^^^^^^^^^^^^^^^^^^^^
name type tab destination
```
`name` is the text displayed to the user on the webpage. It is optional; If blank, it will display the destination directly to the user.
`type` can be either `internal` or `external`, and changes what is deemed a valid `destination`.
If `tab` is `new`, the link will open the `destination` in a new tab. If `tab` is `replace`, it will open in the current tab.
`destination` is where the link will take the user when clicked. Its value depends on the value of `type` as follows:
- If `type` is `external`: `destination` is any link starting with `http://` or `https://`
- `http://example.com/`
- `https://google.com/`
- `https://viossa.net/`
- If `type` is `internal`: `destination` consists of a `route` and `id`, in any of the following patterns:
- `route`-only: brings the user to another route on the website
- `/`
- `/resources`
- `/kotoba`
- `/discord/rules`
- `id`-only: jumps the user to a specific element ID on the current route
- `#top`
- `#header`
- `#rule-1`
- `route` with `id`: brings the user to another route and jumps to an element ID on that page
- `/discord/rules#rule-1`
- `/#top`

View file

@ -1,4 +1,4 @@
import { compileLocale, type InferLocale } from "@/new-i18n-lib/config";
import { type InferLocale } from "@/new-i18n-lib/config";
import {
bundleToUncompiledLocale,
loadFluentBundle,
@ -12,6 +12,7 @@ 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 "@/new-i18n-lib/compile";
export const LOCALE_IDS = ["en-US", "vp-VL", "wp-VL"] as const;
@ -61,28 +62,31 @@ async function loadLocale(
return { type: "ok", ok: bundle };
}
interface SetupLocaleFallback {
bundle: FluentBundle;
locale: Locale;
}
function setupLocale(
localeBundle: FluentBundle,
fallbackLocaleBundle: FluentBundle | undefined,
fallback: SetupLocaleFallback | undefined,
): Result<Locale, string> {
const fallbackBundle = fallback?.bundle;
const fallbackLocale = fallback?.locale;
const maybeFallbackedBundle = (() => {
if (fallbackLocaleBundle === undefined) {
if (fallbackBundle === undefined) {
return localeBundle;
}
console.log(localeBundle);
console.log(fallbackLocaleBundle);
const localeMessageIds = new Set(localeBundle._messages.keys());
const fallbackMessageIds = new Set(
fallbackLocaleBundle._messages.keys(),
);
const fallbackMessageIds = new Set(fallbackBundle._messages.keys());
const missingMessageIds =
fallbackMessageIds.difference(localeMessageIds);
for (const id of missingMessageIds) {
const fallbackMessage = fallbackLocaleBundle._messages.get(id);
const fallbackMessage = fallbackBundle._messages.get(id);
if (fallbackMessage) {
localeBundle._messages.set(id, fallbackMessage);
}
@ -98,13 +102,16 @@ function setupLocale(
const uncompiled = uncompiledRes.ok;
const localeRes = compileLocale({ config: localeConfig, uncompiled });
const localeRes = compileLocale({
config: localeConfig,
bundle: uncompiled.bundle,
record: uncompiled.record,
fallback: fallbackLocale,
});
if (localeRes.type === "err") {
return localeRes;
}
console.error(localeRes.errors);
const locale = localeRes.ok;
const locale = localeRes.locale;
return { type: "ok", ok: locale };
}
@ -124,7 +131,8 @@ function deepReadonly<T>(value: T): DeepReadonly<T> {
return value as DeepReadonly<T>;
}
const DEFAULT_LOCALE = unwrap(await loadLocale("en-US", enUsFtlSrc));
const DEFAULT_LOCALE_BUNDLE = unwrap(await loadLocale("en-US", enUsFtlSrc));
const DEFAULT_LOCALE = unwrap(setupLocale(DEFAULT_LOCALE_BUNDLE, undefined));
const doItAllForLocale = async (
localeId: LocaleId,
@ -132,10 +140,10 @@ const doItAllForLocale = async (
): Promise<DeepReadonly<Locale>> =>
deepReadonly(
unwrap(
setupLocale(
unwrap(await loadLocale(localeId, localeFtlSrc)),
DEFAULT_LOCALE,
),
setupLocale(unwrap(await loadLocale(localeId, localeFtlSrc)), {
bundle: DEFAULT_LOCALE_BUNDLE,
locale: DEFAULT_LOCALE,
}),
),
);
@ -145,7 +153,7 @@ const [vpVl, wpVl] = await Promise.all([
]);
const localeIdToLocale = {
"en-US": deepReadonly(unwrap(setupLocale(DEFAULT_LOCALE, DEFAULT_LOCALE))),
"en-US": deepReadonly(DEFAULT_LOCALE),
"vp-VL": vpVl,
"wp-VL": wpVl,
} as const satisfies Record<LocaleId, DeepReadonly<Locale>>;

View file

@ -1,8 +1,6 @@
import { createRouter, createWebHistory } from "vue-router";
import { routes, handleHotUpdate } from "vue-router/auto-routes";
console.log(routes);
const router = createRouter({ history: createWebHistory(), routes });
if (import.meta.hot) {