Skip to content

Commit 836a016

Browse files
committed
chore: lazy load locales
1 parent 0fadd97 commit 836a016

2 files changed

Lines changed: 50 additions & 27 deletions

File tree

‎packages/hub-ui/src/client/i18n/i18n.test.ts‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
import { afterEach, describe, expect, it } from 'vitest'
1+
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
22
import { HUB_UI_LOCALES, matchLocale } from '../../locales'
3-
import { locale, messages, setHostLocale, setLocalePreference, t, tAround } from './index'
3+
import { loadLocale, locale, setHostLocale, setLocalePreference, t, tAround } from './index'
4+
import en from './locales/en.json'
45

5-
const en = messages.en
6+
const files = import.meta.glob<Record<string, string>>('./locales/*.json', { import: 'default' })
67
const keys = Object.keys(en)
8+
9+
beforeAll(() => Promise.all(Object.keys(HUB_UI_LOCALES).map(code => loadLocale(code as keyof typeof HUB_UI_LOCALES))))
710
const slots = (message: string) => [...message.matchAll(/\{(\w+)\}/g)].map(match => match[1]).sort()
811

912
afterEach(() => {
@@ -12,8 +15,8 @@ afterEach(() => {
1215
})
1316

1417
describe('locale files', () => {
15-
it.each(Object.keys(HUB_UI_LOCALES))('%s translates every key and keeps the slots', (code) => {
16-
const file = messages[code as keyof typeof messages]
18+
it.each(Object.keys(HUB_UI_LOCALES))('%s translates every key and keeps the slots', async (code) => {
19+
const file = await files[`./locales/${code}.json`]!()
1720
expect(Object.keys(file).sort()).toEqual([...keys].sort())
1821
for (const key of keys) {
1922
const source = en[key as keyof typeof en]!

‎packages/hub-ui/src/client/i18n/index.ts‎

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,50 @@
11
import type { HubUiLocale } from '../../locales'
22
import { usePreferredLanguages, useStorage } from '@vueuse/core'
3-
import { computed, ref } from 'vue'
3+
import { computed, ref, shallowReactive, watch } from 'vue'
44
import { DEFAULT_LOCALE, matchLocale } from '../../locales'
5-
import de from './locales/de.json'
65
import en from './locales/en.json'
7-
import es from './locales/es.json'
8-
import fr from './locales/fr.json'
9-
import ja from './locales/ja.json'
10-
import ko from './locales/ko.json'
11-
import ptBR from './locales/pt-BR.json'
12-
import ru from './locales/ru.json'
13-
import zhCN from './locales/zh-CN.json'
14-
import zhTW from './locales/zh-TW.json'
156

167
/** Every UI string has a key in `en.json`; the other files translate them. */
178
export type MessageKey = keyof typeof en
189

19-
export const messages: Record<HubUiLocale, Partial<Record<MessageKey, string>>> = {
20-
'en': en,
21-
'zh-CN': zhCN,
22-
'zh-TW': zhTW,
23-
ja,
24-
ko,
25-
es,
26-
fr,
27-
de,
28-
'pt-BR': ptBR,
29-
ru,
10+
type Messages = Partial<Record<MessageKey, string>>
11+
12+
/** English is bundled: it is the fallback, so `t()` never waits on a network. */
13+
const loaders: Record<Exclude<HubUiLocale, 'en'>, () => Promise<{ default: Messages }>> = {
14+
'zh-CN': () => import('./locales/zh-CN.json'),
15+
'zh-TW': () => import('./locales/zh-TW.json'),
16+
'ja': () => import('./locales/ja.json'),
17+
'ko': () => import('./locales/ko.json'),
18+
'es': () => import('./locales/es.json'),
19+
'fr': () => import('./locales/fr.json'),
20+
'de': () => import('./locales/de.json'),
21+
'pt-BR': () => import('./locales/pt-BR.json'),
22+
'ru': () => import('./locales/ru.json'),
23+
}
24+
25+
const loaded = shallowReactive<Partial<Record<HubUiLocale, Messages>>>({ en })
26+
const pending = new Map<HubUiLocale, Promise<void>>()
27+
28+
/**
29+
* Fetch the chunk for `code` once. `t()` reads `loaded` reactively, so the UI
30+
* shows English until the chunk lands, then re-renders in the new language.
31+
*/
32+
export function loadLocale(code: HubUiLocale): Promise<void> {
33+
if (loaded[code])
34+
return Promise.resolve()
35+
let request = pending.get(code)
36+
if (!request) {
37+
request = loaders[code as keyof typeof loaders]()
38+
.then((module) => {
39+
loaded[code] = module.default
40+
})
41+
.catch(() => {
42+
// Stay on English, and allow a retry on the next pick.
43+
})
44+
.finally(() => pending.delete(code))
45+
pending.set(code, request)
46+
}
47+
return request
3048
}
3149

3250
export type LocalePreference = 'auto' | HubUiLocale
@@ -57,6 +75,8 @@ export const locale = computed<HubUiLocale>(() => {
5775
return DEFAULT_LOCALE
5876
})
5977

78+
watch(locale, code => void loadLocale(code), { immediate: true })
79+
6080
/** Seed the `auto` choice from `ConnectionMeta.configs.ui.locale`. */
6181
export function setHostLocale(tag: string | undefined): void {
6282
hostLocale.value = matchLocale(tag)
@@ -68,7 +88,7 @@ export function setLocalePreference(preference: LocalePreference): void {
6888

6989
/** Translate `key` in the current locale, filling `{name}` slots from `params`. */
7090
export function t(key: MessageKey, params?: Record<string, string | number>): string {
71-
const message = messages[locale.value][key] ?? en[key]
91+
const message = loaded[locale.value]?.[key] ?? en[key]
7292
if (!params)
7393
return message
7494
return message.replace(/\{(\w+)\}/g, (slot, name: string) => name in params ? String(params[name]) : slot)

0 commit comments

Comments
 (0)