wip: i18n compiler organization/improvements, separated string & markdown messages in config builder

This commit is contained in:
Benjamin Singleton 2026-03-01 22:32:45 -06:00
parent 44ab34133c
commit 17e1c34a3b
6 changed files with 384 additions and 268 deletions

View file

@ -48,6 +48,10 @@ export default defineConfig([
{ allowInterfaces: "with-single-extends" },
],
"vue/no-ref-object-reactivity-loss": ["error"],
"@typescript/no-unnecessary-conditions": [
"error",
{ allowConstantLoopConditions: "only-allowed-literals" },
],
},
},
// disable multi-word-component-names for unplugin-vue-router

View file

@ -2,23 +2,42 @@ import type { Result, Value } from "@/utils/types";
import {
computeAllVariants,
selectionChainToString,
type L10nRecord,
type PatternVariant,
type UncompiledLocale,
} from "./setup";
import type { FluentBundle, FluentVariable } from "@fluent/bundle";
import type { FluentBundle, FluentVariable, Message } from "@fluent/bundle";
import { parseMarkdown, type Markdown } from "./markdown";
import {
configMessageSymbol,
type ConfigMessage,
type InferLocale,
configMarkdownSymbol,
configMessageTypeSymbol,
configStringSymbol,
type ConfigMarkdown,
type ConfigString,
type InferLocaleFromConfig,
type LocaleConfig,
} from "./config";
import type { Pattern } from "@fluent/bundle/esm/ast";
type GenericLocale = { [id: string]: GenericLocale | GenericMessageFn };
type GenericMessageFn = GenericStringMessageFn | GenericMarkdownMessageFn;
type GenericStringMessageFn = (
placeableArgs?: GenericMessageFnPlaceableArgs,
) => string;
type GenericMarkdownMessageFn = (
placeableArgs?: GenericMessageFnPlaceableArgs,
) => Markdown;
type GenericMessageFnPlaceableArgs = Record<string, FluentVariable>;
export interface CompileLocaleCtx<Config extends LocaleConfig> {
bundle: FluentBundle;
record: L10nRecord | undefined;
uncompiled: UncompiledLocale;
config: Config;
fallback?: InferLocale<Config>;
fallback: InferLocaleFromConfig<Config> | undefined;
messageIdChain: readonly string[];
}
export interface CompileLocaleRes<Locale> {
@ -26,81 +45,98 @@ export interface CompileLocaleRes<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;
): CompileLocaleRes<InferLocaleFromConfig<Config>> {
const {
bundle,
uncompiled,
config,
fallback,
messageIdChain: localeMessageIdChain = [],
} = ctx;
const errors: string[] = [];
const recordKeys = new Set(Object.keys(record ?? {}));
const uncompiledKeys = new Set(Object.keys(uncompiled ?? {}));
const configKeys = new Set(Object.keys(config));
const excessKeys = recordKeys.difference(configKeys);
const excessKeys = uncompiledKeys.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 uncompiledValue = uncompiled?.[messageId];
const fallbackValue = fallback?.[messageId];
const valueMessageIdChain = [
...localeMessageIdChain,
messageId,
] as const;
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") {
if (configMessageTypeSymbol in configValue) {
const compiledMessage = (() => {
if (uncompiledValue?.type !== "message") {
errors.push(
`Expected subrecord for key \`${messageId}\`, found: ${typeof recordValue}`,
`Expected message for key \`${messageId}\`, found: ${typeof uncompiledValue}`,
);
return undefined;
}
return recordValue.subrecord;
const uncompiledMessage = uncompiledValue.message;
const compiledMessageRes = compileMessage({
bundle,
messageIdChain: valueMessageIdChain,
configValue,
uncompiledMessage,
});
if (compiledMessageRes.type === "err") {
errors.push(compiledMessageRes.err);
return undefined;
}
const compiledMessage = compiledMessageRes.ok;
return compiledMessage;
})();
if (compiledMessage !== undefined) {
return compiledMessage;
}
if (
fallbackValue !== undefined
&& typeof fallbackValue === "function"
) {
return fallbackValue as GenericMessageFn;
}
return () => `[#${fmtMessageIdChain(valueMessageIdChain)}#]`;
} else {
const uncompiledSubrecord = (() => {
if (uncompiledValue?.type !== "subrecord") {
errors.push(
`Expected subrecord for key \`${messageId}\`, found: ${typeof uncompiledValue}`,
);
return undefined;
}
return uncompiledValue.subrecord;
})();
return compileSublocale({
subconfig: configValue,
recordSublocale: subrecord,
uncompiledSublocale: uncompiledSubrecord,
fallbackSublocale:
typeof fallbackValue === "function" ? undefined : (
fallbackValue
),
errors,
bundle,
messageId,
messageIdChain: valueMessageIdChain,
});
}
})();
@ -109,35 +145,32 @@ export function compileLocale<Config extends LocaleConfig>(
}
// SAFETY: validated above that all keys exist and are the correct type
return { locale: locale as InferLocale<Config>, errors };
return { locale: locale as InferLocaleFromConfig<Config>, errors };
}
function fmtMessageIdChain(
messageIdChain: readonly [...string[], string],
): string {
return messageIdChain.join("-");
}
interface CompileMessageCtx {
bundle: FluentBundle;
messageId: string;
configValue: ConfigMessage;
recordValue: Value<UncompiledLocale["record"]> | undefined;
messageIdChain: readonly [...string[], string];
configValue: ConfigString<object> | ConfigMarkdown<object, object>;
uncompiledMessage: Message;
}
function compileMessage(
ctx: CompileMessageCtx,
): Result<GenericMessageFn, string> {
const { bundle, messageId, configValue, recordValue } = ctx;
const { bundle, messageIdChain, configValue, uncompiledMessage } = 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;
const pattern = uncompiledMessage.value;
if (pattern === null) {
return {
type: "err",
err: `Pattern is null for message with ID: ${messageId}`,
err: `Pattern is null for message with ID: ${fmtMessageIdChain(messageIdChain)}`,
};
}
@ -154,7 +187,7 @@ function compileMessage(
if (selector.type !== "var") {
return {
type: "err",
err: `Expected selector to be a var expression for key: ${messageId}; Found: ${selector.type}`,
err: `Expected selector to be a var expression for ID: ${fmtMessageIdChain(messageIdChain)}; Found: ${selector.type}`,
};
}
@ -165,7 +198,7 @@ function compileMessage(
) {
return {
type: "err",
err: `Found unexpected placeable name \`${selector.name}\` for key: ${messageId}`,
err: `Found unexpected placeable name \`${selector.name}\` for ID: ${fmtMessageIdChain(messageIdChain)}`,
};
}
@ -179,7 +212,7 @@ function compileMessage(
) {
return {
type: "err",
err: `Found unexpected placeable name \`${element.name}\` for key: ${messageId}`,
err: `Found unexpected placeable name \`${element.name}\` for ID: ${fmtMessageIdChain(messageIdChain)}`,
};
}
@ -200,171 +233,215 @@ function compileMessage(
if (allVariantsRes.type === "err") {
return {
type: "err",
err: `Failed to compute variants for key \`${messageId}\`:\n${allVariantsRes.err}`,
err: `Failed to compute variants for ID \`${fmtMessageIdChain(messageIdChain)}\`:\n${allVariantsRes.err}`,
};
}
const allVariants = allVariantsRes.ok;
// create function
const markdown = configValue.markdown;
switch (configValue[configMessageTypeSymbol]) {
case configStringSymbol: {
return compileStringMessage({
bundle,
messageIdChain,
allVariants,
pattern,
});
}
case configMarkdownSymbol: {
return compileMarkdownMessage({
bundle,
configMarkdown: configValue,
messageIdChain,
allVariants,
pattern,
});
}
}
}
if (markdown !== null) {
// typecheck markdown
interface CompileStringMessageCtx {
bundle: FluentBundle;
messageIdChain: readonly [...string[], string];
allVariants: readonly PatternVariant[];
pattern: Pattern;
}
const markdownSlots = markdown.slots ?? [];
function compileStringMessage(
ctx: CompileStringMessageCtx,
): Result<GenericStringMessageFn, string> {
const { bundle, messageIdChain, allVariants, pattern } = ctx;
// check if all variants are valid markdown
for (const variant of allVariants) {
const markdownLiteralRes = parseMessageLiteral(
"md",
variant.string,
);
// typecheck string
// check if all variants are valid markdown
for (const variant of allVariants) {
const stringLiteralRes = parseMessageLiteral("string", 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}`,
};
}
if (stringLiteralRes.type === "err") {
return {
type: "err",
err: `Invalid literal for variant \`${selectionChainToString(variant.selectionChain)}\` of ID \`${fmtMessageIdChain(messageIdChain)}\`:\n${stringLiteralRes.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),
);
const stringLiteral = stringLiteralRes.ok;
const stringRes = parseString(stringLiteral);
if (stringRes.type === "err") {
return {
type: "err",
err: `Invalid string for variant \`${selectionChainToString(variant.selectionChain)}\` of ID \`${fmtMessageIdChain(messageIdChain)}\`:\n${stringRes.err}`,
};
}
}
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) {
// 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",
variant.string,
bundle.formatPattern(pattern, args),
);
if (stringLiteralRes.type === "err") {
return {
type: "err",
err: `Invalid literal for variant \`${selectionChainToString(variant.selectionChain)}\` of key \`${messageId}\`:\n${stringLiteralRes.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 stringRes = parseString(stringLiteral);
if (stringRes.type === "err") {
return {
type: "err",
err: `Invalid string for variant \`${selectionChainToString(variant.selectionChain)}\` of key \`${messageId}\`:\n${stringRes.err}`,
};
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 CompileMarkdownMessageCtx {
bundle: FluentBundle;
messageIdChain: readonly [...string[], string];
configMarkdown: ConfigMarkdown<object, object>;
allVariants: readonly PatternVariant[];
pattern: Pattern;
}
function compileMarkdownMessage(
ctx: CompileMarkdownMessageCtx,
): Result<GenericMarkdownMessageFn, string> {
const { bundle, messageIdChain, configMarkdown, allVariants, pattern } =
ctx;
// typecheck markdown
const markdownSlots = configMarkdown.features.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 ID \`${fmtMessageIdChain(messageIdChain)}\`:\n${markdownLiteralRes.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),
);
const markdownLiteral = markdownLiteralRes.ok;
const markdownRes = parseMarkdown(
markdownLiteral,
Object.keys(markdownSlots),
);
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 (markdownRes.type === "err") {
return {
type: "err",
err: `Invalid markdown for variant \`${selectionChainToString(variant.selectionChain)}\` of ID \`${fmtMessageIdChain(messageIdChain)}\`:\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,
Object.keys(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;
},
};
}
interface CompileSublocaleCtx<Subconfig extends LocaleConfig> {
subconfig: Subconfig;
recordSublocale: L10nRecord | undefined;
fallbackSublocale: InferLocale<Subconfig> | undefined;
uncompiledSublocale: UncompiledLocale | undefined;
fallbackSublocale: InferLocaleFromConfig<Subconfig> | undefined;
errors: string[];
bundle: FluentBundle;
messageId: string;
messageIdChain: readonly [...string[], string];
}
function compileSublocale<Subconfig extends LocaleConfig>(
ctx: CompileSublocaleCtx<Subconfig>,
): InferLocale<Subconfig> {
): InferLocaleFromConfig<Subconfig> {
const {
subconfig: configValue,
recordSublocale: recordValue,
uncompiledSublocale: recordValue,
fallbackSublocale: fallbackValue,
errors,
bundle,
messageId,
messageIdChain,
} = ctx;
const subrecord = recordValue;
const compiledSubrecordRes = compileLocale({
bundle,
record: subrecord,
uncompiled: subrecord ?? {},
config: configValue,
fallback: fallbackValue,
messageIdChain,
});
errors.push(
...compiledSubrecordRes.errors.map(
(err) =>
`Error when compiling subrecord with key: \`${messageId}\`:\n${err}`,
`Error when compiling subrecord with ID: \`${fmtMessageIdChain(messageIdChain)}\`:\n${err}`,
),
);

View file

@ -1,14 +1,24 @@
import { type Markdown } from "./markdown";
export const configMessageSymbol: unique symbol = Symbol("configMessage");
export interface ConfigMessage<
Placeables extends { [name in string]?: ConfigPlaceableInfo } = object,
Markdown extends
ConfigMarkdown<string> | null = ConfigMarkdown<string> | null,
export const configMessageTypeSymbol: unique symbol =
Symbol("configMessageType");
export const configStringSymbol: unique symbol = Symbol("configString");
export interface ConfigString<
Placeables extends Partial<Record<string, ConfigPlaceableInfo>>,
> {
[configMessageSymbol]: true;
[configMessageTypeSymbol]: typeof configStringSymbol;
placeables: Placeables;
markdown: Markdown;
}
export const configMarkdownSymbol: unique symbol = Symbol("configMarkdown");
export interface ConfigMarkdown<
Placeables extends Partial<Record<string, ConfigPlaceableInfo>>,
Slots extends Partial<Record<string, ConfigSlotInfo>>,
> {
[configMessageTypeSymbol]: typeof configMarkdownSymbol;
placeables: Placeables;
features: ConfigMarkdownFeatures<Slots>;
}
export interface ConfigPlaceableInfo<
@ -17,47 +27,56 @@ export interface ConfigPlaceableInfo<
type: Type;
}
export interface ConfigMarkdown<Slot extends string> {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface ConfigSlotInfo {}
export interface ConfigMarkdownFeatures<
Slots extends Partial<Record<string, ConfigSlotInfo>>,
> {
bold?: boolean;
italic?: boolean;
header?: boolean;
link?: boolean;
ulist?: boolean;
slots?: Slot[];
slots: Slots;
}
export function message<
const Placeables extends {
[name in string]?: ConfigPlaceableInfo;
} = object,
const Markdown extends ConfigMarkdown<string> | null = null,
export function string<
const Placeables extends Partial<Record<string, ConfigPlaceableInfo>>,
>(
opt: Partial<
Omit<
ConfigMessage<Placeables, ConfigMarkdown<never> | Markdown>,
typeof configMessageSymbol
>
> = {},
): ConfigMessage<Placeables, Markdown> {
opt: Omit<ConfigString<Placeables>, typeof configMessageTypeSymbol>,
): ConfigString<Placeables> {
return {
[configMessageSymbol]: true,
placeables: opt.placeables ?? {},
markdown: opt.markdown ?? null,
[configMessageTypeSymbol]: configStringSymbol,
placeables: opt.placeables,
};
}
export type LocaleConfig<
Placeables extends { [name in string]?: ConfigPlaceableInfo } = object,
Markdown extends
ConfigMarkdown<string> | null = ConfigMarkdown<string> | null,
> = {
export function markdown<
const Placeables extends Partial<Record<string, ConfigPlaceableInfo>>,
const Slots extends Partial<Record<string, ConfigSlotInfo>>,
>(
opt: Omit<
ConfigMarkdown<Placeables, Slots>,
typeof configMessageTypeSymbol
>,
): ConfigMarkdown<Placeables, Slots> {
return {
[configMessageTypeSymbol]: configMarkdownSymbol,
placeables: opt.placeables,
features: opt.features,
};
}
export type LocaleConfig = {
[id: string]:
| ConfigMessage<Placeables, Markdown>
| LocaleConfig<Placeables, Markdown>;
| ConfigString<object>
| ConfigMarkdown<object, object>
| LocaleConfig;
};
type MessageCtx<
Placeables extends { [name in string]?: ConfigPlaceableInfo } = object,
Placeables extends Partial<Record<string, ConfigPlaceableInfo>>,
> = {
[K in keyof Placeables]: ResolvedPlaceableType<
PlaceableType<Exclude<Placeables[K], undefined>>
@ -72,18 +91,17 @@ type ResolvedPlaceableType<Type extends "string" | "number"> =
: Type extends "number" ? number
: never;
export type InferLocale<Config extends LocaleConfig> = {
[K in keyof Config]: Config[K] extends LocaleConfig ? InferLocale<Config[K]>
: Config[K] extends ConfigMessage<infer Placeables, infer Md> ?
export type InferLocaleFromConfig<Config extends LocaleConfig> = {
[K in keyof Config]: Config[K] extends LocaleConfig ?
InferLocaleFromConfig<Config[K]>
: Config[K] extends ConfigString<infer Placeables> ?
object extends MessageCtx<Placeables> ?
() => null extends Md ? string
: Md extends ConfigMarkdown<never> ? Markdown<never>
: Md extends ConfigMarkdown<infer Slot> ? Markdown<Slot>
: never
: (ctx: MessageCtx<Placeables>) => null extends Md ? string
: Md extends ConfigMarkdown<never> ? Markdown<never>
: Md extends ConfigMarkdown<infer Slot> ? Markdown<Slot>
: never
() => string
: (ctx: MessageCtx<Placeables>) => string
: Config[K] extends ConfigMarkdown<infer Placeables, object> ?
object extends MessageCtx<Placeables> ?
() => Markdown
: (ctx: MessageCtx<Placeables>) => Markdown
: never;
};

View file

@ -36,21 +36,16 @@ export async function loadFluentBundle(
return { type: "ok", ok: bundle };
}
export type L10nRecord = {
export type UncompiledLocale = {
[id: string]:
| { type: "message"; message: Message }
| { type: "subrecord"; subrecord: L10nRecord };
| { type: "subrecord"; subrecord: UncompiledLocale };
};
export interface UncompiledLocale {
bundle: FluentBundle;
record: L10nRecord;
}
export function bundleToUncompiledLocale(
export function bundleToUncompiledLocaleRecord(
bundle: FluentBundle,
): Result<UncompiledLocale, string> {
const record: L10nRecord = {};
const record: UncompiledLocale = {};
for (const [id, message] of bundle._messages) {
const idChain = id.split("-");
let subrecord = record;
@ -85,7 +80,7 @@ export function bundleToUncompiledLocale(
}
}
return { type: "ok", ok: { bundle, record } };
return { type: "ok", ok: record };
}
export type SelectionChain = (Literal | SelectionChain)[];
@ -109,7 +104,7 @@ export function selectionChainToString(chain: SelectionChain): string {
.join("+");
}
interface PatternVariant {
export interface PatternVariant {
selectionChain: SelectionChain;
string: string;
}

View file

@ -1,43 +1,62 @@
import {
message,
string,
markdown,
record,
type InferLocale,
type InferLocaleFromConfig,
type LocaleConfig,
type ConfigString,
} from "@/new-i18n-lib/config";
const homeSectionConfig = { title: message(), body: message() };
function plainString(): ConfigString<object> {
return string({ placeables: {} });
}
const imageConfig = { alt: message() };
export interface Image extends InferLocale<typeof imageConfig> {}
const homeSectionConfig = { title: plainString(), body: plainString() };
const buttonConfig = { label: message() };
const imageConfig = { alt: plainString() };
export interface Image extends InferLocaleFromConfig<typeof imageConfig> {}
const buttonConfig = { label: plainString() };
const resourceConfig = <ButtonKey extends string>(buttonKeys: ButtonKey[]) => ({
title: message(),
subtitle: message(),
desc: message(),
title: plainString(),
subtitle: plainString(),
desc: plainString(),
buttons: record(buttonKeys, () => buttonConfig),
});
const discordRuleConfig = {
overview: {
text: message({ markdown: { bold: true, italic: true, link: true } }),
subtext: message({
markdown: { bold: true, italic: true, link: true },
text: markdown({
placeables: {},
features: { bold: true, italic: true, link: true, slots: {} },
}),
subtext: markdown({
placeables: {},
features: { bold: true, italic: true, link: true, slots: {} },
}),
},
section: {
header: message({ placeables: { ruleNumber: { type: "number" } } }),
body: message({
markdown: { bold: true, header: true, italic: true, link: true },
header: string({ placeables: { ruleNumber: { type: "number" } } }),
body: markdown({
placeables: {},
features: {
bold: true,
header: true,
italic: true,
link: true,
slots: {},
},
}),
},
};
export const localeConfig = {
localeName: message(),
vilanticLangs: record(["viossa", "wodox"], () => message()),
navbar: record(["whatIsViossa", "resources", "kotoba"], () => message()),
localeName: plainString(),
vilanticLangs: record(["viossa", "wodox"], () => plainString()),
navbar: record(["whatIsViossa", "resources", "kotoba"], () =>
plainString(),
),
home: {
sections: record(
["whatIsViossa", "historyOfViossa", "community"],
@ -46,15 +65,15 @@ export const localeConfig = {
images: record(["viossaFlag"], () => imageConfig),
},
resources: {
title: message(),
title: plainString(),
resources: { discord: resourceConfig(["join", "rules"]) },
images: record(["discordLogo"], () => imageConfig),
},
kotoba: { title: message(), searchHelp: message() },
kotoba: { title: plainString(), searchHelp: plainString() },
discord: {
rulesPage: {
title: message(),
overview: { title: message(), help: message() },
title: plainString(),
overview: { title: plainString(), help: plainString() },
rules: record(
[
"noTranslation",

View file

@ -1,6 +1,6 @@
import { type InferLocale } from "@/new-i18n-lib/config";
import { type InferLocaleFromConfig } from "@/new-i18n-lib/config";
import {
bundleToUncompiledLocale,
bundleToUncompiledLocaleRecord,
loadFluentBundle,
} from "@/new-i18n-lib/setup";
import { localeConfig } from "./config";
@ -47,7 +47,7 @@ export const localeId = computed({
},
});
export interface Locale extends InferLocale<typeof localeConfig> {}
export interface Locale extends InferLocaleFromConfig<typeof localeConfig> {}
async function loadLocale(
localeId: LocaleId,
@ -95,18 +95,21 @@ function setupLocale(
return localeBundle;
})();
const uncompiledRes = bundleToUncompiledLocale(maybeFallbackedBundle);
if (uncompiledRes.type === "err") {
return uncompiledRes;
const uncompiledLocaleRecordRes = bundleToUncompiledLocaleRecord(
maybeFallbackedBundle,
);
if (uncompiledLocaleRecordRes.type === "err") {
return uncompiledLocaleRecordRes;
}
const uncompiled = uncompiledRes.ok;
const uncompiledLocaleRecord = uncompiledLocaleRecordRes.ok;
const localeRes = compileLocale({
config: localeConfig,
bundle: uncompiled.bundle,
record: uncompiled.record,
bundle: maybeFallbackedBundle,
uncompiled: uncompiledLocaleRecord,
fallback: fallbackLocale,
messageIdChain: [],
});
console.error(localeRes.errors);