@pistonite/pure 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +8 -0
- package/package.json +33 -0
- package/src/fs/FsError.ts +55 -0
- package/src/fs/FsFile.ts +67 -0
- package/src/fs/FsFileImpl.ts +225 -0
- package/src/fs/FsFileMgr.ts +29 -0
- package/src/fs/FsFileSystem.ts +71 -0
- package/src/fs/FsFileSystemInternal.ts +30 -0
- package/src/fs/FsImplEntryAPI.ts +188 -0
- package/src/fs/FsImplFileAPI.ts +126 -0
- package/src/fs/FsImplHandleAPI.ts +237 -0
- package/src/fs/FsOpen.ts +307 -0
- package/src/fs/FsPath.ts +137 -0
- package/src/fs/FsSave.ts +12 -0
- package/src/fs/FsSupportStatus.ts +91 -0
- package/src/fs/index.ts +129 -0
- package/src/log/index.ts +56 -0
- package/src/pref/dark.ts +184 -0
- package/src/pref/index.ts +12 -0
- package/src/pref/injectStyle.ts +22 -0
- package/src/pref/locale.ts +341 -0
- package/src/result/index.ts +215 -0
- package/src/sync/Debounce.ts +35 -0
- package/src/sync/Latest.ts +75 -0
- package/src/sync/RwLock.ts +95 -0
- package/src/sync/Serial.ts +170 -0
- package/src/sync/index.ts +12 -0
package/src/log/index.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client side log util
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Denque from "denque";
|
|
8
|
+
import { errstr } from "../result/index.ts";
|
|
9
|
+
|
|
10
|
+
const LIMIT = 500;
|
|
11
|
+
|
|
12
|
+
/** Global log queue */
|
|
13
|
+
const LogQueue = new Denque<string>();
|
|
14
|
+
function pushLog(msg: string) {
|
|
15
|
+
if (LogQueue.length > LIMIT) {
|
|
16
|
+
LogQueue.shift();
|
|
17
|
+
}
|
|
18
|
+
LogQueue.push(`[${new Date().toISOString()}]${msg}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Get the current log */
|
|
22
|
+
export function getLogLines(): string[] {
|
|
23
|
+
return LogQueue.toArray();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A general-purpose client side logger */
|
|
27
|
+
export class Logger {
|
|
28
|
+
/** The prefix of the logger */
|
|
29
|
+
private prefix: string;
|
|
30
|
+
|
|
31
|
+
constructor(prefix: string) {
|
|
32
|
+
this.prefix = prefix;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Log an info message */
|
|
36
|
+
public info(msg: string) {
|
|
37
|
+
const msgWithPrefix = `[${this.prefix}] ${msg}`;
|
|
38
|
+
self.console.info(msgWithPrefix);
|
|
39
|
+
pushLog(msgWithPrefix);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Log a warning message */
|
|
43
|
+
public warn(msg: string) {
|
|
44
|
+
const msgWithPrefix = `[${this.prefix}] ${msg}`;
|
|
45
|
+
self.console.warn(msgWithPrefix);
|
|
46
|
+
pushLog(msgWithPrefix);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Log an error message */
|
|
50
|
+
public error(msg: unknown) {
|
|
51
|
+
const msgWithPrefix = `[${this.prefix}] ${errstr(msg)}`;
|
|
52
|
+
self.console.error(msgWithPrefix);
|
|
53
|
+
self.console.error(msg);
|
|
54
|
+
pushLog(msgWithPrefix);
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/pref/dark.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dark mode wrappers
|
|
3
|
+
*
|
|
4
|
+
* ## Detect user preference
|
|
5
|
+
* User preference is detected with `matchMedia` API, if available.
|
|
6
|
+
* ```typescript
|
|
7
|
+
* import { prefersDarkMode } from "@pistonite/pure/dark";
|
|
8
|
+
*
|
|
9
|
+
* console.log(prefersDarkMode());
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* ## Global dark mode state
|
|
13
|
+
* `initDark` initializes the dark mode state.
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { initDark, isDark, setDark, addDarkSubscriber } from "@pistonite/pure/dark";
|
|
16
|
+
*
|
|
17
|
+
* initDark();
|
|
18
|
+
* console.log(isDark());
|
|
19
|
+
*
|
|
20
|
+
* addDarkSubscriber((dark) => { console.log("Dark mode changed: ", dark); });
|
|
21
|
+
* setDark(true); // will trigger the subscriber
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* ## Use with React
|
|
25
|
+
* A React hook is provided in the [`pure-react`](https://jsr.io/@pistonite/pure-react/doc/pref) package
|
|
26
|
+
* to get the dark mode state from React components.
|
|
27
|
+
*
|
|
28
|
+
* Use `setDark` to change the dark mode state from React compoenents like you would from anywhere else.
|
|
29
|
+
*
|
|
30
|
+
* ## Persisting to localStorage
|
|
31
|
+
* You can persist the dark mode preference to by passing `persist: true` to `initDark`.
|
|
32
|
+
* This will make `initDark` also load the preference from localStorage.
|
|
33
|
+
* ```typescript
|
|
34
|
+
* import { initDark } from "@pistonite/pure/dark";
|
|
35
|
+
*
|
|
36
|
+
* initDark({ persist: true });
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* ## Setting `color-scheme` CSS property
|
|
40
|
+
* The `color-scheme` property handles dark mode for native components like buttons
|
|
41
|
+
* and scrollbars. By default, `initDark` will handle setting this property for the `:root` selector.
|
|
42
|
+
* You can override this by passing a `selector` option.
|
|
43
|
+
* ```typescript
|
|
44
|
+
* import { initDark } from "@pistonite/pure/dark";
|
|
45
|
+
*
|
|
46
|
+
* // will set `.my-app { color-scheme: dark }`
|
|
47
|
+
* initDark({ selector: ".my-app" });
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @module
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
import { injectStyle } from "./injectStyle.ts";
|
|
54
|
+
|
|
55
|
+
const KEY = "Pure.Dark";
|
|
56
|
+
|
|
57
|
+
let dark = false;
|
|
58
|
+
const subscribers: ((dark: boolean) => void)[] = [];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Returns if dark mode is prefered in the browser environment
|
|
62
|
+
*
|
|
63
|
+
* If `window.matchMedia` is not available, it will return `false`
|
|
64
|
+
*/
|
|
65
|
+
export const prefersDarkMode = (): boolean => {
|
|
66
|
+
return !!globalThis.matchMedia?.("(prefers-color-scheme: dark)").matches;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** Value for the `color-scheme` CSS property */
|
|
70
|
+
export type ColorScheme = "light" | "dark";
|
|
71
|
+
/** Option for initializing dark mode */
|
|
72
|
+
export type DarkOptions = {
|
|
73
|
+
/**
|
|
74
|
+
* Initial value for dark mode
|
|
75
|
+
*
|
|
76
|
+
* If not set, it will default to calling `prefersDarkMode()`.
|
|
77
|
+
*
|
|
78
|
+
* If `persist` is `true`, it will also check the value from localStorage
|
|
79
|
+
*/
|
|
80
|
+
initial?: boolean;
|
|
81
|
+
/** Persist the dark mode preference to localStorage */
|
|
82
|
+
persist?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* The selector to set `color-scheme` property
|
|
85
|
+
*
|
|
86
|
+
* Defaults to `:root`. If set to empty string, CSS will not be updated
|
|
87
|
+
*/
|
|
88
|
+
selector?: string;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Initializes dark mode
|
|
93
|
+
*
|
|
94
|
+
* @param options Options for initializing dark mode
|
|
95
|
+
*/
|
|
96
|
+
export const initDark = (options: DarkOptions = {}): void => {
|
|
97
|
+
let _dark = options.initial || prefersDarkMode();
|
|
98
|
+
|
|
99
|
+
if (options.persist) {
|
|
100
|
+
const value = localStorage.getItem(KEY);
|
|
101
|
+
if (value !== null) {
|
|
102
|
+
_dark = !!value;
|
|
103
|
+
}
|
|
104
|
+
addDarkSubscriber((dark: boolean) => {
|
|
105
|
+
localStorage.setItem(KEY, dark ? "1" : "");
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
localStorage.removeItem(KEY);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
setDark(_dark);
|
|
112
|
+
|
|
113
|
+
const selector = options.selector ?? ":root";
|
|
114
|
+
if (selector) {
|
|
115
|
+
// notify immediately to update the style initially
|
|
116
|
+
addDarkSubscriber((dark: boolean) => {
|
|
117
|
+
updateStyle(dark, selector);
|
|
118
|
+
}, true /* notify */);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Clears the persisted dark mode preference
|
|
124
|
+
*
|
|
125
|
+
* If you are doing this, you should probably call `setDark`
|
|
126
|
+
* with `prefersDarkMode()` or some initial value immediately before this,
|
|
127
|
+
* so the current dark mode is set to user's preferred mode.
|
|
128
|
+
*
|
|
129
|
+
* Note if `persist` is `true` when initializing,
|
|
130
|
+
* subsequence `setDark` calls will still persist the value.
|
|
131
|
+
*/
|
|
132
|
+
export const clearPersistedDarkPerference = (): void => {
|
|
133
|
+
localStorage.removeItem(KEY);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Gets the current value of dark mode
|
|
138
|
+
*/
|
|
139
|
+
export const isDark = (): boolean => dark;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Set the value of dark mode
|
|
143
|
+
*/
|
|
144
|
+
export const setDark = (value: boolean): void => {
|
|
145
|
+
if (dark === value) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
dark = value;
|
|
149
|
+
const len = subscribers.length;
|
|
150
|
+
for (let i = 0; i < len; i++) {
|
|
151
|
+
subscribers[i](dark);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Add a subscriber to dark mode changes
|
|
156
|
+
*
|
|
157
|
+
* If `notifyImmediately` is `true`, the subscriber will be called immediately with the current value
|
|
158
|
+
*/
|
|
159
|
+
export const addDarkSubscriber = (
|
|
160
|
+
subscriber: (dark: boolean) => void,
|
|
161
|
+
notifyImmediately?: boolean,
|
|
162
|
+
): void => {
|
|
163
|
+
subscribers.push(subscriber);
|
|
164
|
+
if (notifyImmediately) {
|
|
165
|
+
subscriber(dark);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Remove a subscriber from dark mode changes
|
|
171
|
+
*/
|
|
172
|
+
export const removeDarkSubscriber = (
|
|
173
|
+
subscriber: (dark: boolean) => void,
|
|
174
|
+
): void => {
|
|
175
|
+
const index = subscribers.indexOf(subscriber);
|
|
176
|
+
if (index >= 0) {
|
|
177
|
+
subscribers.splice(index, 1);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const updateStyle = (dark: boolean, selector: string) => {
|
|
182
|
+
const text = `${selector} { color-scheme: ${dark ? "dark" : "light"}; }`;
|
|
183
|
+
injectStyle(KEY, text);
|
|
184
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* # pure/pref
|
|
3
|
+
* Preference utilities (things like locale and theme).
|
|
4
|
+
*
|
|
5
|
+
* These deal with raw CSS and DOM API, and is probably not
|
|
6
|
+
* very useful outside of browser environment.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
export * from "./dark.ts";
|
|
11
|
+
export * from "./injectStyle.ts";
|
|
12
|
+
export * from "./locale.ts";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inject a css string into a style tag identified by the id
|
|
3
|
+
*
|
|
4
|
+
* Will remove the old style tag(s) if exist
|
|
5
|
+
*/
|
|
6
|
+
export function injectStyle(id: string, style: string) {
|
|
7
|
+
const styleTags = document.querySelectorAll(`style[data-inject="${id}"`);
|
|
8
|
+
if (styleTags.length !== 1) {
|
|
9
|
+
const styleTag = document.createElement("style");
|
|
10
|
+
styleTag.setAttribute("data-inject", id);
|
|
11
|
+
styleTag.innerText = style;
|
|
12
|
+
document.head.appendChild(styleTag);
|
|
13
|
+
setTimeout(() => {
|
|
14
|
+
styleTags.forEach((tag) => tag.remove());
|
|
15
|
+
}, 0);
|
|
16
|
+
} else {
|
|
17
|
+
const e = styleTags[0] as HTMLStyleElement;
|
|
18
|
+
if (e.innerText !== style) {
|
|
19
|
+
e.innerText = style;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale utilities and integration with i18next
|
|
3
|
+
*
|
|
4
|
+
* ## Initialization
|
|
5
|
+
* `initLocale` must be called before using the other functions.
|
|
6
|
+
*
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { initLocale } from "@pistonite/pure/pref";
|
|
9
|
+
*
|
|
10
|
+
* initLocale({
|
|
11
|
+
* // required
|
|
12
|
+
* supported: ["en", "zh-CN", "zh-TW"] as const,
|
|
13
|
+
* default: "en",
|
|
14
|
+
*
|
|
15
|
+
* // optional
|
|
16
|
+
* persist: true, // save to localStorage
|
|
17
|
+
* initial: "en-US", // initial value, instead of detecting
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* ## Connecting with i18next
|
|
22
|
+
* The typical usage for this component is to use i18next for localization.
|
|
23
|
+
* This module provides 2 plugins:
|
|
24
|
+
* - `detectLocale`:
|
|
25
|
+
* - Provide the current language to i18next (as a language detector)
|
|
26
|
+
* - Update the global locale state whenever `i18next.changeLanguage` is called
|
|
27
|
+
* - `connectI18next`:
|
|
28
|
+
* - Call `i18next.changeLanguage` whenever `setLocale` is called
|
|
29
|
+
*
|
|
30
|
+
* You might only need one of these plugins, depending on your use case.
|
|
31
|
+
* For example, if you will never call `setLocale` manually, then you don't need `connectI18next`.
|
|
32
|
+
*
|
|
33
|
+
* ```typescript
|
|
34
|
+
* import i18next from "i18next";
|
|
35
|
+
* import { initLocale, detectLocale, connectI18next } from "@pistonite/pure/pref";
|
|
36
|
+
*
|
|
37
|
+
* // initialize locale
|
|
38
|
+
* initLocale({ supported: ["en", "es"], default: "en", persist: true });
|
|
39
|
+
*
|
|
40
|
+
* // connect with i18next
|
|
41
|
+
* i18next.use(detectLocale).use(connectI18next).init({
|
|
42
|
+
* // ...other options not shown
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* ## Use with React
|
|
47
|
+
* A React hook is provided in the [`pure-react`](https://jsr.io/@pistonite/pure-react/doc/pref) package
|
|
48
|
+
* to get the current locale from React components.
|
|
49
|
+
*
|
|
50
|
+
* Changing the locale from React components is the same as from outside React,
|
|
51
|
+
* with `setLocale` or `i18next.changeLanguage`, depending on your setup.
|
|
52
|
+
*
|
|
53
|
+
* @module
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
const KEY = "Pure.Locale";
|
|
57
|
+
|
|
58
|
+
let supportedLocales: readonly string[] = [];
|
|
59
|
+
let locale: string = "";
|
|
60
|
+
let defaultLocale: string = "";
|
|
61
|
+
const subscribers: ((locale: string) => void)[] = [];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Use browser API to guess user's preferred locale
|
|
65
|
+
*/
|
|
66
|
+
export const getPreferredLocale = (): string => {
|
|
67
|
+
if (globalThis.Intl) {
|
|
68
|
+
try {
|
|
69
|
+
return globalThis.Intl.NumberFormat().resolvedOptions().locale;
|
|
70
|
+
} catch {
|
|
71
|
+
// ignore
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (globalThis.navigator?.languages) {
|
|
75
|
+
return globalThis.navigator.languages[0];
|
|
76
|
+
}
|
|
77
|
+
return "";
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type LocaleOptions<TLocale extends string> = {
|
|
81
|
+
/**
|
|
82
|
+
* List of supported locale or languages.
|
|
83
|
+
* These can be full locale strings like "en-US" or just languages like "en"
|
|
84
|
+
*/
|
|
85
|
+
supported: readonly TLocale[];
|
|
86
|
+
/**
|
|
87
|
+
* The default locale if the user's preferred locale is not supported.
|
|
88
|
+
* This must be one of the items in `supported`.
|
|
89
|
+
*/
|
|
90
|
+
default: TLocale;
|
|
91
|
+
/**
|
|
92
|
+
* Initial value for locale
|
|
93
|
+
*
|
|
94
|
+
* If not set, it will default to calling `getPreferredLocale()`,
|
|
95
|
+
* which is based on the browser's language settings.
|
|
96
|
+
*
|
|
97
|
+
* If `persist` is `true`, it will also check the value from localStorage
|
|
98
|
+
*
|
|
99
|
+
* If the initial value is not supported, it will default to the default locale
|
|
100
|
+
*/
|
|
101
|
+
initial?: TLocale;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Persist the locale preference to localStorage
|
|
105
|
+
*/
|
|
106
|
+
persist?: boolean;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** Initialize locale global state */
|
|
110
|
+
export const initLocale = <TLocale extends string>(
|
|
111
|
+
options: LocaleOptions<TLocale>,
|
|
112
|
+
): void => {
|
|
113
|
+
let _locale = "";
|
|
114
|
+
supportedLocales = options.supported;
|
|
115
|
+
if (options.initial) {
|
|
116
|
+
_locale = options.initial;
|
|
117
|
+
} else {
|
|
118
|
+
_locale =
|
|
119
|
+
convertToSupportedLocale(getPreferredLocale()) || options.default;
|
|
120
|
+
}
|
|
121
|
+
defaultLocale = options.default;
|
|
122
|
+
if (options.persist) {
|
|
123
|
+
const value = localStorage.getItem(KEY);
|
|
124
|
+
if (value !== null) {
|
|
125
|
+
const supported = convertToSupportedLocale(value);
|
|
126
|
+
if (supported) {
|
|
127
|
+
_locale = supported;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
addLocaleSubscriber((locale: string) => {
|
|
131
|
+
localStorage.setItem(KEY, locale);
|
|
132
|
+
});
|
|
133
|
+
} else {
|
|
134
|
+
localStorage.removeItem(KEY);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
setLocale(_locale);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Clear the locale preference previously presisted to localStorage
|
|
142
|
+
*
|
|
143
|
+
* If you are doing this, you should probably call `setLocale`
|
|
144
|
+
* or `i18next.changeLanguage` (depending on your setup) immediately
|
|
145
|
+
* before this with `convertToSupportedLocaleOrDefault(getPreferredLocale())`
|
|
146
|
+
* so the current locale is set to user's preferred locale.
|
|
147
|
+
*
|
|
148
|
+
* Note if `persist` is `true` when initializing,
|
|
149
|
+
* subsequence `setLocale` calls will still persist the value.
|
|
150
|
+
*/
|
|
151
|
+
export const clearPersistedLocalePreference = (): void => {
|
|
152
|
+
localStorage.removeItem(KEY);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/** Get the current selected locale */
|
|
156
|
+
export const getLocale = (): string => {
|
|
157
|
+
return locale;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/** Get the default locale when initialized */
|
|
161
|
+
export const getDefaultLocale = (): string => {
|
|
162
|
+
return defaultLocale;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Set the selected locale
|
|
167
|
+
*
|
|
168
|
+
* Returns `false` if the locale is not supported
|
|
169
|
+
*/
|
|
170
|
+
export const setLocale = (newLocale: string): boolean => {
|
|
171
|
+
const supported = convertToSupportedLocale(newLocale);
|
|
172
|
+
if (!supported) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
if (supported === locale) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
locale = supported;
|
|
179
|
+
const len = subscribers.length;
|
|
180
|
+
for (let i = 0; i < len; i++) {
|
|
181
|
+
subscribers[i](locale);
|
|
182
|
+
}
|
|
183
|
+
return true;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Convert a locale/language to a supported locale/language
|
|
188
|
+
*
|
|
189
|
+
* Returns `undefined` if no supported locale is found
|
|
190
|
+
*
|
|
191
|
+
* # Example
|
|
192
|
+
* It will first try to find an exact match for a locale (not language).
|
|
193
|
+
* If not found, it will try:
|
|
194
|
+
* - the first supported locale with a matching language
|
|
195
|
+
* - the first supported language
|
|
196
|
+
* ```typescript
|
|
197
|
+
* import { convertToSupportedLocale } from "@pistonite/pure/pref";
|
|
198
|
+
*
|
|
199
|
+
* // suppose supported locales are ["en", "zh", "zh-CN"]
|
|
200
|
+
* console.log(convertToSupportedLocale("en")); // "en"
|
|
201
|
+
* console.log(convertToSupportedLocale("en-US")); // "en"
|
|
202
|
+
* console.log(convertToSupportedLocale("zh")); // "zh-CN"
|
|
203
|
+
* console.log(convertToSupportedLocale("zh-CN")); // "zh-CN"
|
|
204
|
+
* console.log(convertToSupportedLocale("zh-TW")); // "zh"
|
|
205
|
+
* console.log(convertToSupportedLocale("es")); // undefined
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
export const convertToSupportedLocale = (
|
|
209
|
+
newLocale: string,
|
|
210
|
+
): string | undefined => {
|
|
211
|
+
if (supportedLocales.includes(newLocale)) {
|
|
212
|
+
return newLocale;
|
|
213
|
+
}
|
|
214
|
+
const language = newLocale.split("-", 2)[0];
|
|
215
|
+
const len = supportedLocales.length;
|
|
216
|
+
for (let i = 0; i < len; i++) {
|
|
217
|
+
if (supportedLocales[i].startsWith(language)) {
|
|
218
|
+
return supportedLocales[i];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return undefined;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Convert a locale/language to a supported locale/language,
|
|
226
|
+
* or return the default locale if not found.
|
|
227
|
+
*
|
|
228
|
+
* This is a thin wrapper for `convertToSupportedLocale`.
|
|
229
|
+
* See that function for more details.
|
|
230
|
+
*/
|
|
231
|
+
export const convertToSupportedLocaleOrDefault = (
|
|
232
|
+
newLocale: string,
|
|
233
|
+
): string => {
|
|
234
|
+
return convertToSupportedLocale(newLocale) || defaultLocale;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Add a subscriber to be notified when the locale changes
|
|
239
|
+
*
|
|
240
|
+
* If `notifyImmediately` is `true`, the subscriber will be called immediately with the current locale
|
|
241
|
+
*/
|
|
242
|
+
export const addLocaleSubscriber = (
|
|
243
|
+
fn: (locale: string) => void,
|
|
244
|
+
notifyImmediately?: boolean,
|
|
245
|
+
): void => {
|
|
246
|
+
subscribers.push(fn);
|
|
247
|
+
if (notifyImmediately) {
|
|
248
|
+
fn(locale);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Remove a subscriber from locale changes
|
|
254
|
+
*/
|
|
255
|
+
export const removeLocaleSubscriber = (fn: (locale: string) => void): void => {
|
|
256
|
+
const index = subscribers.indexOf(fn);
|
|
257
|
+
if (index !== -1) {
|
|
258
|
+
subscribers.splice(index, 1);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Language detector plugin for i18next
|
|
264
|
+
*
|
|
265
|
+
* **Must call `initLocale` before initializaing i18next**
|
|
266
|
+
*
|
|
267
|
+
* This also sets the global locale state whenever `i18next.changeLanguage` is called.
|
|
268
|
+
*
|
|
269
|
+
* # Example
|
|
270
|
+
* ```typescript
|
|
271
|
+
* import i18next from "i18next";
|
|
272
|
+
* import { initLocale, detectLocale } from "@pistonite/pure/pref";
|
|
273
|
+
*
|
|
274
|
+
* initLocale({ supported: ["en", "es"], default: "en", persist: true });
|
|
275
|
+
*
|
|
276
|
+
* i18next.use(detectLocale).init({
|
|
277
|
+
* // don't need to specify `lng` here
|
|
278
|
+
*
|
|
279
|
+
* // ...other options not shown
|
|
280
|
+
* });
|
|
281
|
+
* ```
|
|
282
|
+
*
|
|
283
|
+
*/
|
|
284
|
+
export const detectLocale = {
|
|
285
|
+
type: "languageDetector" as const,
|
|
286
|
+
detect: () => locale,
|
|
287
|
+
cacheUserLanguage: (lng: string): void => {
|
|
288
|
+
setLocale(lng);
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Bind the locale state to i18next, so whenever `setLocale`
|
|
294
|
+
* is called, it will also call `i18next.changeLanguage`.
|
|
295
|
+
*
|
|
296
|
+
* # Example
|
|
297
|
+
* ```typescript
|
|
298
|
+
* import i18next from "i18next";
|
|
299
|
+
* import { connectI18next, initLocale } from "@pistonite/pure/pref";
|
|
300
|
+
*
|
|
301
|
+
* initLocale({ supported: ["en", "es"], default: "en", persist: true });
|
|
302
|
+
* i18next.use(connectI18next).init({
|
|
303
|
+
* // ...options
|
|
304
|
+
* });
|
|
305
|
+
*
|
|
306
|
+
*/
|
|
307
|
+
export const connectI18next = {
|
|
308
|
+
type: "3rdParty" as const,
|
|
309
|
+
init: (i18next: any): void => {
|
|
310
|
+
addLocaleSubscriber((locale) => {
|
|
311
|
+
if (i18next.language !== locale) {
|
|
312
|
+
i18next.changeLanguage(locale);
|
|
313
|
+
}
|
|
314
|
+
}, true);
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const localizedLanguageNames = new Map();
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Get the localized name of a language using `Intl.DisplayNames`.
|
|
322
|
+
*
|
|
323
|
+
* The results are interanlly cached, so you don't need to cache this yourself.
|
|
324
|
+
*/
|
|
325
|
+
export const getLocalizedLanguageName = (language: string): string => {
|
|
326
|
+
if (language === "zh" || language === "zh-CN") {
|
|
327
|
+
return "\u7b80\u4f53\u4e2d\u6587";
|
|
328
|
+
}
|
|
329
|
+
if (language === "zh-TW") {
|
|
330
|
+
return "\u7e41\u9ad4\u4e2d\u6587";
|
|
331
|
+
}
|
|
332
|
+
if (localizedLanguageNames.has(language)) {
|
|
333
|
+
return localizedLanguageNames.get(language);
|
|
334
|
+
}
|
|
335
|
+
const languageWithoutLocale = language.split("-")[0];
|
|
336
|
+
const localized = new Intl.DisplayNames([language], {
|
|
337
|
+
type: "language",
|
|
338
|
+
}).of(languageWithoutLocale);
|
|
339
|
+
localizedLanguageNames.set(language, localized);
|
|
340
|
+
return localized || language;
|
|
341
|
+
};
|