refactor: updated locale structure, moved layout info from locales to vue component files
This commit is contained in:
parent
60ce6d75f7
commit
de86791e2a
22 changed files with 354 additions and 233 deletions
|
|
@ -34,6 +34,12 @@ export default defineConfig([
|
|||
message: "Use SmartLink instead of RouterLink.",
|
||||
},
|
||||
],
|
||||
// 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" },
|
||||
],
|
||||
},
|
||||
},
|
||||
// disable multi-word-component-names for unplugin-vue-router
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import "./assets/style.scss";
|
||||
import { ref, type Ref } from "vue";
|
||||
import { computed, ref, type Ref } from "vue";
|
||||
import LocalePicker from "./components/organisms/LocalePicker.vue";
|
||||
import { vOnClickOutside } from "@vueuse/components";
|
||||
import { useLocale } from "./i18n";
|
||||
import { RouterLink, useRouter } from "vue-router";
|
||||
import SmartLink from "./components/organisms/SmartLink.vue";
|
||||
import type { SmartDest } from "./utils/smart-dest";
|
||||
import type { Locale } from "./i18n/locale";
|
||||
|
||||
const burgerOpen: Ref<boolean> = ref<boolean>(false);
|
||||
|
||||
|
|
@ -23,6 +25,39 @@ const router = useRouter();
|
|||
router.beforeEach(() => {
|
||||
closeBurger();
|
||||
});
|
||||
|
||||
interface NavbarItem {
|
||||
to: SmartDest;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const NAVBAR_ITEM_ORDER = [
|
||||
"whatIsViossa",
|
||||
"resources",
|
||||
"kotoba",
|
||||
] as const satisfies (keyof Locale["navbar"])[];
|
||||
|
||||
const navbarItems = computed(() =>
|
||||
NAVBAR_ITEM_ORDER.map((id): NavbarItem => {
|
||||
const label = locale.value.navbar[id];
|
||||
|
||||
const to = ((): SmartDest => {
|
||||
switch (id) {
|
||||
case "whatIsViossa": {
|
||||
return { type: "internal", internal: "/" };
|
||||
}
|
||||
case "resources": {
|
||||
return { type: "internal", internal: "/resources" };
|
||||
}
|
||||
case "kotoba": {
|
||||
return { type: "internal", internal: "/kotoba" };
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return { to, label };
|
||||
}),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -37,7 +72,7 @@ router.beforeEach(() => {
|
|||
><img src="@/assets/ViossaFlagRect.svg" alt=""
|
||||
/></RouterLink>
|
||||
|
||||
<div class="navbar-item">
|
||||
<div class="navbar-item is-hidden-desktop">
|
||||
<button
|
||||
type="button"
|
||||
@click="toggleBurger()"
|
||||
|
|
@ -51,14 +86,12 @@ router.beforeEach(() => {
|
|||
|
||||
<div :class="`navbar-menu ${burgerOpen ? 'is-active' : ''}`">
|
||||
<div class="navbar-start">
|
||||
<SmartLink class="navbar-item" internal to="/">{{
|
||||
locale.navbar.whatIsViossa
|
||||
}}</SmartLink>
|
||||
<SmartLink class="navbar-item" internal to="/resources">{{
|
||||
locale.navbar.resources
|
||||
}}</SmartLink>
|
||||
<SmartLink class="navbar-item" internal to="/kotoba">
|
||||
{{ locale.navbar.kotoba }}
|
||||
<SmartLink
|
||||
v-for="(item, index) in navbarItems"
|
||||
:key="index"
|
||||
class="navbar-item"
|
||||
:to="item.to"
|
||||
>{{ item.label }}
|
||||
</SmartLink>
|
||||
<LocalePicker class="navbar-item" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,47 @@
|
|||
<script setup lang="ts">
|
||||
import SmartLink, { type SmartLinkProps } from "../organisms/SmartLink.vue";
|
||||
import type { CssClass } from "@/utils/css";
|
||||
|
||||
export interface ResourceButton {
|
||||
label: string;
|
||||
link: SmartLinkProps;
|
||||
style: ResourceButtonStyle;
|
||||
}
|
||||
|
||||
export interface ResourceButtonStyle {
|
||||
color: "primary" | "warning";
|
||||
outlined?: boolean;
|
||||
}
|
||||
|
||||
function buttonStyleToClasses(style: ResourceButtonStyle): CssClass[] {
|
||||
const colorClass = (() => {
|
||||
switch (style.color) {
|
||||
case "primary": {
|
||||
return "is-primary";
|
||||
}
|
||||
case "warning": {
|
||||
return "is-warning";
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return [colorClass, style.outlined && "is-outlined"];
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
title: string;
|
||||
subtitle: string;
|
||||
desc: string;
|
||||
image?: { src: string; alt: string };
|
||||
buttons: ResourceButton[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="box columns is-vcentered is-gap-4">
|
||||
<div class="column is-one-quarter" v-if="image">
|
||||
<figure class="image">
|
||||
<img :src="image" :alt="alt" :title="alt" />
|
||||
<img :src="image.src" :alt="image.alt" :title="image.alt" />
|
||||
</figure>
|
||||
</div>
|
||||
<div class="column">
|
||||
|
|
@ -11,44 +50,18 @@
|
|||
<p class="content">{{ desc }}</p>
|
||||
|
||||
<div class="level">
|
||||
<a
|
||||
:href="link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
class="button is-primary is-medium"
|
||||
>{{ joinText }}</a
|
||||
>
|
||||
|
||||
<a
|
||||
:href="``"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
class="button is-info is-outlined is-medium"
|
||||
>{{ rulesText }}</a
|
||||
>
|
||||
|
||||
<a
|
||||
:href="rulesLink"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
class="button is-warning is-outlined is-medium"
|
||||
>{{ rulesText }}</a
|
||||
<SmartLink
|
||||
v-for="(button, index) in buttons"
|
||||
:key="index"
|
||||
v-bind="button.link"
|
||||
:class="[
|
||||
'button',
|
||||
'is-medium',
|
||||
buttonStyleToClasses(button.style),
|
||||
]"
|
||||
>{{ button.label }}</SmartLink
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
title: String,
|
||||
subtitle: String,
|
||||
desc: String,
|
||||
link: String,
|
||||
rulesLink: String,
|
||||
joinText: String,
|
||||
rulesText: String,
|
||||
image: { type: String, required: false },
|
||||
alt: { type: String, required: false },
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,43 @@
|
|||
<script setup lang="ts">
|
||||
import type { RouterLinkProps } from "vue-router";
|
||||
import type { RouteNamedMap } from "vue-router/auto-routes";
|
||||
import type { SmartDest, SmartInternalDest } from "../../utils/smart-dest"; // needs to be relative for vue sfc compiler
|
||||
|
||||
defineProps<
|
||||
Omit<RouterLinkProps, "to">
|
||||
& (
|
||||
| { internal: true; to: keyof RouteNamedMap }
|
||||
| { external: true; to: `https://${string}` | `http://${string}` }
|
||||
)
|
||||
>();
|
||||
export interface SmartLinkProps {
|
||||
to: SmartDest;
|
||||
newTab?: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<SmartLinkProps>();
|
||||
|
||||
type To =
|
||||
| { type: "a"; a: string }
|
||||
| { type: "routerLink"; routerLink: SmartInternalDest };
|
||||
|
||||
const to = ((): To => {
|
||||
const { to, newTab } = props;
|
||||
switch (to.type) {
|
||||
case "external": {
|
||||
return { type: "a", a: to.external };
|
||||
}
|
||||
case "internal": {
|
||||
if (newTab) {
|
||||
return { type: "a", a: to.internal };
|
||||
}
|
||||
|
||||
return { type: "routerLink", routerLink: to.internal };
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterLink v-bind="$props">
|
||||
<a
|
||||
v-if="to.type === 'a'"
|
||||
:href="to.a"
|
||||
:target="newTab ? '_blank' : undefined"
|
||||
rel="noopener noreferrer nofollow">
|
||||
<slot />
|
||||
</a>
|
||||
<RouterLink v-else :to="to.routerLink">
|
||||
<slot />
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,15 @@ 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 {
|
||||
fallback,
|
||||
type Fallback,
|
||||
type Locale,
|
||||
type LocaleMask,
|
||||
} from "./locale";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import { type } from "arktype";
|
||||
import type { DeepPartial } from "@/utils/types";
|
||||
|
||||
export const LOCALE_IDS = ["en_US", "vp_VL", "wp_VL"] as const;
|
||||
|
||||
|
|
@ -15,8 +20,11 @@ export const LocaleId = type.enumerated(...LOCALE_IDS);
|
|||
export const DEFAULT_LOCALE_ID = "en_US" satisfies LocaleId;
|
||||
|
||||
const locales = { en_US, vp_VL, wp_VL } as const satisfies {
|
||||
en_US: Locale;
|
||||
} & Record<Exclude<LocaleId, typeof DEFAULT_LOCALE_ID>, DeepPartial<Locale>>;
|
||||
[DEFAULT_LOCALE_ID]: Locale;
|
||||
} & Record<
|
||||
Exclude<LocaleId, typeof DEFAULT_LOCALE_ID>,
|
||||
DeepPartial<LocaleMask>
|
||||
>;
|
||||
|
||||
// users could manually edit localStorage to make this value anything, so we need to validate it
|
||||
const localStorageLocaleId = useLocalStorage<unknown>(
|
||||
|
|
@ -68,37 +76,42 @@ function deepReadonly<T>(value: T): DeepReadonly<T> {
|
|||
return value as DeepReadonly<T>;
|
||||
}
|
||||
|
||||
function fallbackProxy<T extends object>(
|
||||
mask: DeepPartial<T>,
|
||||
fallback: T,
|
||||
): DeepReadonly<T> {
|
||||
const proxy = new Proxy(fallback, {
|
||||
get: (_target, key): DeepReadonly<T[keyof T]> => {
|
||||
type DeepFallbackable<T> = {
|
||||
[K in keyof T]?: DeepFallbackable<T[K]> | Fallback;
|
||||
};
|
||||
|
||||
function fallbackProxy<Fallback extends object>(
|
||||
maskObj: DeepFallbackable<Fallback>,
|
||||
fallbackObj: Fallback,
|
||||
): DeepReadonly<Fallback> {
|
||||
type Mask = typeof maskObj;
|
||||
const proxy = new Proxy(fallbackObj, {
|
||||
get: (_target, rawKey): DeepReadonly<Fallback[keyof Fallback]> => {
|
||||
// SAFETY: typescript should ensure we're only ever trying to access keys
|
||||
// that exist on T, and if the key doesn't,
|
||||
// that exist on Fallback, and if the key doesn't,
|
||||
// just process its fallback as if it did,
|
||||
// everything should work as expected still
|
||||
const tKey = key as keyof T;
|
||||
const key = rawKey as keyof Fallback;
|
||||
|
||||
// value may not exist on mask
|
||||
const value: DeepPartial<T>[keyof T] | undefined = mask[tKey];
|
||||
const maskValue: Mask[keyof Fallback] | undefined = maskObj[key];
|
||||
|
||||
// all values exist on fallback
|
||||
const fallbackValue: T[keyof T] = fallback[tKey];
|
||||
const fallbackValue: Fallback[keyof Fallback] = fallbackObj[key];
|
||||
|
||||
// this is *not* T[keyof T], because if value is an object,
|
||||
// it may still have missing properties.
|
||||
// this only handles the case where the current value is undefined, not nested ones.
|
||||
// thus, `finalValue` is still a `DeepPartial<T[keyof T]>` (but not undefined)
|
||||
const finalValue: DeepPartial<T>[keyof T] =
|
||||
value === undefined ? fallbackValue : value;
|
||||
// thus, `finalValue` is still deeply partial (but not undefined or Fallback)
|
||||
const finalValue: Mask[keyof Fallback] =
|
||||
maskValue === undefined || maskValue === fallback ?
|
||||
fallbackValue
|
||||
: maskValue;
|
||||
|
||||
// check if finalValue is not an object
|
||||
// if not, it is a primitive
|
||||
if (!isObject(finalValue)) {
|
||||
// SAFETY: finalValue is not an object, so it is not affected by DeepPartial
|
||||
// so `DeepPartial<T>[keyof T]` is the same as `T[keyof T]`
|
||||
return deepReadonly(finalValue as T[keyof T]);
|
||||
// so `Mask[keyof Fallback]` is the same as `Fallback[keyof Fallback]`
|
||||
return deepReadonly(finalValue as Fallback[keyof Fallback]);
|
||||
}
|
||||
|
||||
// else, finalValue is an object, so we need to proxy it as well
|
||||
|
|
@ -111,7 +124,7 @@ function fallbackProxy<T extends object>(
|
|||
}
|
||||
|
||||
// else, proxy the returned object to support deep fallback proxying
|
||||
return fallbackProxy<T[keyof T] & object>(
|
||||
return fallbackProxy<Fallback[keyof Fallback] & object>(
|
||||
finalValue,
|
||||
fallbackValue,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,19 @@
|
|||
import type { VilanticId } from "./vilantic";
|
||||
|
||||
export interface Locale {
|
||||
// special symbol to explicitly specify when to fallback to default (en_US) translation
|
||||
// instead of translating a specific key, which still not being marked as a missing translation.
|
||||
// can only be used on keys which are typed as being able to use a fallback.
|
||||
export const fallback: unique symbol = Symbol("fallback");
|
||||
export type Fallback = typeof fallback;
|
||||
|
||||
type DeepRemoveFallback<T> = Exclude<
|
||||
{ [K in keyof T]: DeepRemoveFallback<T[K]> },
|
||||
Fallback
|
||||
>;
|
||||
|
||||
export interface Locale extends DeepRemoveFallback<LocaleMask> {}
|
||||
|
||||
export interface LocaleMask {
|
||||
localeName: string;
|
||||
vilanticLangs: VilanticLangs;
|
||||
navbar: Navbar;
|
||||
|
|
@ -9,58 +22,55 @@ export interface Locale {
|
|||
kotoba: KotobaPage;
|
||||
}
|
||||
|
||||
export interface Layout<T> {
|
||||
order: (keyof T)[];
|
||||
data: { [K in keyof T]: T[K] };
|
||||
}
|
||||
export interface VilanticLangs extends Record<VilanticId, string> {}
|
||||
|
||||
export type VilanticLangs = Record<VilanticId, string>;
|
||||
|
||||
export interface Navbar {
|
||||
whatIsViossa: string;
|
||||
resources: string;
|
||||
kotoba: string;
|
||||
}
|
||||
export interface Navbar
|
||||
extends Record<"whatIsViossa" | "resources" | "kotoba", string> {}
|
||||
|
||||
export interface HomePage {
|
||||
layout: Layout<HomeSections>;
|
||||
sections: HomeSections;
|
||||
}
|
||||
|
||||
export interface HomeSections {
|
||||
whatIsViossa: HomeSection;
|
||||
historyOfViossa: HomeSection;
|
||||
community: HomeSection;
|
||||
}
|
||||
export interface HomeSections
|
||||
extends Record<
|
||||
"whatIsViossa" | "historyOfViossa" | "community",
|
||||
HomeSection
|
||||
> {}
|
||||
|
||||
export interface HomeSection {
|
||||
title: string;
|
||||
text: string;
|
||||
image: string | null;
|
||||
alt: string | null;
|
||||
image: Image | null;
|
||||
}
|
||||
|
||||
export interface ResourcesPage {
|
||||
title: string;
|
||||
layout: Layout<Resources>;
|
||||
resources: Resources;
|
||||
}
|
||||
|
||||
export interface Resources {
|
||||
discord: Resource;
|
||||
discord: Resource<"join" | "rules">;
|
||||
}
|
||||
|
||||
export interface Resource {
|
||||
export interface Resource<ButtonKey extends string> {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
desc: string;
|
||||
link: string;
|
||||
rulesLink: string;
|
||||
image: string;
|
||||
alt: string;
|
||||
joinText: string;
|
||||
rulesText: string;
|
||||
image: Image | null;
|
||||
buttons: Record<ButtonKey, Button>;
|
||||
}
|
||||
|
||||
export interface KotobaPage {
|
||||
title: string;
|
||||
searchHelp: string;
|
||||
}
|
||||
|
||||
export interface Button {
|
||||
label: string;
|
||||
}
|
||||
|
||||
// coupled to require alt text for all images
|
||||
export interface Image {
|
||||
src: string | Fallback; // fallback can be used if image doesn't need to be translated
|
||||
alt: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,47 +11,34 @@ export default {
|
|||
kotoba: "Kotoba",
|
||||
},
|
||||
home: {
|
||||
layout: {
|
||||
order: ["whatIsViossa", "historyOfViossa", "community"],
|
||||
data: {
|
||||
whatIsViossa: {
|
||||
title: "What is Viossa?",
|
||||
text: "Viossa is a community-created artificial pidgin language, created to simulate the formation of natural pidgin languages. Viossa is characterized by its lack of standardization, with each speaker developing a personal idiolect. Spelling and pronunciation can vary greatly, and serve as a form of personal self-expression. Viossa is learnt and taught entirely by immersion — translation is prohibited while learning.",
|
||||
image: flakkaImg,
|
||||
alt: "Flag of the Viossa Language",
|
||||
},
|
||||
historyOfViossa: {
|
||||
title: "History of Viossa",
|
||||
text: "Viossa began as a Skype group in 2014, created by members of the r/conlangs community on Reddit, as an experiment to simulate the formation of a pidgin language. Pidgins are simplified languages resulting from contact between populations with no shared common language. Unlike most pidgins, which usually have two to three contributor languages, Viossa comes from many diverse languages. This is because people from all around the world helped to contribute to Viossa's vocabulary.",
|
||||
image: flakkaImg,
|
||||
alt: "Flag of the Viossa Language",
|
||||
},
|
||||
community: {
|
||||
title: "Community",
|
||||
text: "The Viossa community is rich and colourful, drawing from many global traditions due to its worldwide online membership. Since the teaching culture puts an emphasis on linguistic immersion, and discourages prescriptivism, the culture of Viossa is as diverse and varied as the language and the people who speak it. For many, their personal dialect is a key form of identity and expression. The fluid nature of Viossa and lack of defined meanings makes Viossa popular for creative purposes, such as poetry and songwriting.",
|
||||
image: null,
|
||||
alt: null,
|
||||
},
|
||||
sections: {
|
||||
whatIsViossa: {
|
||||
title: "What is Viossa?",
|
||||
text: "Viossa is a community-created artificial pidgin language, created to simulate the formation of natural pidgin languages. Viossa is characterized by its lack of standardization, with each speaker developing a personal idiolect. Spelling and pronunciation can vary greatly, and serve as a form of personal self-expression. Viossa is learnt and taught entirely by immersion — translation is prohibited while learning.",
|
||||
image: { src: flakkaImg, alt: "Flag of the Viossa Language" },
|
||||
},
|
||||
historyOfViossa: {
|
||||
title: "History of Viossa",
|
||||
text: "Viossa began as a Skype group in 2014, created by members of the r/conlangs community on Reddit, as an experiment to simulate the formation of a pidgin language. Pidgins are simplified languages resulting from contact between populations with no shared common language. Unlike most pidgins, which usually have two to three contributor languages, Viossa comes from many diverse languages. This is because people from all around the world helped to contribute to Viossa's vocabulary.",
|
||||
image: { src: flakkaImg, alt: "Flag of the Viossa Language" },
|
||||
},
|
||||
community: {
|
||||
title: "Community",
|
||||
text: "The Viossa community is rich and colourful, drawing from many global traditions due to its worldwide online membership. Since the teaching culture puts an emphasis on linguistic immersion, and discourages prescriptivism, the culture of Viossa is as diverse and varied as the language and the people who speak it. For many, their personal dialect is a key form of identity and expression. The fluid nature of Viossa and lack of defined meanings makes Viossa popular for creative purposes, such as poetry and songwriting.",
|
||||
image: { src: flakkaImg, alt: "Flag of the Viossa Language" },
|
||||
},
|
||||
},
|
||||
},
|
||||
resources: {
|
||||
title: "Learning Resources",
|
||||
layout: {
|
||||
order: ["discord"],
|
||||
data: {
|
||||
discord: {
|
||||
title: "Discord Server",
|
||||
subtitle:
|
||||
"This is where most of the action happens! Hop on in!",
|
||||
desc: "Originally started in 2015 something something read the rules here, then click the link below to join!",
|
||||
link: "https://discord.gg/g3mG2gYjZD",
|
||||
rulesLink: "https://viossadiskordserver.github.io/rules",
|
||||
image: discordImg,
|
||||
alt: "Discord logo",
|
||||
joinText: "Join",
|
||||
rulesText: "Rules",
|
||||
},
|
||||
resources: {
|
||||
discord: {
|
||||
title: "Discord Server",
|
||||
subtitle:
|
||||
"This is where most of the action happens! Hop on in!",
|
||||
desc: "Originally started in 2015 something something read the rules here, then click the link below to join!",
|
||||
image: { src: discordImg, alt: "Discord logo" },
|
||||
buttons: { join: { label: "Join" }, rules: { label: "Rules" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,19 +1,14 @@
|
|||
import type { Locale } from "@/i18n/locale";
|
||||
import flakkaImg from "@/assets/flakka.png";
|
||||
import type { DeepPartial } from "@/utils/deep-partial";
|
||||
import { type LocaleMask } from "@/i18n/locale";
|
||||
import type { DeepPartial } from "@/utils/types";
|
||||
|
||||
export default {
|
||||
localeName: "Viossa",
|
||||
home: {
|
||||
layout: {
|
||||
data: {
|
||||
whatIsViossa: {
|
||||
title: "Kafaen afto Viossa",
|
||||
text: "Viossa tte glossa mahena grun vi nai vil fshtojena na bakadjin, grun vi svinnur ja! De aldjin zovti lera ne",
|
||||
image: flakkaImg,
|
||||
alt: "Flag of the Viossa Language",
|
||||
},
|
||||
sections: {
|
||||
whatIsViossa: {
|
||||
title: "Kafaen afto Viossa",
|
||||
text: "Viossa tte glossa mahena grun vi nai vil fshtojena na bakadjin, grun vi svinnur ja! De aldjin zovti lera ne",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const satisfies DeepPartial<Locale>;
|
||||
} as const satisfies DeepPartial<LocaleMask>;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import type { Locale } from "@/i18n/locale";
|
||||
import flakkaImg from "@/assets/flakka.png";
|
||||
import discordImg from "@/assets/discord.png";
|
||||
import type { DeepPartial } from "@/utils/deep-partial";
|
||||
import { fallback, type LocaleMask } from "@/i18n/locale";
|
||||
import type { DeepPartial } from "@/utils/types";
|
||||
|
||||
export default {
|
||||
localeName: "wodox",
|
||||
|
|
@ -12,45 +10,35 @@ export default {
|
|||
kotoba: "mot o viosox",
|
||||
},
|
||||
home: {
|
||||
layout: {
|
||||
order: ["whatIsViossa", "historyOfViossa", "community"],
|
||||
data: {
|
||||
whatIsViossa: {
|
||||
title: "viosox e ano?",
|
||||
text: "viosox e hez ox pamzal, zoz stende zalkun tuo mit multa nengwi ox. zal o viosox stende lik zal o hez il ox keta, zalilkun wi tuo mit multa nengwi ox. mono i fal o viosox stendenai; omni axsi o viosox zal nengokun fal o viosox, de falmot wi falax o il stende e keko trenengwi tua o nengwi stende, ge fala e keko lik ro o tuo viosoxsi. genil viosox ibe il wi nengwi stende axkun ge pisakun po tuo ox — stende gen muskunnai mit zaiox.",
|
||||
image: flakkaImg,
|
||||
alt: "fomma o viosox",
|
||||
},
|
||||
historyOfViossa: {
|
||||
title: "zal o viosox",
|
||||
text: "wi o zal o viosox stende po multa o Skype wi 2014 ibe stendera o multa r/conlangs o Reddit. zalsi o viosox danzalgo hez, tuo zal o viosox e lik zal o hez il ox keta, zalil tuo ox mit nengwi multa ox ibe zalsi fiemnaikun sama i ox. viosox e nengwi tuo ox pamzal keta; zalilkun keko ox keta mit lik du wi tre ox, aga zalil viosox mit multa wi plus obo o ox na il ox keta ibe zalsi o viosox stende po multa mi o mo.",
|
||||
image: flakkaImg,
|
||||
alt: "fomma o viosox",
|
||||
},
|
||||
community: {
|
||||
title: "viosoxsi",
|
||||
text: "nengwi multa ro o viosoxsi stende ibe stendenura po nengwi multa mi o mo ge wekakunnura zai nengwi viosoxsi po jilobo. ibe mono i fal o viosox stendenai ge ibe viosoxsi zalkun nengwi multa fal o viosox, de nengwi zoz ko o ro lik ro o viosoxsi stende po ro o viosa. po multa hez viosoxsi, falmot wi falax o tuo stende stende po ro o tuo stende. ibe mono i fal o viosox stendenai ge ibe ro o mot inkun nengwi po nengwi viosoxsi, de multa stende amanata hez, zal zalgonukun surat au mola au sucik.",
|
||||
image: null,
|
||||
alt: null,
|
||||
},
|
||||
sections: {
|
||||
whatIsViossa: {
|
||||
title: "viosox e ano?",
|
||||
text: "viosox e hez ox pamzal, zoz stende zalkun tuo mit multa nengwi ox. zal o viosox stende lik zal o hez il ox keta, zalilkun wi tuo mit multa nengwi ox. mono i fal o viosox stendenai; omni axsi o viosox zal nengokun fal o viosox, de falmot wi falax o il stende e keko trenengwi tua o nengwi stende, ge fala e keko lik ro o tuo viosoxsi. genil viosox ibe il wi nengwi stende axkun ge pisakun po tuo ox — stende gen muskunnai mit zaiox.",
|
||||
image: { src: fallback, alt: "fomma o viosox" },
|
||||
},
|
||||
historyOfViossa: {
|
||||
title: "zal o viosox",
|
||||
text: "wi o zal o viosox stende po multa o Skype wi 2014 ibe stendera o multa r/conlangs o Reddit. zalsi o viosox danzalgo hez, tuo zal o viosox e lik zal o hez il ox keta, zalil tuo ox mit nengwi multa ox ibe zalsi fiemnaikun sama i ox. viosox e nengwi tuo ox pamzal keta; zalilkun keko ox keta mit lik du wi tre ox, aga zalil viosox mit multa wi plus obo o ox na il ox keta ibe zalsi o viosox stende po multa mi o mo.",
|
||||
image: { src: fallback, alt: "fomma o viosox" },
|
||||
},
|
||||
community: {
|
||||
title: "viosoxsi",
|
||||
text: "nengwi multa ro o viosoxsi stende ibe stendenura po nengwi multa mi o mo ge wekakunnura zai nengwi viosoxsi po jilobo. ibe mono i fal o viosox stendenai ge ibe viosoxsi zalkun nengwi multa fal o viosox, de nengwi zoz ko o ro lik ro o viosoxsi stende po ro o viosa. po multa hez viosoxsi, falmot wi falax o tuo stende stende po ro o tuo stende. ibe mono i fal o viosox stendenai ge ibe ro o mot inkun nengwi po nengwi viosoxsi, de multa stende amanata hez, zal zalgonukun surat au mola au sucik.",
|
||||
image: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
resources: {
|
||||
title: "tropos o gen",
|
||||
layout: {
|
||||
order: ["discord"],
|
||||
data: {
|
||||
discord: {
|
||||
title: "server o Diskord",
|
||||
subtitle: "axilkun ge genilkun po ce! wekatutsa!",
|
||||
desc: "danzalil hez server po 2015. ibe dutukun musra po ce, de ibe wiftutsakun dof po pam, de wekatukun po server!",
|
||||
link: "https://discord.gg/g3mG2gYjZD",
|
||||
rulesLink: "https://viossadiskordserver.github.io/rules",
|
||||
image: discordImg,
|
||||
alt: "surat o Diskord",
|
||||
joinText: "wekatutsa",
|
||||
rulesText: "musra",
|
||||
resources: {
|
||||
discord: {
|
||||
title: "server o Diskord",
|
||||
subtitle: "axilkun ge genilkun po ce! wekatutsa!",
|
||||
desc: "danzalil hez server po 2015. ibe dutukun musra po ce, de ibe wiftutsakun dof po pam, de wekatukun po server!",
|
||||
image: { src: fallback, alt: "surat o Diskord" },
|
||||
buttons: {
|
||||
join: { label: "wekatutsa" },
|
||||
rules: { label: "musra" },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -60,4 +48,4 @@ export default {
|
|||
searchHelp:
|
||||
"ibe tastatukun il falmot o mot o viosox po pam, de zalkuketukun.",
|
||||
},
|
||||
} as const satisfies DeepPartial<Locale>;
|
||||
} as const satisfies DeepPartial<LocaleMask>;
|
||||
|
|
|
|||
1
apps/vdn-static/src/pages/discord/rules.vue
Normal file
1
apps/vdn-static/src/pages/discord/rules.vue
Normal file
|
|
@ -0,0 +1 @@
|
|||
<template>Rules</template>
|
||||
|
|
@ -2,12 +2,23 @@
|
|||
import HomeSectionWrapper from "@/components/molecules/HomeSectionWrapper.vue";
|
||||
import { useLocale } from "@/i18n";
|
||||
import { GREETINGS, type Greeting } from "@/i18n/greeting";
|
||||
import type { Locale } from "@/i18n/locale";
|
||||
import { VILANTIC_ID_TO_FLAG } from "@/i18n/vilantic";
|
||||
import { localizeLayout } from "@/utils/localizeLayout";
|
||||
import { randomElement } from "@/utils/random";
|
||||
import { computed } from "vue";
|
||||
|
||||
const locale = useLocale();
|
||||
const greeting: Greeting = randomElement(GREETINGS);
|
||||
|
||||
const SECTION_ORDER = [
|
||||
"whatIsViossa",
|
||||
"historyOfViossa",
|
||||
"community",
|
||||
] as const satisfies (keyof Locale["home"]["sections"])[];
|
||||
|
||||
const sections = computed(() =>
|
||||
SECTION_ORDER.map((id) => locale.value.home.sections[id]),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -34,12 +45,12 @@ const greeting: Greeting = randomElement(GREETINGS);
|
|||
|
||||
<section class="section container">
|
||||
<HomeSectionWrapper
|
||||
v-for="(section, index) in localizeLayout(locale.home.layout)"
|
||||
v-for="(section, index) in sections"
|
||||
:key="index"
|
||||
:title="section.title"
|
||||
:text="section.text"
|
||||
:image="section.image ?? undefined"
|
||||
:alt="section.alt ?? undefined"
|
||||
:image="section.image?.src"
|
||||
:alt="section.image?.alt"
|
||||
:reverse="index % 2 !== 0" />
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,51 @@
|
|||
<script setup lang="ts">
|
||||
import LearningResourceWrapper from "@/components/molecules/LearningResourceWrapper.vue";
|
||||
import LearningResourceWrapper, {
|
||||
type ResourceButton,
|
||||
} from "@/components/molecules/LearningResourceWrapper.vue";
|
||||
import { useLocale } from "@/i18n";
|
||||
import { localizeLayout } from "@/utils/localizeLayout";
|
||||
import type { Locale } from "@/i18n/locale";
|
||||
import { ignore } from "@/utils/ignore";
|
||||
import { computed } from "vue";
|
||||
|
||||
const locale = useLocale();
|
||||
|
||||
const resourceIdToResource = computed(() => locale.value.resources.resources);
|
||||
|
||||
const RESOURCE_ORDER = [
|
||||
"discord",
|
||||
] as const satisfies (keyof Locale["resources"]["resources"])[];
|
||||
|
||||
const resources = computed(() =>
|
||||
RESOURCE_ORDER.map((id) => [id, resourceIdToResource.value[id]] as const),
|
||||
);
|
||||
|
||||
const computeButtons = (
|
||||
id: keyof Locale["resources"]["resources"],
|
||||
): ResourceButton[] => {
|
||||
// will warn us if a new variant is added that isn't handled, and so we should add a switch
|
||||
// once we have a switch statement, this won't be needed as that will check for exhaustiveness
|
||||
ignore<"discord">(id);
|
||||
|
||||
const buttons = resourceIdToResource.value[id].buttons;
|
||||
return [
|
||||
{
|
||||
link: {
|
||||
to: {
|
||||
type: "external",
|
||||
external: "https://discord.viossa.net",
|
||||
},
|
||||
newTab: true,
|
||||
},
|
||||
label: buttons.join.label,
|
||||
style: { color: "primary" },
|
||||
},
|
||||
{
|
||||
link: { to: { type: "internal", internal: "/discord/rules" } },
|
||||
label: buttons.rules.label,
|
||||
style: { color: "warning", outlined: true },
|
||||
},
|
||||
];
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -14,19 +56,13 @@ const locale = useLocale();
|
|||
|
||||
<section class="section container">
|
||||
<LearningResourceWrapper
|
||||
v-for="(resource, index) in localizeLayout(
|
||||
locale.resources.layout,
|
||||
)"
|
||||
v-for="([resourceId, resource], index) in resources"
|
||||
:key="index"
|
||||
:title="resource.title"
|
||||
:subtitle="resource.subtitle"
|
||||
:desc="resource.desc"
|
||||
:link="resource.link"
|
||||
:rulesLink="resource.rulesLink"
|
||||
:image="resource.image"
|
||||
:alt="resource.alt"
|
||||
:joinText="resource.joinText"
|
||||
:rulesText="resource.rulesText" />
|
||||
:image="resource.image ?? undefined"
|
||||
:buttons="computeButtons(resourceId)" />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
1
apps/vdn-static/src/typed-router.d.ts
vendored
1
apps/vdn-static/src/typed-router.d.ts
vendored
|
|
@ -19,6 +19,7 @@ declare module 'vue-router/auto-routes' {
|
|||
*/
|
||||
export interface RouteNamedMap {
|
||||
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
|
||||
'/discord/rules': RouteRecordInfo<'/discord/rules', '/discord/rules', Record<never, never>, Record<never, never>>,
|
||||
'/kotoba': RouteRecordInfo<'/kotoba', '/kotoba', Record<never, never>, Record<never, never>>,
|
||||
'/resources': RouteRecordInfo<'/resources', '/resources', Record<never, never>, Record<never, never>>,
|
||||
}
|
||||
|
|
|
|||
1
apps/vdn-static/src/utils/css.ts
Normal file
1
apps/vdn-static/src/utils/css.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export type CssClass = string | false | null | undefined;
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export type DeepPartial<T extends object> = {
|
||||
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
|
||||
};
|
||||
2
apps/vdn-static/src/utils/ignore.ts
Normal file
2
apps/vdn-static/src/utils/ignore.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters, @typescript-eslint/no-unused-vars
|
||||
export function ignore<T>(_: T): void {}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import type { Layout } from "@/i18n/locale";
|
||||
import type { DeepReadonly } from "vue";
|
||||
|
||||
export function localizeLayout<T>(
|
||||
layout: DeepReadonly<Layout<T>>,
|
||||
): T[keyof T][] {
|
||||
const sections: T[keyof T][] = [];
|
||||
for (const sectionId of layout.order ?? []) {
|
||||
const section = (layout.data as T)[sectionId as keyof T];
|
||||
if (section) {
|
||||
sections.push(section);
|
||||
}
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
8
apps/vdn-static/src/utils/smart-dest.ts
Normal file
8
apps/vdn-static/src/utils/smart-dest.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import type { RouteNamedMap } from "vue-router/auto-routes";
|
||||
|
||||
export type SmartDest =
|
||||
| { type: "internal"; internal: SmartInternalDest }
|
||||
| { type: "external"; external: SmartExternalDest };
|
||||
|
||||
export type SmartInternalDest = keyof RouteNamedMap;
|
||||
export type SmartExternalDest = `https://${string}` | `http://${string}`;
|
||||
6
apps/vdn-static/src/utils/types.ts
Normal file
6
apps/vdn-static/src/utils/types.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export type DeepPartial<T extends object> = {
|
||||
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
|
||||
};
|
||||
|
||||
export type Prettify<T> = T extends object ? { [K in keyof T]: T[K] } & {} : T;
|
||||
export type Value<T> = T[keyof T];
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
|
|
@ -73,7 +73,7 @@ importers:
|
|||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue