feat(i18n): i18nize everything

This commit is contained in:
2026-01-30 22:58:07 +01:00
parent 13e4ae64b5
commit 9deeb42cfd
16 changed files with 142 additions and 78 deletions

View File

@@ -53,8 +53,8 @@ onUnmounted(() => {
<Buttons <Buttons
v-if="confirmationModal.onConfirm" v-if="confirmationModal.onConfirm"
:y-offset="confirmationModal.modalButtonsYOffset" :y-offset="confirmationModal.modalButtonsYOffset"
b-label="Cancel" :b-label="$t('common.cancel')"
a-label="Confirm" :a-label="$t('common.confirm')"
@activate-a="handleActivateA" @activate-a="handleActivateA"
@activate-b="handleActivateB" @activate-b="handleActivateB"
/> />

View File

@@ -10,13 +10,17 @@ const achievements = useAchievementsStore();
const confirmationModal = useConfirmationModal(); const confirmationModal = useConfirmationModal();
const ACTIONS = { const ACTIONS = {
github: ["Open", "Github profile", "https://github.com/pihkaal"], github: [
email: ["Copy", "Email", "hello@pihkaal.me"], "open",
website: ["Copy", "Website link", "https://pihkaal.me"], "contact.actions.githubProfile",
cv: ["Open", "CV", "https://pihkaal.me/cv"], "https://github.com/pihkaal",
],
email: ["copy", "contact.actions.email", "hello@pihkaal.me"],
website: ["copy", "contact.actions.websiteLink", "https://pihkaal.me"],
cv: ["open", "contact.actions.cv", "https://pihkaal.me/cv"],
} as const satisfies Record< } as const satisfies Record<
string, string,
[action: "Copy" | "Open", verb: string, content: string] [action: "copy" | "open", verbKey: string, content: string]
>; >;
const { selected, selectorPosition } = useButtonNavigation({ const { selected, selectorPosition } = useButtonNavigation({
@@ -54,20 +58,21 @@ const { selected, selectorPosition } = useButtonNavigation({
}); });
const actionateButton = async (button: (typeof selected)["value"]) => { const actionateButton = async (button: (typeof selected)["value"]) => {
const [action, verb, content] = ACTIONS[button]; const [action, verbKey, content] = ACTIONS[button];
if (action === "Copy") { const verb = $t(verbKey);
if (action === "copy") {
try { try {
await navigator.clipboard.writeText(content); await navigator.clipboard.writeText(content);
store.pushNotification(`${verb} copied to clipboard`); store.pushNotification($t("contact.copiedToClipboard", { item: verb }));
} catch (error) { } catch (error) {
console.error("Failed to copy to clipboard:", error); console.error("Failed to copy to clipboard:", error);
} }
} else { } else {
const url = content.replace(/^https?:\/\//, ""); const url = content.replace(/^https?:\/\//, "");
confirmationModal.open({ confirmationModal.open({
text: `Open ${url}?`, text: $t("contact.openUrl", { url }),
onConfirm: async () => { onConfirm: async () => {
store.pushNotification(`${verb} opened`); store.pushNotification($t("contact.opened", { item: verb }));
await sleep(100); await sleep(100);
await navigateTo(content, { open: { target: "_blank " } }); await navigateTo(content, { open: { target: "_blank " } });
@@ -94,7 +99,7 @@ const actionateButton = async (button: (typeof selected)["value"]) => {
/> />
<CommonBars <CommonBars
title="Choose a Chat Room to join." :title="$t('contact.title')"
:opacity=" :opacity="
store.isIntro ? store.intro.stage3Opacity : store.outro.stage2Opacity store.isIntro ? store.intro.stage3Opacity : store.outro.stage2Opacity
" "
@@ -112,8 +117,8 @@ const actionateButton = async (button: (typeof selected)["value"]) => {
? store.outro.stage2Opacity ? store.outro.stage2Opacity
: 1 : 1
" "
b-label="Quit" :b-label="$t('common.quit')"
:a-label="ACTIONS[selected][0]" :a-label="$t(`contact.actions.${ACTIONS[selected][0]}`)"
@activate-b="store.animateOutro()" @activate-b="store.animateOutro()"
/> />
</template> </template>

View File

@@ -71,7 +71,11 @@ const renderFrame = (timestamp: number) => {
for (const callback of sortedCallbacks) { for (const callback of sortedCallbacks) {
ctx.save(); ctx.save();
callback(ctx, deltaTime, lastRealFrameTime); try {
callback(ctx, deltaTime, lastRealFrameTime);
} catch (error: unknown) {
console.error(error);
}
ctx.restore(); ctx.restore();
} }

View File

@@ -87,7 +87,8 @@ onRender((ctx) => {
ctx.font = "10px NDS10"; ctx.font = "10px NDS10";
ctx.textBaseline = "top"; ctx.textBaseline = "top";
ctx.fillStyle = "#010101"; ctx.fillStyle = "#010101";
const { actualBoundingBoxRight: textWidth } = ctx.measureText("View All"); const viewAllText = $t("settings.clock.achievements.viewAll");
const { actualBoundingBoxRight: textWidth } = ctx.measureText(viewAllText);
const totalWidth = achievementAssets.X.rect.width + GAP + textWidth; const totalWidth = achievementAssets.X.rect.width + GAP + textWidth;
const left = Math.ceil( const left = Math.ceil(
@@ -99,7 +100,7 @@ onRender((ctx) => {
achievementAssets.X.draw(ctx, left, 7); achievementAssets.X.draw(ctx, left, 7);
fillTextHCentered( fillTextHCentered(
ctx, ctx,
"View All", viewAllText,
left + achievementAssets.X.rect.width + GAP, left + achievementAssets.X.rect.width + GAP,
7, 7,
textWidth, textWidth,
@@ -110,7 +111,7 @@ onRender((ctx) => {
<template> <template>
<NumberInput <NumberInput
:model-value="achievements.achievements.length" :model-value="achievements.achievements.length"
title="Obtained" :title="$t('settings.clock.achievements.obtained')"
:x="4 * 16 - 1" :x="4 * 16 - 1"
:selected="achievements.allObtained" :selected="achievements.allObtained"
:disabled="true" :disabled="true"
@@ -118,7 +119,7 @@ onRender((ctx) => {
<NumberInput <NumberInput
:model-value="ACHIEVEMENTS.length" :model-value="ACHIEVEMENTS.length"
title="Total" :title="$t('settings.clock.achievements.total')"
:x="9 * 16 - 1" :x="9 * 16 - 1"
:selected="achievements.allObtained" :selected="achievements.allObtained"
:disabled="true" :disabled="true"
@@ -126,8 +127,8 @@ onRender((ctx) => {
<CommonButtons <CommonButtons
:y-offset="confirmationModal.buttonsYOffset" :y-offset="confirmationModal.buttonsYOffset"
b-label="Cancel" :b-label="$t('common.cancel')"
a-label="Reset" :a-label="$t('common.reset')"
@activate-b="handleCancel()" @activate-b="handleCancel()"
@activate-a="handleReset()" @activate-a="handleReset()"
/> />

View File

@@ -30,19 +30,19 @@ defineOptions({ render: () => null });
<template> <template>
<NumberInput <NumberInput
:model-value="now.getMonth() + 1" :model-value="now.getMonth() + 1"
title="Month" :title="$t('settings.clock.date.month')"
:x="1 * 16 - 1" :x="1 * 16 - 1"
:disabled="true" :disabled="true"
/> />
<NumberInput <NumberInput
:model-value="now.getDate()" :model-value="now.getDate()"
title="Day" :title="$t('settings.clock.date.day')"
:x="5 * 16 - 1" :x="5 * 16 - 1"
:disabled="true" :disabled="true"
/> />
<NumberInput <NumberInput
:model-value="now.getFullYear()" :model-value="now.getFullYear()"
title="Year" :title="$t('settings.clock.date.year')"
:digits="4" :digits="4"
:x="9 * 16 - 1" :x="9 * 16 - 1"
:disabled="true" :disabled="true"
@@ -50,8 +50,8 @@ defineOptions({ render: () => null });
<CommonButtons <CommonButtons
:y-offset="0" :y-offset="0"
b-label="Cancel" :b-label="$t('common.cancel')"
a-label="Confirm" :a-label="$t('common.confirm')"
@activate-b="handleCancel" @activate-b="handleCancel"
@activate-a="handleConfirm" @activate-a="handleConfirm"
/> />

View File

@@ -32,21 +32,21 @@ defineOptions({ render: () => null });
<template> <template>
<NumberInput <NumberInput
:model-value="now.getHours()" :model-value="now.getHours()"
title="Hour" :title="$t('settings.clock.time.hour')"
:x="4 * 16 - 1" :x="4 * 16 - 1"
:disabled="true" :disabled="true"
/> />
<NumberInput <NumberInput
:model-value="now.getMinutes()" :model-value="now.getMinutes()"
title="Minute" :title="$t('settings.clock.time.minute')"
:x="9 * 16 - 1" :x="9 * 16 - 1"
:disabled="true" :disabled="true"
/> />
<CommonButtons <CommonButtons
:y-offset="0" :y-offset="0"
b-label="Cancel" :b-label="$t('common.cancel')"
a-label="Confirm" :a-label="$t('common.confirm')"
@activate-b="handleCancel" @activate-b="handleCancel"
@activate-a="handleConfirm" @activate-a="handleConfirm"
/> />

View File

@@ -198,15 +198,15 @@ const viewComponents: Record<string, Component> = {
<CommonButtons <CommonButtons
v-if="isSubmenuSelected" v-if="isSubmenuSelected"
:y-offset="0" :y-offset="0"
b-label="Go back" :b-label="$t('common.goBack')"
a-label="Select" :a-label="$t('common.select')"
@activate-b="select(getParentMenu(selected))" @activate-b="select(getParentMenu(selected))"
/> />
<CommonButtons <CommonButtons
v-else v-else
:y-offset="0" :y-offset="0"
b-label="Quit" :b-label="$t('common.quit')"
a-label="Select" :a-label="$t('common.select')"
@activate-b="app.navigateTo('home')" @activate-b="app.navigateTo('home')"
/> />
</template> </template>

View File

@@ -19,7 +19,7 @@ const handleActivateA = () => {
} }
let confirmed = false; let confirmed = false;
confirmationModal.open({ confirmationModal.open({
text: "Restart game?", text: $t("settings.options.2048.restartConfirmation"),
onConfirm: () => { onConfirm: () => {
confirmed = true; confirmed = true;
}, },
@@ -95,7 +95,7 @@ let animating = false;
const showRestartModal = () => { const showRestartModal = () => {
let confirmed = false; let confirmed = false;
confirmationModal.open({ confirmationModal.open({
text: "Game Over!\nRestart?", text: $t("settings.options.2048.gameOver"),
onConfirm: () => { onConfirm: () => {
confirmed = true; confirmed = true;
}, },
@@ -179,11 +179,15 @@ onRender((ctx) => {
ctx.fillStyle = "#010101"; ctx.fillStyle = "#010101";
// score // score
ctx.fillText("Score:", SCORE_X, SCORE_Y); ctx.fillText(`${$t("settings.options.2048.score")}:`, SCORE_X, SCORE_Y);
ctx.fillText(score.toString(), SCORE_X, SCORE_Y + 16); ctx.fillText(score.toString(), SCORE_X, SCORE_Y + 16);
// high score // high score
ctx.fillText("High:", SCORE_X, HIGH_SCORE_Y); ctx.fillText(
`${$t("settings.options.2048.highScore")}:`,
SCORE_X,
HIGH_SCORE_Y,
);
ctx.fillText( ctx.fillText(
savedState.value.highScore.toString(), savedState.value.highScore.toString(),
SCORE_X, SCORE_X,
@@ -500,8 +504,8 @@ useKeyDown((key) => {
<template> <template>
<CommonButtons <CommonButtons
:y-offset="confirmationModal.buttonsYOffset" :y-offset="confirmationModal.buttonsYOffset"
b-label="Cancel" :b-label="$t('common.quit')"
a-label="Restart" :a-label="$t('common.restart')"
@activate-b="handleActivateB()" @activate-b="handleActivateB()"
@activate-a="handleActivateA()" @activate-a="handleActivateA()"
/> />

View File

@@ -126,8 +126,8 @@ defineOptions({
<template> <template>
<CommonButtons <CommonButtons
:y-offset="0" :y-offset="0"
b-label="Cancel" :b-label="$t('common.cancel')"
a-label="Confirm" :a-label="$t('common.confirm')"
@activate-b="handleCancel" @activate-b="handleCancel"
@activate-a="handleConfirm" @activate-a="handleConfirm"
/> />

View File

@@ -35,7 +35,10 @@ const handleConfirm = () => {
const showConfirmation = () => { const showConfirmation = () => {
confirmationModal.open({ confirmationModal.open({
text: `Rendering mode set to ${mode === "3d" ? "3D" : "2D"}`, text:
mode === "3d"
? $t("settings.options.startUp.confirmation3d")
: $t("settings.options.startUp.confirmation2d"),
onClosed: () => { onClosed: () => {
store.closeSubMenu(); store.closeSubMenu();
}, },
@@ -75,21 +78,20 @@ onRender((ctx) => {
fillImageTextHCentered(ctx, logo, title, 0, 4, buttonWidth, 4); fillImageTextHCentered(ctx, logo, title, 0, 4, buttonWidth, 4);
ctx.fillStyle = "#282828"; ctx.fillStyle = "#282828";
const text = const text = $t("settings.options.startUp.autoStart");
"The Main Menu will appear\nautomatically when you turn\nthe power on.";
fillTextHCenteredMultiline(ctx, text, 0, y, buttonWidth, 15); fillTextHCenteredMultiline(ctx, text, 0, y, buttonWidth, 15);
ctx.restore(); ctx.restore();
}; };
drawButton( drawButton(
"3D Mode", $t("settings.options.startUp.3dMode"),
renderingModeAssets._3dMode, renderingModeAssets._3dMode,
32, 32,
selected.value === "_3dMode", selected.value === "_3dMode",
); );
drawButton( drawButton(
"2D Mode", $t("settings.options.startUp.2dMode"),
renderingModeAssets._2dMode, renderingModeAssets._2dMode,
96, 96,
selected.value === "_2dMode", selected.value === "_2dMode",
@@ -100,8 +102,8 @@ onRender((ctx) => {
<template> <template>
<CommonButtons <CommonButtons
:y-offset="confirmationModal.buttonsYOffset" :y-offset="confirmationModal.buttonsYOffset"
b-label="Cancel" :b-label="$t('common.cancel')"
a-label="Confirm" :a-label="$t('common.confirm')"
@activate-b="handleCancel" @activate-b="handleCancel"
@activate-a="handleConfirm" @activate-a="handleConfirm"
/> />

View File

@@ -56,19 +56,19 @@ defineOptions({ render: () => null });
<!-- date format may change based on the language (d/m/y is superior btw) --> <!-- date format may change based on the language (d/m/y is superior btw) -->
<NumberInput <NumberInput
:model-value="BIRTHDAY_MONTH" :model-value="BIRTHDAY_MONTH"
title="Month" :title="$t('settings.user.birthday.month')"
:x="1 * 16" :x="1 * 16"
:disabled="true" :disabled="true"
/> />
<NumberInput <NumberInput
:model-value="BIRTHDAY_DAY" :model-value="BIRTHDAY_DAY"
title="Day" :title="$t('settings.user.birthday.day')"
:x="5 * 16" :x="5 * 16"
:disabled="true" :disabled="true"
/> />
<NumberInput <NumberInput
:model-value="BIRTHDAY_YEAR" :model-value="BIRTHDAY_YEAR"
title="Day" :title="$t('settings.user.birthday.year')"
:digits="4" :digits="4"
:x="9 * 16" :x="9 * 16"
:disabled="true" :disabled="true"
@@ -76,8 +76,8 @@ defineOptions({ render: () => null });
<CommonButtons <CommonButtons
:y-offset="0" :y-offset="0"
b-label="Cancel" :b-label="$t('common.cancel')"
a-label="Confirm" :a-label="$t('common.confirm')"
@activate-b="handleActivateB" @activate-b="handleActivateB"
@activate-a="handleActivateA" @activate-a="handleActivateA"
/> />

View File

@@ -135,8 +135,8 @@ const handleConfirm = () => {
<template> <template>
<CommonButtons <CommonButtons
:y-offset="0" :y-offset="0"
b-label="Cancel" :b-label="$t('common.cancel')"
a-label="Confirm" :a-label="$t('common.confirm')"
@activate-b="handleCancel" @activate-b="handleCancel"
@activate-a="handleConfirm" @activate-a="handleConfirm"
/> />

View File

@@ -253,8 +253,8 @@ defineOptions({ render: () => null });
<template> <template>
<CommonButtons <CommonButtons
:y-offset="confirmationModal.buttonsYOffset" :y-offset="confirmationModal.buttonsYOffset"
b-label="Quit" :b-label="$t('common.quit')"
:a-label="state === 'waiting' ? 'Start' : 'Restart'" :a-label="state === 'waiting' ? $t('common.start') : $t('common.restart')"
@activate-b="handleCancel" @activate-b="handleCancel"
@activate-a="handleConfirm" @activate-a="handleConfirm"
/> />

View File

@@ -56,8 +56,8 @@ defineOptions({ render: () => null });
<template> <template>
<CommonButtons <CommonButtons
:y-offset="0" :y-offset="0"
b-label="Cancel" :b-label="$t('common.cancel')"
a-label="Confirm" :a-label="$t('common.confirm')"
@activate-b="handleCancel" @activate-b="handleCancel"
@activate-a="handleConfirm" @activate-a="handleConfirm"
/> />

View File

@@ -68,10 +68,9 @@ const FADE_IN_DURATION = 1.5;
const FADE_IN_X_FACTOR = 0.15; const FADE_IN_X_FACTOR = 0.15;
const FADE_IN_Y_FACTOR = 0.075; const FADE_IN_Y_FACTOR = 0.075;
const TITLE = "Pihkaal's Gallery"; const title = computed(() => $t("gallery.title"));
const DESCRIPTION = const description = computed(() => $t("gallery.description"));
"Started on March 2025. I love taking photos of plants, insects, and arachnids."; const backButton = computed(() => $t("gallery.backToHome"));
const BACK_BUTTON = "Back to Home";
const typeText = ( const typeText = (
target: Ref<string>, target: Ref<string>,
@@ -98,14 +97,14 @@ const animateIntro = async () => {
isAnimating.value = true; isAnimating.value = true;
typeText(backButtonText, BACK_BUTTON, BACK_BUTTON_DURATION, false, { typeText(backButtonText, backButton.value, BACK_BUTTON_DURATION, false, {
onStart: () => (showBackButtonIcon.value = true), onStart: () => (showBackButtonIcon.value = true),
}).delay(ANIMATION_SLEEP + FADE_IN_DELAY); }).delay(ANIMATION_SLEEP + FADE_IN_DELAY);
typeText(titleText, TITLE, TITLE_DURATION).delay( typeText(titleText, title.value, TITLE_DURATION).delay(
ANIMATION_SLEEP + FADE_IN_DELAY, ANIMATION_SLEEP + FADE_IN_DELAY,
); );
typeText(descriptionText, DESCRIPTION, DESCRIPTION_DURATION).delay( typeText(descriptionText, description.value, DESCRIPTION_DURATION).delay(
ANIMATION_SLEEP + FADE_IN_DELAY, ANIMATION_SLEEP + FADE_IN_DELAY,
); );
@@ -149,13 +148,13 @@ const animateOutro = async () => {
ease: "power2.out", ease: "power2.out",
}); });
typeText(backButtonText, BACK_BUTTON, BACK_BUTTON_DURATION, true, { typeText(backButtonText, backButton.value, BACK_BUTTON_DURATION, true, {
onComplete: () => (showBackButtonIcon.value = false), onComplete: () => (showBackButtonIcon.value = false),
}); });
typeText(titleText, TITLE, TITLE_DURATION, true); typeText(titleText, title.value, TITLE_DURATION, true);
typeText(descriptionText, DESCRIPTION, DESCRIPTION_DURATION, true, { typeText(descriptionText, description.value, DESCRIPTION_DURATION, true, {
onComplete: async () => { onComplete: async () => {
await sleep(ANIMATION_SLEEP * 1000); await sleep(ANIMATION_SLEEP * 1000);
galleryStore.shouldAnimateOutro = true; galleryStore.shouldAnimateOutro = true;
@@ -200,11 +199,11 @@ onMounted(() => {
> >
<h1 class="text-3xl mt-3 font-bold relative"> <h1 class="text-3xl mt-3 font-bold relative">
<span class="invisible">{{ TITLE }}</span> <span class="invisible">{{ title }}</span>
<span class="absolute inset-0">{{ titleText }}</span> <span class="absolute inset-0">{{ titleText }}</span>
</h1> </h1>
<p class="text-neutral-600 text-sm mt-2 w-4/5 relative"> <p class="text-neutral-600 text-sm mt-2 w-4/5 relative">
<span class="invisible">{{ DESCRIPTION }}</span> <span class="invisible">{{ description }}</span>
<span class="absolute inset-0">{{ descriptionText }}</span> <span class="absolute inset-0">{{ descriptionText }}</span>
</p> </p>
</header> </header>

View File

@@ -1,4 +1,14 @@
{ {
"common": {
"cancel": "Cancel",
"confirm": "Confirm",
"quit": "Quit",
"start": "Start",
"restart": "Restart",
"reset": "Reset",
"select": "Select",
"goBack": "Go back"
},
"achievementsScreen": { "achievementsScreen": {
"title": "Achievements" "title": "Achievements"
}, },
@@ -38,7 +48,12 @@
"startUp": { "startUp": {
"title": "Start-up", "title": "Start-up",
"description": "Set how you would like your system to\nstart up when you turn the power on." "description": "Set how you would like your system to\nstart up when you turn the power on.",
"3dMode": "3D Mode",
"2dMode": "2D Mode",
"autoStart": "The Main Menu will appear\nautomatically when you turn\nthe power on.",
"confirmation3d": "Rendering mode set to 3D",
"confirmation2d": "Rendering mode set to 2D"
}, },
"language": { "language": {
"title": "Language", "title": "Language",
@@ -47,7 +62,11 @@
}, },
"2048": { "2048": {
"title": "2048", "title": "2048",
"description": "Play 2048!" "description": "Play 2048!",
"restartConfirmation": "Restart game?",
"gameOver": "Game Over!\nRestart?",
"score": "Score",
"highScore": "Best"
} }
}, },
"clock": { "clock": {
@@ -58,17 +77,25 @@
"title": "Achievements", "title": "Achievements",
"description": "Manage your achievements.", "description": "Manage your achievements.",
"resetButton": "Reset Achievements", "resetButton": "Reset Achievements",
"resetConfirmation": "Reset all achievements?" "resetConfirmation": "Reset all achievements?",
"viewAll": "View All",
"obtained": "Obtained",
"total": "Total"
}, },
"date": { "date": {
"title": "Date", "title": "Date",
"description": "Today's date." "description": "Today's date.",
"month": "Month",
"day": "Day",
"year": "Year"
}, },
"time": { "time": {
"title": "Time", "title": "Time",
"description": "Current time." "description": "Current time.",
"hour": "Hour",
"minute": "Minute"
} }
}, },
"user": { "user": {
@@ -83,6 +110,9 @@
"birthday": { "birthday": {
"title": "Birthday", "title": "Birthday",
"description": "This is my birthday.", "description": "This is my birthday.",
"month": "Month",
"day": "Day",
"year": "Year",
"confirmation": { "confirmation": {
"today": "Yes, it's today!", "today": "Yes, it's today!",
"future": "Don't forget to wish me in {days} days!" "future": "Don't forget to wish me in {days} days!"
@@ -114,6 +144,25 @@
"description": "TODO" "description": "TODO"
} }
}, },
"contact": {
"title": "Choose a Chat Room to join.",
"actions": {
"open": "Open",
"copy": "Copy",
"githubProfile": "Github profile",
"email": "Email",
"websiteLink": "Website link",
"cv": "CV"
},
"copiedToClipboard": "{item} copied to clipboard",
"openUrl": "Open {url}?",
"opened": "{item} opened"
},
"gallery": {
"title": "Pihkaal's Gallery",
"description": "Started on March 2025. I love taking photos of plants, insects, and arachnids.",
"backToHome": "Back to Home"
},
"projects": { "projects": {
"linkConformationPopup": { "linkConformationPopup": {
"yes": "yes", "yes": "yes",