From 59674292be7fe4b3d5f43b9925596e86aa6c86b4 Mon Sep 17 00:00:00 2001 From: Benjamin Singleton <19498453+tetrogem@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:56:52 -0600 Subject: [PATCH] feat: store locale in localStorage to persist across page loads --- apps/vdn-static/package.json | 3 +- apps/vdn-static/src/i18n/index.ts | 42 ++++++++++++-- pnpm-lock.yaml | 94 +++++++++++-------------------- 3 files changed, 70 insertions(+), 69 deletions(-) diff --git a/apps/vdn-static/package.json b/apps/vdn-static/package.json index 6f95f02..25a0d3c 100644 --- a/apps/vdn-static/package.json +++ b/apps/vdn-static/package.json @@ -11,9 +11,10 @@ "dependencies": { "@tailwindcss/vite": "^4.1.6", "@types/node": "^22.15.17", - "axios": "^1.11.0", "@vueuse/components": "^13.3.0", "@vueuse/core": "^13.3.0", + "arktype": "^2.1.29", + "axios": "^1.11.0", "bulma": "^1.0.4", "tailwindcss": "^4.1.6", "vue": "^3.5.13", diff --git a/apps/vdn-static/src/i18n/index.ts b/apps/vdn-static/src/i18n/index.ts index ef4c791..b1cf67f 100644 --- a/apps/vdn-static/src/i18n/index.ts +++ b/apps/vdn-static/src/i18n/index.ts @@ -1,20 +1,50 @@ 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, type DeepReadonly } from "vue"; +import { computed, readonly, type DeepReadonly } from "vue"; import type { Locale } from "./locale"; import type { DeepPartial } from "@/utils/deep-partial"; +import { useLocalStorage } from "@vueuse/core"; +import { type } from "arktype"; export const LOCALE_IDS = ["en_US", "vp_VL", "wp_VL"] as const; -export type LocaleId = (typeof LOCALE_IDS)[number]; + +export type LocaleId = typeof LocaleId.infer; +export const LocaleId = type.enumerated(...LOCALE_IDS); + +export const DEFAULT_LOCALE_ID = "en_US" satisfies LocaleId; const locales = { en_US, vp_VL, wp_VL } as const satisfies { en_US: Locale; -} & Record, DeepPartial>; +} & Record, DeepPartial>; -export const localeId = ref("en_US"); +// users could manually edit localStorage to make this value anything, so we need to validate it +const localStorageLocaleId = useLocalStorage( + "localeId", + DEFAULT_LOCALE_ID, +); -export function useLocale(opt: UseLocaleOptions = {}) { +export const localeId = computed({ + get: (): LocaleId => { + const localeIdRes = LocaleId(localStorageLocaleId.value); + if (localeIdRes instanceof type.errors) { + // if invalid LocaleId, reset to default + localStorageLocaleId.value = DEFAULT_LOCALE_ID; + return DEFAULT_LOCALE_ID; + } + + // else return user's selection + const localeId = localeIdRes; + return localeId; + }, + // custom setter to ensure it is only set to a valid LocaleId by our code + // (since the localStorage ref is typed as `unknown`, it can be set to any value) + set: (id: LocaleId) => { + localStorageLocaleId.value = id; + }, +}); + +export const useLocale = (opt: UseLocaleOptions = {}) => { const locale = computed>(() => { return fallbackProxy( locales[opt.locale ?? localeId.value], @@ -23,7 +53,7 @@ export function useLocale(opt: UseLocaleOptions = {}) { }); return readonly(locale); -} +}; export interface UseLocaleOptions { locale?: LocaleId; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a71f2a3..b5f4828 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: devDependencies: turbo: specifier: ^2.5.4 - version: 2.5.4 + version: 2.8.0 apps/vdb-backend: dependencies: @@ -81,6 +81,9 @@ importers: '@vueuse/core': specifier: ^13.3.0 version: 13.3.0(vue@3.5.16(typescript@5.8.3)) + arktype: + specifier: ^2.1.29 + version: 2.1.29 axios: specifier: ^1.11.0 version: 1.11.0 @@ -156,6 +159,12 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@ark/schema@0.56.0': + resolution: {integrity: sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA==} + + '@ark/util@0.56.0': + resolution: {integrity: sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==} + '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -1019,6 +1028,12 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + arkregex@0.0.5: + resolution: {integrity: sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw==} + + arktype@2.1.29: + resolution: {integrity: sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ==} + arrify@2.0.1: resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} engines: {node: '>=8'} @@ -2504,70 +2519,36 @@ packages: tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} - turbo-darwin-64@2.5.4: - resolution: {integrity: sha512-ah6YnH2dErojhFooxEzmvsoZQTMImaruZhFPfMKPBq8sb+hALRdvBNLqfc8NWlZq576FkfRZ/MSi4SHvVFT9PQ==} - cpu: [x64] - os: [darwin] - turbo-darwin-64@2.8.0: resolution: {integrity: sha512-N7f4PYqz25yk8c5kituk09bJ89tE4wPPqKXgYccT6nbEQnGnrdvlyCHLyqViNObTgjjrddqjb1hmDkv7VcxE0g==} cpu: [x64] os: [darwin] - turbo-darwin-arm64@2.5.4: - resolution: {integrity: sha512-2+Nx6LAyuXw2MdXb7pxqle3MYignLvS7OwtsP9SgtSBaMlnNlxl9BovzqdYAgkUW3AsYiQMJ/wBRb7d+xemM5A==} - cpu: [arm64] - os: [darwin] - turbo-darwin-arm64@2.8.0: resolution: {integrity: sha512-eVzejaP5fn51gmJAPW68U6mSjFaAZ26rPiE36mMdk+tMC4XBGmJHT/fIgrhcrXMvINCl27RF8VmguRe+MBlSuQ==} cpu: [arm64] os: [darwin] - turbo-linux-64@2.5.4: - resolution: {integrity: sha512-5May2kjWbc8w4XxswGAl74GZ5eM4Gr6IiroqdLhXeXyfvWEdm2mFYCSWOzz0/z5cAgqyGidF1jt1qzUR8hTmOA==} - cpu: [x64] - os: [linux] - turbo-linux-64@2.8.0: resolution: {integrity: sha512-ILR45zviYae3icf4cmUISdj8X17ybNcMh3Ms4cRdJF5sS50qDDTv8qeWqO/lPeHsu6r43gVWDofbDZYVuXYL7Q==} cpu: [x64] os: [linux] - turbo-linux-arm64@2.5.4: - resolution: {integrity: sha512-/2yqFaS3TbfxV3P5yG2JUI79P7OUQKOUvAnx4MV9Bdz6jqHsHwc9WZPpO4QseQm+NvmgY6ICORnoVPODxGUiJg==} - cpu: [arm64] - os: [linux] - turbo-linux-arm64@2.8.0: resolution: {integrity: sha512-z9pUa8ENFuHmadPfjEYMRWlXO82t1F/XBDx2XTg+cWWRZHf85FnEB6D4ForJn/GoKEEvwdPhFLzvvhOssom2ug==} cpu: [arm64] os: [linux] - turbo-windows-64@2.5.4: - resolution: {integrity: sha512-EQUO4SmaCDhO6zYohxIjJpOKRN3wlfU7jMAj3CgcyTPvQR/UFLEKAYHqJOnJtymbQmiiM/ihX6c6W6Uq0yC7mA==} - cpu: [x64] - os: [win32] - turbo-windows-64@2.8.0: resolution: {integrity: sha512-J6juRSRjmSErEqJCv7nVIq2DgZ2NHXqyeV8NQTFSyIvrThKiWe7FDOO6oYpuR06+C1NW82aoN4qQt4/gYvz25w==} cpu: [x64] os: [win32] - turbo-windows-arm64@2.5.4: - resolution: {integrity: sha512-oQ8RrK1VS8lrxkLriotFq+PiF7iiGgkZtfLKF4DDKsmdbPo0O9R2mQxm7jHLuXraRCuIQDWMIw6dpcr7Iykf4A==} - cpu: [arm64] - os: [win32] - turbo-windows-arm64@2.8.0: resolution: {integrity: sha512-qarBZvCu6uka35739TS+y/3CBU3zScrVAfohAkKHG+So+93Wn+5tKArs8HrO2fuTaGou8fMIeTV7V5NgzCVkSQ==} cpu: [arm64] os: [win32] - turbo@2.5.4: - resolution: {integrity: sha512-kc8ZibdRcuWUG1pbYSBFWqmIjynlD8Lp7IB6U3vIzvOv9VG+6Sp8bzyeBWE3Oi8XV5KsQrznyRTBPvrf99E4mA==} - hasBin: true - turbo@2.8.0: resolution: {integrity: sha512-hYbxnLEdvJF+DLALS+Ia+PbfNtn0sDP0hH2u9AFoskSUDmcVHSrtwHpzdX94MrRJKo9D9tYxY3MyP20gnlrWyA==} hasBin: true @@ -2862,6 +2843,12 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 + '@ark/schema@0.56.0': + dependencies: + '@ark/util': 0.56.0 + + '@ark/util@0.56.0': {} + '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.27.1': {} @@ -3637,6 +3624,16 @@ snapshots: argparse@2.0.1: {} + arkregex@0.0.5: + dependencies: + '@ark/util': 0.56.0 + + arktype@2.1.29: + dependencies: + '@ark/schema': 0.56.0 + '@ark/util': 0.56.0 + arkregex: 0.0.5 + arrify@2.0.1: {} ast-kit@1.4.3: @@ -5287,51 +5284,24 @@ snapshots: dependencies: safe-buffer: 5.2.1 - turbo-darwin-64@2.5.4: - optional: true - turbo-darwin-64@2.8.0: optional: true - turbo-darwin-arm64@2.5.4: - optional: true - turbo-darwin-arm64@2.8.0: optional: true - turbo-linux-64@2.5.4: - optional: true - turbo-linux-64@2.8.0: optional: true - turbo-linux-arm64@2.5.4: - optional: true - turbo-linux-arm64@2.8.0: optional: true - turbo-windows-64@2.5.4: - optional: true - turbo-windows-64@2.8.0: optional: true - turbo-windows-arm64@2.5.4: - optional: true - turbo-windows-arm64@2.8.0: optional: true - turbo@2.5.4: - optionalDependencies: - turbo-darwin-64: 2.5.4 - turbo-darwin-arm64: 2.5.4 - turbo-linux-64: 2.5.4 - turbo-linux-arm64: 2.5.4 - turbo-windows-64: 2.5.4 - turbo-windows-arm64: 2.5.4 - turbo@2.8.0: optionalDependencies: turbo-darwin-64: 2.8.0