diff --git a/apps/vdb-backend/src/index.ts b/apps/vdb-backend/src/index.ts index 189373a..e195029 100644 --- a/apps/vdb-backend/src/index.ts +++ b/apps/vdb-backend/src/index.ts @@ -1,205 +1,159 @@ -import "reflect-metadata"; -import { SAMPLE } from "@repo/common/sample"; -import express from "express"; -import crypto from "crypto" -import fs from 'fs'; -;import { appDataSource } from "./config/dbconfig.js"; -import { Lemma, WordForm, Lect } from "./db/dbmodel.js"; -import "@total-typescript/ts-reset"; +import { compileLocale, type InferLocale } from "@/new-i18n-lib/config"; import { - Like, In -} from "typeorm"; - -const RELOAD_SHEET_ON_START = false; -const SOURCE_FILE = 'res/sample.tsv' - -appDataSource - .initialize() - .then(async () => { - initExpress(); - - if (RELOAD_SHEET_ON_START) { - await loadSheet(); + bundleToUncompiledLocale, + loadFluentBundle, +} from "@/new-i18n-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 miVlFtlSrc from "@/assets/locale/mi_VL.ftl"; +import wpVlFtlSrc from "@/assets/locale/wp_VL.ftl"; +import type { FluentBundle } from "@fluent/bundle"; + +export const LOCALE_IDS = ["en-US", "vp-VL", "mi-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 localStorageLocaleId = useLocalStorage( + "localeId", + DEFAULT_LOCALE_ID, +); + +export const localeId = computed({ + get: (): LocaleId => { + const localeIdRes = LocaleId(localStorageLocaleId.value); + if (localeIdRes instanceof type.errors) { + localStorageLocaleId.value = DEFAULT_LOCALE_ID; + return DEFAULT_LOCALE_ID; } - }) - .catch((error) => console.log(error)); - -function initExpress() { - const app = express(); - const PORT = 1225; - - const lect_repository = appDataSource.getRepository(Lect); - const word_form_repository = appDataSource.getRepository(WordForm); - const lemma_repository = appDataSource.getRepository(Lemma); - app.get("/sample", (_req, res) => { - res.status(200).send(SAMPLE); - }); + // else return user's selection + const localeId = localeIdRes; + return localeId; + }, + set: (id: LocaleId) => { + localStorageLocaleId.value = id; + }, +}); + +export interface Locale extends InferLocale {} + +async function loadLocale( + localeId: LocaleId, + localeFtlSrc: string, +): Promise> { + const bundleRes = await loadFluentBundle(localeId, localeFtlSrc); + if (bundleRes.type === "err") { + return bundleRes; + } - app.get("/search", async (req, res) => { - const search_term = req.query.search_term?.toString(); + const bundle = bundleRes.ok; + return { type: "ok", ok: bundle }; +} - if (!search_term) { - return void res.sendStatus(400); +function setupLocale( + localeBundle: FluentBundle, + fallbackLocaleBundle: FluentBundle | undefined, +): Result { + const maybeFallbackedBundle = (() => { + if (fallbackLocaleBundle === undefined) { + return localeBundle; } - const word_forms: WordForm[] = await WordForm.find({ - where:{word_form: Like(`%${search_term}%`)}, - relations: { lemma: true } - }); - - let lemma_ids = word_forms.map(w=>w.lemma.lemma_name); + console.log(localeBundle); + console.log(fallbackLocaleBundle); - const lemmas: Lemma[] = await Lemma.find({ - where: { lemma_name: In(lemma_ids)}, - relations: { word_forms: { lect: true }} - }) - - res.status(200).send({ - terms: lemmas.length, - results: lemmas - }); - }); + const localeMessageIds = new Set(localeBundle._messages.keys()); + const fallbackMessageIds = new Set( + fallbackLocaleBundle._messages.keys(), + ); - app.get("/lect", async (req, res) => { - const name = req.query.name?.toString(); + const missingMessageIds = + fallbackMessageIds.difference(localeMessageIds); - if (!name) { - return void res.sendStatus(400); + for (const id of missingMessageIds) { + const fallbackMessage = fallbackLocaleBundle._messages.get(id); + if (fallbackMessage) { + localeBundle._messages.set(id, fallbackMessage); + } } - const lect = await Lect.findOne({ - where: { name: name }, - relations: { word_forms: { lemma: true } }, - }); - - res.status(200).send({ lect }); - }); - - app.get("/lects", async (_req, res) => { - const lects = await Lect.find(); - res.status(200).send({ - lects, - }); - }); - - app.listen(PORT, () => { - console.log(`Backend started @ http://localhost:${PORT} !`); - console.log(SAMPLE); - }); -} - -async function loadSheet() { - const lect_repository = appDataSource.getRepository(Lect); - const word_form_repository = appDataSource.getRepository(WordForm); - const lemma_repository = appDataSource.getRepository(Lemma); - - await word_form_repository.clear(); - await lemma_repository.clear(); - await lect_repository.clear(); - - - const rawData: string = fs.readFileSync(SOURCE_FILE, 'utf8'); - const rows: string[] = rawData.split('\n'); + return localeBundle; + })(); - if (!rows || rows.length === 0) { - console.error("No data found."); - return; + const uncompiledRes = bundleToUncompiledLocale(maybeFallbackedBundle); + if (uncompiledRes.type === "err") { + return uncompiledRes; } - const lect_names = rows.shift()?.split('\t'); - - const keys = rows.map(row=>row.split('\t')[0]?.split(';')[0]); - console.log(keys); - - if (keys.length != rows.length) { - console.error("Lemma count doesn't match number of rows."); - return; - } - - if (!lect_names) { - console.error("No lects found"); - return; - } + const uncompiled = uncompiledRes.ok; - const lects = Array(); + const localeRes = compileLocale({ config: localeConfig, uncompiled }); - for (const lect of lect_names) { - let l = new Lect(); - l.name = lect; - l.save(); - lects.push(l) - console.log(l); + if (localeRes.type === "err") { + return localeRes; } - const lemmas = Array(); - - for (let i = 0; i < rows.length; i++) { - const row = rows[i]?.split('\t'); - const lect_name = lect_names[i]; - - if(!lect_name){ - console.error("No lect name"); - break; - } - - if(!row){ - console.error("Row doesn't exist"); - continue; - } - - if(row.length !== lect_names.length){ - console.error("Mismatched row size"); - continue; - } - - const lemma_key = row[0]; - - const lemma = new Lemma(); + const locale = localeRes.ok; + return { type: "ok", ok: locale }; +} - if (lemma_key == null || lemma_key.length == 0) { - //assign a random UUID to the lemma as punishment for our failures - lemma.lemma_name = crypto.randomUUID(); - console.log( - `womp womp, missing lemma name, calling it ${lemma.lemma_name}`, - ); - } else { - lemma.lemma_name = lemma_key; +function unwrap(result: Result): T { + switch (result.type) { + case "ok": { + return result.ok; } - - lemmas.push(lemma); - lemma.word_forms = Array(); - - for (let j = 0; j < row.length; j++) { - const cell = row?.[j]; - const lect = lects[j]; - - if ( - cell === null || - cell === undefined || - (typeof cell === "string" && cell.length === 0) || - !lect - ) { - continue; - } - - for (let word_form of cell.split(";")) { - const f = new WordForm(); - f.word_form = word_form; - f.lect = lect; - lemma.word_forms.push(f); - } + case "err": { + throw new Error(String(result.err)); } } +} - for (let i = 0; i < lemmas.length; i += 100) { - await lemma_repository.save(lemmas.slice(i, i + 100)); - console.log( - `Saved ${Math.min(i + 100, lemmas.length)} / ${ - lemmas.length - } lemmas...`, - ); - } +function deepReadonly(value: T): DeepReadonly { + return value as DeepReadonly; +} - console.log(`Loaded ${lemmas.length} lemmas!`); +const DEFAULT_LOCALE = unwrap(await loadLocale("en-US", enUsFtlSrc)); + +const doItAllForLocale = async ( + localeId: LocaleId, + localeFtlSrc: string, +): Promise> => + deepReadonly( + unwrap( + setupLocale( + unwrap(await loadLocale(localeId, localeFtlSrc)), + DEFAULT_LOCALE, + ), + ), + ); + +const [vpVl, miVl, wpVl] = await Promise.all([ + doItAllForLocale("vp-VL", vpVlFtlSrc), + doItAllForLocale("mi-VL", miVlFtlSrc), + doItAllForLocale("wp-VL", wpVlFtlSrc), +]); + +const localeIdToLocale = { + "en-US": deepReadonly(unwrap(setupLocale(DEFAULT_LOCALE, DEFAULT_LOCALE))), + "vp-VL": vpVl, + "mi-VL": miVl, + "wp-VL": wpVl, +} as const satisfies Record>; + +export interface UseLocaleOptions { + locale?: LocaleId; } + +export const useLocale = (opt: UseLocaleOptions = {}) => + computed>(() => { + const localLocaleId = opt.locale ?? localeId.value; + return localeIdToLocale[localLocaleId]; + }); \ No newline at end of file diff --git a/apps/vdn-static/eslint.config.js b/apps/vdn-static/eslint.config.js index 6bf661b..63391d7 100644 --- a/apps/vdn-static/eslint.config.js +++ b/apps/vdn-static/eslint.config.js @@ -1,24 +1,58 @@ -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'] }, - { - extends: [ - js.configs.recommended, - ...ts.configs.recommendedTypeChecked, - ...vue.configs.recommendedTypeChecked, - ], - files: ['**/*.{js,ts,vue}'], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - parserOptions: { - projectService: true, - tsconfigRootDir: import.meta.dirname, - }, - }, - }, -) +export default defineConfig([ + globalIgnores(["dist"]), + { + extends: [ + js.configs.recommended, + ts.configs.strictTypeChecked, + ...vue.configs["flat/essential"], + ], + files: ["./src/**/*.{js,ts,vue}"], + plugins: { vue }, + languageOptions: { + 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 instead.", + }, + { element: ["i18n-t"], message: "Use instead." }, + { + element: ["RichTemplateParts"], + message: + "Do not use the internal component. Use 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"], + }, + }, + // disable multi-word-component-names for unplugin-vue-router + { + files: ["src/pages/**/*.vue"], + rules: { "vue/multi-word-component-names": "off" }, + }, +]); diff --git a/apps/vdn-static/index.html b/apps/vdn-static/index.html index 0dc687b..f968c16 100644 --- a/apps/vdn-static/index.html +++ b/apps/vdn-static/index.html @@ -1,15 +1,19 @@ - - - - - - - Viossa.net - - -
- - + + + + + + + + Viossa.net + + + +
+ + + diff --git a/apps/vdn-static/package.json b/apps/vdn-static/package.json index 25a0d3c..8bd09d9 100644 --- a/apps/vdn-static/package.json +++ b/apps/vdn-static/package.json @@ -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" diff --git a/apps/vdn-static/public/Vimivera 2025 Paemara Sentakuena.pdf b/apps/vdn-static/public/Vimivera 2025 Paemara Sentakuena.pdf new file mode 100644 index 0000000..d94999a Binary files /dev/null and b/apps/vdn-static/public/Vimivera 2025 Paemara Sentakuena.pdf differ diff --git a/apps/vdn-static/public/Vimiveracover.webp b/apps/vdn-static/public/Vimiveracover.webp new file mode 100644 index 0000000..ace23b7 Binary files /dev/null and b/apps/vdn-static/public/Vimiveracover.webp differ diff --git a/apps/vdn-static/public/korohtella.png b/apps/vdn-static/public/korohtella.png new file mode 100644 index 0000000..88da142 Binary files /dev/null and b/apps/vdn-static/public/korohtella.png differ diff --git a/apps/vdn-static/src/App.vue b/apps/vdn-static/src/App.vue index 21b089e..ad3e1bd 100644 --- a/apps/vdn-static/src/App.vue +++ b/apps/vdn-static/src/App.vue @@ -1,68 +1,126 @@ +
+ +
+ + + + \ No newline at end of file diff --git a/apps/vdn-static/src/assets.d.ts b/apps/vdn-static/src/assets.d.ts new file mode 100644 index 0000000..bbb8b63 --- /dev/null +++ b/apps/vdn-static/src/assets.d.ts @@ -0,0 +1,4 @@ +declare module "*.ftl" { + const src: string; + export default src; +} diff --git a/apps/vdn-static/src/assets/Vimivera 2025 Paemara Sentakuena.pdf b/apps/vdn-static/src/assets/Vimivera 2025 Paemara Sentakuena.pdf new file mode 100644 index 0000000..d94999a Binary files /dev/null and b/apps/vdn-static/src/assets/Vimivera 2025 Paemara Sentakuena.pdf differ diff --git a/apps/vdn-static/src/assets/korohtella.png b/apps/vdn-static/src/assets/korohtella.png new file mode 100644 index 0000000..88da142 Binary files /dev/null and b/apps/vdn-static/src/assets/korohtella.png differ diff --git a/apps/vdn-static/src/assets/locale/en_US.ftl b/apps/vdn-static/src/assets/locale/en_US.ftl new file mode 100644 index 0000000..2d0e752 --- /dev/null +++ b/apps/vdn-static/src/assets/locale/en_US.ftl @@ -0,0 +1,105 @@ +localeName = "English" +vilanticLangs-viossa = "Viossa" +vilanticLangs-wodox = "Wodoch" +vilanticLangs-minemiaha = "Minemiaha" +navbar-whatIsViossa = "What is Viossa?" +navbar-resources = "Resources" +navbar-resourcesLearning = "Learning Resources" +navbar-resourcesCultural = "Cultural 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 = "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-resources-vikoli-title = "Vikoli" +resources-resources-vikoli-subtitle = "The Viossa encyclopedia." +resources-resources-vikoli-desc = "Vikoli hosts a number of learning and cultural resources. It is intended for use by advanced speakers of Viossa seeking to further their language abilities or teachers seeking to streamline their process with aides." +resources-resources-vikoli-buttons-visit-label = "Visit" +resources-resources-daviSpil-title = "Davi Spil" +resources-resources-daviSpil-subtitle = "Gaming server for people who already speak Viossa." +resources-resources-daviSpil-desc = "The central hub for Viossa gaming events. Davi Spil is a dedicated gaming community for Viossa speakers. Various games are played here, including but not limited to Minecraft, Peak, Magic the Gathering, and Sakana (also known as Viossa Chess)." +resources-resources-daviSpil-buttons-join-label = "Join" +resources-resources-vimivera2025-title = "Vimivera 2025 Anthology" +resources-resources-vimivera2025-subtitle = "Selected poems from the 2025 Vimivera Literature Festival Poetry Competition." +resources-resources-vimivera2025-desc = "" +resources-resources-vimivera2025-buttons-read-label = "Read now" +resources-resources-korohtella-title = "Korohtella" +resources-resources-korohtella-subtitle = "Viossa music" +resources-resources-korohtella-desc = "Perhaps Viossa's most famous collection of music, by Djima. A heartfelt album including a mix of original and translated works." +resources-resources-korohtella-buttons-spotify-label = "Listen on Spotify" +resources-resources-korohtella-buttons-youtube-label = "Listen on YouTube" +resources-resources-piik-title = "Peak — ViPIIK" +resources-resources-piik-subtitle = "Viossa mod for Peak" +resources-resources-piik-desc = "Mod for the videogame Peak that adds Viossa compatibility." +resources-resources-piik-buttons-thunderstore-label = "Thunderstore" +resources-images-discordLogo-alt = "Discord logo" +resources-images-viossaFlag-alt = "Flag of the Viossa Language" +resources-images-vimivera2025-alt = "Vimivera 2025 Anthology cover" +resources-images-korohtella-alt = "Korohtella album cover" +resources-images-piik-alt = "ViPIIK mod" +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/)" diff --git a/apps/vdn-static/src/assets/locale/mi_VL.ftl b/apps/vdn-static/src/assets/locale/mi_VL.ftl new file mode 100644 index 0000000..4228c77 --- /dev/null +++ b/apps/vdn-static/src/assets/locale/mi_VL.ftl @@ -0,0 +1,105 @@ +localeName = "Minemiaha" +vilanticLangs-viossa = "Viossa" +vilanticLangs-wodox = "Wodoch" +vilanticLangs-minemiaha = "Minemiaha" +navbar-whatIsViossa = "What is Viossa?" +navbar-resources = "Resources" +navbar-resourcesLearning = "Learning Resources" +navbar-resourcesCultural = "Cultural 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 = "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-resources-vikoli-title = "Vikoli" +resources-resources-vikoli-subtitle = "The Viossa encyclopedia." +resources-resources-vikoli-desc = "Vikoli hosts a number of learning and cultural resources. It is intended for use by advanced speakers of Viossa seeking to further their language abilities or teachers seeking to streamline their process with aides." +resources-resources-vikoli-buttons-visit-label = "Visit" +resources-resources-daviSpil-title = "Davi Spil" +resources-resources-daviSpil-subtitle = "Gaming server for people who already speak Viossa." +resources-resources-daviSpil-desc = "The central hub for Viossa gaming events. Davi Spil is a dedicated gaming community for Viossa speakers. Various games are played here, including but not limited to Minecraft, Peak, Magic the Gathering, and Sakana (also known as Viossa Chess)." +resources-resources-daviSpil-buttons-join-label = "Join" +resources-resources-vimivera2025-title = "Vimivera 2025 Anthology" +resources-resources-vimivera2025-subtitle = "Selected poems from the 2025 Vimivera Literature Festival Poetry Competition." +resources-resources-vimivera2025-desc = "" +resources-resources-vimivera2025-buttons-read-label = "Read now" +resources-resources-korohtella-title = "Korohtella" +resources-resources-korohtella-subtitle = "Viossa music" +resources-resources-korohtella-desc = "Perhaps Viossa's most famous collection of music, by Djima. A heartfelt album including a mix of original and translated works." +resources-resources-korohtella-buttons-spotify-label = "Listen on Spotify" +resources-resources-korohtella-buttons-youtube-label = "Listen on YouTube" +resources-resources-piik-title = "Peak — ViPIIK" +resources-resources-piik-subtitle = "Viossa mod for Peak" +resources-resources-piik-desc = "Mod for the videogame Peak that adds Viossa compatibility." +resources-resources-piik-buttons-thunderstore-label = "Thunderstore" +resources-images-discordLogo-alt = "Discord logo" +resources-images-viossaFlag-alt = "Flag of the Viossa Language" +resources-images-vimivera2025-alt = "Vimivera 2025 Anthology cover" +resources-images-korohtella-alt = "Korohtella album cover" +resources-images-piik-alt = "ViPIIK mod" +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/)" diff --git a/apps/vdn-static/src/assets/locale/vp_VL.ftl b/apps/vdn-static/src/assets/locale/vp_VL.ftl new file mode 100644 index 0000000..0f26b4e --- /dev/null +++ b/apps/vdn-static/src/assets/locale/vp_VL.ftl @@ -0,0 +1,50 @@ +localeName = "Viossa" +vilanticLangs-viossa = "Viossa" +vilanticLangs-wodox = "Wodossa" +vilanticLangs-minemiaha = "Razhanossa" +navbar-whatIsViossa = "Ka Viossa?" +navbar-resources = "Lerakran" +navbar-resourcesLearning = "Lerakran" +navbar-resourcesCultural = "Shakaiting" +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-resources-vikoli-title = "Vikoli" +resources-resources-vikoli-subtitle = "Shirulehti Vilanta" +resources-resources-vikoli-desc = "Yam na Vikoli mangge lerakran au shakaiting. Mena, afto zelehti brukena haaste grun mono bruk Viossa. Bra kran per Viossadjin ke vill maha shiruzma sui plubra os na opetadjin ke vill maha opetatropos plusimper met opetakran." +resources-resources-vikoli-buttons-visit-label = "Zeki" +resources-resources-daviSpil-title = "Davi Spil" +resources-resources-daviSpil-subtitle = "Server per spildjin ke hanu Viossa." +resources-resources-daviSpil-desc = "Viossa Spilserver. Davi Spil tak spilhuomi fu Viossadjin. Mangge spil yam hjer: Shahtamaha, Piik, Taikafesta, au Sakana (Viossatjesu) prosta na tatoeba." +resources-resources-daviSpil-buttons-join-label = "Zetulla" +resources-resources-vimivera2025-title = "Paemliber Vimivera 2025" +resources-resources-vimivera2025-subtitle = "Paemara sentakujena Vimivera 2025 Paemtuvat kara." +resources-resources-vimivera2025-desc = "" +resources-resources-vimivera2025-buttons-read-label = "Lesa" +resources-resources-korohtella-title = "Korohtella" +resources-resources-korohtella-subtitle = "Viliid" +resources-resources-korohtella-desc = "Lesteshiruena Viossaliidklaani, Djima kara. Ain kokorodai liidtumam, jokku mahajena au jokku kjannosena." +resources-resources-korohtella-buttons-spotify-label = "Anhore na Spotify" +resources-resources-korohtella-buttons-youtube-label = "Anhore na YouTube" +resources-resources-piik-title = "Peak — ViPIIK" +resources-resources-piik-subtitle = "Piik" +resources-resources-piik-desc = "Vikjannos per Piikspil. Ima tshangki na glossa fu vi." +resources-resources-piik-buttons-thunderstore-label = "Zedvera" +resources-images-discordLogo-alt = "Riso fu Diskord" +resources-images-viossaFlag-alt = "Flakka fu Viossa" +resources-images-vimivera2025-alt = "Paemliber Vimivera 2025 Kara" +resources-images-korohtella-alt = "Korohtella album kara" +resources-images-piik-alt = "ViPIIK mod" +kotoba-title = "Tropos-egal suha" +kotoba-searchHelp = "Li vil suha uten tro-egal, tastatsa joku ko os fras na una." \ No newline at end of file diff --git a/apps/vdn-static/src/assets/locale/wp_VL.ftl b/apps/vdn-static/src/assets/locale/wp_VL.ftl new file mode 100644 index 0000000..96a92e3 --- /dev/null +++ b/apps/vdn-static/src/assets/locale/wp_VL.ftl @@ -0,0 +1,50 @@ +localeName = "wodox" +vilanticLangs-viossa = "viosox" +vilanticLangs-wodox = "wodox" +vilanticLangs-minemiaha = "minemiox" +navbar-whatIsViossa = "viosox e ano?" +navbar-resources = "tropos" +navbar-resourcesLearning = "tropos o gen" +navbar-resourcesCultural = "tropos o ro" +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 2015. 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-resources-vikoli-title = "Vikoli" +resources-resources-vikoli-subtitle = "Shirulehti Vilanta" +resources-resources-vikoli-desc = "Yam na Vikoli mangge lerakran au shakaiting. Mena, afto zelehti brukena haaste grun mono bruk Viossa. Bra kran per Viossadjin ke vill maha shiruzma sui plubra os na opetadjin ke vill maha opetatropos plusimper met opetakran." +resources-resources-vikoli-buttons-visit-label = "Zeki" +resources-resources-daviSpil-title = "Davi Spil" +resources-resources-daviSpil-subtitle = "Server per spildjin ke hanu Viossa." +resources-resources-daviSpil-desc = "Viossa Spilserver. Davi Spil tak spilhuomi fu Viossadjin. Mangge spil yam hjer: Shahtamaha, Piik, Taikafesta, au Sakana (Viossatjesu) prosta na tatoeba." +resources-resources-daviSpil-buttons-join-label = "wekatutsa" +resources-resources-vimivera2025-title = "Paemliber Vimivera 2025" +resources-resources-vimivera2025-subtitle = "Paemara sentakujena Vimivera 2025 Paemtuvat kara." +resources-resources-vimivera2025-desc = "" +resources-resources-vimivera2025-buttons-read-label = "Lesa" +resources-resources-korohtella-title = "Korohtella" +resources-resources-korohtella-subtitle = "Viliid" +resources-resources-korohtella-desc = "Lesteshiruena Viossaliidklaani, Djima kara. Ain kokorodai liidtumam, jokku mahajena au jokku kjannosena." +resources-resources-korohtella-buttons-spotify-label = "Anhore na Spotify" +resources-resources-korohtella-buttons-youtube-label = "Anhore na YouTube" +resources-resources-piik-title = "Peak — ViPIIK" +resources-resources-piik-subtitle = "Piik" +resources-resources-piik-desc = "Vikjannos per Piikspil. Ima tshangki na glossa fu vi." +resources-resources-piik-buttons-thunderstore-label = "Zedvera" +resources-images-discordLogo-alt = "surat o Diskord" +resources-images-viossaFlag-alt = "fomma o viosox" +resources-images-vimivera2025-alt = "Paemliber Vimivera 2025 Kara" +resources-images-korohtella-alt = "Korohtella album kara" +resources-images-piik-alt = "ViPIIK mod" +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." \ No newline at end of file diff --git a/apps/vdn-static/src/assets/minemiahaFlag.webp b/apps/vdn-static/src/assets/minemiahaFlag.webp new file mode 100644 index 0000000..a461e33 Binary files /dev/null and b/apps/vdn-static/src/assets/minemiahaFlag.webp differ diff --git a/apps/vdn-static/src/assets/piik.webp b/apps/vdn-static/src/assets/piik.webp new file mode 100644 index 0000000..ea93b8d Binary files /dev/null and b/apps/vdn-static/src/assets/piik.webp differ diff --git a/apps/vdn-static/src/assets/style.scss b/apps/vdn-static/src/assets/style.scss index 73b13c1..1721cdb 100644 --- a/apps/vdn-static/src/assets/style.scss +++ b/apps/vdn-static/src/assets/style.scss @@ -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; -}; \ No newline at end of file +// Correct for fixed-top header when jumping to element by id in link (anchor) +html { + scroll-padding-top: var(--bulma-navbar-height); +} diff --git a/apps/vdn-static/src/assets/vimivera2025.webp b/apps/vdn-static/src/assets/vimivera2025.webp new file mode 100644 index 0000000..32f3c3a Binary files /dev/null and b/apps/vdn-static/src/assets/vimivera2025.webp differ diff --git a/apps/vdn-static/src/components/atoms/DropdownItem.vue b/apps/vdn-static/src/components/atoms/DropdownItem.vue new file mode 100644 index 0000000..8310122 --- /dev/null +++ b/apps/vdn-static/src/components/atoms/DropdownItem.vue @@ -0,0 +1,14 @@ + + + diff --git a/apps/vdn-static/src/components/atoms/MarkdownDisplay.vue b/apps/vdn-static/src/components/atoms/MarkdownDisplay.vue new file mode 100644 index 0000000..9fc378c --- /dev/null +++ b/apps/vdn-static/src/components/atoms/MarkdownDisplay.vue @@ -0,0 +1,108 @@ + + + diff --git a/apps/vdn-static/src/components/atoms/MarkdownParts.vue b/apps/vdn-static/src/components/atoms/MarkdownParts.vue new file mode 100644 index 0000000..b37f179 --- /dev/null +++ b/apps/vdn-static/src/components/atoms/MarkdownParts.vue @@ -0,0 +1,67 @@ + + + diff --git a/apps/vdn-static/src/components/atoms/OptionalParent.vue b/apps/vdn-static/src/components/atoms/OptionalParent.vue new file mode 100644 index 0000000..4454b82 --- /dev/null +++ b/apps/vdn-static/src/components/atoms/OptionalParent.vue @@ -0,0 +1,12 @@ + + + diff --git a/apps/vdn-static/src/components/atoms/RichText.vue b/apps/vdn-static/src/components/atoms/RichText.vue new file mode 100644 index 0000000..f7ac465 --- /dev/null +++ b/apps/vdn-static/src/components/atoms/RichText.vue @@ -0,0 +1,31 @@ + + + diff --git a/apps/vdn-static/src/components/atoms/SmartLink.ts b/apps/vdn-static/src/components/atoms/SmartLink.ts new file mode 100644 index 0000000..205334e --- /dev/null +++ b/apps/vdn-static/src/components/atoms/SmartLink.ts @@ -0,0 +1,7 @@ +import type { SmartDest } from "@/utils/smart-dest"; + +export interface SmartLinkProps { + to: SmartDest; + newTab?: boolean; + covert?: boolean; +} diff --git a/apps/vdn-static/src/components/atoms/SmartLink.vue b/apps/vdn-static/src/components/atoms/SmartLink.vue new file mode 100644 index 0000000..9552713 --- /dev/null +++ b/apps/vdn-static/src/components/atoms/SmartLink.vue @@ -0,0 +1,56 @@ + + + diff --git a/apps/vdn-static/src/components/molecules/DiscordRuleOverview.vue b/apps/vdn-static/src/components/molecules/DiscordRuleOverview.vue new file mode 100644 index 0000000..76914c5 --- /dev/null +++ b/apps/vdn-static/src/components/molecules/DiscordRuleOverview.vue @@ -0,0 +1,38 @@ + + + diff --git a/apps/vdn-static/src/components/molecules/DiscordRuleSection.vue b/apps/vdn-static/src/components/molecules/DiscordRuleSection.vue new file mode 100644 index 0000000..a4adb7b --- /dev/null +++ b/apps/vdn-static/src/components/molecules/DiscordRuleSection.vue @@ -0,0 +1,20 @@ + + + diff --git a/apps/vdn-static/src/components/molecules/LearningResourceWrapper.vue b/apps/vdn-static/src/components/molecules/LearningResourceWrapper.vue index 0a36ebd..710e71c 100644 --- a/apps/vdn-static/src/components/molecules/LearningResourceWrapper.vue +++ b/apps/vdn-static/src/components/molecules/LearningResourceWrapper.vue @@ -1,53 +1,83 @@ + + +
+
+
+ +
+
+
+

{{ title }}

+

{{ subtitle }}

+

{{ desc }}

- \ No newline at end of file +
+ +
+
+
+ \ No newline at end of file diff --git a/apps/vdn-static/src/components/organisms/LocalePicker.vue b/apps/vdn-static/src/components/organisms/LocalePicker.vue index c922d0b..453331a 100644 --- a/apps/vdn-static/src/components/organisms/LocalePicker.vue +++ b/apps/vdn-static/src/components/organisms/LocalePicker.vue @@ -1,7 +1,10 @@ - - diff --git a/apps/vdn-static/src/components/pages/ResourcesPage.vue b/apps/vdn-static/src/components/pages/ResourcesPage.vue index df62e19..1a0b6c4 100644 --- a/apps/vdn-static/src/components/pages/ResourcesPage.vue +++ b/apps/vdn-static/src/components/pages/ResourcesPage.vue @@ -1,32 +1,194 @@ + \ No newline at end of file diff --git a/apps/vdn-static/src/i18n/greeting.ts b/apps/vdn-static/src/i18n/greeting.ts deleted file mode 100644 index d1e63ba..0000000 --- a/apps/vdn-static/src/i18n/greeting.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { VilanticId } from "./vilantic"; - -export interface Greeting { - title: string; - subtitle: string; - author: string; - lang: VilanticId; -} - -export const GREETINGS = [ - { - title: "BRÅTULA VIOSSA.NET MÅDE", - subtitle: "Hadjiplas per lera para Viossa – glossa fu vi", - author: "Jez", - lang: "viossa", - }, - { - title: "akka po viossa.net!", - subtitle: "kenomasufobo o gen wi tropos o viosox", - author: "Tetro", - lang: "wodox", - }, -] as const satisfies Greeting[]; diff --git a/apps/vdn-static/src/i18n/index.ts b/apps/vdn-static/src/i18n/index.ts deleted file mode 100644 index b1cf67f..0000000 --- a/apps/vdn-static/src/i18n/index.ts +++ /dev/null @@ -1,126 +0,0 @@ -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 { useLocalStorage } from "@vueuse/core"; -import { type } from "arktype"; - -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, DeepPartial>; - -// users could manually edit localStorage to make this value anything, so we need to validate it -const localStorageLocaleId = useLocalStorage( - "localeId", - DEFAULT_LOCALE_ID, -); - -export const localeId = computed({ - get: (): LocaleId => { - const localeIdRes = LocaleId(localStorageLocaleId.value); - if (localeIdRes instanceof type.errors) { - // if invalid LocaleId, reset to default - localStorageLocaleId.value = DEFAULT_LOCALE_ID; - return DEFAULT_LOCALE_ID; - } - - // else return user's selection - const localeId = localeIdRes; - return localeId; - }, - // custom setter to ensure it is only set to a valid LocaleId by our code - // (since the localStorage ref is typed as `unknown`, it can be set to any value) - set: (id: LocaleId) => { - localStorageLocaleId.value = id; - }, -}); - -export const useLocale = (opt: UseLocaleOptions = {}) => { - const locale = computed>(() => { - return fallbackProxy( - locales[opt.locale ?? localeId.value], - locales["en_US"], - ); - }); - - return readonly(locale); -}; - -export interface UseLocaleOptions { - locale?: LocaleId; -} - -function isObject(value: unknown) { - return typeof value === "object" && value !== null; -} - -function deepReadonly(value: T): DeepReadonly { - // SAFETY: we're just making an immutable view to the type, this isn't dangerous - return value as DeepReadonly; -} - -function fallbackProxy( - mask: DeepPartial, - fallback: T, -): DeepReadonly { - const proxy = new Proxy(fallback, { - get: (_target, key): DeepReadonly => { - // 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; - - // value may not exist on mask - const value: DeepPartial[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` (but not undefined) - const finalValue: DeepPartial[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[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( - finalValue, - fallbackValue, - ); - }, - 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); -} diff --git a/apps/vdn-static/src/i18n/locale.ts b/apps/vdn-static/src/i18n/locale.ts index d928a85..41fb166 100644 --- a/apps/vdn-static/src/i18n/locale.ts +++ b/apps/vdn-static/src/i18n/locale.ts @@ -19,6 +19,8 @@ export type VilanticLangs = Record; export interface Navbar { whatIsViossa: string; resources: string; + resourcesLearning: string; + resourcesCultural: string; kotoba: string; } @@ -46,13 +48,23 @@ export interface ResourcesPage { export interface Resources { discord: Resource; + daviSpil: Resource; + vimivera2025: Resource; + korohtella: Resource; + piik: Resource; + vikoli: Resource; } export interface Resource { + category: "learning" | "cultural"; title: string; subtitle: string; desc: string; link: string; + linkColor: string; + link2: string; + link2Text: string; + link2Color: string; rulesLink: string; image: string; alt: string; @@ -63,4 +75,4 @@ export interface Resource { export interface KotobaPage { title: string; searchHelp: string; -} +} \ No newline at end of file diff --git a/apps/vdn-static/src/i18n/vilantic.ts b/apps/vdn-static/src/i18n/vilantic.ts index 759c19f..52131da 100644 --- a/apps/vdn-static/src/i18n/vilantic.ts +++ b/apps/vdn-static/src/i18n/vilantic.ts @@ -1,9 +1,11 @@ import viossaFlag from "@/assets/flag_vp.webp"; import wodoxFlag from "@/assets/flag_wp.webp"; +import minemiahaFlag from "@/assets/flag_mi.webp"; -export type VilanticId = "viossa" | "wodox"; +export type VilanticId = "viossa" | "wodox" | "minemiaha"; export const VILANTIC_ID_TO_FLAG = { viossa: viossaFlag, wodox: wodoxFlag, + minemiaha: minemiahaFlag, } as const satisfies Record; diff --git a/apps/vdn-static/src/locales/en_US.ts b/apps/vdn-static/src/locales/en_US.ts index a366ae3..b373075 100644 --- a/apps/vdn-static/src/locales/en_US.ts +++ b/apps/vdn-static/src/locales/en_US.ts @@ -1,13 +1,17 @@ import type { Locale } from "@/i18n/locale"; import flakkaImg from "@/assets/flakka.png"; import discordImg from "@/assets/discord.png"; +import vimiveraImg from "@/assets/vimivera2025.webp"; +import korohtella from "@/assets/korohtella.png"; +import piikImg from "@/assets/piik.webp"; export default { localeName: "English", - vilanticLangs: { viossa: "Viossa", wodox: "Wodoch" }, - navbar: { +vilanticLangs: { viossa: "Viossa", wodox: "Wodoch", minemiaha: "Minemiaha" }, navbar: { whatIsViossa: "What is Viossa?", resources: "Resources", + resourcesLearning: "Learning Resources", + resourcesCultural: "Cultural Resources", kotoba: "Kotoba", }, home: { @@ -36,22 +40,106 @@ export default { }, }, resources: { - title: "Learning Resources", + title: "Resources", layout: { - order: ["discord"], + order: ["discord", "vikoli", "daviSpil", "vimivera2025", "korohtella", "piik"], data: { discord: { + category: "learning", title: "Discord Server", - subtitle: - "This is where most of the action happens! Hop on in!", + 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", + linkColor: "primary", + link2: "", + link2Text: "", + link2Color: "", rulesLink: "https://viossadiskordserver.github.io/rules", image: discordImg, alt: "Discord logo", joinText: "Join", rulesText: "Rules", }, + vikoli: { + category: "learning", + title: "Vikoli", + subtitle: "The Viossa encyclopedia.", + desc: "Vikoli hosts a number of learning and cultural resources. It is intended for use by advanced speakers of Viossa seeking to further their language abilities or teachers seeking to streamline their process with aides.", + link: "https://vikoli.org/Huomilehti", + linkColor: "primary", + link2: "", + link2Text: "", + link2Color: "", + rulesLink: "", + image: flakkaImg, + alt: "Flag of the Viossa Language", + joinText: "Visit", + rulesText: "", + }, + daviSpil: { + category: "cultural", + title: "Davi Spil", + subtitle: "Gaming server for people who already speak Viossa.", + desc: "The central hub for Viossa gaming events.\n\nDavi Spil is a dedicated gaming community for Viossa speakers. Various games are played here, including but not limited to Minecraft, Peak, Magic the Gathering, and Sakana (also known as Viossa Chess).", + link: "https://discord.gg/6QgRhw5DRJ", + linkColor: "primary", + link2: "", + link2Text: "", + link2Color: "", + rulesLink: "", + image: discordImg, + alt: "Discord logo", + joinText: "Join", + rulesText: "", + }, + vimivera2025: { + category: "cultural", + title: "Vimivera 2025 Anthology", + subtitle: "Selected poems from the 2025 Vimivera Literature Festival Poetry Competition.", + desc: "", + link: "/Vimivera_2025_Paemara_Sentakuena.pdf", + linkColor: "primary", + link2: "", + link2Text: "", + link2Color: "", + rulesLink: "", + image: vimiveraImg, + alt: "Vimivera 2025 Anthology cover", + joinText: "Read now", + rulesText: "", + }, + korohtella: { + category: "cultural", + title: "Korohtella", + subtitle: "Viossa music", + desc: "Perhaps Viossa's most famous collection of music, by Djima. A heartfelt album including a mix of original and translated works.", + link: "https://open.spotify.com/album/0skzWl5HU7ulKPm7qGssAX?si=jYzB26xSRE6m3C0LYYNWNw", + linkColor: "success", + link2: "https://www.youtube.com/watch?v=KsmTrqJFtjo&list=OLAK5uy_n2GcJf52fbN7nYB3PDCO-oWXEFV4Jdcrk", + link2Text: "Listen on YouTube", + link2Color: "danger", + rulesLink: "", + image: korohtella, + alt: "Korohtella album cover", + joinText: "Listen on Spotify", + rulesText: "", + }, + piik: { + category: "cultural", + title: "Peak — ViPIIK", + subtitle: "Viossa mod for Peak", + desc: "Mod for the videogame Peak that adds Viossa compatibility.", + link: "https://thunderstore.io/c/peak/p/vimik/ViPIIK/", + linkColor: "primary", + link2: "", + link2Text: "", + link2Color: "", + rulesLink: "", + image: piikImg, + alt: "ViPIIK mod", + joinText: "Thunderstore", + rulesText: "", + }, }, }, }, @@ -59,4 +147,4 @@ export default { title: "Tropos-agnostic search", searchHelp: "To searcn tropos-agnostically, enter a term below.", }, -} as const satisfies Locale; +} as const satisfies Locale; \ No newline at end of file diff --git a/apps/vdn-static/src/locales/mi_VL.ts b/apps/vdn-static/src/locales/mi_VL.ts new file mode 100644 index 0000000..e69de29 diff --git a/apps/vdn-static/src/locales/vp_VL.ts b/apps/vdn-static/src/locales/vp_VL.ts index 81f5566..d036acb 100644 --- a/apps/vdn-static/src/locales/vp_VL.ts +++ b/apps/vdn-static/src/locales/vp_VL.ts @@ -1,13 +1,18 @@ +import discordImg from "@/assets/discord.png"; import type { Locale } from "@/i18n/locale"; import flakkaImg from "@/assets/flakka.png"; +import vimiveraImg from "@/assets/vimivera2025.webp"; +import korohtella from "@/assets/korohtella.png"; +import piikImg from "@/assets/piik.webp"; import type { DeepPartial } from "@/utils/deep-partial"; export default { localeName: "Viossa", - vilanticLangs: { viossa: "Viossa", wodox: "Wodossa" }, - navbar: { +vilanticLangs: { viossa: "Viossa", wodox: "Wodossa", minemiaha: "Razhanossa" }, navbar: { whatIsViossa: "Ka Viossa?", resources: "Lerakran", + resourcesLearning: "Lerakran", + resourcesCultural: "Shakaiting", kotoba: "Kotoba", }, home: { @@ -38,20 +43,104 @@ export default { resources: { title: "Lerakran", layout: { - order: ["discord"], + order: ["discord", "vikoli", "daviSpil", "vimivera2025", "korohtella", "piik"], data: { discord: { + category: "learning", title: "Diskordserver", - subtitle: - "Alting Viossa tsuite slucha na her! Da zetulla jo!", + 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", + linkColor: "primary", + link2: "", + link2Text: "", + link2Color: "", rulesLink: "https://viossadiskordserver.github.io/rules", image: discordImg, alt: "Riso fu Diskord", joinText: "Zetulla", rulesText: "Ruuru", }, + vikoli: { + category: "learning", + title: "Vikoli", + subtitle: "Shirulehti Vilanta.", + desc: "Yam na Vikoli mangge lerakran au shakaiting. Mena, afto zelehti brukena haaste grun mono bruk Viossa. Bra kran per Viossadjin ke vill maha shiruzma sui plubra os na opetadjin ke vill maha opetatropos plusimper met opetakran.", + link: "https://vikoli.org/Huomilehti", + linkColor: "primary", + link2: "", + link2Text: "", + link2Color: "", + rulesLink: "", + image: flakkaImg, + alt: "Flakka fu Viossa", + joinText: "Suha", + rulesText: "", + }, + daviSpil: { + category: "cultural", + title: "Davi Spil", + subtitle: "Server per spildjin ke hanu Viossa.", + desc: "Viossa Spilserver.\n\nDavi Spil tak spilhuomi fu Viossadjin. Mangge spil yam hjer: Shahtamaha, Piik, Taikafesta, au Sakana (Viossatjesu) prosta na tatoeba.", + link: "https://discord.gg/6QgRhw5DRJ", + linkColor: "primary", + link2: "", + link2Text: "", + link2Color: "", + rulesLink: "", + image: discordImg, + alt: "Riso fu Diskord", + joinText: "Zetulla", + rulesText: "", + }, + vimivera2025: { + category: "cultural", + title: "Paemliber Vimivera 2025", + subtitle: "Paemara sentakujena Vimivera 2025 Paemtuvat kara.", + desc: "", + link: "/Vimivera_2025_Paemara_Sentakuena.pdf", + linkColor: "primary", + link2: "", + link2Text: "", + link2Color: "", + rulesLink: "", + image: vimiveraImg, + alt: "Paemliber Vimivera 2025 Kara", + joinText: "Lesa", + rulesText: "", + }, + korohtella: { + category: "cultural", + title: "Korohtella", + subtitle: "Viliid", + desc: "Lesteshiruena Viossaliidklaani, Djima kara. Ain kokorodai liidtumam, jokku mahajena au jokku kjannosena.", + link: "https://open.spotify.com/album/0skzWl5HU7ulKPm7qGssAX?si=jYzB26xSRE6m3C0LYYNWNw", + linkColor: "success", + link2: "https://www.youtube.com/watch?v=KsmTrqJFtjo&list=OLAK5uy_n2GcJf52fbN7nYB3PDCO-oWXEFV4Jdcrk", + link2Text: "Anhore na YouTube", + link2Color: "danger", + rulesLink: "", + image: korohtella, + alt: "Korohtella album kara", + joinText: "Anhore na Spotify", + rulesText: "", + }, + piik: { + category: "cultural", + title: "Peak — ViPIIK", + subtitle: "Piik", + desc: "Vikjannos per Piikspil. Ima tshangki na glossa fu vi.", + link: "https://thunderstore.io/c/peak/p/vimik/ViPIIK/", + linkColor: "primary", + link2: "", + link2Text: "", + link2Color: "", + rulesLink: "", + image: piikImg, + alt: "ViPIIK mod", + joinText: "Zedvera", + rulesText: "", + }, }, }, }, @@ -59,4 +148,4 @@ export default { title: "Tropos-egal suha", searchHelp: "Li vil suha uten tro-egal, tastatsa joku ko os fras na una.", }, -} as const satisfies DeepPartial; +} as const satisfies DeepPartial; \ No newline at end of file diff --git a/apps/vdn-static/src/locales/wp_VL.ts b/apps/vdn-static/src/locales/wp_VL.ts index 800978b..f0a02c6 100644 --- a/apps/vdn-static/src/locales/wp_VL.ts +++ b/apps/vdn-static/src/locales/wp_VL.ts @@ -1,14 +1,18 @@ import type { Locale } from "@/i18n/locale"; import flakkaImg from "@/assets/flakka.png"; import discordImg from "@/assets/discord.png"; +import vimiveraImg from "@/assets/vimivera2025.webp"; +import korohtella from "@/assets/korohtella.png"; +import piikImg from "@/assets/piik.webp"; import type { DeepPartial } from "@/utils/deep-partial"; export default { localeName: "wodox", - vilanticLangs: { viossa: "viosox", wodox: "wodox" }, - navbar: { +vilanticLangs: { viossa: "viosox", wodox: "wodox", minemiaha: "minemiox" }, navbar: { whatIsViossa: "viosox e ano?", resources: "tropos", + resourcesLearning: "tropos o gen", + resourcesCultural: "tropos o ro", kotoba: "mot o viosox", }, home: { @@ -39,25 +43,114 @@ export default { resources: { title: "tropos o gen", layout: { - order: ["discord"], + order: ["discord", "vikoli", "daviSpil", "vimivera2025", "korohtella", "piik"], data: { discord: { + category: "learning", 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", + linkColor: "primary", + link2: "", + link2Text: "", + link2Color: "", rulesLink: "https://viossadiskordserver.github.io/rules", image: discordImg, alt: "surat o Diskord", joinText: "wekatutsa", rulesText: "musra", }, + vikoli: { + // TODO: translate to wodox + category: "learning", + title: "Vikoli", + subtitle: "Shirulehti Vilanta.", + desc: "Yam na Vikoli mangge lerakran au shakaiting. Mena, afto zelehti brukena haaste grun mono bruk Viossa. Bra kran per Viossadjin ke vill maha shiruzma sui plubra os na opetadjin ke vill maha opetatropos plusimper met opetakran.", + link: "https://vikoli.org/Huomilehti", + linkColor: "primary", + link2: "", + link2Text: "", + link2Color: "", + rulesLink: "", + image: flakkaImg, + alt: "Flakka fu Viossa", + joinText: "Suha", + rulesText: "", + }, + daviSpil: { + // TODO: translate to wodox + category: "cultural", + title: "Davi Spil", + subtitle: "Server per spildjin ke hanu Viossa.", + desc: "Viossa Spilserver.\n\nDavi Spil tak spilhuomi fu Viossadjin. Mangge spil yam hjer: Shahtamaha, Piik, Taikafesta, au Sakana (Viossatjesu) prosta na tatoeba.", + link: "https://discord.gg/6QgRhw5DRJ", + linkColor: "primary", + link2: "", + link2Text: "", + link2Color: "", + rulesLink: "", + image: discordImg, + alt: "surat o Diskord", + joinText: "wekatutsa", + rulesText: "", + }, + vimivera2025: { + // TODO: translate to wodox + category: "cultural", + title: "Paemliber Vimivera 2025", + subtitle: "Paemara sentakujena Vimivera 2025 Paemtuvat kara.", + desc: "", + link: "/Vimivera_2025_Paemara_Sentakuena.pdf", + linkColor: "primary", + link2: "", + link2Text: "", + link2Color: "", + rulesLink: "", + image: vimiveraImg, + alt: "Paemliber Vimivera 2025 Kara", + joinText: "Lesa", + rulesText: "", + }, + korohtella: { + // TODO: translate to wodox + category: "cultural", + title: "Korohtella", + subtitle: "Viliid", + desc: "Lesteshiruena Viossaliidklaani, Djima kara. Ain kokorodai liidtumam, jokku mahajena au jokku kjannosena.", + link: "https://open.spotify.com/album/0skzWl5HU7ulKPm7qGssAX?si=jYzB26xSRE6m3C0LYYNWNw", + linkColor: "success", + link2: "https://www.youtube.com/watch?v=KsmTrqJFtjo&list=OLAK5uy_n2GcJf52fbN7nYB3PDCO-oWXEFV4Jdcrk", + link2Text: "Anhore na YouTube", + link2Color: "danger", + rulesLink: "", + image: korohtella, + alt: "Korohtella album kara", + joinText: "Anhore na Spotify", + rulesText: "", + }, + piik: { + // TODO: translate to wodox + category: "cultural", + title: "Peak — ViPIIK", + subtitle: "Piik", + desc: "Vikjannos per Piikspil. Ima tshangki na glossa fu vi.", + link: "https://thunderstore.io/c/peak/p/vimik/ViPIIK/", + linkColor: "primary", + link2: "", + link2Text: "", + link2Color: "", + rulesLink: "", + image: piikImg, + alt: "ViPIIK mod", + joinText: "Zedvera", + rulesText: "", + }, }, }, }, kotoba: { title: "zalkuketutsa mot o viosox mit il o omni falmot", - searchHelp: - "ibe tastatukun il falmot o mot o viosox po pam, de zalkuketukun.", + searchHelp: "ibe tastatukun il falmot o mot o viosox po pam, de zalkuketukun.", }, -} as const satisfies DeepPartial; +} as const satisfies DeepPartial; \ No newline at end of file diff --git a/apps/vdn-static/src/main.ts b/apps/vdn-static/src/main.ts index 66cde5b..10ea69d 100644 --- a/apps/vdn-static/src/main.ts +++ b/apps/vdn-static/src/main.ts @@ -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"); diff --git a/apps/vdn-static/src/new-i18n-lib/config.ts b/apps/vdn-static/src/new-i18n-lib/config.ts new file mode 100644 index 0000000..6940e69 --- /dev/null +++ b/apps/vdn-static/src/new-i18n-lib/config.ts @@ -0,0 +1,470 @@ +import type { Result } from "@/utils/types"; +import { + computeAllVariants, + selectionChainToString, + type UncompiledLocale, +} from "./setup"; +import type { FluentVariable } from "@fluent/bundle"; +import { parseMarkdown, type Markdown } from "./markdown"; +export const configMessageSymbol: unique symbol = Symbol("configMessage"); +export interface ConfigMessage< + Placeables extends { [name in string]?: ConfigPlaceableInfo } = { + [name in string]: ConfigPlaceableInfo; + }, + Markdown extends + ConfigMarkdown | null = ConfigMarkdown | null, +> { + [configMessageSymbol]: true; + placeables: Placeables; + markdown: Markdown; +} + +export interface ConfigPlaceableInfo< + Type extends "string" | "number" = "string" | "number", +> { + type: Type; +} + +export interface ConfigMarkdown { + bold?: boolean; + italic?: boolean; + header?: boolean; + link?: boolean; + ulist?: boolean; + slots?: Slot[]; +} + +export function message< + const Placeables extends { + [name in string]?: ConfigPlaceableInfo; + } = object, + const Markdown extends ConfigMarkdown | null = null, +>( + opt: Partial< + Omit< + ConfigMessage | Markdown>, + typeof configMessageSymbol + > + > = {}, +): ConfigMessage { + return { + [configMessageSymbol]: true, + placeables: (opt.placeables ?? {}) as Placeables, + markdown: (opt.markdown ?? null) as Markdown, + }; +} + +export type LocaleConfig< + Placeables extends { [name in string]?: ConfigPlaceableInfo } = object, + Markdown extends + ConfigMarkdown | null = ConfigMarkdown | null, +> = { + [id: string]: + | ConfigMessage + | LocaleConfig; +}; + +type MessageCtx< + Placeables extends { [name in string]?: ConfigPlaceableInfo } = object, +> = { + [K in keyof Placeables]: ResolvedPlaceableType< + PlaceableType> + >; +}; + +type PlaceableType = + Var extends ConfigPlaceableInfo ? Type : never; + +type ResolvedPlaceableType = + Type extends "string" ? string + : Type extends "number" ? number + : never; + +export type InferLocale = { + [K in keyof Config]: Config[K] extends LocaleConfig ? InferLocale + : Config[K] extends ConfigMessage ? + object extends MessageCtx ? + () => null extends Md ? string + : Md extends ConfigMarkdown ? Markdown + : Md extends ConfigMarkdown ? Markdown + : never + : (ctx: MessageCtx) => null extends Md ? string + : Md extends ConfigMarkdown ? Markdown + : Md extends ConfigMarkdown ? Markdown + : never + : never; +}; + +export function record( + keys: readonly Key[], + initializer: () => T, +): Record { + const obj: Partial> = {}; + for (const key of keys) { + obj[key] = initializer(); + } + + // SAFETY: we set all properties from keys array above + return obj as Record; +} + +export interface CompileLocaleCtx { + uncompiled: UncompiledLocale; + config: Config; +} + +export function compileLocale( + ctx: CompileLocaleCtx, +): Result, string> { + const { uncompiled, config } = ctx; + + const { record, bundle } = uncompiled; + + const recordKeys = new Set(Object.keys(record)); + const configKeys = new Set(Object.keys(config)); + const excessKeys = recordKeys.difference(configKeys); + console.log(recordKeys, configKeys, excessKeys); + if (excessKeys.size > 0) { + return { + type: "err", + err: `Excess keys in record: ${[...excessKeys].join(", ")}`, + }; + } + + type GenericLocale = { + [id: string]: + | GenericLocale + | ((args: Record) => string) + | ((args: Record) => Markdown); + }; + + const locale: GenericLocale = {}; + for (const [id, configValue] of Object.entries(config)) { + const recordValue = record[id]; + + if (configMessageSymbol in configValue) { + if (recordValue?.type !== "message") { + return { + type: "err", + err: `Expected message for key \`${id}\`, found: ${typeof recordValue}`, + }; + } + + const message = recordValue.message; + console.log(message); + + const pattern = message.value; + if (pattern === null) { + return { + type: "err", + err: `Pattern is null for message with ID: ${id}`, + }; + } + + // validate placeables + console.log(pattern); + if (typeof pattern !== "string") { + for (const element of pattern) { + if (typeof element === "string") { + continue; + } + + switch (element.type) { + case "select": { + const { selector } = element; + if (selector.type !== "var") { + return { + type: "err", + err: `Expected selector to be a var expression for key: ${id}; Found: ${selector.type}`, + }; + } + + if ( + !Object.keys(configValue.placeables).includes( + selector.name, + ) + ) { + return { + type: "err", + err: `Found unexpected placeable name \`${selector.name}\` for key: ${id}`, + }; + } + + break; + } + case "var": { + if ( + !Object.keys(configValue.placeables).includes( + element.name, + ) + ) { + return { + type: "err", + err: `Found unexpected placeable name \`${element.name}\` for key: ${id}`, + }; + } + + break; + } + case "term": + case "mesg": + case "func": + case "str": + case "num": { + break; // ignore + } + } + } + } + + const allVariantsRes = computeAllVariants(pattern); + if (allVariantsRes.type === "err") { + return { + type: "err", + err: `Failed to compute variants for key \`${id}\`:\n${allVariantsRes.err}`, + }; + } + + const allVariants = allVariantsRes.ok; + console.log(allVariants); + + // create function + const markdown = configValue.markdown; + const compiledFnRes = ((): Result< + | ((args?: Record) => string) + | ((args?: Record) => Markdown), + string + > => { + if (markdown !== null) { + // typecheck markdown + + const markdownSlots = markdown.slots ?? []; + + // check if all variants are valid markdown + for (const variant of allVariants) { + const markdownLiteralRes = parseMessageLiteral( + "md", + variant.string, + ); + + if (markdownLiteralRes.type === "err") { + return { + type: "err", + err: `Invalid literal for variant \`${selectionChainToString(variant.selectionChain)}\` of key \`${id}\`:\n${markdownLiteralRes.err}`, + }; + } + + const markdownLiteral = markdownLiteralRes.ok; + const markdownRes = parseMarkdown( + markdownLiteral, + markdownSlots, + ); + console.log(markdownRes); + if (markdownRes.type === "err") { + return { + type: "err", + err: `Invalid markdown for variant \`${selectionChainToString(variant.selectionChain)}\` of key \`${id}\`:\n${markdownRes.err}`, + }; + } + } + + // TODO: will need to make sure markdown/slots are escapes when inserting variable values + return { + type: "ok", + ok: (args: Record = {}) => { + const markdownLiteralRes = parseMessageLiteral( + "md", + bundle.formatPattern(pattern, args), + ); + + if (markdownLiteralRes.type === "err") { + // This should hopefully never happen since we've already + // verified all message variants parse as valid markdown above + throw new Error( + `Failed to parse markdown literal after compilation!\n${markdownLiteralRes.err}`, + ); + } + + const markdownLiteral = markdownLiteralRes.ok; + const res = parseMarkdown( + markdownLiteral, + markdownSlots, + ); + + if (res.type === "err") { + // This should hopefully never happen since we've already + // verified all message variants parse as valid markdown above + throw new Error( + `Failed to parse markdown after compilation!\n${res.err}`, + ); + } + + return res.ok; + }, + }; + } else { + // typecheck string + // check if all variants are valid markdown + for (const variant of allVariants) { + const stringLiteralRes = parseMessageLiteral( + "string", + variant.string, + ); + + if (stringLiteralRes.type === "err") { + return { + type: "err", + err: `Invalid literal for variant \`${selectionChainToString(variant.selectionChain)}\` of key \`${id}\`:\n${stringLiteralRes.err}`, + }; + } + + const stringLiteral = stringLiteralRes.ok; + const stringRes = parseString(stringLiteral); + console.log(stringRes); + if (stringRes.type === "err") { + return { + type: "err", + err: `Invalid string for variant \`${selectionChainToString(variant.selectionChain)}\` of key \`${id}\`:\n${stringRes.err}`, + }; + } + } + + // TODO: will need to make sure markdown/slots are escapes when inserting variable values + return { + type: "ok", + ok: (args: Record = {}) => { + const stringLiteralRes = parseMessageLiteral( + "string", + bundle.formatPattern(pattern, args), + ); + + if (stringLiteralRes.type === "err") { + // This should hopefully never happen since we've already + // verified all message variants parse as valid strings above + throw new Error( + `Failed to parse string literal after compilation!\n${stringLiteralRes.err}`, + ); + } + + const stringLiteral = stringLiteralRes.ok; + + const res = parseString(stringLiteral); + + if (res.type === "err") { + // This should hopefully never happen since we've already + // verified all message variants parse as valid strings above + // TODO: no we dont, do that + throw new Error( + `Failed to parse string after compilation!\n${res.err}`, + ); + } + + return res.ok; + }, + }; + } + })(); + + if (compiledFnRes.type === "err") { + return compiledFnRes; + } + + const compiledFn = compiledFnRes.ok; + locale[id] = compiledFn; + } else { + if (recordValue?.type !== "subrecord") { + return { + type: "err", + err: `Expected subrecord for key \`${id}\`, found: ${typeof recordValue}`, + }; + } + + const subrecord = recordValue.subrecord; + const compiledSubrecordRes = compileLocale({ + uncompiled: { bundle, record: subrecord }, + config: configValue, + }); + + if (compiledSubrecordRes.type === "err") { + return { + type: "err", + err: `Error when compiling subrecord with key: \`${id}\`:\n${compiledSubrecordRes.err}`, + }; + } + + const compiledSubrecord = compiledSubrecordRes.ok; + locale[id] = compiledSubrecord; + } + } + + // SAFETY: validated above that all keys exist and are the correct type + return { type: "ok", ok: locale as InferLocale }; +} + +function parseMessageLiteral( + type: "string" | "md", + message: string, +): Result { + const trimmedMessage = message.trim(); + + console.log(trimmedMessage); + + const maybeStartIndexes: number[] = []; + const firstQuoteIndex = trimmedMessage.indexOf('"'); + if (firstQuoteIndex !== -1) { + maybeStartIndexes.push(firstQuoteIndex); + } + + const firstDashIndex = trimmedMessage.indexOf("-"); + if (firstDashIndex !== -1) { + maybeStartIndexes.push(firstDashIndex); + } + + const stringStartIndex = Math.min(...maybeStartIndexes); + + console.log(stringStartIndex); + const actualPrefix = trimmedMessage.substring(0, stringStartIndex).trim(); + const expectedPrefix = (() => { + switch (type) { + case "string": { + return ""; + } + case "md": { + return "md"; + } + } + })(); + + if (actualPrefix !== expectedPrefix) { + return { + type: "err", + err: `Expected prefix "${expectedPrefix}" for message with type \`${type}\`; Found: "${actualPrefix}"`, + }; + } + + return { + type: "ok", + ok: trimmedMessage.substring(actualPrefix.length).trim(), + }; +} + +function parseString(message: string): Result { + const AFFIX = '"'; + if (!message.startsWith(AFFIX)) { + return { + type: "err", + err: `String message expected to start with \`${AFFIX}\``, + }; + } + + if (!message.endsWith(AFFIX)) { + return { + type: "err", + err: `String message expected to end with \`${AFFIX}\``, + }; + } + + const deprefixed = message.substring(AFFIX.length); + const dequoted = deprefixed.substring(0, deprefixed.length - AFFIX.length); + return { type: "ok", ok: dequoted }; +} \ No newline at end of file diff --git a/apps/vdn-static/src/new-i18n-lib/markdown.ts b/apps/vdn-static/src/new-i18n-lib/markdown.ts new file mode 100644 index 0000000..c237a2d --- /dev/null +++ b/apps/vdn-static/src/new-i18n-lib/markdown.ts @@ -0,0 +1,743 @@ +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 { + lines: MarkdownLine[]; + slots: Slot[]; +} + +export type MarkdownLine = { + type: "paragraph" | "header"; + elements: MarkdownLineElement[]; +}; + +export type MarkdownFeature = + | Exclude + | Exclude; + +export type MarkdownLineElement = + | { type: "plain"; plain: string } + | { type: "italic"; italic: MarkdownLineElement[] } + | { type: "bold"; bold: MarkdownLineElement[] } + | { + type: "link"; + link: { + label: MarkdownLineElement[]; + to: SmartDest; + newTab: boolean; + }; + } + | { type: "slot"; slot: Slot }; + +export function parseMarkdown( + markdownString: string, + slots: readonly Slot[], +): Result, string> { + if (markdownString.trim() === "--") { + return { type: "ok", ok: { lines: [], slots: [] } }; + } + + 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[] = []; + 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: { lines: markdownLines, slots: [...slots] } }; +} + +function parseMarkdownLine( + line: string, + slots: readonly Slot[], +): Result, string> { + if (line.startsWith("#")) { + const elementsRes = parseMarkdownLineElements(line.substring(1), slots); + + if (elementsRes.type === "err") { + return { + type: "err", + err: `While parsing elements:\n${elementsRes.err}`, + }; + } + + const elements = elementsRes.ok; + return { type: "ok", ok: { type: "header", elements } }; + } + + const elementsRes = parseMarkdownLineElements(line, slots); + if (elementsRes.type === "err") { + return { + type: "err", + err: `While parsing elements:\n${elementsRes.err}`, + }; + } + + const elements = elementsRes.ok; + return { type: "ok", ok: { type: "paragraph", elements } }; +} + +function parseMarkdownLineElements( + line: string, + slots: readonly Slot[], +): Result[], 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 trimmedLine = line.trim(); + const chars = trimmedLine.split(""); + const elementsRes = readMarkdownLineElements(chars, slots, { + inItalic: false, + inBold: false, + }); + if (elementsRes.type === "err") { + return elementsRes; + } + + const elements = elementsRes.ok; + return { type: "ok", ok: elements }; +} + +interface ReadMarkdownLineElementCtx { + inItalic: boolean; + inBold: boolean; +} + +function readMarkdownLineElements( + chars: string[], + slots: readonly Slot[], + ctx: ReadMarkdownLineElementCtx, +): Result[], string> { + const elements: MarkdownLineElement[] = []; + while (true) { + const elementRes = readMarkdownLineElement(chars, slots, ctx); + if (elementRes.type === "err") { + return elementRes; + } + + const element = elementRes.ok; + if (element.length === 0) { + break; + } + + elements.push(...element); + } + + return { type: "ok", ok: elements }; +} + +function readMarkdownLineElement( + chars: string[], + slots: readonly Slot[], + ctx: ReadMarkdownLineElementCtx, +): Result[], string> { + const [firstChar, secondChar] = chars; + if (firstChar === undefined) { + return { type: "ok", ok: [] }; + } else if (firstChar === "<") { + return readMarkdownLineElementSlot(chars, slots); + } else if (firstChar === "[") { + return readMarkdownLineElementLink(chars, slots, ctx); + } else if (firstChar === "*" && secondChar === "*" && !ctx.inBold) { + return readMarkdownLineElementBold(chars, slots, ctx); + } else if (firstChar === "*" && secondChar !== "*" && !ctx.inItalic) { + return readMarkdownLineElementItalic(chars, slots, ctx); + } else { + return readMarkdownLineElementPlain(chars); + } +} + +function isInArray(value: T, array: readonly U[]): value is U { + // SAFETY: this is just an equality check, it is safe to pass in any value + return array.includes(value as U); +} + +function readMarkdownLineElementSlot( + chars: string[], + slots: readonly Slot[], +): Result[], 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 (!isInArray(slotName, slots)) { + return { type: "err", err: `Unexpected slot name: ${slotName}` }; + } + + return { type: "ok", ok: [{ type: "slot", slot: slotName }] }; +} + +function readMarkdownLineElementLink( + chars: string[], + slots: readonly Slot[], + ctx: ReadMarkdownLineElementCtx, +): Result[], string> { + const openSquareRes = expectReadChar(chars, "["); + if (openSquareRes.type === "err") { + return openSquareRes; + } + + const labelElementsRes = readMarkdownLineElements(chars, slots, ctx); + 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 => { + 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 => { + 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 => { + 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: MarkdownLineElement[] = + 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 { + 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 { + 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 readMarkdownLineElementBold( + chars: string[], + slots: readonly Slot[], + ctx: ReadMarkdownLineElementCtx, +): Result[], string> { + const firstStarRes = expectReadChar(chars, "*"); + if (firstStarRes.type === "err") { + return firstStarRes; + } + + const secondStarRes = expectReadChar(chars, "*"); + if (secondStarRes.type === "err") { + return secondStarRes; + } + + ctx.inBold = true; + const elements: MarkdownLineElement[] = []; + let closed = false; + while (true) { + const elementRes = readMarkdownLineElement(chars, slots, ctx); + if (elementRes.type === "err") { + return elementRes; + } + + const element = elementRes.ok; + if (element.length === 0) { + break; + } + + elements.push(...element); + const [firstChar, secondChar] = chars; + if (firstChar === "*" && secondChar === "*") { + chars.shift(); + chars.shift(); + closed = true; + break; + } else if (firstChar === undefined) { + closed = false; + break; + } + } + + ctx.inBold = !closed; + if (closed) { + return { type: "ok", ok: [{ type: "bold", bold: elements }] }; + } else { + return { + type: "ok", + ok: [{ type: "plain", plain: "**" }, ...elements], + }; + } +} + +function readMarkdownLineElementItalic( + chars: string[], + slots: readonly Slot[], + ctx: ReadMarkdownLineElementCtx, +): Result[], string> { + const starRes = expectReadChar(chars, "*"); + if (starRes.type === "err") { + return starRes; + } + + ctx.inItalic = true; + const elements: MarkdownLineElement[] = []; + let closed = false; + while (true) { + const elementRes = readMarkdownLineElement(chars, slots, ctx); + if (elementRes.type === "err") { + return elementRes; + } + + const element = elementRes.ok; + if (element.length === 0) { + break; + } + + elements.push(...element); + const [firstChar] = chars; + if (firstChar === "*") { + chars.shift(); + closed = true; + break; + } else if (firstChar === undefined) { + closed = false; + break; + } + } + + ctx.inItalic = !closed; + if (closed) { + return { type: "ok", ok: [{ type: "italic", italic: elements }] }; + } else { + return { type: "ok", ok: [{ type: "plain", plain: "*" }, ...elements] }; + } +} + +function readMarkdownLineElementPlain( + chars: string[], +): Result[], 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 ? [{ type: "plain", plain }] : [], + }; +} + +function expectReadChar( + chars: string[], + expectedChar: string, +): Result { + 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 { + 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 { + 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.lines.length === 0; +} diff --git a/apps/vdn-static/src/new-i18n-lib/setup.ts b/apps/vdn-static/src/new-i18n-lib/setup.ts new file mode 100644 index 0000000..92534be --- /dev/null +++ b/apps/vdn-static/src/new-i18n-lib/setup.ts @@ -0,0 +1,214 @@ +import type { Result } from "@/utils/types"; +import { + FluentBundle, + FluentResource, + type FluentVariable, +} 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> { + 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; + console.log(ftlFileText); + const resource = new FluentResource(ftlFileText); + + console.log(resource.body); + + const bundle = new FluentBundle(localeId); + const errors = bundle.addResource(resource); + if (errors.length > 0) { + return { + type: "err", + err: `Failed to add Fluent resource to bundle:\n${errors.join("\n")}`, + }; + } + + return { type: "ok", ok: bundle }; +} + +export type L10nRecord = { + [id: string]: + | { type: "message"; message: Message } + | { type: "subrecord"; subrecord: L10nRecord }; +}; + +export interface UncompiledLocale { + bundle: FluentBundle; + record: L10nRecord; +} + +export function bundleToUncompiledLocale( + bundle: FluentBundle, +): Result { + const record: L10nRecord = {}; + 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: { bundle, 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("+"); +} + +interface PatternVariant { + selectionChain: SelectionChain; + string: string; +} + +export function computeAllVariants( + pattern: Pattern, +): Result { + if (typeof pattern === "string") { + return { type: "ok", ok: [{ selectionChain: [], string: pattern }] }; + } + + let variants: PatternVariant[] = [{ selectionChain: [], string: "" }]; + console.log(pattern); + 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 }; +} diff --git a/apps/vdn-static/src/new-i18n/config.ts b/apps/vdn-static/src/new-i18n/config.ts new file mode 100644 index 0000000..307d397 --- /dev/null +++ b/apps/vdn-static/src/new-i18n/config.ts @@ -0,0 +1,82 @@ +import { + message, + record, + type InferLocale, + type LocaleConfig, +} from "@/new-i18n-lib/config"; + +const homeSectionConfig = { title: message(), body: message() }; + +const imageConfig = { alt: message() }; +export interface Image extends InferLocale {} + +const buttonConfig = { label: message() }; + +const resourceConfig = (buttonKeys: ButtonKey[]) => ({ + title: message(), + subtitle: message(), + desc: message(), + buttons: record(buttonKeys, () => buttonConfig), +}); + +const discordRuleConfig = { + overview: { + text: message({ markdown: { bold: true, italic: true, link: true } }), + subtext: message({ + markdown: { bold: true, italic: true, link: true }, + }), + }, + section: { + header: message({ placeables: { ruleNumber: { type: "number" } } }), + body: message({ + markdown: { bold: true, header: true, italic: true, link: true }, + }), + }, +}; + +export const localeConfig = { + localeName: message(), + vilanticLangs: record(["viossa", "wodox", "minemiaha"], () => message()), + navbar: record(["whatIsViossa", "resources", "resourcesLearning", "resourcesCultural", "kotoba"], () => message()), + home: { + sections: record( + ["whatIsViossa", "historyOfViossa", "community"], + () => homeSectionConfig, + ), + images: record(["viossaFlag"], () => imageConfig), + }, + resources: { + title: message(), + resources: { + discord: resourceConfig(["join", "rules"]), + vikoli: resourceConfig(["visit"]), + daviSpil: resourceConfig(["join"]), + vimivera2025: resourceConfig(["read"]), + korohtella: resourceConfig(["spotify", "youtube"]), + piik: resourceConfig(["thunderstore"]), + }, + images: record( + ["discordLogo", "viossaFlag", "vimivera2025", "korohtella", "piik"], + () => imageConfig, + ), + }, + kotoba: { title: message(), searchHelp: message() }, + discord: { + rulesPage: { + title: message(), + overview: { title: message(), help: message() }, + rules: record( + [ + "noTranslation", + "lfsv", + "viossaOnlyChats", + "sfw", + "respectOthers", + "respectStaff", + "controversialTopics", + ], + () => discordRuleConfig, + ), + }, + }, +} as const satisfies LocaleConfig; \ No newline at end of file diff --git a/apps/vdn-static/src/new-i18n/greeting.ts b/apps/vdn-static/src/new-i18n/greeting.ts new file mode 100644 index 0000000..800eb99 --- /dev/null +++ b/apps/vdn-static/src/new-i18n/greeting.ts @@ -0,0 +1,83 @@ +import type { VilanticId } from "./vilantic"; + +export interface Greeting { + title: string; + subtitle: string; + author: string; + lang: VilanticId; +} + +export const GREETINGS = [ + { + title: "BRÅTULA VIOSSA.NET MÅDE", + subtitle: "Hadjiplas per lera para Viossa – glossa fu vi", + author: "Jez", + lang: "viossa", + }, + { + title: "akka po viossa.net!", + subtitle: "kenomasufobo o gen wi tropos o viosox", + author: "Tetro", + lang: "wodox", + }, + { + title: "VIOSSA.NET VR̄ATULAŢAJO", + subtitle: "Hažilɛ̄ti na viɔssalɛɾa! Viɔssa lɛstɛvr̄ā ɡlɔssa﹐tɛndɔţa!", + author: "Rju", + lang: "viossa", + }, + { + title: "bratsatulla na viossa.net made!", + subtitle: "Furalehti vilanta", + author: "Niko", + lang: "viossa", + }, + { + title: "Bratula na viossa.net!", + subtitle: "Davi lera vjosa medrio!", + author: "2o3ka", + lang: "viossa", + }, + { + title: "ברא־תולהצה viossa.net מדא!", + subtitle: "דבֿי לרה איו הנסו שתוף צויתה נא בֿיוסה", + author: "Visa Chin", + lang: "viossa", + }, + { + title: "Bratullatsa viossa.net made!", + subtitle: "Leratsa Viossa au letstehal sztof andra derna", + author: "Visa Chin", + lang: "viossa", + }, + { + title: "BRATULA VIOSSA.NET MADE", + subtitle: "Hadжilehti pęr ʋjosalera! Nintendotsa", + author: "Kurokot", + lang: "viossa", + }, + { + title: "бrατuλα αδ viossa.net mαᴅε!!", + subtitle: "xαᵹιλε̃τι v́ ʋιossα λεrαᴅѥτ! nιnτεnᴅocα nαruγα!", + author: "Orenge", + lang: "viossa", + }, + { + title: "Bratulla na viossa.net made!", + subtitle: "Hazsilehti fu viossaklani—yuenttsa na her yo!", + author: "Zsiyo", + lang: "viossa", + }, + { + title: "Alú ri viossa.net-ssa!", + subtitle: "Ya vir atuarpik Viossáha druzsai-mais!", + author: "Zsiyo", + lang: "minemiaha", + }, + { + title: "Bratulla Viossa.net made!", + subtitle: "Hadjiplas per lera Viossa para.", + author: "Maikaelja", + lang: "viossa", + }, +] as const satisfies Greeting[]; diff --git a/apps/vdn-static/src/new-i18n/index.ts b/apps/vdn-static/src/new-i18n/index.ts new file mode 100644 index 0000000..afd3bdf --- /dev/null +++ b/apps/vdn-static/src/new-i18n/index.ts @@ -0,0 +1,164 @@ +import { compileLocale, type InferLocale } from "@/new-i18n-lib/config"; +import { + bundleToUncompiledLocale, + loadFluentBundle, +} from "@/new-i18n-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 miVlFtlSrc from "@/assets/locale/mi_VL.ftl"; +import type { FluentBundle } from "@fluent/bundle"; + +export const LOCALE_IDS = ["en-US", "vp-VL", "mi-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; + +// users could manually edit localStorage to make this value anything, so we need to validate it +const localStorageLocaleId = useLocalStorage( + "localeId", + DEFAULT_LOCALE_ID, +); + +export const localeId = computed({ + get: (): LocaleId => { + const localeIdRes = LocaleId(localStorageLocaleId.value); + if (localeIdRes instanceof type.errors) { + // if invalid LocaleId, reset to default + localStorageLocaleId.value = DEFAULT_LOCALE_ID; + return DEFAULT_LOCALE_ID; + } + + // else return user's selection + const localeId = localeIdRes; + return localeId; + }, + // custom setter to ensure it is only set to a valid LocaleId by our code + // (since the localStorage ref is typed as `unknown`, it can be set to any value) + set: (id: LocaleId) => { + localStorageLocaleId.value = id; + }, +}); + +export interface Locale extends InferLocale {} + +async function loadLocale( + localeId: LocaleId, + localeFtlSrc: string, +): Promise> { + const bundleRes = await loadFluentBundle(localeId, localeFtlSrc); + if (bundleRes.type === "err") { + return bundleRes; + } + + const bundle = bundleRes.ok; + return { type: "ok", ok: bundle }; +} + +function setupLocale( + localeBundle: FluentBundle, + fallbackLocaleBundle: FluentBundle | undefined, +): Result { + const maybeFallbackedBundle = (() => { + if (fallbackLocaleBundle === undefined) { + return localeBundle; + } + + console.log(localeBundle); + console.log(fallbackLocaleBundle); + + const localeMessageIds = new Set(localeBundle._messages.keys()); + const fallbackMessageIds = new Set( + fallbackLocaleBundle._messages.keys(), + ); + + const missingMessageIds = + fallbackMessageIds.difference(localeMessageIds); + + for (const id of missingMessageIds) { + const fallbackMessage = fallbackLocaleBundle._messages.get(id); + if (fallbackMessage) { + localeBundle._messages.set(id, fallbackMessage); + } + } + + return localeBundle; + })(); + + const uncompiledRes = bundleToUncompiledLocale(maybeFallbackedBundle); + if (uncompiledRes.type === "err") { + return uncompiledRes; + } + + const uncompiled = uncompiledRes.ok; + + const localeRes = compileLocale({ config: localeConfig, uncompiled }); + + if (localeRes.type === "err") { + return localeRes; + } + + const locale = localeRes.ok; + return { type: "ok", ok: locale }; +} + +function unwrap(result: Result): T { + switch (result.type) { + case "ok": { + return result.ok; + } + case "err": { + throw new Error(String(result.err)); + } + } +} + +function deepReadonly(value: T): DeepReadonly { + // SAFETY: we're just making an immutable view to the type, this isn't dangerous + return value as DeepReadonly; +} + +const DEFAULT_LOCALE = unwrap(await loadLocale("en-US", enUsFtlSrc)); + +const doItAllForLocale = async ( + localeId: LocaleId, + localeFtlSrc: string, +): Promise> => + deepReadonly( + unwrap( + setupLocale( + unwrap(await loadLocale(localeId, localeFtlSrc)), + DEFAULT_LOCALE, + ), + ), + ); + +const [vpVl, wpVl, miVl] = await Promise.all([ + doItAllForLocale("vp-VL", vpVlFtlSrc), + doItAllForLocale("wp-VL", wpVlFtlSrc), + doItAllForLocale("mi-VL", miVlFtlSrc), +]); + +const localeIdToLocale = { + "en-US": deepReadonly(unwrap(setupLocale(DEFAULT_LOCALE, DEFAULT_LOCALE))), + "vp-VL": vpVl, + "wp-VL": wpVl, + "mi-VL": miVl, +} as const satisfies Record>; + +export interface UseLocaleOptions { + locale?: LocaleId; +} + +export const useLocale = (opt: UseLocaleOptions = {}) => + computed>(() => { + const localLocaleId = opt.locale ?? localeId.value; + return localeIdToLocale[localLocaleId]; + }); \ No newline at end of file diff --git a/apps/vdn-static/src/new-i18n/vilantic.ts b/apps/vdn-static/src/new-i18n/vilantic.ts new file mode 100644 index 0000000..0548f02 --- /dev/null +++ b/apps/vdn-static/src/new-i18n/vilantic.ts @@ -0,0 +1,11 @@ +import viossaFlag from "@/assets/flag_vp.webp"; +import wodoxFlag from "@/assets/flag_wp.webp"; +import minemiahaFlag from "@/assets/minemiahaFlag.webp"; + +export type VilanticId = "viossa" | "wodox" | "minemiaha"; + +export const VILANTIC_ID_TO_FLAG = { + viossa: viossaFlag, + wodox: wodoxFlag, + minemiaha: minemiahaFlag, +} as const satisfies Record; \ No newline at end of file diff --git a/apps/vdn-static/src/pages/discord/rules.vue b/apps/vdn-static/src/pages/discord/rules.vue new file mode 100644 index 0000000..903e63c --- /dev/null +++ b/apps/vdn-static/src/pages/discord/rules.vue @@ -0,0 +1,49 @@ + + + diff --git a/apps/vdn-static/src/pages/index.vue b/apps/vdn-static/src/pages/index.vue new file mode 100644 index 0000000..2c2a434 --- /dev/null +++ b/apps/vdn-static/src/pages/index.vue @@ -0,0 +1,81 @@ + + + diff --git a/apps/vdn-static/src/components/pages/KotobaPage.vue b/apps/vdn-static/src/pages/kotoba.vue similarity index 59% rename from apps/vdn-static/src/components/pages/KotobaPage.vue rename to apps/vdn-static/src/pages/kotoba.vue index 0961316..419af61 100644 --- a/apps/vdn-static/src/components/pages/KotobaPage.vue +++ b/apps/vdn-static/src/pages/kotoba.vue @@ -1,30 +1,25 @@ - diff --git a/apps/vdn-static/src/pages/resources.vue b/apps/vdn-static/src/pages/resources.vue new file mode 100644 index 0000000..6347f7b --- /dev/null +++ b/apps/vdn-static/src/pages/resources.vue @@ -0,0 +1,192 @@ + + + \ No newline at end of file diff --git a/apps/vdn-static/src/router.ts b/apps/vdn-static/src/router.ts new file mode 100644 index 0000000..5c15a90 --- /dev/null +++ b/apps/vdn-static/src/router.ts @@ -0,0 +1,12 @@ +import { createRouter, createWebHistory } from "vue-router"; +import { routes, handleHotUpdate } from "vue-router/auto-routes"; + +console.log(routes); + +const router = createRouter({ history: createWebHistory(), routes }); + +if (import.meta.hot) { + handleHotUpdate(router); +} + +export default router; diff --git a/apps/vdn-static/src/routes/index.ts b/apps/vdn-static/src/routes/index.ts deleted file mode 100644 index d98e3fc..0000000 --- a/apps/vdn-static/src/routes/index.ts +++ /dev/null @@ -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; diff --git a/apps/vdn-static/src/typed-router.d.ts b/apps/vdn-static/src/typed-router.d.ts index 507aa91..d6bc7b8 100644 --- a/apps/vdn-static/src/typed-router.d.ts +++ b/apps/vdn-static/src/typed-router.d.ts @@ -18,5 +18,9 @@ declare module 'vue-router/auto-routes' { * Route name map generated by unplugin-vue-router */ export interface RouteNamedMap { + '/': RouteRecordInfo<'/', '/', Record, Record>, + '/discord/rules': RouteRecordInfo<'/discord/rules', '/discord/rules', Record, Record>, + '/kotoba': RouteRecordInfo<'/kotoba', '/kotoba', Record, Record>, + '/resources': RouteRecordInfo<'/resources', '/resources', Record, Record>, } } diff --git a/apps/vdn-static/src/utils/css.ts b/apps/vdn-static/src/utils/css.ts new file mode 100644 index 0000000..fa1fd48 --- /dev/null +++ b/apps/vdn-static/src/utils/css.ts @@ -0,0 +1 @@ +export type CssClass = string | false | null | undefined | CssClass[]; diff --git a/apps/vdn-static/src/utils/ignore.ts b/apps/vdn-static/src/utils/ignore.ts new file mode 100644 index 0000000..dcea82a --- /dev/null +++ b/apps/vdn-static/src/utils/ignore.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters, @typescript-eslint/no-unused-vars +export function ignore(_: T): void {} diff --git a/apps/vdn-static/src/utils/smart-dest.ts b/apps/vdn-static/src/utils/smart-dest.ts new file mode 100644 index 0000000..500c9df --- /dev/null +++ b/apps/vdn-static/src/utils/smart-dest.ts @@ -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}`; diff --git a/apps/vdn-static/src/utils/types.ts b/apps/vdn-static/src/utils/types.ts new file mode 100644 index 0000000..540f315 --- /dev/null +++ b/apps/vdn-static/src/utils/types.ts @@ -0,0 +1,8 @@ +export type DeepPartial = + T extends Function ? T + : { [K in keyof T]?: T[K] extends object ? DeepPartial : T[K] }; + +export type Prettify = T extends object ? { [K in keyof T]: T[K] } & {} : T; +export type Value = T[keyof T]; + +export type Result = { type: "ok"; ok: T } | { type: "err"; err: E }; diff --git a/apps/vdn-static/src/utils/unsafe.ts b/apps/vdn-static/src/utils/unsafe.ts new file mode 100644 index 0000000..5d1badb --- /dev/null +++ b/apps/vdn-static/src/utils/unsafe.ts @@ -0,0 +1,11 @@ +import type { Result } from "./types"; + +export async function unsafeAsync( + f: () => Promise, +): Promise> { + try { + return { type: "ok", ok: await f() }; + } catch (e) { + return { type: "err", err: e }; + } +} diff --git a/apps/vdn-static/tsconfig.app.json b/apps/vdn-static/tsconfig.app.json index 6ed66d2..cdf1230 100644 --- a/apps/vdn-static/tsconfig.app.json +++ b/apps/vdn-static/tsconfig.app.json @@ -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" ] } diff --git a/apps/vdn-static/tsconfig.json b/apps/vdn-static/tsconfig.json index 1ffef60..ea9d0cd 100644 --- a/apps/vdn-static/tsconfig.json +++ b/apps/vdn-static/tsconfig.json @@ -1,7 +1,11 @@ { "files": [], "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } ] } diff --git a/apps/vdn-static/vite.config.ts b/apps/vdn-static/vite.config.ts index 4dcc263..2d72b3d 100644 --- a/apps/vdn-static/vite.config.ts +++ b/apps/vdn-static/vite.config.ts @@ -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"], }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b5f4828..6a31b66 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.4.2))': + dependencies: + eslint: 9.39.2(jiti@2.4.2) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.20.0': + '@eslint/config-array@0.21.1': dependencies: - '@eslint/object-schema': 2.1.6 - debug: 4.4.1 + '@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