From 44ab34133ce835dea64d276023562a336b7fe052 Mon Sep 17 00:00:00 2001 From: Benjamin Singleton <19498453+tetrogem@users.noreply.github.com> Date: Sun, 1 Mar 2026 16:30:11 -0600 Subject: [PATCH] wip: fallback behavior for missing/malformed messages instead of panicking --- apps/vdn-static/src/new-i18n-lib/compile.ts | 437 ++++++++++++++++++++ apps/vdn-static/src/new-i18n-lib/config.ts | 375 +---------------- apps/vdn-static/src/new-i18n-lib/setup.ts | 10 +- apps/vdn-static/src/new-i18n-lib/spec.md | 79 ++++ apps/vdn-static/src/new-i18n/index.ts | 50 ++- apps/vdn-static/src/router.ts | 2 - 6 files changed, 548 insertions(+), 405 deletions(-) create mode 100644 apps/vdn-static/src/new-i18n-lib/compile.ts create mode 100644 apps/vdn-static/src/new-i18n-lib/spec.md diff --git a/apps/vdn-static/src/new-i18n-lib/compile.ts b/apps/vdn-static/src/new-i18n-lib/compile.ts new file mode 100644 index 0000000..865474f --- /dev/null +++ b/apps/vdn-static/src/new-i18n-lib/compile.ts @@ -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 { + bundle: FluentBundle; + record: L10nRecord | undefined; + config: Config; + fallback?: InferLocale; +} + +export interface CompileLocaleRes { + locale: Locale; + errors: string[]; +} + +type GenericLocale = { [id: string]: GenericLocale | GenericMessageFn }; + +type GenericMessageFn = + | ((args?: Record) => string) + | ((args?: Record) => Markdown); + +export function compileLocale( + ctx: CompileLocaleCtx, +): CompileLocaleRes> { + 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 => { + 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, errors }; +} + +interface CompileMessageCtx { + bundle: FluentBundle; + messageId: string; + configValue: ConfigMessage; + recordValue: Value | undefined; +} + +function compileMessage( + ctx: CompileMessageCtx, +): Result { + 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 = {}) => { + 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 = {}) => { + 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: Subconfig; + recordSublocale: L10nRecord | undefined; + fallbackSublocale: InferLocale | undefined; + errors: string[]; + bundle: FluentBundle; + messageId: string; +} + +function compileSublocale( + ctx: CompileSublocaleCtx, +): InferLocale { + 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 { + 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 { + 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 }; +} diff --git a/apps/vdn-static/src/new-i18n-lib/config.ts b/apps/vdn-static/src/new-i18n-lib/config.ts index 8531cd9..e1805ec 100644 --- a/apps/vdn-static/src/new-i18n-lib/config.ts +++ b/apps/vdn-static/src/new-i18n-lib/config.ts @@ -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 | null = ConfigMarkdown | null, > { @@ -109,364 +99,3 @@ export function record( // SAFETY: we set all properties from keys array above return obj as Record; } - -export interface CompileLocaleCtx { - uncompiled: UncompiledLocale; - config: Config; -} - -export function compileLocale( - ctx: CompileLocaleCtx, -): Result, 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) - | ((args: Record) => 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) - | ((args?: Record) => 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 = {}) => { - 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 = {}) => { - 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 }; -} - -function parseMessageLiteral( - type: "string" | "md", - message: string, -): Result { - 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 { - 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 }; -} diff --git a/apps/vdn-static/src/new-i18n-lib/setup.ts b/apps/vdn-static/src/new-i18n-lib/setup.ts index 92534be..0419f74 100644 --- a/apps/vdn-static/src/new-i18n-lib/setup.ts +++ b/apps/vdn-static/src/new-i18n-lib/setup.ts @@ -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) => ({ diff --git a/apps/vdn-static/src/new-i18n-lib/spec.md b/apps/vdn-static/src/new-i18n-lib/spec.md new file mode 100644 index 0000000..55c9374 --- /dev/null +++ b/apps/vdn-static/src/new-i18n-lib/spec.md @@ -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: `` + +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` diff --git a/apps/vdn-static/src/new-i18n/index.ts b/apps/vdn-static/src/new-i18n/index.ts index 71ad742..6a7101a 100644 --- a/apps/vdn-static/src/new-i18n/index.ts +++ b/apps/vdn-static/src/new-i18n/index.ts @@ -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 { + 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(value: T): DeepReadonly { return value as DeepReadonly; } -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( 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>; diff --git a/apps/vdn-static/src/router.ts b/apps/vdn-static/src/router.ts index 5c15a90..841dfa8 100644 --- a/apps/vdn-static/src/router.ts +++ b/apps/vdn-static/src/router.ts @@ -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) {