Auto Router, Discord Rules Page, & Vi18n lib (#52)

feat: Auto Router, Discord Rules Page, & Vi18n lib (#52)
This commit is contained in:
Benjamin Singleton 2026-04-04 03:03:41 -05:00 committed by GitHub
parent 262b8c578f
commit 8c59485898
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 3479 additions and 782 deletions

View file

@ -1,24 +1,62 @@
import js from '@eslint/js';
import globals from 'globals';
import ts from 'typescript-eslint';
import vue from 'eslint-plugin-vue';
import js from "@eslint/js";
import globals from "globals";
import ts from "typescript-eslint";
import vue from "eslint-plugin-vue";
import { defineConfig, globalIgnores } from "eslint/config";
import vueParser from "vue-eslint-parser";
export default ts.config(
{ ignores: ['dist'] },
export default defineConfig([
globalIgnores(["dist"]),
{
extends: [
js.configs.recommended,
...ts.configs.recommendedTypeChecked,
...vue.configs.recommendedTypeChecked,
ts.configs.strictTypeChecked,
...vue.configs["flat/essential"],
],
files: ['**/*.{js,ts,vue}'],
files: ["./src/**/*.{js,ts,vue}"],
plugins: { vue },
languageOptions: {
ecmaVersion: 2020,
ecmaVersion: "latest",
sourceType: "module",
globals: globals.browser,
parser: vueParser,
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
parser: ts.parser,
extraFileExtensions: [".vue"],
},
},
rules: {
"vue/no-restricted-html-elements": [
"error",
{
element: ["a", "RouterLink"],
message: "Use <SmartLink> instead.",
},
)
{ element: ["i18n-t"], message: "Use <RichTemplate> instead." },
{
element: ["RichTemplateParts"],
message:
"Do not use the internal <RichTemplateParts> component. Use <RichTemplate> instead.",
},
],
// allow interfaces to only extend another interface without adding properties
// good for aliasing more complex types
"@typescript-eslint/no-empty-object-type": [
"error",
{ 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
{
files: ["src/pages/**/*.vue"],
rules: { "vue/multi-word-component-names": "off" },
},
]);

View file

@ -1,15 +1,19 @@
<!doctype html>
<html lang="en" class="has-navbar-fixed-top">
<head>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/viossa_circle.svg"/>
<link rel="icon" type="image/svg+xml" href="/viossa_circle.svg" />
<link href='https://cdn.boxicons.com/fonts/basic/boxicons.min.css' rel='stylesheet'>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap" />
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Viossa.net</title>
</head>
<body>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</body>
</html>

View file

@ -9,8 +9,9 @@
"preview": "vite preview"
},
"dependencies": {
"@fluent/bundle": "^0.19.1",
"@tailwindcss/vite": "^4.1.6",
"@types/node": "^22.15.17",
"@types/node": "^22.15.31",
"@vueuse/components": "^13.3.0",
"@vueuse/core": "^13.3.0",
"arktype": "^2.1.29",
@ -22,17 +23,20 @@
"vue-router": "^4.5.1"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
"@repo/common": "workspace:*",
"@vitejs/plugin-vue": "^5.2.3",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.26.0",
"eslint-plugin-vue": "^10.1.0",
"eslint": "^9.39.2",
"eslint-plugin-vue": "^10.7.0",
"globals": "^17.3.0",
"prettier": "^3.5.3",
"sass": "^1.87.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.32.1",
"typescript-eslint": "^8.55.0",
"unplugin-vue-router": "^0.12.0",
"vite": "^6.3.5",
"vue-eslint-parser": "^10.2.0",
"vue-tsc": "^2.2.8"
},
"packageManager": "pnpm@10.11.0"

View file

@ -1,9 +1,14 @@
<script setup lang="ts">
import "./assets/style.scss";
import { ref, type Ref } from "vue";
import { computed, ref, type Ref } from "vue";
import LocalePicker from "./components/organisms/LocalePicker.vue";
import { vOnClickOutside } from "@vueuse/components";
import { useLocale } from "./i18n";
import { useRouter } from "vue-router";
import SmartLink from "./components/atoms/SmartLink.vue";
import type { SmartDest } from "./utils/smart-dest";
import { useLocale, type Locale } from "./i18n";
const locale = useLocale();
const burgerOpen: Ref<boolean> = ref<boolean>(false);
@ -15,7 +20,46 @@ const closeBurger = (): void => {
burgerOpen.value = false;
};
const locale = useLocale();
const router = useRouter();
router.beforeEach(() => {
closeBurger();
});
interface NavbarItem {
to: SmartDest;
label: string;
}
const NAVBAR_ITEM_ORDER = [
"whatIsViossa",
"resources",
"kotoba",
] as const satisfies (keyof Locale["navbar"])[];
const navbarItems = computed(() =>
NAVBAR_ITEM_ORDER.map((id): NavbarItem => {
const label = locale.value.navbar[id]();
const to = ((): SmartDest => {
switch (id) {
case "whatIsViossa": {
return { type: "internal", internal: { route: "/" } };
}
case "resources": {
return {
type: "internal",
internal: { route: "/resources" },
};
}
case "kotoba": {
return { type: "internal", internal: { route: "/kotoba" } };
}
}
})();
return { to, label };
}),
);
</script>
<template>
@ -26,11 +70,13 @@ const locale = useLocale();
role="navigation"
aria-label="main navigation">
<div class="navbar-brand">
<RouterLink class="navbar-item has-text-weight-bold" to="/"
><img src="@/assets/ViossaFlagRect.svg" alt=""
/></RouterLink>
<SmartLink
class="navbar-item has-text-weight-bold"
:to="{ type: 'internal', internal: { route: '/' } }">
<img src="@/assets/ViossaFlagRect.svg" alt="" />
</SmartLink>
<div class="navbar-item">
<div class="navbar-item is-hidden-desktop">
<button
type="button"
@click="toggleBurger()"
@ -44,21 +90,13 @@ const locale = useLocale();
<div :class="`navbar-menu ${burgerOpen ? 'is-active' : ''}`">
<div class="navbar-start">
<RouterLink
<SmartLink
v-for="(item, index) in navbarItems"
:key="index"
class="navbar-item"
to="/"
@click="closeBurger()"
>{{ locale.navbar.whatIsViossa }}</RouterLink
>
<RouterLink
class="navbar-item"
to="/resources"
@click="closeBurger()"
>{{ locale.navbar.resources }}</RouterLink
>
<RouterLink class="navbar-item" to="/kotoba">
{{ locale.navbar.kotoba }}
</RouterLink>
:to="item.to"
>{{ item.label }}
</SmartLink>
<LocalePicker class="navbar-item" />
</div>
</div>

4
apps/vdn-static/src/assets.d.ts vendored Normal file
View file

@ -0,0 +1,4 @@
declare module "*.ftl" {
const src: string;
export default src;
}

View file

@ -0,0 +1,78 @@
localeName = "English"
vilanticLangs-viossa = "Viossa"
vilanticLangs-wodox = "Wodoch"
vilanticLangs-minemiaha = "Minemiaha"
navbar-whatIsViossa = "What is Viossa?"
navbar-resources = "Resources"
navbar-kotoba = "Kotoba"
home-sections-whatIsViossa-title = "What is Viossa?"
home-sections-whatIsViossa-body = "Viossa is a community-created artificial pidgin language, created to simulate the formation of natural pidgin languages. Viossa is characterized by its lack of standardization, with each speaker developing a personal idiolect. Spelling and pronunciation can vary greatly, and serve as a form of personal self-expression. Viossa is learnt and taught entirely by immersion — translation is prohibited while learning."
home-sections-historyOfViossa-title = "History of Viossa"
home-sections-historyOfViossa-body = "Viossa began as a Skype group in 2014, created by members of the r/conlangs community on Reddit, as an experiment to simulate the formation of a pidgin language. Pidgins are simplified languages resulting from contact between populations with no shared common language. Unlike most pidgins, which usually have two to three contributor languages, Viossa comes from many diverse languages. This is because people from all around the world helped to contribute to Viossa's vocabulary."
home-sections-community-title = "Community"
home-sections-community-body = "The Viossa community is rich and colourful, drawing from many global traditions due to its worldwide online membership. Since the teaching culture puts an emphasis on linguistic immersion, and discourages prescriptivism, the culture of Viossa is as diverse and varied as the language and the people who speak it. For many, their personal dialect is a key form of identity and expression. The fluid nature of Viossa and lack of defined meanings makes Viossa popular for creative purposes, such as poetry and songwriting."
home-images-viossaFlag-alt = "Flag of the Viossa Language"
resources-title = "Learning Resources"
resources-resources-discord-title = "Discord Server"
resources-resources-discord-subtitle = "This is where most of the action happens! Hop on in!"
resources-resources-discord-desc = "Viossa Diskordserver (VDS) was founded in 2016, as the successor to the original Viossa chat on Skype, since then it has grown to have over 6,000 members. Via the buttons, please read the rules and then join the server!"
resources-resources-discord-buttons-join-label = "Join"
resources-resources-discord-buttons-rules-label = "Rules"
resources-images-discordLogo-alt = "Discord logo"
kotoba-title = "Tropos-agnostic search"
kotoba-searchHelp = "To searcn tropos-agnostically, enter a term below."
discord-rulesPage-title = "Discord Server Rules"
discord-rulesPage-overview-title = "Overview"
discord-rulesPage-overview-help = "Click any rule to see details."
discord-rulesPage-rules-noTranslation-overview-text = md "No translation! Do not translate to/from Viossa on the server, except the big four translatables (you can learn in hard mode without them!)"
discord-rulesPage-rules-noTranslation-overview-subtext = md --
discord-rulesPage-rules-noTranslation-section-header = "Rule { $ruleNumber }: No translation"
discord-rulesPage-rules-noTranslation-section-body = md
"Translation is not how we learn and teach Viossa. Instead, we teach using pictures, diagrams, video calls, and other aids to couple words to meaning."
"On the Viossa Diskordserver, you are allowed to translate the following four words. If you want an extra challenge, don't unspoiler the text:"
"*TODO - big 4*"
"Outside of the teaching-learning cycle, we also make an exception for artistic translations (such as those of songs, books, or poems), as well as for academic translations (such as for a formal research paper). In both cases, this exception is dependent on translations of either class appearing in the appropriate place. If you're not sure where that is, please ask."
"Additionally, please don't attempt to derive or share translation-based learning materials on-server, or poach members for such a purpose."
discord-rulesPage-rules-lfsv-overview-text = md "If it's understood, it's Viossa."
discord-rulesPage-rules-lfsv-overview-subtext = md --
discord-rulesPage-rules-lfsv-section-header = "Rule { $ruleNumber }: If it's understood, it's Viossa"
discord-rulesPage-rules-lfsv-section-body = md
"All that is required to speak Viossa is that other speakers be able to understand you. There is no right or wrong way to speak or write, and no global standard."
"However, Viossa is a collaborative group project: members should strive to make others understand them, and in return make an effort to understand others."
discord-rulesPage-rules-viossaOnlyChats-overview-text = md "The chats in the Viossa Only category are Viossa only."
discord-rulesPage-rules-viossaOnlyChats-overview-subtext = md --
discord-rulesPage-rules-viossaOnlyChats-section-header = "Rule { $ruleNumber }: Viossa-only chats"
discord-rulesPage-rules-viossaOnlyChats-section-body = md
"Chats in the Viossa Only section do not permit English. If you must use English to coach learners on the learning process, go to **#meta** instead."
"This doesn't mean that other channels are English-only, though! Viossa is allowed everywhere."
discord-rulesPage-rules-sfw-overview-text = md "This server is SFW. No sexually explicit, gory, or violent content."
discord-rulesPage-rules-sfw-overview-subtext = md --
discord-rulesPage-rules-sfw-section-header = "Rule { $ruleNumber }: SFW"
discord-rulesPage-rules-sfw-section-body = md
"If a mod does not like what you have posted, they will inform you; see [Rule 6](internal.replace:#rule-6). This is a public Discord server; think before you post."
discord-rulesPage-rules-respectOthers-overview-text = md "Don't use hate speech, and respect each other."
discord-rulesPage-rules-respectOthers-overview-subtext = md --
discord-rulesPage-rules-respectOthers-section-header = "Rule { $ruleNumber }: Respect one another"
discord-rulesPage-rules-respectOthers-section-body = md
"Respect one another. Using slurs or hate speech against others, whether on- or off-server, or advocating for violence are not welcome. This is an LGBTQ+ friendly international community."
discord-rulesPage-rules-respectStaff-overview-text = md "Respect the rulings of the staff (**@Yewald** and **@Yewaldnen**)."
discord-rulesPage-rules-respectStaff-overview-subtext = md --
discord-rulesPage-rules-respectStaff-section-header = "Rule { $ruleNumber }: Respect the staff's rulings"
discord-rulesPage-rules-respectStaff-section-body = md
"The word of staff (the Yewald as well as the Yewaldnen) is final, and they may kick, ban, or mute members or change members' access permissions to make sure this environment stays respectful and puts the Viossa community first."
"Appeals will always be considered, and if you feel that a mod action was inappropriate, you can DM any Yewald or open a ticket with YAGPDB's /tickets open command."
"If you are banned, there will be instructions on how to appeal the ban, however, please take the time to reflect on the ban reason before appealing."
discord-rulesPage-rules-controversialTopics-overview-text = md "Discussion of controversial topics (politics, war, etc.) should be directed to **#polite**, which requires the **@Ike** role to view, which is itself locked behind **@Viossadjin** and **@mellandjin**."
discord-rulesPage-rules-controversialTopics-overview-subtext = md "**#feels-and-advice** is for talking about your feelings openly, but we draw the line at suicidal or violent ideation. These are trains of thought to be brought to a therapist, and are not jokes. Because of their seriousness, they simply don't belong here."
discord-rulesPage-rules-controversialTopics-section-header = "Rule { $ruleNumber }: #polite and ike"
discord-rulesPage-rules-controversialTopics-section-body = md
"Many are life's troubling realities, and vast is our need to discuss them. The ike category is an opt-in set of chats where discussion of heavy, sensitive, or potentially contentious topics is allowed, provided that users are especially respectful of each other during such discussions. By accepting the ike role, you agree to adhere to this rule and encourage others to do the same."
"# Venting vs seeking advice"
"Sometimes you want to let people know that you're dealing with an issue and just be acknowledged, other times you want help in solving a problem. If you are open to one but not the other, it's often a good idea to let people know as part of the discussion so that you can receive the kind of responses you are looking for."
"# Self-harm and Violence"
"While discussing self-harm in general is allowed (with appropriate and clear use of content warnings), this server in itself is not an emergency mental health resource, and is not a substitute for professional help. Asking others for advice in finding support or resources off-server is fine, but asking others to participate in talking you down is inappropriate."
"You should not use this space to:"
"- express intent or desire to harm yourself or others"
"- solicit help in stopping yourself from harming yourself or someone else"
"By crossing these boundaries, please be aware that you are asking members of the server (including moderators and the owner) to perform a role for which they are not trained or equipped. At the moderators' sole discretion, this may not be tolerated and may result in a warning, timeout, removal of the **@ike** role, or removal from the server."
"If you are struggling with thoughts of this nature, but are not immediately in danger, please consider seeking counseling. If you are experiencing an immediate crisis, please call **988** (in the United States), **999** (in the UK), or locate an emergency hotline appropriate for you. A list of resources by country exists here: [](external.new:https://blog.opencounseling.com/suicide-hotlines/)"

View file

@ -0,0 +1,22 @@
localeName = "Viossa"
vilanticLangs-viossa = "Viossa"
vilanticLangs-wodox = "Wodossa"
navbar-whatIsViossa = "Ka Viossa?"
navbar-resources = "Lerakran"
navbar-kotoba = "Kotoba"
home-sections-whatIsViossa-title = "Ka Viossa?"
home-sections-whatIsViossa-body = "Viossa viskena-mahaossa mahajena na klaani, per mverm hur gvir viskossa mahajena. Viossa nai har rasmi, tont pashun bruk aparchigau tropos. Kakutro au hanutro deki chigaudai, au deki brukena per impla pashun. Viossa lerajena au opetajena na hel na hanu/kaku — dekinai kjannos per lera."
home-sections-historyOfViossa-title = "Danvimi fu Viossa"
home-sections-historyOfViossa-body = "Viossa hadjidan na Skype na 2014, mahajena na klaani fu r/conlangs na Reddit, grun tuvat per mverm hur viskossa mahajena. Viskossa plussimper fal fu glossa grun klaani uten kamagglossa na sama plas. Na leste viskossa jam na snano 2-3 ranyaossa, men Viossa mahajena grun mange chigau ranyaossa. Grun mangedjin gele gaja apudan per maha viko."
home-sections-community-title = "Klaani"
home-sections-community-body = "Klaani fu Viossa surudan mange au stranidai, mange rurret kara, na hel gaja, grun na zerjet. Opetaklupau maha uten kjannos os metahanu plussnano au hel uslovanai ke joku tro plusbra kena andr. Viossaklupau mange chigau likk glossa au hanudjin. Na mangedjin, tro awen tel fu sebja. Grun Viossa deki chigaudai au naijam mange tsatain imi znachi ke Viossa blogeta na ishu grunan, likk maha paem os liid."
home-images-viossaFlag-alt = "Flakka fu Viossa"
resources-title = "Lerakran"
resources-resources-discord-title = "Diskordserver"
resources-resources-discord-subtitle = "Alting Viossa tsuite slucha na her! Da zetulla jo!"
resources-resources-discord-desc = "Mahajena na 2016, server rupnejena na mange, na ima jam plus kena 6000 pashun long. Bitte da se ruuru au de bruk zedvera na una per zetulla!"
resources-resources-discord-buttons-join-label = "Zetulla"
resources-resources-discord-buttons-rules-label = "Ruuru"
resources-images-discordLogo-alt = "Riso fu Diskord"
kotoba-title = "Tropos-egal suha"
kotoba-searchHelp = "Li vil suha uten tro-egal, tastatsa joku ko os fras na una."

View file

@ -0,0 +1,23 @@
localeName = "wodox"
vilanticLangs-viossa = "viosox"
vilanticLangs-wodox = "wodox"
vilanticLangs-minemiaha = "minemiox"
navbar-whatIsViossa = "viosox e ano?"
navbar-resources = "tropos"
navbar-kotoba = "mot o viosox"
home-sections-whatIsViossa-title = "viosox e ano?"
home-sections-whatIsViossa-body = "viosox e hez ox pamzal, zoz stende zalkun tuo mit multa nengwi ox. zal o viosox stende lik zal o hez il ox keta, zalilkun wi tuo mit multa nengwi ox. mono i fal o viosox stendenai; omni axsi o viosox zal nengokun fal o viosox, de falmot wi falax o il stende e keko trenengwi tua o nengwi stende, ge fala e keko lik ro o tuo viosoxsi. genil viosox ibe il wi nengwi stende axkun ge pisakun po tuo ox — stende gen muskunnai mit zaiox."
home-sections-historyOfViossa-title = "zal o viosox"
home-sections-historyOfViossa-body = "wi o zal o viosox stende po multa o Skype wi 2014 ibe stendera o multa r/conlangs o Reddit. zalsi o viosox danzalgo hez, tuo zal o viosox e lik zal o hez il ox keta, zalil tuo ox mit nengwi multa ox ibe zalsi fiemnaikun sama i ox. viosox e nengwi tuo ox pamzal keta; zalilkun keko ox keta mit lik du wi tre ox, aga zalil viosox mit multa wi plus obo o ox na il ox keta ibe zalsi o viosox stende po multa mi o mo."
home-sections-community-title = "viosoxsi"
home-sections-community-body = "nengwi multa ro o viosoxsi stende ibe stendenura po nengwi multa mi o mo ge wekakunnura zai nengwi viosoxsi po jilobo. ibe mono i fal o viosox stendenai ge ibe viosoxsi zalkun nengwi multa fal o viosox, de nengwi zoz ko o ro lik ro o viosoxsi stende po ro o viosa. po multa hez viosoxsi, falmot wi falax o tuo stende stende po ro o tuo stende. ibe mono i fal o viosox stendenai ge ibe ro o mot inkun nengwi po nengwi viosoxsi, de multa stende amanata hez, zal zalgonukun surat au mola au sucik."
home-images-viossaFlag-alt = "fomma o viosox"
resources-title = "tropos o gen"
resources-resources-discord-title = "server o Diskord"
resources-resources-discord-subtitle = "axilkun ge genilkun po ce! wekatutsa!"
resources-resources-discord-desc = "danzalil hez server po 2016. ibe dutukun musra po ce, de ibe wiftutsakun dof po pam, de wekatukun po server!"
resources-resources-discord-buttons-join-label = "wekatutsa"
resources-resources-discord-buttons-rules-label = "musra"
resources-images-discordLogo-alt = "surat o Diskord"
kotoba-title = "zalkuketutsa mot o viosox mit il o omni falmot"
kotoba-searchHelp = "ibe tastatukun il falmot o mot o viosox po pam, de zalkuketukun."

View file

@ -17,10 +17,12 @@
--bulma-info-l: 50%;
--bulma-info-s: 45%;
--bulma-family-primary: Nunito, Inter, SF Pro, Segoe UI, Roboto, Oxygen, Ubuntu, Helvetica Neue, Helvetica, Arial, sans-serif;
--bulma-family-secondary: Nunito, Inter, SF Pro, Segoe UI, Roboto, Oxygen, Ubuntu, Helvetica Neue, Helvetica, Arial, sans-serif;
--bulma-body-family: Nunito, Inter, SF Pro, Segoe UI, Roboto, Oxygen, Ubuntu, Helvetica Neue, Helvetica, Arial, sans-serif;
}
--bulma-family-primary: Nunito,Inter,SF Pro,Segoe UI,Roboto,Oxygen,Ubuntu,Helvetica Neue,Helvetica,Arial,sans-serif;
--bulma-family-secondary: Nunito,Inter,SF Pro,Segoe UI,Roboto,Oxygen,Ubuntu,Helvetica Neue,Helvetica,Arial,sans-serif;
--bulma-body-family: Nunito,Inter,SF Pro,Segoe UI,Roboto,Oxygen,Ubuntu,Helvetica Neue,Helvetica,Arial,sans-serif;
};
// Correct for fixed-top header when jumping to element by id in link (anchor)
html {
scroll-padding-top: var(--bulma-navbar-height);
}

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { AnchorHTMLAttributes } from "vue";
defineProps<{
onClick: AnchorHTMLAttributes["onClick"];
}>() satisfies AnchorHTMLAttributes;
</script>
<template>
<!-- eslint-disable-next-line vue/no-restricted-html-elements - we're wrapping it into a useable type -->
<a v-bind="$props">
<slot />
</a>
</template>

View file

@ -0,0 +1,115 @@
<script setup lang="ts" generic="Slot extends string">
import {
getCurrentInstance,
onMounted,
type DeepReadonly,
type VNode,
} from "vue";
import MarkdownParts from "./MarkdownParts.vue";
import OptionalParent from "./OptionalParent.vue";
import type { Markdown } from "@/vi18n-lib/markdown";
import type { CssClass } from "@/utils/css";
const props = defineProps<{
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-arguments -- I don't know why this error is here
markdown: DeepReadonly<Markdown<Slot>>;
lineClass?: CssClass;
tag?: string;
}>();
const providedSlots =
defineSlots<{ [K in DeepReadonly<Slot>]: () => VNode[] }>();
function tryResolveComponentName(type: unknown): string | undefined {
if (!type || typeof type !== "object") return undefined;
const maybeType = type as { name?: string; __file?: string };
if (maybeType.name) return maybeType.name;
if (maybeType.__file) {
const parts = maybeType.__file.split(/[\\/]/);
const filename = parts.at(-1);
if (filename === undefined) {
return undefined;
}
const filenameParts = filename.split(".");
filenameParts.pop();
return filenameParts.join(".");
}
return undefined;
}
function resolveComponentName(type: unknown): string {
return tryResolveComponentName(type) ?? "(unresolvable)";
}
const getComponentStack = () => {
const instance = getCurrentInstance();
if (!instance) return "";
const names: string[] = [resolveComponentName(instance.type)];
let current = instance.parent;
while (current) {
names.push(resolveComponentName(current.type));
current = current.parent;
}
return names.length > 0 ? `\n\tComponent Stack: ${names.join(" > ")}` : "";
};
// Validate required slots at runtime
onMounted(() => {
const requiredSlots = props.markdown.slots;
const missingSlots = [...requiredSlots].filter(
(slot) => providedSlots[slot] === undefined,
);
if (missingSlots.length > 0) {
const componentStack = getComponentStack();
throw new Error(
`Markdown component is missing slots!\n\tMissing Slots: ${missingSlots.join(", ")}${componentStack}`,
);
}
});
</script>
<template>
<OptionalParent :is="tag">
<template v-for="(line, index) in markdown.elements" :key="index">
<p v-if="line.type === 'paragraph'" :class="lineClass">
<MarkdownParts
:elements="line.paragraph.spans"
:slots="markdown.slots">
<template
v-for="(slot, name) in providedSlots"
:key="name"
#[name]>
<component :is="slot" />
</template>
</MarkdownParts>
</p>
<h3 v-else-if="line.type === 'header'" :class="lineClass">
<MarkdownParts
:elements="line.header.spans"
:slots="markdown.slots">
<template
v-for="(slot, name) in providedSlots"
:key="name"
#[name]>
<component :is="slot" />
</template>
</MarkdownParts>
</h3>
<ul v-else-if="line.type === 'ulist'" :class="lineClass">
<li v-for="item in line.ulist.items">
<MarkdownParts :elements="item" :slots="markdown.slots">
<template
v-for="(slot, name) in providedSlots"
:key="name"
#[name]>
<component :is="slot" />
</template>
</MarkdownParts>
</li>
</ul>
</template>
</OptionalParent>
</template>

View file

@ -0,0 +1,67 @@
<script setup lang="ts" generic="Slot extends string">
import { type DeepReadonly, type VNode } from "vue";
import SmartLink from "../atoms/SmartLink.vue";
import type { MarkdownSpan } from "@/vi18n-lib/markdown";
defineProps<{
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-arguments
elements: DeepReadonly<MarkdownSpan<Slot>[]>;
slots: DeepReadonly<ReadonlySet<Slot>>;
}>();
const vueSlots = defineSlots<{ [K in Slot]: () => VNode[] }>();
</script>
<template>
<template v-for="(part, index) in elements" :key="index">
<template v-if="part.type === 'plain'">
{{ part.plain }}
</template>
<template v-else-if="part.type === 'slot'">
<template v-for="(slot, name) in vueSlots" :key="name">
<template v-if="name === part.slot">
<component :is="slot" />
</template>
</template>
</template>
<template v-else-if="part.type === 'bold'">
<b>
<!-- eslint-disable-next-line vue/no-restricted-html-elements - it can use itself -->
<MarkdownParts :elements="part.bold" :slots="slots">
<template
v-for="(slot, name) in vueSlots"
:key="name"
#[name]>
<component :is="slot" />
</template>
</MarkdownParts>
</b>
</template>
<template v-else-if="part.type === 'italic'">
<i>
<!-- eslint-disable-next-line vue/no-restricted-html-elements - it can use itself -->
<MarkdownParts :elements="part.italic" :slots="slots">
<template
v-for="(slot, name) in vueSlots"
:key="name"
#[name]>
<component :is="slot" />
</template>
</MarkdownParts>
</i>
</template>
<template v-else-if="part.type === 'link'">
<SmartLink :to="part.link.to" :new-tab="part.link.newTab">
<!-- eslint-disable-next-line vue/no-restricted-html-elements - it can use itself -->
<MarkdownParts :elements="part.link.label" :slots="slots">
<template
v-for="(slot, name) in vueSlots"
:key="name"
#[name]>
<component :is="slot" />
</template>
</MarkdownParts>
</SmartLink>
</template>
</template>
</template>

View file

@ -0,0 +1,12 @@
<script setup lang="ts">
defineProps<{ is?: string | object | null | undefined }>();
</script>
<template>
<component v-if="is !== null && is !== undefined" :is="is">
<slot />
</component>
<template v-else>
<slot />
</template>
</template>

View file

@ -0,0 +1,7 @@
import type { SmartDest } from "@/utils/smart-dest";
export interface SmartLinkProps {
to: SmartDest;
newTab?: boolean;
covert?: boolean;
}

View file

@ -0,0 +1,56 @@
<script setup lang="ts">
import type { CssClass } from "@/utils/css";
import { computed, ref } from "vue";
import type { SmartLinkProps } from "./SmartLink";
const props = defineProps<SmartLinkProps>();
type To = { type: "a"; a: string } | { type: "routerLink"; routerLink: string };
const to = ((): To => {
const { to, newTab } = props;
switch (to.type) {
case "external": {
return { type: "a", a: to.external };
}
case "internal": {
const { route, id } = to.internal;
const endpoint = `${route ?? ""}${id === undefined ? "" : `#${id}`}`;
if (newTab || id !== undefined) {
return { type: "a", a: endpoint };
}
// <RouterLink> can only be used for route-only endpoints
// that require no other special functionality of <a>
return { type: "routerLink", routerLink: endpoint };
}
}
})();
const isHovered = ref(false);
const classes = computed<CssClass>(() => [
props.covert && !isHovered.value && "has-text-text",
]);
</script>
<template>
<!-- eslint-disable-next-line vue/no-restricted-html-elements - we need to at least use <a> once to create this wrapper type -->
<a
v-if="to.type === 'a'"
:href="to.a"
:target="newTab ? '_blank' : undefined"
rel="noopener noreferrer nofollow"
@mouseenter="isHovered = true"
@mouseleave="isHovered = false"
:class="classes">
<slot />
</a>
<!-- eslint-disable-next-line vue/no-restricted-html-elements - we need to at least use <RouterLink> once to create this wrapper type -->
<RouterLink v-else :to="to.routerLink" :class="classes">
<span @mouseenter="isHovered = true" @mouseleave="isHovered = false">
<slot />
</span>
</RouterLink>
</template>

View file

@ -0,0 +1,38 @@
<script setup lang="ts">
import SmartLink from "../atoms/SmartLink.vue";
import type { Value } from "@/utils/types";
import MarkdownDisplay from "../atoms/MarkdownDisplay.vue";
import type { Locale } from "@/i18n";
import type { DeepReadonly } from "vue";
import { isEmptyMarkdown } from "@/vi18n-lib/markdown";
const props = defineProps<{
overview: DeepReadonly<
Value<Locale["discord"]["rulesPage"]["rules"]>["overview"]
>;
ruleNumber: number;
}>();
const subtext = props.overview.subtext();
</script>
<template>
<span>
<SmartLink
covert
:to="{ type: 'internal', internal: { id: `rule-${ruleNumber}` } }"
:style="{ width: 'fit-content', display: 'inline-block' }">
<li :style="{ width: 'fit-content' }">
<MarkdownDisplay
:markdown="overview.text()"
line-class="mb-0" />
<ul v-if="!isEmptyMarkdown(subtext)" class="mt-0 w-fit">
<MarkdownDisplay
tag="li"
:style="{ width: 'fit-content' }"
:markdown="subtext" />
</ul>
</li>
</SmartLink>
</span>
</template>

View file

@ -0,0 +1,20 @@
<script setup lang="ts">
import type { Locale } from "@/i18n";
import type { Value } from "@/utils/types";
import type { DeepReadonly } from "vue";
import MarkdownDisplay from "../atoms/MarkdownDisplay.vue";
defineProps<{
section: DeepReadonly<
Value<Locale["discord"]["rulesPage"]["rules"]>["section"]
>;
ruleNumber: number;
}>();
</script>
<template>
<section class="section content" :id="`rule-${ruleNumber}`">
<h2>{{ section.header({ ruleNumber }) }}</h2>
<MarkdownDisplay :markdown="section.body()" />
</section>
</template>

View file

@ -1,8 +1,48 @@
<script setup lang="ts">
import type { SmartLinkProps } from "../atoms/SmartLink";
import type { CssClass } from "@/utils/css";
import SmartLink from "../atoms/SmartLink.vue";
export interface ResourceButton {
label: string;
link: SmartLinkProps;
style: ResourceButtonStyle;
}
export interface ResourceButtonStyle {
color: "primary" | "warning";
outlined?: boolean;
}
function buttonStyleToClasses(style: ResourceButtonStyle): CssClass[] {
const colorClass = (() => {
switch (style.color) {
case "primary": {
return "is-primary";
}
case "warning": {
return "is-warning";
}
}
})();
return [colorClass, style.outlined && "is-outlined"];
}
defineProps<{
title: string;
subtitle: string;
desc: string;
image?: { src: string; alt: string };
buttons: ResourceButton[];
}>();
</script>
<template>
<div class="box columns is-vcentered is-gap-4">
<div class="column is-one-quarter" v-if="image">
<figure class="image">
<img :src="image" :alt="alt" :title="alt" />
<img :src="image.src" :alt="image.alt" :title="image.alt" />
</figure>
</div>
<div class="column">
@ -11,43 +51,18 @@
<p class="content">{{ desc }}</p>
<div class="level">
<a :href="link"
target="_blank"
rel="noopener noreferrer nofollow"
class="button is-primary is-medium"
:text="joinText">
</a>
<a :href="``"
target="_blank"
rel="noopener noreferrer nofollow"
class="button is-info is-outlined is-medium"
:text="rulesText">
</a>
<a :href="rulesLink"
target="_blank"
rel="noopener noreferrer nofollow"
class="button is-warning is-outlined is-medium"
:text="rulesText">
</a>
<SmartLink
v-for="(button, index) in buttons"
:key="index"
v-bind="button.link"
:class="[
'button',
'is-medium',
buttonStyleToClasses(button.style),
]"
>{{ button.label }}</SmartLink
>
</div>
</div>
</div>
</template>
<script setup lang="ts">
defineProps({
title: String,
subtitle: String,
desc: String,
link: String,
rulesLink: String,
joinText: String,
rulesText: String,
image: { type: String, required: false },
alt: { type: String, required: false },
})
</script>

View file

@ -2,6 +2,9 @@
import { LOCALE_IDS, localeId, useLocale, type LocaleId } from "@/i18n";
import { ref } from "vue";
import { vOnClickOutside } from "@vueuse/components";
import DropdownItem from "../atoms/DropdownItem.vue";
const locale = useLocale();
const isOpen = ref<boolean>(false);
@ -29,7 +32,7 @@ const setLocaleId = (id: LocaleId): void => {
aria-haspopup="true"
aria-controls="dropdown-menu"
@click="toggleOpen()">
<span>{{ useLocale().value.localeName }}</span>
<span>{{ locale.localeName() }}</span>
<span class="icon is-small">
<i class="fas fa-angle-down" aria-hidden="true"></i>
</span>
@ -37,14 +40,16 @@ const setLocaleId = (id: LocaleId): void => {
</div>
<div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content">
<a
<DropdownItem
v-for="(id, index) in LOCALE_IDS"
:key="index"
href="#"
:class="['dropdown-item', localeId === id && 'is-active']"
:class="[
'dropdown-item is-clickable',
localeId === id && 'is-active',
]"
@click="setLocaleId(id)">
{{ useLocale({ locale: id }).value.localeName }}
</a>
{{ useLocale({ locale: id }).value.localeName() }}
</DropdownItem>
</div>
</div>
</div>

View file

@ -1,46 +0,0 @@
<script setup lang="ts">
import HomeSectionWrapper from "@/components/molecules/HomeSectionWrapper.vue";
import { useLocale } from "@/i18n";
import { GREETINGS, type Greeting } from "@/i18n/greeting";
import { VILANTIC_ID_TO_FLAG } from "@/i18n/vilantic";
import { localizeLayout } from "@/utils/localizeLayout";
import { randomElement } from "@/utils/random";
const locale = useLocale();
const greeting: Greeting = randomElement(GREETINGS);
</script>
<template>
<div>
<section class="hero has-background-primary-soft is-primary">
<div
class="hero-body"
style="padding-top: 3.75rem; padding-bottom: 3rem">
<div class="title has-text-text-bold">{{ greeting.title }}</div>
<div class="subtitle has-text-text-bold mb-4">
{{ greeting.subtitle }}
</div>
<div
class="subtitle is-size-6 is-flex is-flex-direction-row is-align-items-center is-gap-1 has-text-text-bold">
&mdash; {{ greeting.author }} ({{
locale.vilanticLangs[greeting.lang]
}})
<figure class="image is-32x32">
<img :src="VILANTIC_ID_TO_FLAG[greeting.lang]" />
</figure>
</div>
</div>
</section>
<section class="section container">
<HomeSectionWrapper
v-for="(section, index) in localizeLayout(locale.home.layout)"
:key="index"
:title="section.title"
:text="section.text"
:image="section.image ?? undefined"
:alt="section.alt ?? undefined"
:reverse="index % 2 !== 0" />
</section>
</div>
</template>

View file

@ -1,32 +0,0 @@
<script setup lang="ts">
import LearningResourceWrapper from "@/components/molecules/LearningResourceWrapper.vue";
import { useLocale } from "@/i18n";
import { localizeLayout } from "@/utils/localizeLayout";
const locale = useLocale();
</script>
<template>
<div>
<section class="section">
<h1 class="title">{{ locale.resources.title }}</h1>
</section>
<section class="section container">
<LearningResourceWrapper
v-for="(resource, index) in localizeLayout(
locale.resources.layout,
)"
:key="index"
:title="resource.title"
:subtitle="resource.subtitle"
:desc="resource.desc"
:link="resource.link"
:rulesLink="resource.rulesLink"
:image="resource.image"
:alt="resource.alt"
:joinText="resource.joinText"
:rulesText="resource.rulesText" />
</section>
</div>
</template>

View file

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

View file

@ -1,22 +1,25 @@
import en_US from "../locales/en_US";
import vp_VL from "../locales/vp_VL";
import wp_VL from "../locales/wp_VL";
import { computed, readonly, type DeepReadonly } from "vue";
import type { Locale } from "./locale";
import type { DeepPartial } from "@/utils/deep-partial";
import { type InferLocaleFromConfig } from "@/vi18n-lib/config";
import {
bundleToUncompiledLocaleRecord,
loadFluentBundle,
} from "@/vi18n-lib/setup";
import { localeConfig } from "./config";
import type { Result } from "@/utils/types";
import { useLocalStorage } from "@vueuse/core";
import { computed, type DeepReadonly } from "vue";
import { type } from "arktype";
import enUsFtlSrc from "@/assets/locale/en_US.ftl";
import vpVlFtlSrc from "@/assets/locale/vp_VL.ftl";
import wpVlFtlSrc from "@/assets/locale/wp_VL.ftl";
import type { FluentBundle } from "@fluent/bundle";
import { compileLocale } from "@/vi18n-lib/compile";
export const LOCALE_IDS = ["en_US", "vp_VL", "wp_VL"] as const;
export const LOCALE_IDS = ["en-US", "vp-VL", "wp-VL"] as const;
export type LocaleId = typeof LocaleId.infer;
export const LocaleId = type.enumerated(...LOCALE_IDS);
export const DEFAULT_LOCALE_ID = "en_US" satisfies LocaleId;
const locales = { en_US, vp_VL, wp_VL } as const satisfies {
en_US: Locale;
} & Record<Exclude<LocaleId, typeof DEFAULT_LOCALE_ID>, DeepPartial<Locale>>;
export const DEFAULT_LOCALE_ID = "en-US" satisfies LocaleId;
// users could manually edit localStorage to make this value anything, so we need to validate it
const localStorageLocaleId = useLocalStorage<unknown>(
@ -44,23 +47,95 @@ export const localeId = computed({
},
});
export const useLocale = (opt: UseLocaleOptions = {}) => {
const locale = computed<DeepReadonly<Locale>>(() => {
return fallbackProxy<Locale>(
locales[opt.locale ?? localeId.value],
locales["en_US"],
);
});
export interface Locale extends InferLocaleFromConfig<typeof localeConfig> {}
return readonly(locale);
};
async function loadLocale(
localeId: LocaleId,
localeFtlSrc: string,
): Promise<Result<FluentBundle, string>> {
const bundleRes = await loadFluentBundle(localeId, localeFtlSrc);
if (bundleRes.type === "err") {
return bundleRes;
}
export interface UseLocaleOptions {
locale?: LocaleId;
const bundle = bundleRes.ok;
return { type: "ok", ok: bundle };
}
function isObject(value: unknown) {
return typeof value === "object" && value !== null;
interface SetupLocaleFallback {
bundle: FluentBundle;
locale: Locale;
}
function setupLocale(
localeId: LocaleId,
localeBundle: FluentBundle,
fallback: SetupLocaleFallback | undefined,
): Result<Locale, string> {
const fallbackBundle = fallback?.bundle;
const fallbackLocale = fallback?.locale;
const maybeFallbackedBundle = (() => {
if (fallbackBundle === undefined) {
return localeBundle;
}
const localeMessageIds = new Set(localeBundle._messages.keys());
const fallbackMessageIds = new Set(fallbackBundle._messages.keys());
const missingMessageIds =
fallbackMessageIds.difference(localeMessageIds);
for (const id of missingMessageIds) {
const fallbackMessage = fallbackBundle._messages.get(id);
if (fallbackMessage) {
localeBundle._messages.set(id, fallbackMessage);
}
}
return localeBundle;
})();
const uncompiledLocaleRecordRes = bundleToUncompiledLocaleRecord(
maybeFallbackedBundle,
);
if (uncompiledLocaleRecordRes.type === "err") {
return uncompiledLocaleRecordRes;
}
const uncompiledLocaleRecord = uncompiledLocaleRecordRes.ok;
const localeRes = compileLocale({
config: localeConfig,
bundle: maybeFallbackedBundle,
uncompiled: uncompiledLocaleRecord,
fallback: fallbackLocale,
messageIdChain: [],
});
const compilationErrorCount = localeRes.errors.length;
if (compilationErrorCount === 0) {
console.log(`[vi18n] Set up locale \`${localeId}\` with no errors!`);
} else {
console.error(
`[vi18n] Set up locale \`${localeId}\` with ${compilationErrorCount} following errors:`,
);
console.error(localeRes.errors);
}
const locale = localeRes.locale;
return { type: "ok", ok: locale };
}
function unwrap<T, E>(result: Result<T, E>): T {
switch (result.type) {
case "ok": {
return result.ok;
}
case "err": {
throw new Error(String(result.err));
}
}
}
function deepReadonly<T>(value: T): DeepReadonly<T> {
@ -68,59 +143,42 @@ function deepReadonly<T>(value: T): DeepReadonly<T> {
return value as DeepReadonly<T>;
}
function fallbackProxy<T extends object>(
mask: DeepPartial<T>,
fallback: T,
): DeepReadonly<T> {
const proxy = new Proxy(fallback, {
get: (_target, key): DeepReadonly<T[keyof T]> => {
// SAFETY: typescript should ensure we're only ever trying to access keys
// that exist on T, and if the key doesn't,
// just process its fallback as if it did,
// everything should work as expected still
const tKey = key as keyof T;
const DEFAULT_LOCALE_BUNDLE = unwrap(await loadLocale("en-US", enUsFtlSrc));
const DEFAULT_LOCALE = unwrap(
setupLocale(DEFAULT_LOCALE_ID, DEFAULT_LOCALE_BUNDLE, undefined),
);
// value may not exist on mask
const value: DeepPartial<T>[keyof T] | undefined = mask[tKey];
// all values exist on fallback
const fallbackValue: T[keyof T] = fallback[tKey];
// this is *not* T[keyof T], because if value is an object,
// it may still have missing properties.
// this only handles the case where the current value is undefined, not nested ones.
// thus, `finalValue` is still a `DeepPartial<T[keyof T]>` (but not undefined)
const finalValue: DeepPartial<T>[keyof T] =
value === undefined ? fallbackValue : value;
// check if finalValue is not an object
// if not, it is a primitive
if (!isObject(finalValue)) {
// SAFETY: finalValue is not an object, so it is not affected by DeepPartial
// so `DeepPartial<T>[keyof T]` is the same as `T[keyof T]`
return deepReadonly(finalValue as T[keyof T]);
}
// else, finalValue is an object, so we need to proxy it as well
// check if fallbackValue is an object so that it can be used as finalValue's fallback
if (!isObject(fallbackValue)) {
// if not, we can't use finalValue as we'll have no fallback for it.
// send the fallbackValue no matter what instead
return deepReadonly(fallbackValue);
}
// else, proxy the returned object to support deep fallback proxying
return fallbackProxy<T[keyof T] & object>(
finalValue,
fallbackValue,
const doItAllForLocale = async (
localeId: LocaleId,
localeFtlSrc: string,
): Promise<DeepReadonly<Locale>> =>
deepReadonly(
unwrap(
setupLocale(
localeId,
unwrap(await loadLocale(localeId, localeFtlSrc)),
{ bundle: DEFAULT_LOCALE_BUNDLE, locale: DEFAULT_LOCALE },
),
),
);
},
set: () => {
throw new Error("Cannot mutate locale at runtime");
},
});
// we're just disallowing mutations to the proxy, since its setter panics if used at runtime
return deepReadonly(proxy);
const [vpVl, wpVl] = await Promise.all([
doItAllForLocale("vp-VL", vpVlFtlSrc),
doItAllForLocale("wp-VL", wpVlFtlSrc),
]);
const localeIdToLocale = {
"en-US": deepReadonly(DEFAULT_LOCALE),
"vp-VL": vpVl,
"wp-VL": wpVl,
} as const satisfies Record<LocaleId, DeepReadonly<Locale>>;
export interface UseLocaleOptions {
locale?: LocaleId;
}
export const useLocale = (opt: UseLocaleOptions = {}) =>
computed<DeepReadonly<Locale>>(() => {
const localLocaleId = opt.locale ?? localeId.value;
return localeIdToLocale[localLocaleId];
});

View file

@ -1,66 +0,0 @@
import type { VilanticId } from "./vilantic";
export interface Locale {
localeName: string;
vilanticLangs: VilanticLangs;
navbar: Navbar;
home: HomePage;
resources: ResourcesPage;
kotoba: KotobaPage;
}
export interface Layout<T> {
order: (keyof T)[];
data: { [K in keyof T]: T[K] };
}
export type VilanticLangs = Record<VilanticId, string>;
export interface Navbar {
whatIsViossa: string;
resources: string;
kotoba: string;
}
export interface HomePage {
layout: Layout<HomeSections>;
}
export interface HomeSections {
whatIsViossa: HomeSection;
historyOfViossa: HomeSection;
community: HomeSection;
}
export interface HomeSection {
title: string;
text: string;
image: string | null;
alt: string | null;
}
export interface ResourcesPage {
title: string;
layout: Layout<Resources>;
}
export interface Resources {
discord: Resource;
}
export interface Resource {
title: string;
subtitle: string;
desc: string;
link: string;
rulesLink: string;
image: string;
alt: string;
joinText: string;
rulesText: string;
}
export interface KotobaPage {
title: string;
searchHelp: string;
}

View file

@ -1,62 +0,0 @@
import type { Locale } from "@/i18n/locale";
import flakkaImg from "@/assets/flakka.png";
import discordImg from "@/assets/discord.png";
export default {
localeName: "English",
vilanticLangs: { viossa: "Viossa", wodox: "Wodoch", minemiaha: "Minemiaha"},
navbar: {
whatIsViossa: "What is Viossa?",
resources: "Resources",
kotoba: "Kotoba",
},
home: {
layout: {
order: ["whatIsViossa", "historyOfViossa", "community"],
data: {
whatIsViossa: {
title: "What is Viossa?",
text: "Viossa is a community-created artificial pidgin language, created to simulate the formation of natural pidgin languages. Viossa is characterized by its lack of standardization, with each speaker developing a personal idiolect. Spelling and pronunciation can vary greatly, and serve as a form of personal self-expression. Viossa is learnt and taught entirely by immersion — translation is prohibited while learning.",
image: flakkaImg,
alt: "Flag of the Viossa Language",
},
historyOfViossa: {
title: "History of Viossa",
text: "Viossa began as a Skype group in 2014, created by members of the r/conlangs community on Reddit, as an experiment to simulate the formation of a pidgin language. Pidgins are simplified languages resulting from contact between populations with no shared common language. Unlike most pidgins, which usually have two to three contributor languages, Viossa comes from many diverse languages. This is because people from all around the world helped to contribute to Viossa's vocabulary.",
image: flakkaImg,
alt: "Flag of the Viossa Language",
},
community: {
title: "Community",
text: "The Viossa community is rich and colourful, drawing from many global traditions due to its worldwide online membership. Since the teaching culture puts an emphasis on linguistic immersion, and discourages prescriptivism, the culture of Viossa is as diverse and varied as the language and the people who speak it. For many, their personal dialect is a key form of identity and expression. The fluid nature of Viossa and lack of defined meanings makes Viossa popular for creative purposes, such as poetry and songwriting.",
image: null,
alt: null,
},
},
},
},
resources: {
title: "Learning Resources",
layout: {
order: ["discord"],
data: {
discord: {
title: "Discord Server",
subtitle:
"This is where most of the action happens! Hop on in!",
desc: "Viossa Diskordserver (VDS) was founded in 2016, as the successor to the original Viossa chat on Skype, since then it has grown to have over 6,000 members. Via the buttons, please read the rules and then join the server!",
link: "https://discord.gg/g3mG2gYjZD",
rulesLink: "https://viossadiskordserver.github.io/rules",
image: discordImg,
alt: "Discord logo",
joinText: "Join",
rulesText: "Rules",
},
},
},
},
kotoba: {
title: "Tropos-agnostic search",
searchHelp: "To searcn tropos-agnostically, enter a term below.",
},
} as const satisfies Locale;

View file

@ -1,63 +0,0 @@
import type { Locale } from "@/i18n/locale";
import flakkaImg from "@/assets/flakka.png";
import type { DeepPartial } from "@/utils/deep-partial";
import discordImg from "@/assets/discord.png";
export default {
localeName: "Viossa",
vilanticLangs: { viossa: "Viossa", wodox: "Wodossa" },
navbar: {
whatIsViossa: "Ka Viossa?",
resources: "Lerakran",
kotoba: "Kotoba",
},
home: {
layout: {
order: ["whatIsViossa", "historyOfViossa", "community"],
data: {
whatIsViossa: {
title: "Ka Viossa?",
text: "Viossa viskena-mahaossa mahajena na klaani, per mverm hur gvir viskossa mahajena. Viossa nai har rasmi, tont pashun bruk aparchigau tropos. Kakutro au hanutro deki chigaudai, au deki brukena per impla pashun. Viossa lerajena au opetajena na hel na hanu/kaku — dekinai kjannos per lera.",
image: flakkaImg,
alt: "Flakka fu Viossa",
},
historyOfViossa: {
title: "Danvimi fu Viossa",
text: "Viossa hadjidan na Skype na 2014, mahajena na klaani fu r/conlangs na Reddit, grun tuvat per mverm hur viskossa mahajena. Viskossa plussimper fal fu glossa grun klaani uten kamagglossa na sama plas. Na leste viskossa jam na snano 2-3 ranyaossa, men Viossa mahajena grun mange chigau ranyaossa. Grun mangedjin gele gaja apudan per maha viko.",
image: flakkaImg,
alt: "Flakka fu Viossa",
},
community: {
title: "Klaani",
text: "Klaani fu Viossa surudan mange au stranidai, mange rurret kara, na hel gaja, grun na zerjet. Opetaklupau maha uten kjannos os metahanu plussnano au hel uslovanai ke joku tro plusbra kena andr. Viossaklupau mange chigau likk glossa au hanudjin. Na mangedjin, tro awen tel fu sebja. Grun Viossa deki chigaudai au naijam mange tsatain imi znachi ke Viossa blogeta na ishu grunan, likk maha paem os liid.",
image: null,
alt: null,
},
},
},
},
resources: {
title: "Lerakran",
layout: {
order: ["discord"],
data: {
discord: {
title: "Diskordserver",
subtitle:
"Alting Viossa tsuite slucha na her! Da zetulla jo!",
desc: "Mahajena na 2016, server rupnejena na mange, na ima jam plus kena 6000 pashun long. Bitte da se ruuru au de bruk zedvera na una per zetulla!",
link: "https://discord.gg/g3mG2gYjZD",
rulesLink: "https://viossadiskordserver.github.io/rules",
image: discordImg,
alt: "Riso fu Diskord",
joinText: "Zetulla",
rulesText: "Ruuru",
},
},
},
},
kotoba: {
title: "Tropos-egal suha",
searchHelp: "Li vil suha uten tro-egal, tastatsa joku ko os fras na una.",
},
} as const satisfies DeepPartial<Locale>;

View file

@ -1,63 +0,0 @@
import type { Locale } from "@/i18n/locale";
import flakkaImg from "@/assets/flakka.png";
import discordImg from "@/assets/discord.png";
import type { DeepPartial } from "@/utils/deep-partial";
export default {
localeName: "wodox",
vilanticLangs: { viossa: "viosox", wodox: "wodox" },
navbar: {
whatIsViossa: "viosox e ano?",
resources: "tropos",
kotoba: "mot o viosox",
},
home: {
layout: {
order: ["whatIsViossa", "historyOfViossa", "community"],
data: {
whatIsViossa: {
title: "viosox e ano?",
text: "viosox e hez ox pamzal, zoz stende zalkun tuo mit multa nengwi ox. zal o viosox stende lik zal o hez il ox keta, zalilkun wi tuo mit multa nengwi ox. mono i fal o viosox stendenai; omni axsi o viosox zal nengokun fal o viosox, de falmot wi falax o il stende e keko trenengwi tua o nengwi stende, ge fala e keko lik ro o tuo viosoxsi. genil viosox ibe il wi nengwi stende axkun ge pisakun po tuo ox — stende gen muskunnai mit zaiox.",
image: flakkaImg,
alt: "fomma o viosox",
},
historyOfViossa: {
title: "zal o viosox",
text: "wi o zal o viosox stende po multa o Skype wi 2014 ibe stendera o multa r/conlangs o Reddit. zalsi o viosox danzalgo hez, tuo zal o viosox e lik zal o hez il ox keta, zalil tuo ox mit nengwi multa ox ibe zalsi fiemnaikun sama i ox. viosox e nengwi tuo ox pamzal keta; zalilkun keko ox keta mit lik du wi tre ox, aga zalil viosox mit multa wi plus obo o ox na il ox keta ibe zalsi o viosox stende po multa mi o mo.",
image: flakkaImg,
alt: "fomma o viosox",
},
community: {
title: "viosoxsi",
text: "nengwi multa ro o viosoxsi stende ibe stendenura po nengwi multa mi o mo ge wekakunnura zai nengwi viosoxsi po jilobo. ibe mono i fal o viosox stendenai ge ibe viosoxsi zalkun nengwi multa fal o viosox, de nengwi zoz ko o ro lik ro o viosoxsi stende po ro o viosa. po multa hez viosoxsi, falmot wi falax o tuo stende stende po ro o tuo stende. ibe mono i fal o viosox stendenai ge ibe ro o mot inkun nengwi po nengwi viosoxsi, de multa stende amanata hez, zal zalgonukun surat au mola au sucik.",
image: null,
alt: null,
},
},
},
},
resources: {
title: "tropos o gen",
layout: {
order: ["discord"],
data: {
discord: {
title: "server o Diskord",
subtitle: "axilkun ge genilkun po ce! wekatutsa!",
desc: "danzalil hez server po 2015. ibe dutukun musra po ce, de ibe wiftutsakun dof po pam, de wekatukun po server!",
link: "https://discord.gg/g3mG2gYjZD",
rulesLink: "https://viossadiskordserver.github.io/rules",
image: discordImg,
alt: "surat o Diskord",
joinText: "wekatutsa",
rulesText: "musra",
},
},
},
},
kotoba: {
title: "zalkuketutsa mot o viosox mit il o omni falmot",
searchHelp:
"ibe tastatukun il falmot o mot o viosox po pam, de zalkuketukun.",
},
} as const satisfies DeepPartial<Locale>;

View file

@ -1,5 +1,5 @@
import { createApp } from "vue";
import App from "./App.vue";
import router from "./routes";
import router from "./router";
createApp(App).use(router).mount("#app");

View file

@ -0,0 +1,49 @@
<script setup lang="ts">
import DiscordRuleOverview from "@/components/molecules/DiscordRuleOverview.vue";
import DiscordRuleSection from "@/components/molecules/DiscordRuleSection.vue";
import { useLocale } from "@/i18n";
import { computed } from "vue";
const locale = useLocale();
const pageI18n = computed(() => locale.value.discord.rulesPage);
const rulesI18n = computed(() => pageI18n.value.rules);
const RULE_ORDER = [
"noTranslation",
"lfsv",
"viossaOnlyChats",
"sfw",
"respectOthers",
"respectStaff",
"controversialTopics",
] as const satisfies (keyof typeof rulesI18n.value)[];
</script>
<template>
<div>
<section class="section">
<h1 class="title">
{{ pageI18n.title() }}
</h1>
</section>
<section class="section content">
<h2>{{ pageI18n.overview.title() }}</h2>
<blockquote>
{{ pageI18n.overview.help() }}
</blockquote>
<ol :style="{ display: 'flex', flexDirection: 'column' }">
<DiscordRuleOverview
v-for="(id, index) in RULE_ORDER"
:key="index"
:rule-number="index + 1"
:overview="rulesI18n[id].overview" />
</ol>
</section>
<DiscordRuleSection
v-for="(id, index) in RULE_ORDER"
:key="index"
:section="rulesI18n[id].section"
:rule-number="index + 1" />
</div>
</template>

View file

@ -0,0 +1,81 @@
<script setup lang="ts">
import HomeSectionWrapper from "@/components/molecules/HomeSectionWrapper.vue";
import { GREETINGS, type Greeting } from "@/i18n/greeting";
import { VILANTIC_ID_TO_FLAG } from "@/i18n/vilantic";
import { randomElement } from "@/utils/random";
import { computed } from "vue";
import flakkaImg from "@/assets/flakka.png";
import { useLocale, type Locale } from "@/i18n";
import type * as i18n from "@/i18n/config";
interface SectionConfig {
id: keyof Locale["home"]["sections"];
image?: keyof typeof imagesI18n.value;
}
const SECTION_CONFIGS = [
{ id: "whatIsViossa", image: "viossaFlag" },
{ id: "historyOfViossa", image: "viossaFlag" },
{ id: "community" },
] as const satisfies SectionConfig[];
const locale = useLocale();
const homeI18n = computed(() => locale.value.home);
interface ImageI18n {
src: string;
metadata: i18n.Image;
}
const imagesI18n = computed(() => {
const imagesI18n = homeI18n.value.images;
return {
viossaFlag: { src: flakkaImg, metadata: imagesI18n.viossaFlag },
} as const satisfies Record<string, ImageI18n>;
});
const greeting: Greeting = randomElement(GREETINGS);
const sectionsI18n = computed(() =>
SECTION_CONFIGS.map(({ id, image }: SectionConfig) => ({
text: homeI18n.value.sections[id],
image: image && imagesI18n.value[image],
})),
);
</script>
<template>
<div>
<section class="hero has-background-primary-soft is-primary">
<div
class="hero-body"
style="padding-top: 3.75rem; padding-bottom: 3rem">
<div class="title has-text-text-bold">{{ greeting.title }}</div>
<div class="subtitle has-text-text-bold mb-4">
{{ greeting.subtitle }}
</div>
<div
class="subtitle is-size-6 is-flex is-flex-direction-row is-align-items-center is-gap-1 has-text-text-bold">
&mdash; {{ greeting.author }} ({{
locale.vilanticLangs[greeting.lang]()
}})
<figure class="image is-32x32">
<img :src="VILANTIC_ID_TO_FLAG[greeting.lang]" />
</figure>
</div>
</div>
</section>
<section class="section container">
<HomeSectionWrapper
v-for="(sectioni18n, index) in sectionsI18n"
:key="index"
:title="sectioni18n.text.title()"
:text="sectioni18n.text.body()"
:image="sectioni18n.image?.src"
:alt="sectioni18n.image?.metadata.alt()"
:reverse="index % 2 !== 0" />
</section>
</div>
</template>

View file

@ -1,30 +1,25 @@
<script setup lang="ts">
import { useLocale } from '@/i18n';
import { useLocale } from "@/i18n";
const locale = useLocale();
</script>
<template>
<div>
<section class="section">
<h1 class="title">{{ locale.kotoba.title }}</h1>
<h1 class="title">{{ locale.kotoba.title() }}</h1>
</section>
<section class="section container">
<div class="notification is-info block">
<p>{{ locale.kotoba.searchHelp }}</p>
<p>{{ locale.kotoba.searchHelp() }}</p>
</div>
<div class="block is-flex is-flex-direction-row is-gap-2">
<input class="input" type="text"></input>
</div>
<div class="container">
<input class="input" type="text" />
</div>
<div class="container"></div>
</section>
</div>
</template>

View file

@ -0,0 +1,101 @@
<script setup lang="ts">
import LearningResourceWrapper, {
type ResourceButton,
} from "@/components/molecules/LearningResourceWrapper.vue";
import { useLocale, type Locale } from "@/i18n";
import type * as i18n from "@/i18n/config";
import { ignore } from "@/utils/ignore";
import { computed } from "vue";
import discordImg from "@/assets/discord.png";
interface ResourceConfig {
id: keyof Locale["resources"]["resources"];
image?: keyof typeof imagesI18n.value;
}
const RESOURCE_CONFIGS = [
{ id: "discord", image: "discordLogo" },
] as const satisfies ResourceConfig[];
const locale = useLocale();
const pageI18n = computed(() => locale.value.resources);
interface ImageI18n {
src: string;
metadata: i18n.Image;
}
const imagesI18n = computed(() => {
const imagesI18n = pageI18n.value.images;
return {
discordLogo: { src: discordImg, metadata: imagesI18n.discordLogo },
} as const satisfies Record<string, ImageI18n>;
});
const resourcesI18n = computed(() =>
RESOURCE_CONFIGS.map(
({ id, image }: ResourceConfig) =>
({
id,
text: pageI18n.value.resources[id],
image: image && imagesI18n.value[image],
}) as const,
),
);
const computeButtons = (
id: keyof Locale["resources"]["resources"],
): ResourceButton[] => {
// will warn us if a new variant is added that isn't handled, and so we should add a switch
// once we have a switch statement, this won't be needed as that will check for exhaustiveness
ignore<"discord">(id);
const buttons = pageI18n.value.resources[id].buttons;
return [
{
link: {
to: {
type: "external",
external: "https://discord.viossa.net",
},
newTab: true,
},
label: buttons.join.label(),
style: { color: "primary" },
},
{
link: {
to: { type: "internal", internal: { route: "/discord/rules" } },
},
label: buttons.rules.label(),
style: { color: "warning", outlined: true },
},
];
};
</script>
<template>
<div>
<section class="section">
<h1 class="title">{{ locale.resources.title() }}</h1>
</section>
<section class="section container">
<LearningResourceWrapper
v-for="(resourceI18n, index) in resourcesI18n"
:key="index"
:title="resourceI18n.text.title()"
:subtitle="resourceI18n.text.subtitle()"
:desc="resourceI18n.text.desc()"
:image="
resourceI18n.image && {
src: resourceI18n.image.src,
alt: resourceI18n.image.metadata.alt(),
}
"
:buttons="computeButtons(resourceI18n.id)" />
</section>
</div>
</template>

View file

@ -0,0 +1,10 @@
import { createRouter, createWebHistory } from "vue-router";
import { routes, handleHotUpdate } from "vue-router/auto-routes";
const router = createRouter({ history: createWebHistory(), routes });
if (import.meta.hot) {
handleHotUpdate(router);
}
export default router;

View file

@ -1,21 +0,0 @@
import { createRouter, createWebHistory } from "vue-router";
import type { RouteRecordRaw } from "vue-router";
import HomePage from "@/components/pages/HomePage.vue";
import ResourcesPage from "@/components/pages/ResourcesPage.vue";
import KotobaPage from "@/components/pages/KotobaPage.vue";
const routes: RouteRecordRaw[] = [
{ path: "/", name: "Home", component: HomePage },
{ path: "/resources", name: "Resources", component: ResourcesPage },
{ path: "/kotoba", name: "Kotoba", component: KotobaPage },
// {
// path: '/:pathMatch(.*)*', // Vue Router 4 catch-all for 404s
// name: 'NotFound',
// component: NotFoundPage,
// },
];
const router = createRouter({ history: createWebHistory(), routes });
export default router;

View file

@ -18,5 +18,9 @@ declare module 'vue-router/auto-routes' {
* Route name map generated by unplugin-vue-router
*/
export interface RouteNamedMap {
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
'/discord/rules': RouteRecordInfo<'/discord/rules', '/discord/rules', Record<never, never>, Record<never, never>>,
'/kotoba': RouteRecordInfo<'/kotoba', '/kotoba', Record<never, never>, Record<never, never>>,
'/resources': RouteRecordInfo<'/resources', '/resources', Record<never, never>, Record<never, never>>,
}
}

View file

@ -0,0 +1 @@
export type CssClass = string | false | null | undefined | CssClass[];

View file

@ -1,3 +0,0 @@
export type DeepPartial<T extends object> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

View file

@ -0,0 +1,2 @@
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters, @typescript-eslint/no-unused-vars
export function ignore<T>(_: T): void {}

View file

@ -1,16 +0,0 @@
import type { Layout } from "@/i18n/locale";
import type { DeepReadonly } from "vue";
export function localizeLayout<T>(
layout: DeepReadonly<Layout<T>>,
): T[keyof T][] {
const sections: T[keyof T][] = [];
for (const sectionId of layout.order ?? []) {
const section = (layout.data as T)[sectionId as keyof T];
if (section) {
sections.push(section);
}
}
return sections;
}

View file

@ -0,0 +1,11 @@
import type { RouteNamedMap } from "vue-router/auto-routes";
export type SmartDest =
| { type: "internal"; internal: SmartInternalDest }
| { type: "external"; external: SmartExternalDest };
export type SmartInternalDest =
| { route: keyof RouteNamedMap; id?: string }
| { route?: keyof RouteNamedMap; id: string };
export type SmartExternalDest = `https://${string}` | `http://${string}`;

View file

@ -0,0 +1,8 @@
export type DeepPartial<T extends object> =
T extends Function ? T
: { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K] };
export type Prettify<T> = T extends object ? { [K in keyof T]: T[K] } & {} : T;
export type Value<T> = T[keyof T];
export type Result<T, E> = { type: "ok"; ok: T } | { type: "err"; err: E };

View file

@ -0,0 +1,11 @@
import type { Result } from "./types";
export async function unsafeAsync<R>(
f: () => Promise<R>,
): Promise<Result<R, unknown>> {
try {
return { type: "ok", ok: await f() };
} catch (e) {
return { type: "err", err: e };
}
}

View file

@ -0,0 +1,636 @@
import type { Result, Value } from "@/utils/types";
import {
computeAllVariants,
selectionChainToString,
type PatternVariant,
type UncompiledLocale,
} from "./setup";
import type { FluentBundle, FluentVariable, Message } from "@fluent/bundle";
import { parseMarkdown, type Markdown } from "./markdown";
import {
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, PlaceableValue>;
type PlaceableValue = string | number;
export interface CompileLocaleCtx<Config extends LocaleConfig> {
bundle: FluentBundle;
uncompiled: UncompiledLocale;
config: Config;
fallback: InferLocaleFromConfig<Config> | undefined;
messageIdChain: readonly string[];
}
export interface CompileLocaleRes<Locale> {
locale: Locale;
errors: readonly string[];
}
export function compileLocale<Config extends LocaleConfig>(
ctx: CompileLocaleCtx<Config>,
): CompileLocaleRes<InferLocaleFromConfig<Config>> {
const {
bundle,
uncompiled,
config,
fallback,
messageIdChain: localeMessageIdChain = [],
} = ctx;
const errors: string[] = [];
const uncompiledKeys = new Set(Object.keys(uncompiled ?? {}));
const configKeys = new Set(Object.keys(config));
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 uncompiledValue = uncompiled?.[messageId];
const fallbackValue = fallback?.[messageId];
const valueMessageIdChain = [
...localeMessageIdChain,
messageId,
] as const;
const compiledValue = ((): Value<GenericLocale> => {
if (configMessageTypeSymbol in configValue) {
const compiledMessage = (() => {
if (uncompiledValue?.type !== "message") {
errors.push(
`Expected message for key \`${messageId}\`, found: ${typeof uncompiledValue}`,
);
return undefined;
}
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;
}
switch (configValue[configMessageTypeSymbol]) {
case configStringSymbol: {
return () =>
createMissingStringFallback(valueMessageIdChain);
}
case configMarkdownSymbol: {
return () =>
createMissingMarkdownFallback(
valueMessageIdChain,
new Set(Object.keys(configValue.slots)),
);
}
}
} 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,
uncompiledSublocale: uncompiledSubrecord,
fallbackSublocale:
typeof fallbackValue === "function" ? undefined : (
fallbackValue
),
errors,
bundle,
messageIdChain: valueMessageIdChain,
});
}
})();
locale[messageId] = compiledValue;
}
// SAFETY: validated above that all keys exist and are the correct type
return { locale: locale as InferLocaleFromConfig<Config>, errors };
}
function fmtMessageIdChain(
messageIdChain: readonly [...string[], string],
): string {
return messageIdChain.join("-");
}
interface CompileMessageCtx {
bundle: FluentBundle;
messageIdChain: readonly [...string[], string];
configValue: ConfigString<object> | ConfigMarkdown<object, object>;
uncompiledMessage: Message;
}
function compileMessage(
ctx: CompileMessageCtx,
): Result<GenericMessageFn, string> {
const { bundle, messageIdChain, configValue, uncompiledMessage } = ctx;
const pattern = uncompiledMessage.value;
if (pattern === null) {
return {
type: "err",
err: `Pattern is null for message with ID: ${fmtMessageIdChain(messageIdChain)}`,
};
}
// 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 ID: ${fmtMessageIdChain(messageIdChain)}; Found: ${selector.type}`,
};
}
if (
!Object.keys(configValue.placeables).includes(
selector.name,
)
) {
return {
type: "err",
err: `Found unexpected placeable name \`${selector.name}\` for ID: ${fmtMessageIdChain(messageIdChain)}`,
};
}
break;
}
case "var": {
if (
!Object.keys(configValue.placeables).includes(
element.name,
)
) {
return {
type: "err",
err: `Found unexpected placeable name \`${element.name}\` for ID: ${fmtMessageIdChain(messageIdChain)}`,
};
}
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 ID \`${fmtMessageIdChain(messageIdChain)}\`:\n${allVariantsRes.err}`,
};
}
const allVariants = allVariantsRes.ok;
switch (configValue[configMessageTypeSymbol]) {
case configStringSymbol: {
return compileStringMessage({
bundle,
messageIdChain,
allVariants,
pattern,
});
}
case configMarkdownSymbol: {
return compileMarkdownMessage({
bundle,
slots: new Set(Object.keys(configValue.slots)),
messageIdChain,
allVariants,
pattern,
});
}
}
}
interface CompileStringMessageCtx {
bundle: FluentBundle;
messageIdChain: readonly [...string[], string];
allVariants: readonly PatternVariant[];
pattern: Pattern;
}
function compileStringMessage(
ctx: CompileStringMessageCtx,
): Result<GenericStringMessageFn, string> {
const { bundle, messageIdChain, allVariants, pattern } = ctx;
// 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 ID \`${fmtMessageIdChain(messageIdChain)}\`:\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 ID \`${fmtMessageIdChain(messageIdChain)}\`:\n${stringRes.err}`,
};
}
}
// TODO: will need to make sure markdown/slots are escapes when inserting variable values
return {
type: "ok",
ok: (args: Record<string, PlaceableValue> = {}) => {
const stringRes = ((): Result<string, string> => {
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
return {
type: "err",
err: `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
return {
type: "err",
err: `Failed to parse string after compilation!\n${res.err}`,
};
}
const string = res.ok;
return { type: "ok", ok: string };
})();
switch (stringRes.type) {
case "ok": {
const string = stringRes.ok;
return string;
}
case "err": {
const error = stringRes.err;
console.error(error);
return createMissingStringFallback(messageIdChain);
}
}
},
};
}
interface CompileMarkdownMessageCtx {
bundle: FluentBundle;
messageIdChain: readonly [...string[], string];
slots: ReadonlySet<string>;
allVariants: readonly PatternVariant[];
pattern: Pattern;
}
function compileMarkdownMessage(
ctx: CompileMarkdownMessageCtx,
): Result<GenericMarkdownMessageFn, string> {
const { bundle, messageIdChain, slots, allVariants, pattern } = ctx;
// typecheck markdown
// 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}`,
};
}
const markdownLiteral = markdownLiteralRes.ok;
const markdownRes = parseMarkdown(markdownLiteral, slots);
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, 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 markdownLiteralRes = parseMessageLiteral(
"md",
bundle.formatPattern(pattern, escapedArgs),
);
if (markdownLiteralRes.type === "err") {
// This should hopefully never happen since we've already
// verified all message variants parse as valid markdown above
return {
type: "err",
err: `Failed to parse markdown literal after compilation!\n${markdownLiteralRes.err}`,
};
}
const markdownLiteral = markdownLiteralRes.ok;
const res = parseMarkdown(markdownLiteral, slots);
if (res.type === "err") {
// This should hopefully never happen since we've already
// verified all message variants parse as valid markdown above
return {
type: "err",
err: `Failed to parse markdown after compilation!\n${res.err}`,
};
}
return { type: "ok", ok: res.ok };
})();
switch (markdownRes.type) {
case "ok": {
const markdown = markdownRes.ok;
return markdown;
}
case "err": {
const error = markdownRes.err;
console.error(error);
return createMissingMarkdownFallback(messageIdChain, slots);
}
}
},
};
}
function createMissingStringFallback(
messageIdChain: readonly [...string[], string],
): string {
return `[#${fmtMessageIdChain(messageIdChain)}#]`;
}
function createMissingMarkdownFallback<Slot extends string>(
messageIdChain: readonly [...string[], string],
slots: ReadonlySet<Slot>,
): Markdown<Slot> {
return {
elements: [
{
type: "paragraph",
paragraph: {
spans: [
{
type: "plain",
plain: createMissingStringFallback(messageIdChain),
},
],
},
},
],
slots,
};
}
interface CompileSublocaleCtx<Subconfig extends LocaleConfig> {
subconfig: Subconfig;
uncompiledSublocale: UncompiledLocale | undefined;
fallbackSublocale: InferLocaleFromConfig<Subconfig> | undefined;
errors: string[];
bundle: FluentBundle;
messageIdChain: readonly [...string[], string];
}
function compileSublocale<Subconfig extends LocaleConfig>(
ctx: CompileSublocaleCtx<Subconfig>,
): InferLocaleFromConfig<Subconfig> {
const {
subconfig: configValue,
uncompiledSublocale: recordValue,
fallbackSublocale: fallbackValue,
errors,
bundle,
messageIdChain,
} = ctx;
const subrecord = recordValue;
const compiledSubrecordRes = compileLocale({
bundle,
uncompiled: subrecord ?? {},
config: configValue,
fallback: fallbackValue,
messageIdChain,
});
errors.push(
...compiledSubrecordRes.errors.map(
(err) =>
`Error when compiling subrecord with ID: \`${fmtMessageIdChain(messageIdChain)}\`:\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

@ -0,0 +1,106 @@
import { type Markdown } from "./markdown";
export const configMessageTypeSymbol: unique symbol =
Symbol("configMessageType");
export const configStringSymbol: unique symbol = Symbol("configString");
export interface ConfigString<
Placeables extends Partial<Record<string, ConfigPlaceableInfo>>,
> {
[configMessageTypeSymbol]: typeof configStringSymbol;
placeables: Placeables;
}
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;
slots: Slots;
bold?: boolean;
italic?: boolean;
header?: boolean;
link?: boolean;
ulist?: boolean;
}
export interface ConfigPlaceableInfo<
Type extends "string" | "number" = "string" | "number",
> {
type: Type;
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface ConfigSlotInfo {}
export function string<
const Placeables extends Partial<Record<string, ConfigPlaceableInfo>>,
>(
opt: Omit<ConfigString<Placeables>, typeof configMessageTypeSymbol>,
): ConfigString<Placeables> {
return { ...opt, [configMessageTypeSymbol]: configStringSymbol };
}
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 { ...opt, [configMessageTypeSymbol]: configMarkdownSymbol };
}
export type LocaleConfig = {
[id: string]:
| ConfigString<object>
| ConfigMarkdown<object, object>
| LocaleConfig;
};
type MessageCtx<
Placeables extends Partial<Record<string, ConfigPlaceableInfo>>,
> = {
[K in keyof Placeables]: ResolvedPlaceableType<
PlaceableType<Exclude<Placeables[K], undefined>>
>;
};
type PlaceableType<Var extends ConfigPlaceableInfo> =
Var extends ConfigPlaceableInfo<infer Type> ? Type : never;
type ResolvedPlaceableType<Type extends "string" | "number"> =
Type extends "string" ? string
: Type extends "number" ? number
: never;
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> ?
() => string
: (ctx: MessageCtx<Placeables>) => string
: Config[K] extends ConfigMarkdown<infer Placeables, object> ?
object extends MessageCtx<Placeables> ?
() => Markdown
: (ctx: MessageCtx<Placeables>) => Markdown
: never;
};
export function record<const Key extends PropertyKey, T>(
keys: readonly Key[],
initializer: () => T,
): Record<Key, T> {
const obj: Partial<Record<Key, T>> = {};
for (const key of keys) {
obj[key] = initializer();
}
// SAFETY: we set all properties from keys array above
return obj as Record<Key, T>;
}

View file

@ -0,0 +1,944 @@
import type {
SmartDest,
SmartExternalDest,
SmartInternalDest,
} from "@/utils/smart-dest";
import type { Result } from "@/utils/types";
import type { RouteNamedMap } from "vue-router/auto-routes";
import { routes } from "vue-router/auto-routes";
export interface Markdown<Slot extends string = string> {
elements: readonly MarkdownElement<Slot>[];
slots: ReadonlySet<Slot>;
}
export type MarkdownElement<Slot extends string = string> =
| { type: "paragraph"; paragraph: { spans: readonly MarkdownSpan<Slot>[] } }
| { type: "header"; header: { spans: readonly MarkdownSpan<Slot>[] } }
| {
type: "ulist";
ulist: { items: readonly (readonly MarkdownSpan<Slot>[])[] };
};
type MarkdownLine<Slot extends string = string> = {
type: "paragraph" | "header" | "ulistItem";
spans: readonly MarkdownSpan<Slot>[];
};
export type MarkdownFeature = "header" | "ulist" | "italic" | "bold" | "link";
export type MarkdownSpan<Slot extends string = string> =
| { type: "plain"; plain: string }
| { type: "italic"; italic: readonly MarkdownSpan[] }
| { type: "bold"; bold: readonly MarkdownSpan[] }
| {
type: "link";
link: {
label: readonly MarkdownSpan[];
to: SmartDest;
newTab: boolean;
};
}
| { type: "slot"; slot: Slot };
export function parseMarkdown<Slot extends string>(
markdownString: string,
slots: ReadonlySet<Slot>,
): Result<Markdown<Slot>, string> {
const linesRes = parseMarkdownLines(markdownString, slots);
if (linesRes.type === "err") {
return linesRes;
}
const lines = linesRes.ok;
const elements: MarkdownElement<Slot>[] = [];
while (true) {
const line = lines.shift();
if (line === undefined) {
break;
}
const element = ((): MarkdownElement<Slot> => {
switch (line.type) {
case "paragraph": {
return {
type: "paragraph",
paragraph: { spans: line.spans },
};
}
case "header": {
return { type: "header", header: { spans: line.spans } };
}
case "ulistItem": {
const items: (readonly MarkdownSpan<Slot>[])[] = [
line.spans,
];
while (true) {
const peekLine = lines[0];
if (
peekLine === undefined
|| peekLine.type !== "ulistItem"
) {
break;
}
items.push(peekLine.spans);
lines.shift();
}
return { type: "ulist", ulist: { items } };
}
}
})();
elements.push(element);
}
return { type: "ok", ok: { elements, slots: new Set(slots) } };
}
function parseMarkdownLines<Slot extends string>(
markdownString: string,
slots: ReadonlySet<Slot>,
): Result<MarkdownLine<Slot>[], string> {
if (markdownString.trim() === "--") {
return { type: "ok", ok: [] };
}
const lines = markdownString.split("\n");
const dequotedLines: string[] = [];
for (const line of lines) {
const MARKDOWN_LINE_AFFIX = '"';
if (!line.startsWith(MARKDOWN_LINE_AFFIX)) {
return {
type: "err",
err: `Line ${String(dequotedLines.length + 1)} of markdown must start with ${MARKDOWN_LINE_AFFIX}`,
};
}
if (!line.endsWith(MARKDOWN_LINE_AFFIX)) {
return {
type: "err",
err: `Line ${String(dequotedLines.length + 1)} of markdown must end with ${MARKDOWN_LINE_AFFIX}`,
};
}
const deprefixed = line.substring(MARKDOWN_LINE_AFFIX.length);
const dequoted = deprefixed.substring(
0,
deprefixed.length - MARKDOWN_LINE_AFFIX.length,
);
dequotedLines.push(dequoted);
}
const markdownLines: MarkdownLine<Slot>[] = [];
for (const line of dequotedLines) {
const markdownLineRes = parseMarkdownLine(line, slots);
if (markdownLineRes.type === "err") {
return {
type: "err",
err: `On line ${String(markdownLines.length + 1)}:\n${markdownLineRes.err}`,
};
}
const markdownLine = markdownLineRes.ok;
markdownLines.push(markdownLine);
}
return { type: "ok", ok: markdownLines };
}
function parseMarkdownLine<Slot extends string>(
line: string,
slots: ReadonlySet<Slot>,
): Result<MarkdownLine<Slot>, string> {
interface ResolvedLine {
deprefixedLine: string;
type: MarkdownLine["type"];
}
const { deprefixedLine, type } = ((): ResolvedLine => {
if (line.startsWith("#")) {
return { deprefixedLine: line.substring(1), type: "header" };
} else if (line.startsWith("-")) {
return { deprefixedLine: line.substring(1), type: "ulistItem" };
} else {
return { deprefixedLine: line, type: "paragraph" };
}
})();
const spansRes = parseMarkdownSpans(deprefixedLine, slots);
if (spansRes.type === "err") {
return {
type: "err",
err: `While parsing ${type} spans:\n${spansRes.err}`,
};
}
const spans = spansRes.ok;
return { type: "ok", ok: { type, spans } };
}
function parseMarkdownSpans<Slot extends string>(
line: string,
slots: ReadonlySet<Slot>,
): Result<MarkdownSpan<Slot>[], string> {
if (line.startsWith("#")) {
// subheaders may be supported in the future,
// so ignoring them or treating them as h1 headers now would be a breaking change when
// subheader support is implemented.
// making subheaders a compile error for now ensures
// all current i18n is backwards-compatible when/if they are implemented
return { type: "err", err: "Subheaders are not supported." };
}
const chars = line.split("");
const spansRes = readMarkdownSpans(
chars,
slots,
ParseMarkdownSpansManager.new(),
);
if (spansRes.type === "err") {
return spansRes;
}
const spans = spansRes.ok;
return { type: "ok", ok: spans };
}
class ParseMarkdownSpansManager {
private inItalic: boolean;
private inBold: boolean;
private constructor() {
this.inItalic = false;
this.inBold = false;
}
public static new(): ParseMarkdownSpansManager {
return new this();
}
public tryUseItalic<R>(f: () => Result<R, string>): Result<R, string> {
if (this.inItalic) {
return {
type: "err",
err: "Cannot nest italic span (*) inside of another italic span",
};
}
this.inItalic = true;
const fRes = f();
if (fRes.type === "err") {
return fRes;
}
this.inItalic = false;
const fOk = fRes.ok;
return { type: "ok", ok: fOk };
}
public tryUseBold<R>(f: () => Result<R, string>): Result<R, string> {
if (this.inBold) {
return {
type: "err",
err: "Cannot nest bold span (**) inside of another bold span",
};
}
this.inBold = true;
const fRes = f();
if (fRes.type === "err") {
return fRes;
}
this.inBold = false;
const fOk = fRes.ok;
return { type: "ok", ok: fOk };
}
}
function readMarkdownSpans<Slot extends string>(
chars: string[],
slots: ReadonlySet<Slot>,
manager: ParseMarkdownSpansManager,
): Result<MarkdownSpan<Slot>[], string> {
const spans: MarkdownSpan<Slot>[] = [];
while (true) {
const spanRes = readMarkdownSpan(chars, slots, manager);
if (spanRes.type === "err") {
return spanRes;
}
const span = spanRes.ok;
if (span === undefined) {
break;
}
spans.push(span);
}
return { type: "ok", ok: spans };
}
function readMarkdownSpan<Slot extends string>(
chars: string[],
slots: ReadonlySet<Slot>,
manager: ParseMarkdownSpansManager,
): Result<MarkdownSpan<Slot> | undefined, string> {
const [firstChar, secondChar, thirdChar] = chars;
if (firstChar === undefined) {
return { type: "ok", ok: undefined };
} else if (firstChar === "<") {
return readMarkdownSpanSlot(chars, slots);
} else if (firstChar === "[") {
return readMarkdownSpanLink(chars, slots, manager);
} else if (firstChar === "*") {
if (secondChar !== "*") {
return readMarkdownSpanItalic(chars, slots, manager);
}
if (thirdChar !== "*") {
return readMarkdownSpanBold(chars, slots, manager);
}
return readMarkdownSpanBoldItalic(chars, slots, manager);
} else {
return readMarkdownSpanPlain(chars);
}
}
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
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>(
chars: string[],
slots: ReadonlySet<Slot>,
): Result<MarkdownSpan<Slot>, string> {
const openAngleRes = expectReadChar(chars, "<");
if (openAngleRes.type === "err") {
return openAngleRes;
}
const slotNameRes = readUntilClosing({
chars,
elementName: "slot",
closingChar: ">",
});
if (slotNameRes.type === "err") {
return slotNameRes;
}
const slotName = slotNameRes.ok;
if (!iterableContains(slots, slotName)) {
return { type: "err", err: `Unexpected slot name: ${slotName}` };
}
return { type: "ok", ok: { type: "slot", slot: slotName } };
}
function readMarkdownSpanLink<Slot extends string>(
chars: string[],
slots: ReadonlySet<Slot>,
manager: ParseMarkdownSpansManager,
): Result<MarkdownSpan<Slot>, string> {
const openSquareRes = expectReadChar(chars, "[");
if (openSquareRes.type === "err") {
return openSquareRes;
}
const labelElementsRes = readMarkdownSpans(chars, slots, manager);
if (labelElementsRes.type === "err") {
return labelElementsRes;
}
const closeSquareRes = expectReadChar(chars, "]");
if (closeSquareRes.type === "err") {
return closeSquareRes;
}
const labelElements = labelElementsRes.ok;
const openParenRes = expectReadChar(chars, "(");
if (openParenRes.type === "err") {
return openParenRes;
}
interface LinkProps {
dest: SmartDest;
newTab: boolean;
}
const linkPropsRes = ((): Result<LinkProps, string> => {
if (peekStringEq(chars, "external")) {
const externalRes = expectReadString(chars, "external");
if (externalRes.type === "err") {
return externalRes;
}
const dotRes = expectReadChar(chars, ".");
if (dotRes.type === "err") {
return dotRes;
}
const tabStringRes = readUntilClosing({
chars,
elementName: "link tab",
closingChar: ":",
});
if (tabStringRes.type === "err") {
return tabStringRes;
}
const tabString = tabStringRes.ok;
const newTabRes = ((): Result<boolean, string> => {
switch (tabString) {
case "new": {
return { type: "ok", ok: true };
}
case "replace": {
return { type: "ok", ok: false };
}
default: {
return {
type: "err",
err: `Expected \`replace\` or \`new\`; Found: ${tabString}`,
};
}
}
})();
if (newTabRes.type === "err") {
return newTabRes;
}
const newTab = newTabRes.ok;
const destRes = readUntilClosing({
chars,
elementName: "link dest",
closingChar: ")",
});
if (destRes.type === "err") {
return destRes;
}
const dest = destRes.ok;
const externalDestRes = validateExternalDest(dest);
if (externalDestRes.type === "err") {
return externalDestRes;
}
const externalDest = externalDestRes.ok;
return {
type: "ok",
ok: {
dest: { type: "external", external: externalDest },
newTab,
},
};
} else if (peekStringEq(chars, "internal")) {
const internalRes = expectReadString(chars, "internal");
if (internalRes.type === "err") {
return internalRes;
}
const dotRes = expectReadChar(chars, ".");
if (dotRes.type === "err") {
return dotRes;
}
const tabStringRes = readUntilClosing({
chars,
elementName: "link tab",
closingChar: ":",
});
if (tabStringRes.type === "err") {
return tabStringRes;
}
const tabString = tabStringRes.ok;
const newTabRes = ((): Result<boolean, string> => {
switch (tabString) {
case "new": {
return { type: "ok", ok: true };
}
case "replace": {
return { type: "ok", ok: false };
}
default: {
return {
type: "err",
err: `Expected \`replace\` or \`new\`; Found: ${tabString}`,
};
}
}
})();
if (newTabRes.type === "err") {
return newTabRes;
}
const newTab = newTabRes.ok;
const destRes = readUntilClosing({
chars,
elementName: "link dest",
closingChar: ")",
});
if (destRes.type === "err") {
return destRes;
}
const dest = destRes.ok;
const internalDestRes = validateInternalDest(dest);
if (internalDestRes.type === "err") {
return internalDestRes;
}
const internalDest = internalDestRes.ok;
return {
type: "ok",
ok: {
dest: { type: "internal", internal: internalDest },
newTab,
},
};
} else {
return {
type: "err",
err: `Expected external or internal link prefix; Found: "${chars.slice(0, 10).join("")}..."`,
};
}
})();
if (linkPropsRes.type === "err") {
return linkPropsRes;
}
const { dest, newTab } = linkPropsRes.ok;
const resolvedLabel: MarkdownSpan[] =
labelElements.length === 0 ?
[
{
type: "plain",
plain:
dest.type === "external" ?
dest.external
: `${window.location.protocol}${window.location.hostname}${dest.internal.route ?? window.location.pathname}${dest.internal.id === undefined ? "" : `#${dest.internal.id}`}`,
},
]
: labelElements;
return {
type: "ok",
ok: { type: "link", link: { label: resolvedLabel, to: dest, newTab } },
};
}
function validateExternalDest(dest: string): Result<SmartExternalDest, string> {
const HTTPS_PREFIX = "https://";
const HTTP_PREFIX = "http://";
if (dest.startsWith(HTTPS_PREFIX)) {
return {
type: "ok",
ok: `${HTTPS_PREFIX}${dest.substring(HTTPS_PREFIX.length)}`,
};
}
if (dest.startsWith(HTTP_PREFIX)) {
return {
type: "ok",
ok: `${HTTP_PREFIX}${dest.substring(HTTP_PREFIX.length)}`,
};
}
return {
type: "err",
err: `External dest must start with https:// or http://`,
};
}
function validateInternalDest(dest: string): Result<SmartInternalDest, string> {
const [routeString, id] = dest.split("#");
const validatedRouteRes = ((): Result<
keyof RouteNamedMap | undefined,
string
> => {
if (routeString === undefined || routeString.length === 0) {
return { type: "ok", ok: undefined };
}
const route = routes.find((route) => route.path === routeString);
if (route === undefined) {
return {
type: "err",
err: `Route with ID \`${routeString}\` does not exist`,
};
}
return {
type: "ok",
// SAFETY: we validated the route exists in the router about
ok: route.path as keyof RouteNamedMap,
};
})();
if (validatedRouteRes.type === "err") {
return validatedRouteRes;
}
const validatedRoute = validatedRouteRes.ok;
if (validatedRoute !== undefined) {
return { type: "ok", ok: { route: validatedRoute, id } };
} else if (id !== undefined) {
return { type: "ok", ok: { route: validatedRoute, id } };
} else {
return {
type: "err",
err: `Either route or ID must be defined for internal dest`,
};
}
}
function readMarkdownSpanItalic<Slot extends string>(
chars: string[],
slots: ReadonlySet<Slot>,
manager: ParseMarkdownSpansManager,
): Result<MarkdownSpan<Slot>, string> {
return manager.tryUseItalic(() => {
const singleStarRes = expectReadString(chars, "*");
if (singleStarRes.type === "err") {
return singleStarRes;
}
const spans: MarkdownSpan<Slot>[] = [];
let closed = false;
while (true) {
const spanRes = readMarkdownSpan(chars, slots, manager);
if (spanRes.type === "err") {
return spanRes;
}
const span = spanRes.ok;
if (span === undefined) {
break;
}
spans.push(span);
const [firstChar] = chars;
if (firstChar === "*") {
chars.shift();
closed = true;
break;
} else if (firstChar === undefined) {
closed = false;
break;
}
}
if (!closed) {
return { type: "err", err: "Italic span (*) is never closed" };
}
return { type: "ok", ok: { type: "italic", italic: spans } };
});
}
function readMarkdownSpanBold<Slot extends string>(
chars: string[],
slots: ReadonlySet<Slot>,
manager: ParseMarkdownSpansManager,
): Result<MarkdownSpan<Slot>, string> {
return manager.tryUseBold(() => {
const doubleStarRes = expectReadString(chars, "**");
if (doubleStarRes.type === "err") {
return doubleStarRes;
}
const spans: MarkdownSpan<Slot>[] = [];
let closed = false;
while (true) {
const spanRes = readMarkdownSpan(chars, slots, manager);
if (spanRes.type === "err") {
return spanRes;
}
const span = spanRes.ok;
if (span === undefined) {
break;
}
spans.push(span);
const [firstChar, secondChar] = chars;
if (firstChar === "*" && secondChar === "*") {
chars.shift();
chars.shift();
closed = true;
break;
} else if (firstChar === undefined) {
closed = false;
break;
}
}
if (!closed) {
return { type: "err", err: "Bold span (**) is never closed" };
}
return { type: "ok", ok: { type: "bold", bold: spans } };
});
}
function readMarkdownSpanBoldItalic<Slot extends string>(
chars: string[],
slots: ReadonlySet<Slot>,
manager: ParseMarkdownSpansManager,
): Result<MarkdownSpan<Slot>, string> {
return manager.tryUseBold(() =>
manager.tryUseItalic(() => {
const tripleStarRes = expectReadString(chars, "***");
if (tripleStarRes.type === "err") {
return tripleStarRes;
}
const spans: MarkdownSpan<Slot>[] = [];
let closed = false;
while (true) {
const spanRes = readMarkdownSpan(chars, slots, manager);
if (spanRes.type === "err") {
return spanRes;
}
const span = spanRes.ok;
if (span === undefined) {
break;
}
spans.push(span);
const [firstChar, secondChar, thirdChar] = chars;
if (
firstChar === "*"
&& secondChar === "*"
&& thirdChar === "*"
) {
chars.shift();
chars.shift();
chars.shift();
closed = true;
break;
} else if (firstChar === undefined) {
closed = false;
break;
}
}
if (!closed) {
return {
type: "err",
err: "Bold italic span (***) is never closed",
};
}
return {
type: "ok",
ok: { type: "bold", bold: [{ type: "italic", italic: spans }] },
};
}),
);
}
function readMarkdownSpanPlain<Slot extends string>(
chars: string[],
): Result<MarkdownSpan<Slot> | undefined, string> {
let plain = "";
let escaped = false;
while (true) {
const peek = chars[0];
if (peek === undefined) {
break;
}
if (escaped) {
escaped = false;
} else {
if (peek === "\\") {
escaped = true;
chars.shift();
continue;
}
if (
peek === "*"
|| peek === "<"
|| peek === ">"
|| peek === "["
|| peek === "]"
) {
break;
}
}
plain += peek;
chars.shift();
}
return {
type: "ok",
ok: plain.length === 0 ? undefined : { type: "plain", plain },
};
}
function expectReadChar(
chars: string[],
expectedChar: string,
): Result<void, string> {
const nextChar = chars.shift();
if (nextChar !== expectedChar) {
return {
type: "err",
err: `Expected: "${expectedChar}"; Found: ${nextChar === undefined ? "undefined" : `"${nextChar}"`}`,
};
}
return { type: "ok", ok: undefined };
}
function peekStringEq(chars: string[], expectedString: string): boolean {
return chars.slice(0, expectedString.length).join("") === expectedString;
}
function expectReadString(
chars: string[],
expectedString: string,
): Result<void, string> {
let foundString: string | undefined = undefined;
for (const expectedChar of expectedString) {
const nextChar = chars.shift();
if (nextChar !== undefined) {
foundString = (foundString ?? "") + nextChar;
}
if (expectedChar !== nextChar) {
return {
type: "err",
err: `Expected: "${expectedString}"; Found: ${foundString === undefined ? "undefined" : `"${foundString}"`}`,
};
}
}
return { type: "ok", ok: undefined };
}
interface ReadUntilClosingCtx {
chars: string[];
elementName: string;
closingChar: string;
}
function readUntilClosing(ctx: ReadUntilClosingCtx): Result<string, string> {
const { chars, elementName, closingChar } = ctx;
let value = "";
let closed = false;
let escaped = false;
while (true) {
const char = chars.shift();
if (char === undefined) {
closed = false;
break;
}
if (char === "\\") {
escaped = true;
continue;
}
if (char === closingChar && !escaped) {
closed = true;
break;
}
value += char;
}
if (!closed) {
return {
type: "err",
err: `Unclosed ${elementName}: <${value.replaceAll(closingChar, `\\${closingChar}`)}`,
};
}
return { type: "ok", ok: value };
}
export function isEmptyMarkdown(markdown: Markdown): boolean {
// const [firstLine] = markdown.lines;
// if (firstLine === undefined) {
// return true;
// }
// const [firstElement] = firstLine.elements;
// if (firstElement === undefined) {
// return true;
// }
// if (firstElement.type === "plain" && firstElement.plain.length === 0) {
// return true;
// }
// return false;
return markdown.elements.length === 0;
}

View file

@ -0,0 +1,201 @@
import type { Result } from "@/utils/types";
import { FluentBundle, FluentResource } from "@fluent/bundle";
import { unsafeAsync } from "@/utils/unsafe";
import type { Literal, Message, Pattern } from "@fluent/bundle/esm/ast";
export async function loadFluentBundle(
localeId: string,
src: string,
): Promise<Result<FluentBundle, string>> {
const ftlFileResponseRes = await unsafeAsync(() => fetch(src));
if (ftlFileResponseRes.type === "err") {
return { type: "err", err: `Failed to fetch locale from src: ${src}` };
}
const ftlFileResponse = ftlFileResponseRes.ok;
const ftlFileTextRes = await unsafeAsync(() => ftlFileResponse.text());
if (ftlFileTextRes.type === "err") {
return {
type: "err",
err: `Failed to fetch text content of FTL file from src: ${src}`,
};
}
const ftlFileText = ftlFileTextRes.ok;
const resource = new FluentResource(ftlFileText);
const bundle = new FluentBundle(localeId);
const errors = bundle.addResource(resource);
if (errors.length > 0) {
return {
type: "err",
err: `Failed to add Fluent resource to bundle:\n${errors.join("\n")}`,
};
}
return { type: "ok", ok: bundle };
}
export type UncompiledLocale = {
[id: string]:
| { type: "message"; message: Message }
| { type: "subrecord"; subrecord: UncompiledLocale };
};
export function bundleToUncompiledLocaleRecord(
bundle: FluentBundle,
): Result<UncompiledLocale, string> {
const record: UncompiledLocale = {};
for (const [id, message] of bundle._messages) {
const idChain = id.split("-");
let subrecord = record;
while (true) {
const subId = idChain.shift();
if (subId === undefined) {
return {
type: "err",
err: `Reached end of message ID chain before terminating for message ID: ${id}`,
};
}
if (idChain.length === 0) {
subrecord[subId] = { type: "message", message };
break;
} else {
const maybeSubrecord = (subrecord[subId] ??= {
type: "subrecord",
subrecord: {},
});
if (maybeSubrecord.type === "subrecord") {
subrecord = maybeSubrecord.subrecord;
} else {
return {
type: "err",
err: `Found message when expected subrecord for message ID: ${id} @ subId: ${subId}`,
};
}
}
}
}
return { type: "ok", ok: record };
}
export type SelectionChain = (Literal | SelectionChain)[];
export function selectionChainToString(chain: SelectionChain): string {
return chain
.map((part) => {
if ("type" in part) {
switch (part.type) {
case "str": {
return `[${part.value}]`;
}
case "num": {
return `[${String(part.value)};${String(part.precision)}]`;
}
}
}
return `(${selectionChainToString(part)})`;
})
.join("+");
}
export interface PatternVariant {
selectionChain: SelectionChain;
string: string;
}
export function computeAllVariants(
pattern: Pattern,
): Result<PatternVariant[], string> {
if (typeof pattern === "string") {
return { type: "ok", ok: [{ selectionChain: [], string: pattern }] };
}
let variants: PatternVariant[] = [{ selectionChain: [], string: "" }];
for (const element of pattern) {
if (typeof element === "string") {
variants = variants.map((variant) => ({
selectionChain: variant.selectionChain,
string: variant.string + element,
}));
continue;
}
switch (element.type) {
case "select": {
const selectVariants: PatternVariant[] = [];
for (const selectVariant of element.variants) {
const variantComputedRes = computeAllVariants(
selectVariant.value,
);
if (variantComputedRes.type === "err") {
return {
type: "err",
err: `Failed to compute select variants:\n${variantComputedRes.err}`,
};
}
const variantComputed = variantComputedRes.ok;
selectVariants.push(
...variantComputed.map(
(v): PatternVariant => ({
selectionChain: [
selectVariant.key,
...v.selectionChain,
],
string: v.string,
}),
),
);
}
variants = variants.flatMap((variant) =>
selectVariants.map(
(selectVariant): PatternVariant => ({
selectionChain: [
...variant.selectionChain,
selectVariant.selectionChain,
],
string: variant.string + selectVariant.string,
}),
),
);
break;
}
case "var": {
// used as a stand-in for runtime-provided values
const DUMMY_STRING = "$$$";
variants = variants.map((variant) => ({
selectionChain: variant.selectionChain,
string: variant.string + DUMMY_STRING,
}));
break;
}
case "str": {
variants = variants.map((variant) => ({
selectionChain: variant.selectionChain,
string: variant.string + element.value,
}));
break;
}
default: {
return {
type: "err",
err: `Unhandled PatternElement type: ${element.type}`,
};
}
}
}
return { type: "ok", ok: variants };
}

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

@ -12,14 +12,25 @@
"noUncheckedIndexedAccess": true,
"module": "esnext",
"moduleResolution": "bundler",
"baseUrl": ".",
"target": "esnext",
"lib": [
"ESNext",
"DOM",
],
"rootDir": "src",
"paths": {
"@/*": [
"src/*"
"./src/*"
]
},
},
"vueCompilerOptions": {
"strictTemplates": true,
},
"include": [
"src"
"src",
],
"exclude": [
"./eslint.config.js"
]
}

View file

@ -1,7 +1,11 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
]
}

View file

@ -1,9 +1,11 @@
import path from "path";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueRouter from "unplugin-vue-router/vite";
export default defineConfig({
plugins: [vue({})],
plugins: [vueRouter({ root: "src", routesFolder: "pages" }), vue({})],
resolve: { alias: { "@": path.resolve(import.meta.dirname, "src") } },
server: { port: 1224 },
assetsInclude: ["**/*.ftl"],
});

482
pnpm-lock.yaml generated
View file

@ -69,11 +69,14 @@ importers:
apps/vdn-static:
dependencies:
'@fluent/bundle':
specifier: ^0.19.1
version: 0.19.1
'@tailwindcss/vite':
specifier: ^4.1.6
version: 4.1.10(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.2)(tsx@4.20.2)(yaml@2.8.0))
'@types/node':
specifier: ^22.15.17
specifier: ^22.15.31
version: 22.15.31
'@vueuse/components':
specifier: ^13.3.0
@ -103,6 +106,9 @@ importers:
specifier: ^4.5.1
version: 4.5.1(vue@3.5.16(typescript@5.8.3))
devDependencies:
'@eslint/js':
specifier: ^9.39.2
version: 9.39.2
'@repo/common':
specifier: workspace:*
version: link:../../libs/common
@ -113,11 +119,14 @@ importers:
specifier: ^0.7.0
version: 0.7.0(typescript@5.8.3)(vue@3.5.16(typescript@5.8.3))
eslint:
specifier: ^9.26.0
version: 9.28.0(jiti@2.4.2)
specifier: ^9.39.2
version: 9.39.2(jiti@2.4.2)
eslint-plugin-vue:
specifier: ^10.1.0
version: 10.2.0(eslint@9.28.0(jiti@2.4.2))(vue-eslint-parser@10.1.3(eslint@9.28.0(jiti@2.4.2)))
specifier: ^10.7.0
version: 10.7.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.4.2))(typescript@5.8.3))(eslint@9.39.2(jiti@2.4.2))(vue-eslint-parser@10.2.0(eslint@9.39.2(jiti@2.4.2)))
globals:
specifier: ^17.3.0
version: 17.3.0
prettier:
specifier: ^3.5.3
version: 3.5.3
@ -128,14 +137,17 @@ importers:
specifier: ~5.8.3
version: 5.8.3
typescript-eslint:
specifier: ^8.32.1
version: 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)
specifier: ^8.55.0
version: 8.55.0(eslint@9.39.2(jiti@2.4.2))(typescript@5.8.3)
unplugin-vue-router:
specifier: ^0.12.0
version: 0.12.0(vue-router@4.5.1(vue@3.5.16(typescript@5.8.3)))(vue@3.5.16(typescript@5.8.3))
vite:
specifier: ^6.3.5
version: 6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.2)(tsx@4.20.2)(yaml@2.8.0)
vue-eslint-parser:
specifier: ^10.2.0
version: 10.2.0(eslint@9.39.2(jiti@2.4.2))
vue-tsc:
specifier: ^2.2.8
version: 2.2.10(typescript@5.8.3)
@ -338,36 +350,42 @@ packages:
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
'@eslint-community/regexpp@4.12.1':
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
'@eslint-community/eslint-utils@4.9.1':
resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
'@eslint-community/regexpp@4.12.2':
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
'@eslint/config-array@0.20.0':
resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==}
'@eslint/config-array@0.21.1':
resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/config-helpers@0.2.2':
resolution: {integrity: sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==}
'@eslint/config-helpers@0.4.2':
resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/core@0.14.0':
resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==}
'@eslint/core@0.17.0':
resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/eslintrc@3.3.1':
resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
'@eslint/eslintrc@3.3.3':
resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/js@9.28.0':
resolution: {integrity: sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==}
'@eslint/js@9.39.2':
resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/object-schema@2.1.6':
resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
'@eslint/object-schema@2.1.7':
resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/plugin-kit@0.3.1':
resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==}
'@eslint/plugin-kit@0.4.1':
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@feathersjs/commons@5.0.34':
@ -382,6 +400,10 @@ packages:
resolution: {integrity: sha512-kLfWnuhbC25CPkR1/TDcVs0rSiv0JLNxrpUivLwc7FUnkyeciRi5VOmC1SOzL2SOagcozu3+m4VQiONyzgfY7w==}
engines: {node: '>= 14'}
'@fluent/bundle@0.19.1':
resolution: {integrity: sha512-SWJLZrPamDPsJlFFOW1nkgN0j0rbPbmSdmK0XAoXlyqKieLtMVl4vzng3aR5pwKoUx0scug8+YY2oct3fdfy9A==}
engines: {node: '>=18.0.0', npm: '>=7.0.0'}
'@gar/promisify@1.1.3':
resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
@ -421,6 +443,14 @@ packages:
resolution: {integrity: sha512-+I4vRzHm38VjLr/CAciEPJhGYFzWWW4HMTm+6H3WqknXLh0ozNX9oC8ogMUwTSXYR/wGUb1/lTpNziiCH5MybQ==}
engines: {node: '>= 16'}
'@isaacs/balanced-match@4.0.1':
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
engines: {node: 20 || >=22}
'@isaacs/brace-expansion@5.0.1':
resolution: {integrity: sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==}
engines: {node: 20 || >=22}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
@ -798,63 +828,63 @@ packages:
'@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
'@typescript-eslint/eslint-plugin@8.34.0':
resolution: {integrity: sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==}
'@typescript-eslint/eslint-plugin@8.55.0':
resolution: {integrity: sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
'@typescript-eslint/parser': ^8.34.0
'@typescript-eslint/parser': ^8.55.0
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <5.9.0'
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/parser@8.34.0':
resolution: {integrity: sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==}
'@typescript-eslint/parser@8.55.0':
resolution: {integrity: sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <5.9.0'
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/project-service@8.34.0':
resolution: {integrity: sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==}
'@typescript-eslint/project-service@8.55.0':
resolution: {integrity: sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <5.9.0'
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/scope-manager@8.34.0':
resolution: {integrity: sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==}
'@typescript-eslint/scope-manager@8.55.0':
resolution: {integrity: sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.34.0':
resolution: {integrity: sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==}
'@typescript-eslint/tsconfig-utils@8.55.0':
resolution: {integrity: sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <5.9.0'
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/type-utils@8.34.0':
resolution: {integrity: sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==}
'@typescript-eslint/type-utils@8.55.0':
resolution: {integrity: sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <5.9.0'
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/types@8.34.0':
resolution: {integrity: sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==}
'@typescript-eslint/types@8.55.0':
resolution: {integrity: sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.34.0':
resolution: {integrity: sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==}
'@typescript-eslint/typescript-estree@8.55.0':
resolution: {integrity: sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <5.9.0'
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/utils@8.34.0':
resolution: {integrity: sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==}
'@typescript-eslint/utils@8.55.0':
resolution: {integrity: sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <5.9.0'
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/visitor-keys@8.34.0':
resolution: {integrity: sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==}
'@typescript-eslint/visitor-keys@8.55.0':
resolution: {integrity: sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@vitejs/plugin-vue@5.2.4':
@ -1237,6 +1267,15 @@ packages:
supports-color:
optional: true
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'}
@ -1360,12 +1399,19 @@ packages:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
eslint-plugin-vue@10.2.0:
resolution: {integrity: sha512-tl9s+KN3z0hN2b8fV2xSs5ytGl7Esk1oSCxULLwFcdaElhZ8btYYZFrWxvh4En+czrSDtuLCeCOGa8HhEZuBdQ==}
eslint-plugin-vue@10.7.0:
resolution: {integrity: sha512-r2XFCK4qlo1sxEoAMIoTTX0PZAdla0JJDt1fmYiworZUX67WeEGqm+JbyAg3M+pGiJ5U6Mp5WQbontXWtIW7TA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
'@stylistic/eslint-plugin': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0
'@typescript-eslint/parser': ^7.0.0 || ^8.0.0
eslint: ^8.57.0 || ^9.0.0
vue-eslint-parser: ^10.0.0
peerDependenciesMeta:
'@stylistic/eslint-plugin':
optional: true
'@typescript-eslint/parser':
optional: true
eslint-scope@8.4.0:
resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
@ -1379,8 +1425,8 @@ packages:
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint@9.28.0:
resolution: {integrity: sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==}
eslint@9.39.2:
resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
hasBin: true
peerDependencies:
@ -1393,8 +1439,8 @@ packages:
resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
esquery@1.6.0:
resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
esquery@1.7.0:
resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
engines: {node: '>=0.10'}
esrecurse@4.3.0:
@ -1458,6 +1504,15 @@ packages:
picomatch:
optional: true
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
fetch-blob@3.2.0:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
@ -1595,6 +1650,10 @@ packages:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
engines: {node: '>=18'}
globals@17.3.0:
resolution: {integrity: sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==}
engines: {node: '>=18'}
google-auth-library@9.15.1:
resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==}
engines: {node: '>=14'}
@ -1618,9 +1677,6 @@ packages:
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
gtoken@7.1.0:
resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==}
engines: {node: '>=14.0.0'}
@ -1784,8 +1840,8 @@ packages:
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
hasBin: true
js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
json-bigint@1.0.0:
@ -1893,9 +1949,6 @@ packages:
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
@ -1958,8 +2011,8 @@ packages:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
engines: {node: '>=10'}
minimatch@10.0.1:
resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==}
minimatch@10.1.2:
resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==}
engines: {node: 20 || >=22}
minimatch@3.1.2:
@ -2179,6 +2232,10 @@ packages:
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
engines: {node: '>=12'}
picomatch@4.0.3:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
@ -2189,8 +2246,8 @@ packages:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
postcss-selector-parser@6.1.2:
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
postcss-selector-parser@7.1.1:
resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
engines: {node: '>=4'}
postcss@8.5.4:
@ -2330,6 +2387,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
semver@7.7.4:
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
engines: {node: '>=10'}
hasBin: true
send@1.2.0:
resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
engines: {node: '>= 18'}
@ -2487,6 +2549,10 @@ packages:
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
engines: {node: '>=12.0.0'}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
to-buffer@1.2.1:
resolution: {integrity: sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==}
engines: {node: '>= 0.4'}
@ -2502,8 +2568,8 @@ packages:
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
ts-api-utils@2.1.0:
resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
ts-api-utils@2.4.0:
resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==}
engines: {node: '>=18.12'}
peerDependencies:
typescript: '>=4.8.4'
@ -2621,12 +2687,12 @@ packages:
typeorm-aurora-data-api-driver:
optional: true
typescript-eslint@8.34.0:
resolution: {integrity: sha512-MRpfN7uYjTrTGigFCt8sRyNqJFhjN0WwZecldaqhWm+wy0gaRt8Edb/3cuUy0zdq2opJWT6iXINKAtewnDOltQ==}
typescript-eslint@8.55.0:
resolution: {integrity: sha512-HE4wj+r5lmDVS9gdaN0/+iqNvPZwGfnJ5lZuz7s5vLlg9ODw0bIiiETaios9LvFI1U94/VBXGm3CB2Y5cNFMpw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <5.9.0'
typescript: '>=4.8.4 <6.0.0'
typescript@5.8.3:
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
@ -2729,8 +2795,8 @@ packages:
vscode-uri@3.1.0:
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
vue-eslint-parser@10.1.3:
resolution: {integrity: sha512-dbCBnd2e02dYWsXoqX5yKUZlOt+ExIpq7hmHKPb5ZqKcjf++Eo0hMseFTZMLKThrUk61m+Uv6A2YSBve6ZvuDQ==}
vue-eslint-parser@10.2.0:
resolution: {integrity: sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
@ -2937,48 +3003,55 @@ snapshots:
'@esbuild/win32-x64@0.25.5':
optional: true
'@eslint-community/eslint-utils@4.7.0(eslint@9.28.0(jiti@2.4.2))':
'@eslint-community/eslint-utils@4.7.0(eslint@9.39.2(jiti@2.4.2))':
dependencies:
eslint: 9.28.0(jiti@2.4.2)
eslint: 9.39.2(jiti@2.4.2)
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.1': {}
'@eslint/config-array@0.20.0':
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.4.2))':
dependencies:
'@eslint/object-schema': 2.1.6
debug: 4.4.1
eslint: 9.39.2(jiti@2.4.2)
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
'@eslint/config-array@0.21.1':
dependencies:
'@eslint/object-schema': 2.1.7
debug: 4.4.3
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
'@eslint/config-helpers@0.2.2': {}
'@eslint/config-helpers@0.4.2':
dependencies:
'@eslint/core': 0.17.0
'@eslint/core@0.14.0':
'@eslint/core@0.17.0':
dependencies:
'@types/json-schema': 7.0.15
'@eslint/eslintrc@3.3.1':
'@eslint/eslintrc@3.3.3':
dependencies:
ajv: 6.12.6
debug: 4.4.1
debug: 4.4.3
espree: 10.4.0
globals: 14.0.0
ignore: 5.3.2
import-fresh: 3.3.1
js-yaml: 4.1.0
js-yaml: 4.1.1
minimatch: 3.1.2
strip-json-comments: 3.1.1
transitivePeerDependencies:
- supports-color
'@eslint/js@9.28.0': {}
'@eslint/js@9.39.2': {}
'@eslint/object-schema@2.1.6': {}
'@eslint/object-schema@2.1.7': {}
'@eslint/plugin-kit@0.3.1':
'@eslint/plugin-kit@0.4.1':
dependencies:
'@eslint/core': 0.14.0
'@eslint/core': 0.17.0
levn: 0.4.1
'@feathersjs/commons@5.0.34': {}
@ -2991,6 +3064,8 @@ snapshots:
'@feathersjs/hooks@0.9.0': {}
'@fluent/bundle@0.19.1': {}
'@gar/promisify@1.1.3':
optional: true
@ -3029,6 +3104,12 @@ snapshots:
'@intlify/shared@11.1.5': {}
'@isaacs/balanced-match@4.0.1': {}
'@isaacs/brace-expansion@5.0.1':
dependencies:
'@isaacs/balanced-match': 4.0.1
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
@ -3074,7 +3155,7 @@ snapshots:
'@npmcli/fs@1.1.1':
dependencies:
'@gar/promisify': 1.1.3
semver: 7.7.2
semver: 7.7.4
optional: true
'@npmcli/move-file@1.1.2':
@ -3338,96 +3419,95 @@ snapshots:
'@types/web-bluetooth@0.0.21': {}
'@typescript-eslint/eslint-plugin@8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)':
'@typescript-eslint/eslint-plugin@8.55.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.4.2))(typescript@5.8.3))(eslint@9.39.2(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
'@typescript-eslint/parser': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/scope-manager': 8.34.0
'@typescript-eslint/type-utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/visitor-keys': 8.34.0
eslint: 9.28.0(jiti@2.4.2)
graphemer: 1.4.0
'@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.55.0(eslint@9.39.2(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/scope-manager': 8.55.0
'@typescript-eslint/type-utils': 8.55.0(eslint@9.39.2(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/utils': 8.55.0(eslint@9.39.2(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/visitor-keys': 8.55.0
eslint: 9.39.2(jiti@2.4.2)
ignore: 7.0.5
natural-compare: 1.4.0
ts-api-utils: 2.1.0(typescript@5.8.3)
ts-api-utils: 2.4.0(typescript@5.8.3)
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)':
'@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.34.0
'@typescript-eslint/types': 8.34.0
'@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3)
'@typescript-eslint/visitor-keys': 8.34.0
debug: 4.4.1
eslint: 9.28.0(jiti@2.4.2)
'@typescript-eslint/scope-manager': 8.55.0
'@typescript-eslint/types': 8.55.0
'@typescript-eslint/typescript-estree': 8.55.0(typescript@5.8.3)
'@typescript-eslint/visitor-keys': 8.55.0
debug: 4.4.3
eslint: 9.39.2(jiti@2.4.2)
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.34.0(typescript@5.8.3)':
'@typescript-eslint/project-service@8.55.0(typescript@5.8.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.34.0(typescript@5.8.3)
'@typescript-eslint/types': 8.34.0
debug: 4.4.1
'@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.8.3)
'@typescript-eslint/types': 8.55.0
debug: 4.4.3
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/scope-manager@8.34.0':
'@typescript-eslint/scope-manager@8.55.0':
dependencies:
'@typescript-eslint/types': 8.34.0
'@typescript-eslint/visitor-keys': 8.34.0
'@typescript-eslint/types': 8.55.0
'@typescript-eslint/visitor-keys': 8.55.0
'@typescript-eslint/tsconfig-utils@8.34.0(typescript@5.8.3)':
'@typescript-eslint/tsconfig-utils@8.55.0(typescript@5.8.3)':
dependencies:
typescript: 5.8.3
'@typescript-eslint/type-utils@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)':
'@typescript-eslint/type-utils@8.55.0(eslint@9.39.2(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
'@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3)
'@typescript-eslint/utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)
debug: 4.4.1
eslint: 9.28.0(jiti@2.4.2)
ts-api-utils: 2.1.0(typescript@5.8.3)
'@typescript-eslint/types': 8.55.0
'@typescript-eslint/typescript-estree': 8.55.0(typescript@5.8.3)
'@typescript-eslint/utils': 8.55.0(eslint@9.39.2(jiti@2.4.2))(typescript@5.8.3)
debug: 4.4.3
eslint: 9.39.2(jiti@2.4.2)
ts-api-utils: 2.4.0(typescript@5.8.3)
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/types@8.34.0': {}
'@typescript-eslint/types@8.55.0': {}
'@typescript-eslint/typescript-estree@8.34.0(typescript@5.8.3)':
'@typescript-eslint/typescript-estree@8.55.0(typescript@5.8.3)':
dependencies:
'@typescript-eslint/project-service': 8.34.0(typescript@5.8.3)
'@typescript-eslint/tsconfig-utils': 8.34.0(typescript@5.8.3)
'@typescript-eslint/types': 8.34.0
'@typescript-eslint/visitor-keys': 8.34.0
debug: 4.4.1
fast-glob: 3.3.3
is-glob: 4.0.3
'@typescript-eslint/project-service': 8.55.0(typescript@5.8.3)
'@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.8.3)
'@typescript-eslint/types': 8.55.0
'@typescript-eslint/visitor-keys': 8.55.0
debug: 4.4.3
minimatch: 9.0.5
semver: 7.7.2
ts-api-utils: 2.1.0(typescript@5.8.3)
semver: 7.7.4
tinyglobby: 0.2.15
ts-api-utils: 2.4.0(typescript@5.8.3)
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)':
'@typescript-eslint/utils@8.55.0(eslint@9.39.2(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0(jiti@2.4.2))
'@typescript-eslint/scope-manager': 8.34.0
'@typescript-eslint/types': 8.34.0
'@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3)
eslint: 9.28.0(jiti@2.4.2)
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.4.2))
'@typescript-eslint/scope-manager': 8.55.0
'@typescript-eslint/types': 8.55.0
'@typescript-eslint/typescript-estree': 8.55.0(typescript@5.8.3)
eslint: 9.39.2(jiti@2.4.2)
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/visitor-keys@8.34.0':
'@typescript-eslint/visitor-keys@8.55.0':
dependencies:
'@typescript-eslint/types': 8.34.0
'@typescript-eslint/types': 8.55.0
eslint-visitor-keys: 4.2.1
'@vitejs/plugin-vue@5.2.4(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.2)(tsx@4.20.2)(yaml@2.8.0))(vue@3.5.16(typescript@5.8.3))':
@ -3572,7 +3652,7 @@ snapshots:
agent-base@6.0.2:
dependencies:
debug: 4.4.1
debug: 4.4.3
transitivePeerDependencies:
- supports-color
optional: true
@ -3686,7 +3766,7 @@ snapshots:
dependencies:
bytes: 3.1.2
content-type: 1.0.5
debug: 4.4.1
debug: 4.4.3
http-errors: 2.0.0
iconv-lite: 0.6.3
on-finished: 2.4.1
@ -3850,6 +3930,10 @@ snapshots:
dependencies:
ms: 2.1.3
debug@4.4.3:
dependencies:
ms: 2.1.3
decompress-response@6.0.0:
dependencies:
mimic-response: 3.1.0
@ -3971,16 +4055,18 @@ snapshots:
escape-string-regexp@4.0.0: {}
eslint-plugin-vue@10.2.0(eslint@9.28.0(jiti@2.4.2))(vue-eslint-parser@10.1.3(eslint@9.28.0(jiti@2.4.2))):
eslint-plugin-vue@10.7.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.4.2))(typescript@5.8.3))(eslint@9.39.2(jiti@2.4.2))(vue-eslint-parser@10.2.0(eslint@9.39.2(jiti@2.4.2))):
dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0(jiti@2.4.2))
eslint: 9.28.0(jiti@2.4.2)
'@eslint-community/eslint-utils': 4.7.0(eslint@9.39.2(jiti@2.4.2))
eslint: 9.39.2(jiti@2.4.2)
natural-compare: 1.4.0
nth-check: 2.1.1
postcss-selector-parser: 6.1.2
postcss-selector-parser: 7.1.1
semver: 7.7.2
vue-eslint-parser: 10.1.3(eslint@9.28.0(jiti@2.4.2))
vue-eslint-parser: 10.2.0(eslint@9.39.2(jiti@2.4.2))
xml-name-validator: 4.0.0
optionalDependencies:
'@typescript-eslint/parser': 8.55.0(eslint@9.39.2(jiti@2.4.2))(typescript@5.8.3)
eslint-scope@8.4.0:
dependencies:
@ -3991,30 +4077,29 @@ snapshots:
eslint-visitor-keys@4.2.1: {}
eslint@9.28.0(jiti@2.4.2):
eslint@9.39.2(jiti@2.4.2):
dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0(jiti@2.4.2))
'@eslint-community/regexpp': 4.12.1
'@eslint/config-array': 0.20.0
'@eslint/config-helpers': 0.2.2
'@eslint/core': 0.14.0
'@eslint/eslintrc': 3.3.1
'@eslint/js': 9.28.0
'@eslint/plugin-kit': 0.3.1
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.4.2))
'@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.21.1
'@eslint/config-helpers': 0.4.2
'@eslint/core': 0.17.0
'@eslint/eslintrc': 3.3.3
'@eslint/js': 9.39.2
'@eslint/plugin-kit': 0.4.1
'@humanfs/node': 0.16.6
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.3
'@types/estree': 1.0.8
'@types/json-schema': 7.0.15
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.6
debug: 4.4.1
debug: 4.4.3
escape-string-regexp: 4.0.0
eslint-scope: 8.4.0
eslint-visitor-keys: 4.2.1
espree: 10.4.0
esquery: 1.6.0
esquery: 1.7.0
esutils: 2.0.3
fast-deep-equal: 3.1.3
file-entry-cache: 8.0.0
@ -4039,7 +4124,7 @@ snapshots:
acorn-jsx: 5.3.2(acorn@8.15.0)
eslint-visitor-keys: 4.2.1
esquery@1.6.0:
esquery@1.7.0:
dependencies:
estraverse: 5.3.0
@ -4117,6 +4202,10 @@ snapshots:
optionalDependencies:
picomatch: 4.0.2
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
fetch-blob@3.2.0:
dependencies:
node-domexception: 1.0.0
@ -4134,7 +4223,7 @@ snapshots:
finalhandler@2.1.0:
dependencies:
debug: 4.4.1
debug: 4.4.3
encodeurl: 2.0.0
escape-html: 1.0.3
on-finished: 2.4.1
@ -4275,7 +4364,7 @@ snapshots:
dependencies:
foreground-child: 3.3.1
jackspeak: 4.1.1
minimatch: 10.0.1
minimatch: 10.1.2
minipass: 7.1.2
package-json-from-dist: 1.0.1
path-scurry: 2.0.0
@ -4292,6 +4381,8 @@ snapshots:
globals@14.0.0: {}
globals@17.3.0: {}
google-auth-library@9.15.1(encoding@0.1.13):
dependencies:
base64-js: 1.5.1
@ -4330,8 +4421,6 @@ snapshots:
graceful-fs@4.2.11: {}
graphemer@1.4.0: {}
gtoken@7.1.0(encoding@0.1.13):
dependencies:
gaxios: 6.7.1(encoding@0.1.13)
@ -4376,7 +4465,7 @@ snapshots:
dependencies:
'@tootallnate/once': 1.1.2
agent-base: 6.0.2
debug: 4.4.1
debug: 4.4.3
transitivePeerDependencies:
- supports-color
optional: true
@ -4384,7 +4473,7 @@ snapshots:
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
debug: 4.4.1
debug: 4.4.3
transitivePeerDependencies:
- supports-color
optional: true
@ -4392,7 +4481,7 @@ snapshots:
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.4
debug: 4.4.1
debug: 4.4.3
transitivePeerDependencies:
- supports-color
@ -4486,7 +4575,7 @@ snapshots:
jiti@2.4.2: {}
js-yaml@4.1.0:
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
@ -4579,8 +4668,6 @@ snapshots:
lodash.merge@4.6.2: {}
lodash@4.17.21: {}
lru-cache@10.4.3: {}
lru-cache@11.1.0: {}
@ -4648,9 +4735,9 @@ snapshots:
mimic-response@3.1.0: {}
minimatch@10.0.1:
minimatch@10.1.2:
dependencies:
brace-expansion: 2.0.1
'@isaacs/brace-expansion': 5.0.1
minimatch@3.1.2:
dependencies:
@ -4738,7 +4825,7 @@ snapshots:
node-abi@3.75.0:
dependencies:
semver: 7.7.2
semver: 7.7.4
node-addon-api@7.1.1: {}
@ -4765,7 +4852,7 @@ snapshots:
nopt: 5.0.0
npmlog: 6.0.2
rimraf: 3.0.2
semver: 7.7.2
semver: 7.7.4
tar: 6.2.1
which: 2.0.2
transitivePeerDependencies:
@ -4864,6 +4951,8 @@ snapshots:
picomatch@4.0.2: {}
picomatch@4.0.3: {}
pkg-types@1.3.1:
dependencies:
confbox: 0.1.8
@ -4878,7 +4967,7 @@ snapshots:
possible-typed-array-names@1.1.0: {}
postcss-selector-parser@6.1.2:
postcss-selector-parser@7.1.1:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
@ -5014,7 +5103,7 @@ snapshots:
router@2.2.0:
dependencies:
debug: 4.4.1
debug: 4.4.3
depd: 2.0.0
is-promise: 4.0.0
parseurl: 1.3.3
@ -5042,9 +5131,11 @@ snapshots:
semver@7.7.2: {}
semver@7.7.4: {}
send@1.2.0:
dependencies:
debug: 4.4.1
debug: 4.4.3
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
@ -5142,7 +5233,7 @@ snapshots:
socks-proxy-agent@6.2.1:
dependencies:
agent-base: 6.0.2
debug: 4.4.1
debug: 4.4.3
socks: 2.8.7
transitivePeerDependencies:
- supports-color
@ -5253,6 +5344,11 @@ snapshots:
fdir: 6.4.5(picomatch@4.0.2)
picomatch: 4.0.2
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
to-buffer@1.2.1:
dependencies:
isarray: 2.0.5
@ -5267,7 +5363,7 @@ snapshots:
tr46@0.0.3: {}
ts-api-utils@2.1.0(typescript@5.8.3):
ts-api-utils@2.4.0(typescript@5.8.3):
dependencies:
typescript: 5.8.3
@ -5351,12 +5447,13 @@ snapshots:
- babel-plugin-macros
- supports-color
typescript-eslint@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3):
typescript-eslint@8.55.0(eslint@9.39.2(jiti@2.4.2))(typescript@5.8.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/parser': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)
eslint: 9.28.0(jiti@2.4.2)
'@typescript-eslint/eslint-plugin': 8.55.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.4.2))(typescript@5.8.3))(eslint@9.39.2(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/parser': 8.55.0(eslint@9.39.2(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/typescript-estree': 8.55.0(typescript@5.8.3)
'@typescript-eslint/utils': 8.55.0(eslint@9.39.2(jiti@2.4.2))(typescript@5.8.3)
eslint: 9.39.2(jiti@2.4.2)
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
@ -5445,16 +5542,15 @@ snapshots:
vscode-uri@3.1.0: {}
vue-eslint-parser@10.1.3(eslint@9.28.0(jiti@2.4.2)):
vue-eslint-parser@10.2.0(eslint@9.39.2(jiti@2.4.2)):
dependencies:
debug: 4.4.1
eslint: 9.28.0(jiti@2.4.2)
debug: 4.4.3
eslint: 9.39.2(jiti@2.4.2)
eslint-scope: 8.4.0
eslint-visitor-keys: 4.2.1
espree: 10.4.0
esquery: 1.6.0
lodash: 4.17.21
semver: 7.7.2
esquery: 1.7.0
semver: 7.7.4
transitivePeerDependencies:
- supports-color