feat: completed wodox locale; improved i18n logic

This commit is contained in:
Benjamin Singleton 2026-02-09 16:10:44 -06:00 committed by Sheldon Cooper
parent 9d5e67136e
commit 00651b8e36
11 changed files with 213 additions and 125 deletions

View file

@ -3,6 +3,7 @@ import "./assets/style.scss";
import { ref, type Ref } from "vue";
import LocalePicker from "./components/organisms/LocalePicker.vue";
import { vOnClickOutside } from "@vueuse/components";
import { useLocale } from "./i18n";
const burgerOpen: Ref<boolean> = ref<boolean>(false);
@ -13,6 +14,8 @@ const toggleBurger = (): void => {
const closeBurger = (): void => {
burgerOpen.value = false;
};
const locale = useLocale();
</script>
<template>
@ -45,18 +48,18 @@ const closeBurger = (): void => {
class="navbar-item"
to="/"
@click="closeBurger()"
>What is Viossa?</RouterLink
>{{ locale.navbar.whatIsViossa }}</RouterLink
>
<RouterLink
class="navbar-item"
to="/resources"
@click="closeBurger()"
>Resources</RouterLink
>{{ locale.navbar.resources }}</RouterLink
>
<RouterLink class="navbar-item" to="/kotoba">
Kotoba
{{ locale.navbar.kotoba }}
</RouterLink>
<LocalePicker class="navbar-item"/>
<LocalePicker class="navbar-item" />
</div>
</div>
</nav>

View file

@ -19,7 +19,7 @@ const locale = useLocale();
<section class="section container">
<HomeSectionWrapper
v-for="(section, index) in localizeLayout(locale.home)"
v-for="(section, index) in localizeLayout(locale.home.layout)"
:key="index"
:title="section.title"
:text="section.text"

View file

@ -1,27 +1,30 @@
<script setup lang="ts">
import { useLocale } from '@/i18n';
const locale = useLocale();
</script>
<template>
<div>
<section class="section">
<h1 class="title">Tropos-agnostic search</h1>
<h1 class="title">{{ locale.kotoba.title }}</h1>
</section>
<section class="section container">
<div class="notification is-info block">
<p>To searcn tropos-agnostically, enter a term below.</p>
<p>{{ locale.kotoba.searchHelp }}</p>
</div>
<div class="block is-flex is-flex-direction-row is-gap-2">
<input class="input" type="text"></input>
</div>
<div class="container">
</div>
</section>
</div>
</template>
<script setup lang="ts">
</script>

View file

@ -9,12 +9,14 @@ const locale = useLocale();
<template>
<div>
<section class="section">
<h1 class="title">Learning Resources</h1>
<h1 class="title">{{ locale.resources.title }}</h1>
</section>
<section class="section container">
<LearningResourceWrapper
v-for="(resource, index) in localizeLayout(locale.resources)"
v-for="(resource, index) in localizeLayout(
locale.resources.layout,
)"
:key="index"
:title="resource.title"
:subtitle="resource.subtitle"

View file

@ -1,22 +1,22 @@
import en_US from "../locales/en_US";
import vp_VL from "../locales/vp_VL";
import wp_VL from "../locales/wp_VL";
import { computed, readonly, ref } from "vue";
import { computed, readonly, ref, type DeepReadonly } from "vue";
import type { Locale } from "./locale";
import type { DeepPartial } from "@/utils/deep-partial";
export const LOCALE_IDS = ["en_US", "vp_VL", "wp_VL"] as const;
export type LocaleId = (typeof LOCALE_IDS)[number];
const locales = { en_US, vp_VL, wp_VL } as const satisfies Record<
LocaleId,
Locale
>;
const locales = { en_US, vp_VL, wp_VL } as const satisfies {
en_US: Locale;
} & Record<Exclude<LocaleId, "en_US">, DeepPartial<Locale>>;
export const localeId = ref<LocaleId>("en_US");
export function useLocale(opt: UseLocaleOptions = {}) {
const locale = computed<Locale>(() => {
return fallbackProxy(
const locale = computed<DeepReadonly<Locale>>(() => {
return fallbackProxy<Locale>(
locales[opt.locale ?? localeId.value],
locales["en_US"],
);
@ -29,32 +29,60 @@ export interface UseLocaleOptions {
locale?: LocaleId;
}
function fallbackProxy(obj: any, fallback: any) {
return new Proxy(obj, {
get: (target, key) => {
const value = target[key];
const fallbackValue = fallback[key];
if (value === undefined) {
return fallbackValue;
function isObject(value: unknown) {
return value !== null && typeof value === "object";
}
function fallbackProxy<T extends object>(
mask: DeepPartial<T>,
fallback: T,
): DeepReadonly<T> {
const proxy = new Proxy(fallback, {
get: (_target, key): DeepReadonly<T[keyof T]> => {
// SAFETY: typescript should ensure we're only ever trying to access keys
// that exist on T, and if the key doesn't, we'll be returning undefined anyway
// which is the expected behavior.
const tKey = key as keyof T;
// value may not exist on mask
const value: DeepPartial<T>[keyof T] | undefined = mask[tKey];
// all values exist on fallback
const fallbackValue: T[keyof T] = fallback[tKey];
// this is *not* T[keyof T], because if value is an object,
// it may still have missing properties.
// this only handles the case where the current value is undefined, not nested ones.
// thus, `finalValue` is still a `DeepPartial<T[keyof T]>` (but not undefined)
const finalValue: DeepPartial<T[keyof T]> =
value === undefined ? fallbackValue : value;
// check if finalValue or fallbackValue is not an object
// (we can't deep proxy unless both of them are objects)
if (!isObject(finalValue) || !isObject(fallbackValue)) {
// SAFETY: finalValue is not an object,
// so `DeepPartial<typeof finalValue>` == `typeof finalValue`
// (it is a primitive type)
// then we apply DeepReadonly to disallow mutations and to satisfy
// the return type of this getter function
return finalValue as DeepReadonly<T[keyof T]>;
}
if (value === null && fallbackValue !== null) {
return fallbackValue;
}
if (
value === null
|| fallbackValue === null
|| typeof value !== "object"
|| typeof fallbackValue !== "object"
) {
return value;
}
return fallbackProxy(value, fallbackValue);
// else, proxy the returned object to support deep fallback proxying
return fallbackProxy<T[keyof T] & object>(
// SAFETY: we validate that finalValue is an object above.
// I don't know why TypeScript isn't narrowing the type for us
// based on that predicate, but oh well
finalValue as DeepPartial<T[keyof T]> & object,
fallbackValue,
);
},
set: () => {
throw new Error("Cannot mutate locale at runtime");
},
});
// SAFETY: we're just disallowing mutations to the proxy,
// since its setter panics if used at runtime
return proxy as DeepReadonly<T>;
}

View file

@ -1,12 +1,24 @@
export interface Locale {
localeName: string;
home: Layout<HomeSections>;
resources: Layout<Resources>;
navbar: Navbar;
home: HomePage;
resources: ResourcesPage;
kotoba: KotobaPage;
}
export interface Layout<T> {
layout: (keyof T)[] | null;
data: { [K in keyof T]: T[K] | null };
order: (keyof T)[];
data: { [K in keyof T]: T[K] };
}
export interface Navbar {
whatIsViossa: string;
resources: string;
kotoba: string;
}
export interface HomePage {
layout: Layout<HomeSections>;
}
export interface HomeSections {
@ -22,6 +34,11 @@ export interface HomeSection {
alt: string | null;
}
export interface ResourcesPage {
title: string;
layout: Layout<Resources>;
}
export interface Resources {
discord: Resource;
}
@ -37,3 +54,8 @@ export interface Resource {
joinText: string;
rulesText: string;
}
export interface KotobaPage {
title: string;
searchHelp: string;
}

View file

@ -4,44 +4,58 @@ import discordImg from "@/assets/discord.png";
export default {
localeName: "English",
navbar: {
whatIsViossa: "What is Viossa?",
resources: "Resources",
kotoba: "Kotoba",
},
home: {
layout: ["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,
layout: {
order: ["whatIsViossa", "historyOfViossa", "community"],
data: {
whatIsViossa: {
title: "What is Viossa?",
text: "Viossa is a community-created artificial pidgin language, created to simulate the formation of natural pidgin languages. Viossa is characterized by its lack of standardization, with each speaker developing a personal idiolect. Spelling and pronunciation can vary greatly, and serve as a form of personal self-expression. Viossa is learnt and taught entirely by immersion — translation is prohibited while learning.",
image: flakkaImg,
alt: "Flag of the Viossa Language",
},
historyOfViossa: {
title: "History of Viossa",
text: "Viossa began as a Skype group in 2014, created by members of the r/conlangs community on Reddit, as an experiment to simulate the formation of a pidgin language. Pidgins are simplified languages resulting from contact between populations with no shared common language. Unlike most pidgins, which usually have two to three contributor languages, Viossa comes from many diverse languages. This is because people from all around the world helped to contribute to Viossa's vocabulary.",
image: flakkaImg,
alt: "Flag of the Viossa Language",
},
community: {
title: "Community",
text: "The Viossa community is rich and colourful, drawing from many global traditions due to its worldwide online membership. Since the teaching culture puts an emphasis on linguistic immersion, and discourages prescriptivism, the culture of Viossa is as diverse and varied as the language and the people who speak it. For many, their personal dialect is a key form of identity and expression. The fluid nature of Viossa and lack of defined meanings makes Viossa popular for creative purposes, such as poetry and songwriting.",
image: null,
alt: null,
},
},
},
},
resources: {
layout: ["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",
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",
},
},
},
},
kotoba: {
title: "Tropos-agnostic search",
searchHelp: "To searcn tropos-agnostically, enter a term below.",
},
} as const satisfies Locale;

View file

@ -1,20 +1,19 @@
import type { Locale } from "@/i18n/locale";
import flakkaImg from "@/assets/flakka.png";
import type { DeepPartial } from "@/utils/deep-partial";
export default {
localeName: "Viossa",
home: {
layout: null,
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",
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",
},
},
historyOfViossa: null,
community: null,
},
},
resources: { layout: null, data: { discord: null } },
} as const satisfies Locale;
} as const satisfies DeepPartial<Locale>;

View file

@ -1,46 +1,62 @@
import type { Locale } from "@/i18n/locale";
import flakkaImg from "@/assets/flakka.png";
import discordImg from "@/assets/discord.png";
import type { DeepPartial } from "@/utils/deep-partial";
export default {
localeName: "Wodox",
localeName: "wodox",
navbar: {
whatIsViossa: "viosox e ano?",
resources: "tropos",
kotoba: "mot o viosox",
},
home: {
layout: ["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,
layout: {
order: ["whatIsViossa", "historyOfViossa", "community"],
data: {
whatIsViossa: {
title: "viosox e ano?",
text: "viosox e hez ox pamzal, zoz stende zalkun tuo mit multa nengwi ox. zal o viosox stende lik zal o hez il ox keta, zalilkun wi tuo mit multa nengwi ox. mono i fal o viosox stendenai; omni axsi o viosox zal nengokun fal o viosox, de falmot wi falax o il stende e keko trenengwi tua o nengwi stende, ge fala e keko lik ro o tuo viosoxsi. genil viosox ibe il wi nengwi stende axkun ge pisakun po tuo ox — stende gen muskunnai mit zaiox.",
image: flakkaImg,
alt: "fomma o viosox",
},
historyOfViossa: {
title: "zal o viosox",
text: "wi o zal o viosox stende po multa o Skype wi 2014 ibe stendera o multa r/conlangs o Reddit. zalsi o viosox danzalgo hez, tuo zal o viosox e lik zal o hez il ox keta, zalil tuo ox mit nengwi multa ox ibe zalsi fiemnaikun sama i ox. viosox e nengwi tuo ox pamzal keta; zalilkun keko ox keta mit lik du wi tre ox, aga zalil viosox mit multa wi plus obo o ox na il ox keta ibe zalsi o viosox stende po multa mi o mo.",
image: flakkaImg,
alt: "fomma o viosox",
},
community: {
title: "viosoxsi",
text: "nengwi multa ro o viosoxsi stende ibe stendenura po nengwi multa mi o mo ge wekakunnura zai nengwi viosoxsi po jilobo. ibe mono i fal o viosox stendenai ge ibe viosoxsi zalkun nengwi multa fal o viosox, de nengwi zoz ko o ro lik ro o viosoxsi stende po ro o viosa. po multa hez viosoxsi, falmot wi falax o tuo stende stende po ro o tuo stende. ibe mono i fal o viosox stendenai ge ibe ro o mot inkun nengwi po nengwi viosoxsi, de multa stende amanata hez, zal zalgonukun surat au mola au sucik.",
image: null,
alt: null,
},
},
},
},
resources: {
layout: ["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",
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",
},
},
},
},
} as const satisfies Locale;
kotoba: {
title: "zalkuketutsa mot o viosox mit il o omni falmot",
searchHelp:
"ibe tastatukun il falmot o mot o viosox po pam, de zalkuketukun.",
},
} as const satisfies DeepPartial<Locale>;

View file

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

View file

@ -5,7 +5,7 @@ export function localizeLayout<T>(
layout: DeepReadonly<Layout<T>>,
): T[keyof T][] {
const sections: T[keyof T][] = [];
for (const sectionId of layout.layout ?? []) {
for (const sectionId of layout.order ?? []) {
const section = (layout.data as T)[sectionId as keyof T];
if (section) {
sections.push(section);