anvilsign in

collin/mahjong

master / src / ui / lang.ts
1import { useCallback, useSyncExternalStore } from 'react';
2import { DEFAULT_PREFS, loadPrefs, type Lang } from '../game/prefs';
3
4/**
5 * Which languages the labels are showing, held module-side rather than passed
6 * down: <Bi> is used at twenty-odd places across the table, and none of them
7 * has any other reason to know about the setting.
8 *
9 * The preference is the source of truth — App writes it here whenever it
10 * changes — but the first value is read straight from storage so a player who
11 * chose English does not watch the lobby repaint itself on every load.
12 */
13let lang: Lang = typeof window === 'undefined' ? DEFAULT_PREFS.lang : loadPrefs().lang;
14
15const listeners = new Set<() => void>();
16
17const subscribe = (cb: () => void) => {
18 listeners.add(cb);
19 return () => {
20 listeners.delete(cb);
21 };
22};
23
24export const getLang = () => lang;
25
26export function setLang(next: Lang) {
27 // <html lang> follows the labels, so a page showing English is not still
28 // claiming to be Chinese. index.html's notranslate stands either way.
29 if (typeof document !== 'undefined') {
30 document.documentElement.lang = next === 'en' ? 'en' : 'zh-TW';
31 }
32 if (next === lang) return;
33 lang = next;
34 for (const cb of listeners) cb();
35}
36
37export function useLang(): Lang {
38 return useSyncExternalStore(subscribe, getLang, () => DEFAULT_PREFS.lang);
39}
40
41/**
42 * One label in the language the table is being read in.
43 *
44 * The two strings stay side by side at the place they are used, which is how
45 * this codebase has always written them — there is no catalogue of keys to go
46 * and look a phrase up in, and adding one would put every label a file away
47 * from the thing it labels.
48 *
49 * `sep` is only ever seen in bilingual, and is whatever the line already used:
50 * a space for a button, ' · ' for a sentence with two halves.
51 */
52export function pick(lang: Lang, zh: string, en: string, sep = ' '): string {
53 if (lang === 'zh') return zh || en;
54 if (lang === 'en') return en || zh;
55 return zh && en ? zh + sep + en : zh || en;
56}
57
58/**
59 * `pick` for a component, and the subscription that goes with it: a label built
60 * with this follows the setting rather than whatever it was on first render.
61 */
62export function useT() {
63 const lang = useLang();
64 return useCallback((zh: string, en: string, sep?: string) => pick(lang, zh, en, sep), [lang]);
65}