- {{
- locale.navbar.whatIsViossa
- }}
- {{
- locale.navbar.resources
- }}
-
- {{ locale.navbar.kotoba }}
+ {{ item.label }}
diff --git a/apps/vdn-static/src/components/molecules/LearningResourceWrapper.vue b/apps/vdn-static/src/components/molecules/LearningResourceWrapper.vue
index f97eb6b..4535d34 100644
--- a/apps/vdn-static/src/components/molecules/LearningResourceWrapper.vue
+++ b/apps/vdn-static/src/components/molecules/LearningResourceWrapper.vue
@@ -1,8 +1,47 @@
+
+
-
+
@@ -11,44 +50,18 @@
{{ desc }}
-
-
diff --git a/apps/vdn-static/src/components/organisms/SmartLink.vue b/apps/vdn-static/src/components/organisms/SmartLink.vue
index 11c7f3e..649f600 100644
--- a/apps/vdn-static/src/components/organisms/SmartLink.vue
+++ b/apps/vdn-static/src/components/organisms/SmartLink.vue
@@ -1,18 +1,43 @@
-
+
+
+
+
diff --git a/apps/vdn-static/src/i18n/index.ts b/apps/vdn-static/src/i18n/index.ts
index b1cf67f..4623c72 100644
--- a/apps/vdn-static/src/i18n/index.ts
+++ b/apps/vdn-static/src/i18n/index.ts
@@ -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
, DeepPartial>;
+ [DEFAULT_LOCALE_ID]: Locale;
+} & Record<
+ Exclude,
+ DeepPartial
+>;
// users could manually edit localStorage to make this value anything, so we need to validate it
const localStorageLocaleId = useLocalStorage(
@@ -68,37 +76,42 @@ function deepReadonly(value: T): DeepReadonly {
return value as DeepReadonly;
}
-function fallbackProxy(
- mask: DeepPartial,
- fallback: T,
-): DeepReadonly {
- const proxy = new Proxy(fallback, {
- get: (_target, key): DeepReadonly => {
+type DeepFallbackable = {
+ [K in keyof T]?: DeepFallbackable | Fallback;
+};
+
+function fallbackProxy(
+ maskObj: DeepFallbackable,
+ fallbackObj: Fallback,
+): DeepReadonly {
+ type Mask = typeof maskObj;
+ const proxy = new Proxy(fallbackObj, {
+ get: (_target, rawKey): DeepReadonly => {
// 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[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` (but not undefined)
- const finalValue: DeepPartial[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[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(
}
// else, proxy the returned object to support deep fallback proxying
- return fallbackProxy(
+ return fallbackProxy(
finalValue,
fallbackValue,
);
diff --git a/apps/vdn-static/src/i18n/locale.ts b/apps/vdn-static/src/i18n/locale.ts
index d928a85..ad2abcc 100644
--- a/apps/vdn-static/src/i18n/locale.ts
+++ b/apps/vdn-static/src/i18n/locale.ts
@@ -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 = Exclude<
+ { [K in keyof T]: DeepRemoveFallback },
+ Fallback
+>;
+
+export interface Locale extends DeepRemoveFallback {}
+
+export interface LocaleMask {
localeName: string;
vilanticLangs: VilanticLangs;
navbar: Navbar;
@@ -9,58 +22,55 @@ export interface Locale {
kotoba: KotobaPage;
}
-export interface Layout {
- order: (keyof T)[];
- data: { [K in keyof T]: T[K] };
-}
+export interface VilanticLangs extends Record {}
-export type VilanticLangs = Record;
-
-export interface Navbar {
- whatIsViossa: string;
- resources: string;
- kotoba: string;
-}
+export interface Navbar
+ extends Record<"whatIsViossa" | "resources" | "kotoba", string> {}
export interface HomePage {
- layout: Layout;
+ 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;
}
export interface Resources {
- discord: Resource;
+ discord: Resource<"join" | "rules">;
}
-export interface Resource {
+export interface Resource {
title: string;
subtitle: string;
desc: string;
- link: string;
- rulesLink: string;
- image: string;
- alt: string;
- joinText: string;
- rulesText: string;
+ image: Image | null;
+ buttons: Record;
}
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;
+}
diff --git a/apps/vdn-static/src/locales/en_US.ts b/apps/vdn-static/src/locales/en_US.ts
index 58be7cc..63343df 100644
--- a/apps/vdn-static/src/locales/en_US.ts
+++ b/apps/vdn-static/src/locales/en_US.ts
@@ -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" } },
},
},
},
diff --git a/apps/vdn-static/src/locales/vp_VL.ts b/apps/vdn-static/src/locales/vp_VL.ts
index 5ed146b..6d4aed6 100644
--- a/apps/vdn-static/src/locales/vp_VL.ts
+++ b/apps/vdn-static/src/locales/vp_VL.ts
@@ -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;
+} as const satisfies DeepPartial;
diff --git a/apps/vdn-static/src/locales/wp_VL.ts b/apps/vdn-static/src/locales/wp_VL.ts
index 800978b..0195bef 100644
--- a/apps/vdn-static/src/locales/wp_VL.ts
+++ b/apps/vdn-static/src/locales/wp_VL.ts
@@ -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;
+} as const satisfies DeepPartial;
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..e88b7ef
--- /dev/null
+++ b/apps/vdn-static/src/pages/discord/rules.vue
@@ -0,0 +1 @@
+Rules
diff --git a/apps/vdn-static/src/pages/index.vue b/apps/vdn-static/src/pages/index.vue
index 1db9218..c29bf56 100644
--- a/apps/vdn-static/src/pages/index.vue
+++ b/apps/vdn-static/src/pages/index.vue
@@ -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]),
+);
@@ -34,12 +45,12 @@ const greeting: Greeting = randomElement(GREETINGS);
diff --git a/apps/vdn-static/src/pages/resources.vue b/apps/vdn-static/src/pages/resources.vue
index df62e19..7afc11d 100644
--- a/apps/vdn-static/src/pages/resources.vue
+++ b/apps/vdn-static/src/pages/resources.vue
@@ -1,9 +1,51 @@