feat: store locale in localStorage to persist across page loads

This commit is contained in:
Benjamin Singleton 2026-02-09 20:56:52 -06:00 committed by Sheldon Cooper
parent 4852f2d5d1
commit 59674292be
3 changed files with 70 additions and 69 deletions

View file

@ -11,9 +11,10 @@
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.6", "@tailwindcss/vite": "^4.1.6",
"@types/node": "^22.15.17", "@types/node": "^22.15.17",
"axios": "^1.11.0",
"@vueuse/components": "^13.3.0", "@vueuse/components": "^13.3.0",
"@vueuse/core": "^13.3.0", "@vueuse/core": "^13.3.0",
"arktype": "^2.1.29",
"axios": "^1.11.0",
"bulma": "^1.0.4", "bulma": "^1.0.4",
"tailwindcss": "^4.1.6", "tailwindcss": "^4.1.6",
"vue": "^3.5.13", "vue": "^3.5.13",

View file

@ -1,20 +1,50 @@
import en_US from "../locales/en_US"; import en_US from "../locales/en_US";
import vp_VL from "../locales/vp_VL"; import vp_VL from "../locales/vp_VL";
import wp_VL from "../locales/wp_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 { Locale } from "./locale";
import type { DeepPartial } from "@/utils/deep-partial"; 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 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 { const locales = { en_US, vp_VL, wp_VL } as const satisfies {
en_US: Locale; en_US: Locale;
} & Record<Exclude<LocaleId, "en_US">, DeepPartial<Locale>>; } & Record<Exclude<LocaleId, typeof DEFAULT_LOCALE_ID>, DeepPartial<Locale>>;
export const localeId = ref<LocaleId>("en_US"); // users could manually edit localStorage to make this value anything, so we need to validate it
const localStorageLocaleId = useLocalStorage<unknown>(
"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<DeepReadonly<Locale>>(() => { const locale = computed<DeepReadonly<Locale>>(() => {
return fallbackProxy<Locale>( return fallbackProxy<Locale>(
locales[opt.locale ?? localeId.value], locales[opt.locale ?? localeId.value],
@ -23,7 +53,7 @@ export function useLocale(opt: UseLocaleOptions = {}) {
}); });
return readonly(locale); return readonly(locale);
} };
export interface UseLocaleOptions { export interface UseLocaleOptions {
locale?: LocaleId; locale?: LocaleId;

94
pnpm-lock.yaml generated
View file

@ -10,7 +10,7 @@ importers:
devDependencies: devDependencies:
turbo: turbo:
specifier: ^2.5.4 specifier: ^2.5.4
version: 2.5.4 version: 2.8.0
apps/vdb-backend: apps/vdb-backend:
dependencies: dependencies:
@ -81,6 +81,9 @@ importers:
'@vueuse/core': '@vueuse/core':
specifier: ^13.3.0 specifier: ^13.3.0
version: 13.3.0(vue@3.5.16(typescript@5.8.3)) version: 13.3.0(vue@3.5.16(typescript@5.8.3))
arktype:
specifier: ^2.1.29
version: 2.1.29
axios: axios:
specifier: ^1.11.0 specifier: ^1.11.0
version: 1.11.0 version: 1.11.0
@ -156,6 +159,12 @@ packages:
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'} 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': '@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@ -1019,6 +1028,12 @@ packages:
argparse@2.0.1: argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 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: arrify@2.0.1:
resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -2504,70 +2519,36 @@ packages:
tunnel-agent@0.6.0: tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} 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: turbo-darwin-64@2.8.0:
resolution: {integrity: sha512-N7f4PYqz25yk8c5kituk09bJ89tE4wPPqKXgYccT6nbEQnGnrdvlyCHLyqViNObTgjjrddqjb1hmDkv7VcxE0g==} resolution: {integrity: sha512-N7f4PYqz25yk8c5kituk09bJ89tE4wPPqKXgYccT6nbEQnGnrdvlyCHLyqViNObTgjjrddqjb1hmDkv7VcxE0g==}
cpu: [x64] cpu: [x64]
os: [darwin] 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: turbo-darwin-arm64@2.8.0:
resolution: {integrity: sha512-eVzejaP5fn51gmJAPW68U6mSjFaAZ26rPiE36mMdk+tMC4XBGmJHT/fIgrhcrXMvINCl27RF8VmguRe+MBlSuQ==} resolution: {integrity: sha512-eVzejaP5fn51gmJAPW68U6mSjFaAZ26rPiE36mMdk+tMC4XBGmJHT/fIgrhcrXMvINCl27RF8VmguRe+MBlSuQ==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
turbo-linux-64@2.5.4:
resolution: {integrity: sha512-5May2kjWbc8w4XxswGAl74GZ5eM4Gr6IiroqdLhXeXyfvWEdm2mFYCSWOzz0/z5cAgqyGidF1jt1qzUR8hTmOA==}
cpu: [x64]
os: [linux]
turbo-linux-64@2.8.0: turbo-linux-64@2.8.0:
resolution: {integrity: sha512-ILR45zviYae3icf4cmUISdj8X17ybNcMh3Ms4cRdJF5sS50qDDTv8qeWqO/lPeHsu6r43gVWDofbDZYVuXYL7Q==} resolution: {integrity: sha512-ILR45zviYae3icf4cmUISdj8X17ybNcMh3Ms4cRdJF5sS50qDDTv8qeWqO/lPeHsu6r43gVWDofbDZYVuXYL7Q==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
turbo-linux-arm64@2.5.4:
resolution: {integrity: sha512-/2yqFaS3TbfxV3P5yG2JUI79P7OUQKOUvAnx4MV9Bdz6jqHsHwc9WZPpO4QseQm+NvmgY6ICORnoVPODxGUiJg==}
cpu: [arm64]
os: [linux]
turbo-linux-arm64@2.8.0: turbo-linux-arm64@2.8.0:
resolution: {integrity: sha512-z9pUa8ENFuHmadPfjEYMRWlXO82t1F/XBDx2XTg+cWWRZHf85FnEB6D4ForJn/GoKEEvwdPhFLzvvhOssom2ug==} resolution: {integrity: sha512-z9pUa8ENFuHmadPfjEYMRWlXO82t1F/XBDx2XTg+cWWRZHf85FnEB6D4ForJn/GoKEEvwdPhFLzvvhOssom2ug==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
turbo-windows-64@2.5.4:
resolution: {integrity: sha512-EQUO4SmaCDhO6zYohxIjJpOKRN3wlfU7jMAj3CgcyTPvQR/UFLEKAYHqJOnJtymbQmiiM/ihX6c6W6Uq0yC7mA==}
cpu: [x64]
os: [win32]
turbo-windows-64@2.8.0: turbo-windows-64@2.8.0:
resolution: {integrity: sha512-J6juRSRjmSErEqJCv7nVIq2DgZ2NHXqyeV8NQTFSyIvrThKiWe7FDOO6oYpuR06+C1NW82aoN4qQt4/gYvz25w==} resolution: {integrity: sha512-J6juRSRjmSErEqJCv7nVIq2DgZ2NHXqyeV8NQTFSyIvrThKiWe7FDOO6oYpuR06+C1NW82aoN4qQt4/gYvz25w==}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
turbo-windows-arm64@2.5.4:
resolution: {integrity: sha512-oQ8RrK1VS8lrxkLriotFq+PiF7iiGgkZtfLKF4DDKsmdbPo0O9R2mQxm7jHLuXraRCuIQDWMIw6dpcr7Iykf4A==}
cpu: [arm64]
os: [win32]
turbo-windows-arm64@2.8.0: turbo-windows-arm64@2.8.0:
resolution: {integrity: sha512-qarBZvCu6uka35739TS+y/3CBU3zScrVAfohAkKHG+So+93Wn+5tKArs8HrO2fuTaGou8fMIeTV7V5NgzCVkSQ==} resolution: {integrity: sha512-qarBZvCu6uka35739TS+y/3CBU3zScrVAfohAkKHG+So+93Wn+5tKArs8HrO2fuTaGou8fMIeTV7V5NgzCVkSQ==}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
turbo@2.5.4:
resolution: {integrity: sha512-kc8ZibdRcuWUG1pbYSBFWqmIjynlD8Lp7IB6U3vIzvOv9VG+6Sp8bzyeBWE3Oi8XV5KsQrznyRTBPvrf99E4mA==}
hasBin: true
turbo@2.8.0: turbo@2.8.0:
resolution: {integrity: sha512-hYbxnLEdvJF+DLALS+Ia+PbfNtn0sDP0hH2u9AFoskSUDmcVHSrtwHpzdX94MrRJKo9D9tYxY3MyP20gnlrWyA==} resolution: {integrity: sha512-hYbxnLEdvJF+DLALS+Ia+PbfNtn0sDP0hH2u9AFoskSUDmcVHSrtwHpzdX94MrRJKo9D9tYxY3MyP20gnlrWyA==}
hasBin: true hasBin: true
@ -2862,6 +2843,12 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.8 '@jridgewell/gen-mapping': 0.3.8
'@jridgewell/trace-mapping': 0.3.25 '@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-string-parser@7.27.1': {}
'@babel/helper-validator-identifier@7.27.1': {} '@babel/helper-validator-identifier@7.27.1': {}
@ -3637,6 +3624,16 @@ snapshots:
argparse@2.0.1: {} 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: {} arrify@2.0.1: {}
ast-kit@1.4.3: ast-kit@1.4.3:
@ -5287,51 +5284,24 @@ snapshots:
dependencies: dependencies:
safe-buffer: 5.2.1 safe-buffer: 5.2.1
turbo-darwin-64@2.5.4:
optional: true
turbo-darwin-64@2.8.0: turbo-darwin-64@2.8.0:
optional: true optional: true
turbo-darwin-arm64@2.5.4:
optional: true
turbo-darwin-arm64@2.8.0: turbo-darwin-arm64@2.8.0:
optional: true optional: true
turbo-linux-64@2.5.4:
optional: true
turbo-linux-64@2.8.0: turbo-linux-64@2.8.0:
optional: true optional: true
turbo-linux-arm64@2.5.4:
optional: true
turbo-linux-arm64@2.8.0: turbo-linux-arm64@2.8.0:
optional: true optional: true
turbo-windows-64@2.5.4:
optional: true
turbo-windows-64@2.8.0: turbo-windows-64@2.8.0:
optional: true optional: true
turbo-windows-arm64@2.5.4:
optional: true
turbo-windows-arm64@2.8.0: turbo-windows-arm64@2.8.0:
optional: true 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: turbo@2.8.0:
optionalDependencies: optionalDependencies:
turbo-darwin-64: 2.8.0 turbo-darwin-64: 2.8.0