feat(i18n): first draft of vi18n lib
This commit is contained in:
parent
f7a504d5a3
commit
4295a83ee1
20 changed files with 194 additions and 132 deletions
|
|
@ -6,7 +6,7 @@ import { vOnClickOutside } from "@vueuse/components";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import SmartLink from "./components/atoms/SmartLink.vue";
|
import SmartLink from "./components/atoms/SmartLink.vue";
|
||||||
import type { SmartDest } from "./utils/smart-dest";
|
import type { SmartDest } from "./utils/smart-dest";
|
||||||
import { useLocale, type Locale } from "./new-i18n";
|
import { useLocale, type Locale } from "./i18n";
|
||||||
|
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
} from "vue";
|
} from "vue";
|
||||||
import MarkdownParts from "./MarkdownParts.vue";
|
import MarkdownParts from "./MarkdownParts.vue";
|
||||||
import OptionalParent from "./OptionalParent.vue";
|
import OptionalParent from "./OptionalParent.vue";
|
||||||
import type { Markdown } from "@/new-i18n-lib/markdown";
|
import type { Markdown } from "@/vi18n-lib/markdown";
|
||||||
import type { CssClass } from "@/utils/css";
|
import type { CssClass } from "@/utils/css";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
@ -58,7 +58,7 @@ const getComponentStack = () => {
|
||||||
// Validate required slots at runtime
|
// Validate required slots at runtime
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const requiredSlots = props.markdown.slots;
|
const requiredSlots = props.markdown.slots;
|
||||||
const missingSlots = requiredSlots.filter(
|
const missingSlots = [...requiredSlots].filter(
|
||||||
(slot) => providedSlots[slot] === undefined,
|
(slot) => providedSlots[slot] === undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
<script setup lang="ts" generic="Slot extends string">
|
<script setup lang="ts" generic="Slot extends string">
|
||||||
import { type DeepReadonly, type VNode } from "vue";
|
import { type DeepReadonly, type VNode } from "vue";
|
||||||
import SmartLink from "../atoms/SmartLink.vue";
|
import SmartLink from "../atoms/SmartLink.vue";
|
||||||
import type { MarkdownSpan } from "@/new-i18n-lib/markdown";
|
import type { MarkdownSpan } from "@/vi18n-lib/markdown";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-arguments
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-arguments
|
||||||
elements: DeepReadonly<MarkdownSpan<Slot>[]>;
|
elements: DeepReadonly<MarkdownSpan<Slot>[]>;
|
||||||
slots: DeepReadonly<Slot[]>;
|
slots: DeepReadonly<ReadonlySet<Slot>>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const vueSlots = defineSlots<{ [K in Slot]: () => VNode[] }>();
|
const vueSlots = defineSlots<{ [K in Slot]: () => VNode[] }>();
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from "vue";
|
|
||||||
import type { RichText, RichTextPart } from "../../i18n/rich"; // relative import for vue sfc compiler
|
|
||||||
|
|
||||||
const props = defineProps<{ content: RichText | RichTextPart[] }>();
|
|
||||||
|
|
||||||
const parts = computed<RichTextPart[]>(() => {
|
|
||||||
const content = props.content;
|
|
||||||
if (Array.isArray(content)) {
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
return content.parts;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(parts.value);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<template v-for="(part, index) in parts" :key="index">
|
|
||||||
<template v-if="typeof part === 'string'">
|
|
||||||
{{ part }}
|
|
||||||
</template>
|
|
||||||
<template v-else-if="part.type === 'bold'">
|
|
||||||
<b><RichText :content="part.bold" /></b>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="part.type === 'italic'">
|
|
||||||
<i><RichText :content="part.italic" /></i>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
import SmartLink from "../atoms/SmartLink.vue";
|
import SmartLink from "../atoms/SmartLink.vue";
|
||||||
import type { Value } from "@/utils/types";
|
import type { Value } from "@/utils/types";
|
||||||
import MarkdownDisplay from "../atoms/MarkdownDisplay.vue";
|
import MarkdownDisplay from "../atoms/MarkdownDisplay.vue";
|
||||||
import type { Locale } from "@/new-i18n";
|
import type { Locale } from "@/i18n";
|
||||||
import type { DeepReadonly } from "vue";
|
import type { DeepReadonly } from "vue";
|
||||||
import { isEmptyMarkdown } from "@/new-i18n-lib/markdown";
|
import { isEmptyMarkdown } from "@/vi18n-lib/markdown";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
overview: DeepReadonly<
|
overview: DeepReadonly<
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Locale } from "@/new-i18n";
|
import type { Locale } from "@/i18n";
|
||||||
import type { Value } from "@/utils/types";
|
import type { Value } from "@/utils/types";
|
||||||
import type { DeepReadonly } from "vue";
|
import type { DeepReadonly } from "vue";
|
||||||
import MarkdownDisplay from "../atoms/MarkdownDisplay.vue";
|
import MarkdownDisplay from "../atoms/MarkdownDisplay.vue";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { LOCALE_IDS, localeId, useLocale, type LocaleId } from "@/new-i18n";
|
import { LOCALE_IDS, localeId, useLocale, type LocaleId } from "@/i18n";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { vOnClickOutside } from "@vueuse/components";
|
import { vOnClickOutside } from "@vueuse/components";
|
||||||
import DropdownItem from "../atoms/DropdownItem.vue";
|
import DropdownItem from "../atoms/DropdownItem.vue";
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import {
|
||||||
type InferLocaleFromConfig,
|
type InferLocaleFromConfig,
|
||||||
type LocaleConfig,
|
type LocaleConfig,
|
||||||
type ConfigString,
|
type ConfigString,
|
||||||
} from "@/new-i18n-lib/config";
|
} from "@/vi18n-lib/config";
|
||||||
|
|
||||||
function plainString(): ConfigString<object> {
|
function plainString(): ConfigString<object> {
|
||||||
return string({ placeables: {} });
|
return string({ placeables: {} });
|
||||||
|
|
@ -29,24 +29,28 @@ const discordRuleConfig = {
|
||||||
overview: {
|
overview: {
|
||||||
text: markdown({
|
text: markdown({
|
||||||
placeables: {},
|
placeables: {},
|
||||||
features: { bold: true, italic: true, link: true, slots: {} },
|
slots: {},
|
||||||
|
bold: true,
|
||||||
|
italic: true,
|
||||||
|
link: true,
|
||||||
}),
|
}),
|
||||||
subtext: markdown({
|
subtext: markdown({
|
||||||
placeables: {},
|
placeables: {},
|
||||||
features: { bold: true, italic: true, link: true, slots: {} },
|
slots: {},
|
||||||
|
bold: true,
|
||||||
|
italic: true,
|
||||||
|
link: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
section: {
|
section: {
|
||||||
header: string({ placeables: { ruleNumber: { type: "number" } } }),
|
header: string({ placeables: { ruleNumber: { type: "number" } } }),
|
||||||
body: markdown({
|
body: markdown({
|
||||||
placeables: {},
|
placeables: {},
|
||||||
features: {
|
slots: {},
|
||||||
bold: true,
|
bold: true,
|
||||||
header: true,
|
header: true,
|
||||||
italic: true,
|
italic: true,
|
||||||
link: true,
|
link: true,
|
||||||
slots: {},
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { type InferLocaleFromConfig } from "@/new-i18n-lib/config";
|
import { type InferLocaleFromConfig } from "@/vi18n-lib/config";
|
||||||
import {
|
import {
|
||||||
bundleToUncompiledLocaleRecord,
|
bundleToUncompiledLocaleRecord,
|
||||||
loadFluentBundle,
|
loadFluentBundle,
|
||||||
} from "@/new-i18n-lib/setup";
|
} from "@/vi18n-lib/setup";
|
||||||
import { localeConfig } from "./config";
|
import { localeConfig } from "./config";
|
||||||
import type { Result } from "@/utils/types";
|
import type { Result } from "@/utils/types";
|
||||||
import { useLocalStorage } from "@vueuse/core";
|
import { useLocalStorage } from "@vueuse/core";
|
||||||
|
|
@ -12,7 +12,7 @@ import enUsFtlSrc from "@/assets/locale/en_US.ftl";
|
||||||
import vpVlFtlSrc from "@/assets/locale/vp_VL.ftl";
|
import vpVlFtlSrc from "@/assets/locale/vp_VL.ftl";
|
||||||
import wpVlFtlSrc from "@/assets/locale/wp_VL.ftl";
|
import wpVlFtlSrc from "@/assets/locale/wp_VL.ftl";
|
||||||
import type { FluentBundle } from "@fluent/bundle";
|
import type { FluentBundle } from "@fluent/bundle";
|
||||||
import { compileLocale } from "@/new-i18n-lib/compile";
|
import { compileLocale } from "@/vi18n-lib/compile";
|
||||||
|
|
||||||
export const LOCALE_IDS = ["en-US", "vp-VL", "wp-VL"] as const;
|
export const LOCALE_IDS = ["en-US", "vp-VL", "wp-VL"] as const;
|
||||||
|
|
||||||
|
|
@ -68,6 +68,7 @@ interface SetupLocaleFallback {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupLocale(
|
function setupLocale(
|
||||||
|
localeId: LocaleId,
|
||||||
localeBundle: FluentBundle,
|
localeBundle: FluentBundle,
|
||||||
fallback: SetupLocaleFallback | undefined,
|
fallback: SetupLocaleFallback | undefined,
|
||||||
): Result<Locale, string> {
|
): Result<Locale, string> {
|
||||||
|
|
@ -112,7 +113,15 @@ function setupLocale(
|
||||||
messageIdChain: [],
|
messageIdChain: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
console.error(localeRes.errors);
|
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;
|
const locale = localeRes.locale;
|
||||||
return { type: "ok", ok: locale };
|
return { type: "ok", ok: locale };
|
||||||
|
|
@ -135,7 +144,9 @@ function deepReadonly<T>(value: T): DeepReadonly<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_LOCALE_BUNDLE = 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 DEFAULT_LOCALE = unwrap(
|
||||||
|
setupLocale(DEFAULT_LOCALE_ID, DEFAULT_LOCALE_BUNDLE, undefined),
|
||||||
|
);
|
||||||
|
|
||||||
const doItAllForLocale = async (
|
const doItAllForLocale = async (
|
||||||
localeId: LocaleId,
|
localeId: LocaleId,
|
||||||
|
|
@ -143,10 +154,11 @@ const doItAllForLocale = async (
|
||||||
): Promise<DeepReadonly<Locale>> =>
|
): Promise<DeepReadonly<Locale>> =>
|
||||||
deepReadonly(
|
deepReadonly(
|
||||||
unwrap(
|
unwrap(
|
||||||
setupLocale(unwrap(await loadLocale(localeId, localeFtlSrc)), {
|
setupLocale(
|
||||||
bundle: DEFAULT_LOCALE_BUNDLE,
|
localeId,
|
||||||
locale: DEFAULT_LOCALE,
|
unwrap(await loadLocale(localeId, localeFtlSrc)),
|
||||||
}),
|
{ bundle: DEFAULT_LOCALE_BUNDLE, locale: DEFAULT_LOCALE },
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import DiscordRuleOverview from "@/components/molecules/DiscordRuleOverview.vue";
|
import DiscordRuleOverview from "@/components/molecules/DiscordRuleOverview.vue";
|
||||||
import DiscordRuleSection from "@/components/molecules/DiscordRuleSection.vue";
|
import DiscordRuleSection from "@/components/molecules/DiscordRuleSection.vue";
|
||||||
import { useLocale } from "@/new-i18n";
|
import { useLocale } from "@/i18n";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import HomeSectionWrapper from "@/components/molecules/HomeSectionWrapper.vue";
|
import HomeSectionWrapper from "@/components/molecules/HomeSectionWrapper.vue";
|
||||||
import { GREETINGS, type Greeting } from "@/new-i18n/greeting";
|
import { GREETINGS, type Greeting } from "@/i18n/greeting";
|
||||||
import { VILANTIC_ID_TO_FLAG } from "@/new-i18n/vilantic";
|
import { VILANTIC_ID_TO_FLAG } from "@/i18n/vilantic";
|
||||||
import { randomElement } from "@/utils/random";
|
import { randomElement } from "@/utils/random";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import flakkaImg from "@/assets/flakka.png";
|
import flakkaImg from "@/assets/flakka.png";
|
||||||
import { useLocale, type Locale } from "@/new-i18n";
|
import { useLocale, type Locale } from "@/i18n";
|
||||||
import type * as i18n from "@/new-i18n/config";
|
import type * as i18n from "@/i18n/config";
|
||||||
|
|
||||||
interface SectionConfig {
|
interface SectionConfig {
|
||||||
id: keyof Locale["home"]["sections"];
|
id: keyof Locale["home"]["sections"];
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useLocale } from "@/new-i18n";
|
import { useLocale } from "@/i18n";
|
||||||
|
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
import LearningResourceWrapper, {
|
import LearningResourceWrapper, {
|
||||||
type ResourceButton,
|
type ResourceButton,
|
||||||
} from "@/components/molecules/LearningResourceWrapper.vue";
|
} from "@/components/molecules/LearningResourceWrapper.vue";
|
||||||
import { useLocale, type Locale } from "@/new-i18n";
|
import { useLocale, type Locale } from "@/i18n";
|
||||||
import type * as i18n from "@/new-i18n/config";
|
import type * as i18n from "@/i18n/config";
|
||||||
import { ignore } from "@/utils/ignore";
|
import { ignore } from "@/utils/ignore";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import discordImg from "@/assets/discord.png";
|
import discordImg from "@/assets/discord.png";
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,9 @@ type GenericMarkdownMessageFn = (
|
||||||
placeableArgs?: GenericMessageFnPlaceableArgs,
|
placeableArgs?: GenericMessageFnPlaceableArgs,
|
||||||
) => Markdown;
|
) => Markdown;
|
||||||
|
|
||||||
type GenericMessageFnPlaceableArgs = Record<string, FluentVariable>;
|
type GenericMessageFnPlaceableArgs = Record<string, PlaceableValue>;
|
||||||
|
|
||||||
|
type PlaceableValue = string | number;
|
||||||
|
|
||||||
export interface CompileLocaleCtx<Config extends LocaleConfig> {
|
export interface CompileLocaleCtx<Config extends LocaleConfig> {
|
||||||
bundle: FluentBundle;
|
bundle: FluentBundle;
|
||||||
|
|
@ -42,7 +44,7 @@ export interface CompileLocaleCtx<Config extends LocaleConfig> {
|
||||||
|
|
||||||
export interface CompileLocaleRes<Locale> {
|
export interface CompileLocaleRes<Locale> {
|
||||||
locale: Locale;
|
locale: Locale;
|
||||||
errors: string[];
|
errors: readonly string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function compileLocale<Config extends LocaleConfig>(
|
export function compileLocale<Config extends LocaleConfig>(
|
||||||
|
|
@ -122,7 +124,7 @@ export function compileLocale<Config extends LocaleConfig>(
|
||||||
return () =>
|
return () =>
|
||||||
createMissingMarkdownFallback(
|
createMissingMarkdownFallback(
|
||||||
valueMessageIdChain,
|
valueMessageIdChain,
|
||||||
Object.keys(configValue.features.slots),
|
new Set(Object.keys(configValue.slots)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -263,7 +265,7 @@ function compileMessage(
|
||||||
case configMarkdownSymbol: {
|
case configMarkdownSymbol: {
|
||||||
return compileMarkdownMessage({
|
return compileMarkdownMessage({
|
||||||
bundle,
|
bundle,
|
||||||
configMarkdown: configValue,
|
slots: new Set(Object.keys(configValue.slots)),
|
||||||
messageIdChain,
|
messageIdChain,
|
||||||
allVariants,
|
allVariants,
|
||||||
pattern,
|
pattern,
|
||||||
|
|
@ -309,7 +311,7 @@ function compileStringMessage(
|
||||||
// TODO: will need to make sure markdown/slots are escapes when inserting variable values
|
// TODO: will need to make sure markdown/slots are escapes when inserting variable values
|
||||||
return {
|
return {
|
||||||
type: "ok",
|
type: "ok",
|
||||||
ok: (args: Record<string, FluentVariable> = {}) => {
|
ok: (args: Record<string, PlaceableValue> = {}) => {
|
||||||
const stringRes = ((): Result<string, string> => {
|
const stringRes = ((): Result<string, string> => {
|
||||||
const stringLiteralRes = parseMessageLiteral(
|
const stringLiteralRes = parseMessageLiteral(
|
||||||
"string",
|
"string",
|
||||||
|
|
@ -361,7 +363,7 @@ function compileStringMessage(
|
||||||
interface CompileMarkdownMessageCtx {
|
interface CompileMarkdownMessageCtx {
|
||||||
bundle: FluentBundle;
|
bundle: FluentBundle;
|
||||||
messageIdChain: readonly [...string[], string];
|
messageIdChain: readonly [...string[], string];
|
||||||
configMarkdown: ConfigMarkdown<object, object>;
|
slots: ReadonlySet<string>;
|
||||||
allVariants: readonly PatternVariant[];
|
allVariants: readonly PatternVariant[];
|
||||||
pattern: Pattern;
|
pattern: Pattern;
|
||||||
}
|
}
|
||||||
|
|
@ -369,13 +371,10 @@ interface CompileMarkdownMessageCtx {
|
||||||
function compileMarkdownMessage(
|
function compileMarkdownMessage(
|
||||||
ctx: CompileMarkdownMessageCtx,
|
ctx: CompileMarkdownMessageCtx,
|
||||||
): Result<GenericMarkdownMessageFn, string> {
|
): Result<GenericMarkdownMessageFn, string> {
|
||||||
const { bundle, messageIdChain, configMarkdown, allVariants, pattern } =
|
const { bundle, messageIdChain, slots, allVariants, pattern } = ctx;
|
||||||
ctx;
|
|
||||||
|
|
||||||
// typecheck markdown
|
// typecheck markdown
|
||||||
|
|
||||||
const markdownSlots = Object.keys(configMarkdown.features.slots);
|
|
||||||
|
|
||||||
// check if all variants are valid markdown
|
// check if all variants are valid markdown
|
||||||
for (const variant of allVariants) {
|
for (const variant of allVariants) {
|
||||||
const markdownLiteralRes = parseMessageLiteral("md", variant.string);
|
const markdownLiteralRes = parseMessageLiteral("md", variant.string);
|
||||||
|
|
@ -388,7 +387,7 @@ function compileMarkdownMessage(
|
||||||
}
|
}
|
||||||
|
|
||||||
const markdownLiteral = markdownLiteralRes.ok;
|
const markdownLiteral = markdownLiteralRes.ok;
|
||||||
const markdownRes = parseMarkdown(markdownLiteral, markdownSlots);
|
const markdownRes = parseMarkdown(markdownLiteral, slots);
|
||||||
|
|
||||||
if (markdownRes.type === "err") {
|
if (markdownRes.type === "err") {
|
||||||
return {
|
return {
|
||||||
|
|
@ -401,11 +400,67 @@ function compileMarkdownMessage(
|
||||||
// TODO: will need to make sure markdown/slots are escapes when inserting variable values
|
// TODO: will need to make sure markdown/slots are escapes when inserting variable values
|
||||||
return {
|
return {
|
||||||
type: "ok",
|
type: "ok",
|
||||||
ok: (args: Record<string, FluentVariable> = {}): Markdown => {
|
ok: (args: Record<string, PlaceableValue> = {}): Markdown => {
|
||||||
|
const escapedArgs = Object.fromEntries(
|
||||||
|
Object.entries(args).map(([id, value]) => {
|
||||||
|
const escapedValue = (() => {
|
||||||
|
switch (typeof value) {
|
||||||
|
case "number": {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
case "string": {
|
||||||
|
return value
|
||||||
|
.split("")
|
||||||
|
.map((c) => {
|
||||||
|
switch (c) {
|
||||||
|
case "\\": {
|
||||||
|
return "\\\\";
|
||||||
|
}
|
||||||
|
case "*": {
|
||||||
|
return "\\*";
|
||||||
|
}
|
||||||
|
case "#": {
|
||||||
|
return "\\#";
|
||||||
|
}
|
||||||
|
case "[": {
|
||||||
|
return "\\[";
|
||||||
|
}
|
||||||
|
case "]": {
|
||||||
|
return "\\]";
|
||||||
|
}
|
||||||
|
case "(": {
|
||||||
|
return "\\(";
|
||||||
|
}
|
||||||
|
case ")": {
|
||||||
|
return "\\)";
|
||||||
|
}
|
||||||
|
case "-": {
|
||||||
|
return "\\-";
|
||||||
|
}
|
||||||
|
case "<": {
|
||||||
|
return "\\<";
|
||||||
|
}
|
||||||
|
case ">": {
|
||||||
|
return "\\>";
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return [id, escapedValue] as const;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const markdownRes = ((): Result<Markdown, string> => {
|
const markdownRes = ((): Result<Markdown, string> => {
|
||||||
const markdownLiteralRes = parseMessageLiteral(
|
const markdownLiteralRes = parseMessageLiteral(
|
||||||
"md",
|
"md",
|
||||||
bundle.formatPattern(pattern, args),
|
bundle.formatPattern(pattern, escapedArgs),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (markdownLiteralRes.type === "err") {
|
if (markdownLiteralRes.type === "err") {
|
||||||
|
|
@ -418,7 +473,7 @@ function compileMarkdownMessage(
|
||||||
}
|
}
|
||||||
|
|
||||||
const markdownLiteral = markdownLiteralRes.ok;
|
const markdownLiteral = markdownLiteralRes.ok;
|
||||||
const res = parseMarkdown(markdownLiteral, markdownSlots);
|
const res = parseMarkdown(markdownLiteral, slots);
|
||||||
|
|
||||||
if (res.type === "err") {
|
if (res.type === "err") {
|
||||||
// This should hopefully never happen since we've already
|
// This should hopefully never happen since we've already
|
||||||
|
|
@ -440,10 +495,7 @@ function compileMarkdownMessage(
|
||||||
case "err": {
|
case "err": {
|
||||||
const error = markdownRes.err;
|
const error = markdownRes.err;
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return createMissingMarkdownFallback(
|
return createMissingMarkdownFallback(messageIdChain, slots);
|
||||||
messageIdChain,
|
|
||||||
markdownSlots,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -458,7 +510,7 @@ function createMissingStringFallback(
|
||||||
|
|
||||||
function createMissingMarkdownFallback<Slot extends string>(
|
function createMissingMarkdownFallback<Slot extends string>(
|
||||||
messageIdChain: readonly [...string[], string],
|
messageIdChain: readonly [...string[], string],
|
||||||
slots: Slot[],
|
slots: ReadonlySet<Slot>,
|
||||||
): Markdown<Slot> {
|
): Markdown<Slot> {
|
||||||
return {
|
return {
|
||||||
elements: [
|
elements: [
|
||||||
|
|
@ -18,7 +18,12 @@ export interface ConfigMarkdown<
|
||||||
> {
|
> {
|
||||||
[configMessageTypeSymbol]: typeof configMarkdownSymbol;
|
[configMessageTypeSymbol]: typeof configMarkdownSymbol;
|
||||||
placeables: Placeables;
|
placeables: Placeables;
|
||||||
features: ConfigMarkdownFeatures<Slots>;
|
slots: Slots;
|
||||||
|
bold?: boolean;
|
||||||
|
italic?: boolean;
|
||||||
|
header?: boolean;
|
||||||
|
link?: boolean;
|
||||||
|
ulist?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigPlaceableInfo<
|
export interface ConfigPlaceableInfo<
|
||||||
|
|
@ -30,26 +35,12 @@ export interface ConfigPlaceableInfo<
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||||
export interface ConfigSlotInfo {}
|
export interface ConfigSlotInfo {}
|
||||||
|
|
||||||
export interface ConfigMarkdownFeatures<
|
|
||||||
Slots extends Partial<Record<string, ConfigSlotInfo>>,
|
|
||||||
> {
|
|
||||||
bold?: boolean;
|
|
||||||
italic?: boolean;
|
|
||||||
header?: boolean;
|
|
||||||
link?: boolean;
|
|
||||||
ulist?: boolean;
|
|
||||||
slots: Slots;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function string<
|
export function string<
|
||||||
const Placeables extends Partial<Record<string, ConfigPlaceableInfo>>,
|
const Placeables extends Partial<Record<string, ConfigPlaceableInfo>>,
|
||||||
>(
|
>(
|
||||||
opt: Omit<ConfigString<Placeables>, typeof configMessageTypeSymbol>,
|
opt: Omit<ConfigString<Placeables>, typeof configMessageTypeSymbol>,
|
||||||
): ConfigString<Placeables> {
|
): ConfigString<Placeables> {
|
||||||
return {
|
return { ...opt, [configMessageTypeSymbol]: configStringSymbol };
|
||||||
[configMessageTypeSymbol]: configStringSymbol,
|
|
||||||
placeables: opt.placeables,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function markdown<
|
export function markdown<
|
||||||
|
|
@ -61,11 +52,7 @@ export function markdown<
|
||||||
typeof configMessageTypeSymbol
|
typeof configMessageTypeSymbol
|
||||||
>,
|
>,
|
||||||
): ConfigMarkdown<Placeables, Slots> {
|
): ConfigMarkdown<Placeables, Slots> {
|
||||||
return {
|
return { ...opt, [configMessageTypeSymbol]: configMarkdownSymbol };
|
||||||
[configMessageTypeSymbol]: configMarkdownSymbol,
|
|
||||||
placeables: opt.placeables,
|
|
||||||
features: opt.features,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LocaleConfig = {
|
export type LocaleConfig = {
|
||||||
|
|
@ -8,35 +8,42 @@ import type { RouteNamedMap } from "vue-router/auto-routes";
|
||||||
import { routes } from "vue-router/auto-routes";
|
import { routes } from "vue-router/auto-routes";
|
||||||
|
|
||||||
export interface Markdown<Slot extends string = string> {
|
export interface Markdown<Slot extends string = string> {
|
||||||
elements: MarkdownElement<Slot>[];
|
elements: readonly MarkdownElement<Slot>[];
|
||||||
slots: Slot[];
|
slots: ReadonlySet<Slot>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MarkdownElement<Slot extends string = string> =
|
export type MarkdownElement<Slot extends string = string> =
|
||||||
| { type: "paragraph"; paragraph: { spans: MarkdownSpan<Slot>[] } }
|
| { type: "paragraph"; paragraph: { spans: readonly MarkdownSpan<Slot>[] } }
|
||||||
| { type: "header"; header: { spans: MarkdownSpan<Slot>[] } }
|
| { type: "header"; header: { spans: readonly MarkdownSpan<Slot>[] } }
|
||||||
| { type: "ulist"; ulist: { items: MarkdownSpan<Slot>[][] } };
|
| {
|
||||||
|
type: "ulist";
|
||||||
|
ulist: { items: readonly (readonly MarkdownSpan<Slot>[])[] };
|
||||||
|
};
|
||||||
|
|
||||||
type MarkdownLine<Slot extends string = string> = {
|
type MarkdownLine<Slot extends string = string> = {
|
||||||
type: "paragraph" | "header" | "ulistItem";
|
type: "paragraph" | "header" | "ulistItem";
|
||||||
spans: MarkdownSpan<Slot>[];
|
spans: readonly MarkdownSpan<Slot>[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MarkdownFeature = "header" | "ulist" | "italic" | "bold" | "link";
|
export type MarkdownFeature = "header" | "ulist" | "italic" | "bold" | "link";
|
||||||
|
|
||||||
export type MarkdownSpan<Slot extends string = string> =
|
export type MarkdownSpan<Slot extends string = string> =
|
||||||
| { type: "plain"; plain: string }
|
| { type: "plain"; plain: string }
|
||||||
| { type: "italic"; italic: MarkdownSpan[] }
|
| { type: "italic"; italic: readonly MarkdownSpan[] }
|
||||||
| { type: "bold"; bold: MarkdownSpan[] }
|
| { type: "bold"; bold: readonly MarkdownSpan[] }
|
||||||
| {
|
| {
|
||||||
type: "link";
|
type: "link";
|
||||||
link: { label: MarkdownSpan[]; to: SmartDest; newTab: boolean };
|
link: {
|
||||||
|
label: readonly MarkdownSpan[];
|
||||||
|
to: SmartDest;
|
||||||
|
newTab: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
| { type: "slot"; slot: Slot };
|
| { type: "slot"; slot: Slot };
|
||||||
|
|
||||||
export function parseMarkdown<Slot extends string>(
|
export function parseMarkdown<Slot extends string>(
|
||||||
markdownString: string,
|
markdownString: string,
|
||||||
slots: readonly Slot[],
|
slots: ReadonlySet<Slot>,
|
||||||
): Result<Markdown<Slot>, string> {
|
): Result<Markdown<Slot>, string> {
|
||||||
const linesRes = parseMarkdownLines(markdownString, slots);
|
const linesRes = parseMarkdownLines(markdownString, slots);
|
||||||
if (linesRes.type === "err") {
|
if (linesRes.type === "err") {
|
||||||
|
|
@ -63,7 +70,9 @@ export function parseMarkdown<Slot extends string>(
|
||||||
return { type: "header", header: { spans: line.spans } };
|
return { type: "header", header: { spans: line.spans } };
|
||||||
}
|
}
|
||||||
case "ulistItem": {
|
case "ulistItem": {
|
||||||
const items: MarkdownSpan<Slot>[][] = [line.spans];
|
const items: (readonly MarkdownSpan<Slot>[])[] = [
|
||||||
|
line.spans,
|
||||||
|
];
|
||||||
while (true) {
|
while (true) {
|
||||||
const peekLine = lines[0];
|
const peekLine = lines[0];
|
||||||
if (
|
if (
|
||||||
|
|
@ -85,12 +94,12 @@ export function parseMarkdown<Slot extends string>(
|
||||||
elements.push(element);
|
elements.push(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { type: "ok", ok: { elements, slots: [...slots] } };
|
return { type: "ok", ok: { elements, slots: new Set(slots) } };
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseMarkdownLines<Slot extends string>(
|
function parseMarkdownLines<Slot extends string>(
|
||||||
markdownString: string,
|
markdownString: string,
|
||||||
slots: readonly Slot[],
|
slots: ReadonlySet<Slot>,
|
||||||
): Result<MarkdownLine<Slot>[], string> {
|
): Result<MarkdownLine<Slot>[], string> {
|
||||||
if (markdownString.trim() === "--") {
|
if (markdownString.trim() === "--") {
|
||||||
return { type: "ok", ok: [] };
|
return { type: "ok", ok: [] };
|
||||||
|
|
@ -142,7 +151,7 @@ function parseMarkdownLines<Slot extends string>(
|
||||||
|
|
||||||
function parseMarkdownLine<Slot extends string>(
|
function parseMarkdownLine<Slot extends string>(
|
||||||
line: string,
|
line: string,
|
||||||
slots: readonly Slot[],
|
slots: ReadonlySet<Slot>,
|
||||||
): Result<MarkdownLine<Slot>, string> {
|
): Result<MarkdownLine<Slot>, string> {
|
||||||
interface ResolvedLine {
|
interface ResolvedLine {
|
||||||
deprefixedLine: string;
|
deprefixedLine: string;
|
||||||
|
|
@ -174,7 +183,7 @@ function parseMarkdownLine<Slot extends string>(
|
||||||
|
|
||||||
function parseMarkdownSpans<Slot extends string>(
|
function parseMarkdownSpans<Slot extends string>(
|
||||||
line: string,
|
line: string,
|
||||||
slots: readonly Slot[],
|
slots: ReadonlySet<Slot>,
|
||||||
): Result<MarkdownSpan<Slot>[], string> {
|
): Result<MarkdownSpan<Slot>[], string> {
|
||||||
if (line.startsWith("#")) {
|
if (line.startsWith("#")) {
|
||||||
// subheaders may be supported in the future,
|
// subheaders may be supported in the future,
|
||||||
|
|
@ -259,7 +268,7 @@ class ParseMarkdownSpansManager {
|
||||||
|
|
||||||
function readMarkdownSpans<Slot extends string>(
|
function readMarkdownSpans<Slot extends string>(
|
||||||
chars: string[],
|
chars: string[],
|
||||||
slots: readonly Slot[],
|
slots: ReadonlySet<Slot>,
|
||||||
manager: ParseMarkdownSpansManager,
|
manager: ParseMarkdownSpansManager,
|
||||||
): Result<MarkdownSpan<Slot>[], string> {
|
): Result<MarkdownSpan<Slot>[], string> {
|
||||||
const spans: MarkdownSpan<Slot>[] = [];
|
const spans: MarkdownSpan<Slot>[] = [];
|
||||||
|
|
@ -282,7 +291,7 @@ function readMarkdownSpans<Slot extends string>(
|
||||||
|
|
||||||
function readMarkdownSpan<Slot extends string>(
|
function readMarkdownSpan<Slot extends string>(
|
||||||
chars: string[],
|
chars: string[],
|
||||||
slots: readonly Slot[],
|
slots: ReadonlySet<Slot>,
|
||||||
manager: ParseMarkdownSpansManager,
|
manager: ParseMarkdownSpansManager,
|
||||||
): Result<MarkdownSpan<Slot> | undefined, string> {
|
): Result<MarkdownSpan<Slot> | undefined, string> {
|
||||||
const [firstChar, secondChar, thirdChar] = chars;
|
const [firstChar, secondChar, thirdChar] = chars;
|
||||||
|
|
@ -307,14 +316,43 @@ function readMarkdownSpan<Slot extends string>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInArray<T, U extends T>(value: T, array: readonly U[]): value is U {
|
function iterableIsArray<T>(iterable: Iterable<T>): iterable is readonly T[] {
|
||||||
|
return Array.isArray(iterable);
|
||||||
|
}
|
||||||
|
|
||||||
|
function iterableIsSet<T>(iterable: Iterable<T>): iterable is ReadonlySet<T> {
|
||||||
|
return iterable instanceof Set;
|
||||||
|
}
|
||||||
|
|
||||||
|
function iterableContains<T, U extends T>(
|
||||||
|
iterable: Iterable<U>,
|
||||||
|
value: T,
|
||||||
|
): value is U {
|
||||||
// SAFETY: this is just an equality check, it is safe to pass in any value
|
// SAFETY: this is just an equality check, it is safe to pass in any value
|
||||||
return array.includes(value as U);
|
const target = value as U;
|
||||||
|
|
||||||
|
// specializations
|
||||||
|
if (iterableIsArray(iterable)) {
|
||||||
|
return iterable.includes(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iterableIsSet(iterable)) {
|
||||||
|
return iterable.has(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// generic case
|
||||||
|
for (const x of iterable) {
|
||||||
|
if (x === target) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readMarkdownSpanSlot<Slot extends string>(
|
function readMarkdownSpanSlot<Slot extends string>(
|
||||||
chars: string[],
|
chars: string[],
|
||||||
slots: readonly Slot[],
|
slots: ReadonlySet<Slot>,
|
||||||
): Result<MarkdownSpan<Slot>, string> {
|
): Result<MarkdownSpan<Slot>, string> {
|
||||||
const openAngleRes = expectReadChar(chars, "<");
|
const openAngleRes = expectReadChar(chars, "<");
|
||||||
if (openAngleRes.type === "err") {
|
if (openAngleRes.type === "err") {
|
||||||
|
|
@ -332,7 +370,7 @@ function readMarkdownSpanSlot<Slot extends string>(
|
||||||
}
|
}
|
||||||
|
|
||||||
const slotName = slotNameRes.ok;
|
const slotName = slotNameRes.ok;
|
||||||
if (!isInArray(slotName, slots)) {
|
if (!iterableContains(slots, slotName)) {
|
||||||
return { type: "err", err: `Unexpected slot name: ${slotName}` };
|
return { type: "err", err: `Unexpected slot name: ${slotName}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -341,7 +379,7 @@ function readMarkdownSpanSlot<Slot extends string>(
|
||||||
|
|
||||||
function readMarkdownSpanLink<Slot extends string>(
|
function readMarkdownSpanLink<Slot extends string>(
|
||||||
chars: string[],
|
chars: string[],
|
||||||
slots: readonly Slot[],
|
slots: ReadonlySet<Slot>,
|
||||||
manager: ParseMarkdownSpansManager,
|
manager: ParseMarkdownSpansManager,
|
||||||
): Result<MarkdownSpan<Slot>, string> {
|
): Result<MarkdownSpan<Slot>, string> {
|
||||||
const openSquareRes = expectReadChar(chars, "[");
|
const openSquareRes = expectReadChar(chars, "[");
|
||||||
|
|
@ -614,7 +652,7 @@ function validateInternalDest(dest: string): Result<SmartInternalDest, string> {
|
||||||
|
|
||||||
function readMarkdownSpanItalic<Slot extends string>(
|
function readMarkdownSpanItalic<Slot extends string>(
|
||||||
chars: string[],
|
chars: string[],
|
||||||
slots: readonly Slot[],
|
slots: ReadonlySet<Slot>,
|
||||||
manager: ParseMarkdownSpansManager,
|
manager: ParseMarkdownSpansManager,
|
||||||
): Result<MarkdownSpan<Slot>, string> {
|
): Result<MarkdownSpan<Slot>, string> {
|
||||||
return manager.tryUseItalic(() => {
|
return manager.tryUseItalic(() => {
|
||||||
|
|
@ -659,7 +697,7 @@ function readMarkdownSpanItalic<Slot extends string>(
|
||||||
|
|
||||||
function readMarkdownSpanBold<Slot extends string>(
|
function readMarkdownSpanBold<Slot extends string>(
|
||||||
chars: string[],
|
chars: string[],
|
||||||
slots: readonly Slot[],
|
slots: ReadonlySet<Slot>,
|
||||||
manager: ParseMarkdownSpansManager,
|
manager: ParseMarkdownSpansManager,
|
||||||
): Result<MarkdownSpan<Slot>, string> {
|
): Result<MarkdownSpan<Slot>, string> {
|
||||||
return manager.tryUseBold(() => {
|
return manager.tryUseBold(() => {
|
||||||
|
|
@ -704,7 +742,7 @@ function readMarkdownSpanBold<Slot extends string>(
|
||||||
|
|
||||||
function readMarkdownSpanBoldItalic<Slot extends string>(
|
function readMarkdownSpanBoldItalic<Slot extends string>(
|
||||||
chars: string[],
|
chars: string[],
|
||||||
slots: readonly Slot[],
|
slots: ReadonlySet<Slot>,
|
||||||
manager: ParseMarkdownSpansManager,
|
manager: ParseMarkdownSpansManager,
|
||||||
): Result<MarkdownSpan<Slot>, string> {
|
): Result<MarkdownSpan<Slot>, string> {
|
||||||
return manager.tryUseBold(() =>
|
return manager.tryUseBold(() =>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue