react-autolocale 0.0.0-stage → 0.2.0
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 +280 -2
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +138 -0
- package/dist/index.d.ts +120 -0
- package/dist/index.js +265 -0
- package/dist/overrides-ZQ6meLck.js +557 -0
- package/dist/suggestion-UZQ8ngd0.js +52 -0
- package/dist/vite.d.ts +44 -0
- package/dist/vite.js +938 -0
- package/package.json +73 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { i as STORAGE_KEY, n as DISMISSED_KEY, o as redirectTarget, r as suggestLanguage, t as BANNER_EN } from "./suggestion-UZQ8ngd0.js";
|
|
2
|
+
import { Fragment, createContext, createElement, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { banner, languages, loaders, original, originalNative, seo } from "virtual:react-autolocale";
|
|
4
|
+
//#region src/runtime/index.ts
|
|
5
|
+
const hasDom = typeof window !== "undefined" && typeof document !== "undefined";
|
|
6
|
+
const codes = /* @__PURE__ */ new Set([original, ...languages.map((l) => l.code)]);
|
|
7
|
+
const cache = /* @__PURE__ */ new Map();
|
|
8
|
+
const PREFIX = /^\/([a-z]{2,3})(?=\/|$)/i;
|
|
9
|
+
function prefixOf(pathname) {
|
|
10
|
+
const code = PREFIX.exec(pathname)?.[1]?.toLowerCase();
|
|
11
|
+
return code && codes.has(code) ? code : null;
|
|
12
|
+
}
|
|
13
|
+
function pathFor(code) {
|
|
14
|
+
const { pathname, search, hash } = window.location;
|
|
15
|
+
return `/${code}${prefixOf(pathname) ? pathname.replace(PREFIX, "") || "/" : pathname}${search}${hash}`;
|
|
16
|
+
}
|
|
17
|
+
function load(code) {
|
|
18
|
+
const hit = cache.get(code);
|
|
19
|
+
if (hit) return Promise.resolve(hit);
|
|
20
|
+
const loader = loaders[code];
|
|
21
|
+
if (!loader) return Promise.resolve({});
|
|
22
|
+
return loader().then((m) => m.default).catch(() => ({})).then((dict) => (cache.set(code, dict), dict));
|
|
23
|
+
}
|
|
24
|
+
const Context = createContext({
|
|
25
|
+
language: original,
|
|
26
|
+
dict: {},
|
|
27
|
+
setLanguage: async () => {}
|
|
28
|
+
});
|
|
29
|
+
function initialState() {
|
|
30
|
+
if (!hasDom) return {
|
|
31
|
+
language: original,
|
|
32
|
+
dict: {}
|
|
33
|
+
};
|
|
34
|
+
const boot = window.__AUTOSCALE__;
|
|
35
|
+
if (boot) cache.set(boot.lang, boot.dict);
|
|
36
|
+
const { pathname, search, hash } = window.location;
|
|
37
|
+
const fromUrl = prefixOf(pathname);
|
|
38
|
+
if (fromUrl === null) {
|
|
39
|
+
let saved = null;
|
|
40
|
+
try {
|
|
41
|
+
saved = window.localStorage.getItem(STORAGE_KEY);
|
|
42
|
+
} catch {}
|
|
43
|
+
const to = redirectTarget({
|
|
44
|
+
pathname,
|
|
45
|
+
search,
|
|
46
|
+
hash,
|
|
47
|
+
saved,
|
|
48
|
+
browser: window.navigator.languages?.length ? window.navigator.languages : [window.navigator.language],
|
|
49
|
+
codes: [...codes],
|
|
50
|
+
original
|
|
51
|
+
});
|
|
52
|
+
if (to) window.location.replace(to);
|
|
53
|
+
else window.history.replaceState(window.history.state, "", `/${original}${pathname}${search}${hash}`);
|
|
54
|
+
}
|
|
55
|
+
const language = fromUrl ?? original;
|
|
56
|
+
return {
|
|
57
|
+
language,
|
|
58
|
+
dict: language === original ? {} : cache.get(language) ?? {}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Wrap your app once. Language comes from the URL (/fr/...), so every language is a real, static page.
|
|
63
|
+
* The props are read at build time by the Vite plugin.
|
|
64
|
+
*/
|
|
65
|
+
function AutoScale({ children }) {
|
|
66
|
+
const [state, setState] = useState(initialState);
|
|
67
|
+
const apply = useCallback(async (code, push) => {
|
|
68
|
+
if (!codes.has(code)) return;
|
|
69
|
+
if (push && hasDom) {
|
|
70
|
+
try {
|
|
71
|
+
window.localStorage.setItem(STORAGE_KEY, code);
|
|
72
|
+
} catch {}
|
|
73
|
+
if (seo) {
|
|
74
|
+
window.location.assign(pathFor(code));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const dict = code === original ? {} : await load(code);
|
|
79
|
+
setState({
|
|
80
|
+
language: code,
|
|
81
|
+
dict
|
|
82
|
+
});
|
|
83
|
+
if (push && hasDom) window.history.pushState(null, "", pathFor(code));
|
|
84
|
+
}, []);
|
|
85
|
+
const setLanguage = useCallback((code) => apply(code, true), [apply]);
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
document.documentElement.lang = state.language;
|
|
88
|
+
document.documentElement.dir = languages.find((l) => l.code === state.language)?.rtl ? "rtl" : "ltr";
|
|
89
|
+
}, [state.language]);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (state.language !== original && !cache.has(state.language)) apply(state.language, false);
|
|
92
|
+
const onPop = () => void apply(prefixOf(window.location.pathname) ?? original, false);
|
|
93
|
+
window.addEventListener("popstate", onPop);
|
|
94
|
+
return () => window.removeEventListener("popstate", onPop);
|
|
95
|
+
}, [apply]);
|
|
96
|
+
const value = useMemo(() => ({
|
|
97
|
+
...state,
|
|
98
|
+
setLanguage
|
|
99
|
+
}), [state, setLanguage]);
|
|
100
|
+
return createElement(Context.Provider, { value }, children);
|
|
101
|
+
}
|
|
102
|
+
/** @internal Inserted by the Vite plugin in place of static JSX text. */
|
|
103
|
+
function __T({ s }) {
|
|
104
|
+
const { dict } = useContext(Context);
|
|
105
|
+
return createElement(Fragment, null, dict[s] ?? s);
|
|
106
|
+
}
|
|
107
|
+
/** @internal Inserted by the Vite plugin for translated attributes (placeholder, alt, ...). */
|
|
108
|
+
function __useT() {
|
|
109
|
+
const { dict } = useContext(Context);
|
|
110
|
+
return useCallback((s) => dict[s] ?? s, [dict]);
|
|
111
|
+
}
|
|
112
|
+
/** @internal Inserted by the Vite plugin for text with variables, e.g. `Hello {name}`. */
|
|
113
|
+
function __TD({ s, v }) {
|
|
114
|
+
const { dict } = useContext(Context);
|
|
115
|
+
const parts = (dict[s] ?? s).split(/\{(\w+)\}/g).map((part, i) => i % 2 ? v[part] : part);
|
|
116
|
+
return createElement(Fragment, null, ...parts.map((p, i) => createElement(Fragment, { key: i }, p)));
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Pick a value by language: `useLocalized({ en: "/pricing", fr: "/tarifs", default: "/pricing" })`.
|
|
120
|
+
* Falls back to `default`, then to the original language's value.
|
|
121
|
+
*/
|
|
122
|
+
function useLocalized(values) {
|
|
123
|
+
const { language } = useContext(Context);
|
|
124
|
+
return values[language] ?? values.default ?? values[original];
|
|
125
|
+
}
|
|
126
|
+
/** Component form of `useLocalized`: `<Localized en={<A />} fr={<B />} default={<A />} />`. */
|
|
127
|
+
function Localized(props) {
|
|
128
|
+
return createElement(Fragment, null, useLocalized(props));
|
|
129
|
+
}
|
|
130
|
+
/** Content that appears for some languages only, such as a local offer or legal notice. */
|
|
131
|
+
function ForLanguages({ only, except, children }) {
|
|
132
|
+
const { language } = useContext(Context);
|
|
133
|
+
if (only && !only.includes(language)) return null;
|
|
134
|
+
if (except?.includes(language)) return null;
|
|
135
|
+
return createElement(Fragment, null, children);
|
|
136
|
+
}
|
|
137
|
+
/** Numbers, prices and dates formatted for the current language: `format.currency(19.99, "EUR")`. */
|
|
138
|
+
function useFormat() {
|
|
139
|
+
const { language } = useContext(Context);
|
|
140
|
+
return useMemo(() => ({
|
|
141
|
+
language,
|
|
142
|
+
number: (value, options) => new Intl.NumberFormat(language, options).format(value),
|
|
143
|
+
currency: (value, currency, options) => new Intl.NumberFormat(language, {
|
|
144
|
+
style: "currency",
|
|
145
|
+
currency,
|
|
146
|
+
...options
|
|
147
|
+
}).format(value),
|
|
148
|
+
percent: (value, options) => new Intl.NumberFormat(language, {
|
|
149
|
+
style: "percent",
|
|
150
|
+
...options
|
|
151
|
+
}).format(value),
|
|
152
|
+
date: (value, options) => new Intl.DateTimeFormat(language, options ?? { dateStyle: "medium" }).format(new Date(value))
|
|
153
|
+
}), [language]);
|
|
154
|
+
}
|
|
155
|
+
function readDismissed() {
|
|
156
|
+
try {
|
|
157
|
+
const v = JSON.parse(window.localStorage.getItem("react-autolocale:banner-dismissed") ?? "[]");
|
|
158
|
+
return Array.isArray(v) ? v : [];
|
|
159
|
+
} catch {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* For building your own banner. Returns null when there is nothing to suggest: the visitor's browser language
|
|
165
|
+
* is the page's language, is not supported, was dismissed, or they already picked a language.
|
|
166
|
+
*/
|
|
167
|
+
function useLanguageSuggestion() {
|
|
168
|
+
const { language, setLanguage } = useContext(Context);
|
|
169
|
+
const [code, setCode] = useState(null);
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
let chosen = false;
|
|
172
|
+
try {
|
|
173
|
+
chosen = window.localStorage.getItem(STORAGE_KEY) !== null;
|
|
174
|
+
} catch {}
|
|
175
|
+
setCode(suggestLanguage({
|
|
176
|
+
browser: window.navigator.languages?.length ? window.navigator.languages : [window.navigator.language],
|
|
177
|
+
codes: [...codes],
|
|
178
|
+
current: language,
|
|
179
|
+
dismissed: readDismissed(),
|
|
180
|
+
chosen
|
|
181
|
+
}));
|
|
182
|
+
}, [language]);
|
|
183
|
+
if (!code) return null;
|
|
184
|
+
const text = banner[code] ?? BANNER_EN;
|
|
185
|
+
const native = code === original ? originalNative : languages.find((l) => l.code === code)?.native ?? code;
|
|
186
|
+
return {
|
|
187
|
+
language: code,
|
|
188
|
+
native,
|
|
189
|
+
text: text.prompt.split("{language}").join(native),
|
|
190
|
+
switchLabel: text.switch.split("{language}").join(native),
|
|
191
|
+
dismissLabel: text.dismiss.split("{language}").join(native),
|
|
192
|
+
accept: () => void setLanguage(code),
|
|
193
|
+
dismiss: () => {
|
|
194
|
+
try {
|
|
195
|
+
window.localStorage.setItem(DISMISSED_KEY, JSON.stringify([...readDismissed(), code]));
|
|
196
|
+
} catch {}
|
|
197
|
+
setCode(null);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const BAR = {
|
|
202
|
+
position: "fixed",
|
|
203
|
+
left: 0,
|
|
204
|
+
right: 0,
|
|
205
|
+
bottom: 0,
|
|
206
|
+
display: "flex",
|
|
207
|
+
gap: 12,
|
|
208
|
+
alignItems: "center",
|
|
209
|
+
justifyContent: "center",
|
|
210
|
+
padding: "10px 16px",
|
|
211
|
+
background: "#111",
|
|
212
|
+
color: "#fff",
|
|
213
|
+
font: "14px system-ui, sans-serif",
|
|
214
|
+
zIndex: 2147483e3
|
|
215
|
+
};
|
|
216
|
+
/** A ready-made suggestion bar. Style it with `className`/`style`, or pass a render function to build your own. */
|
|
217
|
+
function LanguageBanner({ className, style, children }) {
|
|
218
|
+
const s = useLanguageSuggestion();
|
|
219
|
+
if (!s) return null;
|
|
220
|
+
if (children) return createElement(Fragment, null, children(s));
|
|
221
|
+
return createElement("div", {
|
|
222
|
+
role: "region",
|
|
223
|
+
"aria-label": s.native,
|
|
224
|
+
className,
|
|
225
|
+
style: className ? style : {
|
|
226
|
+
...BAR,
|
|
227
|
+
...style
|
|
228
|
+
}
|
|
229
|
+
}, createElement("span", null, s.text), createElement("button", {
|
|
230
|
+
type: "button",
|
|
231
|
+
onClick: s.accept,
|
|
232
|
+
lang: s.language
|
|
233
|
+
}, s.switchLabel), createElement("button", {
|
|
234
|
+
type: "button",
|
|
235
|
+
onClick: s.dismiss
|
|
236
|
+
}, s.dismissLabel));
|
|
237
|
+
}
|
|
238
|
+
function useLanguage() {
|
|
239
|
+
const { language, setLanguage } = useContext(Context);
|
|
240
|
+
return useMemo(() => ({
|
|
241
|
+
language,
|
|
242
|
+
setLanguage,
|
|
243
|
+
languages: [{
|
|
244
|
+
code: original,
|
|
245
|
+
native: originalNative
|
|
246
|
+
}, ...languages],
|
|
247
|
+
basePath: `/${language}`
|
|
248
|
+
}), [language, setLanguage]);
|
|
249
|
+
}
|
|
250
|
+
/** A dropdown that switches the whole app's language instantly, without a reload. */
|
|
251
|
+
function LanguageSwitcher({ className, style, label = "Language" }) {
|
|
252
|
+
const { language, languages: all, setLanguage } = useLanguage();
|
|
253
|
+
return createElement("select", {
|
|
254
|
+
className,
|
|
255
|
+
style,
|
|
256
|
+
value: language,
|
|
257
|
+
"aria-label": label,
|
|
258
|
+
onChange: (e) => void setLanguage(e.target.value)
|
|
259
|
+
}, all.map((l) => createElement("option", {
|
|
260
|
+
key: l.code,
|
|
261
|
+
value: l.code
|
|
262
|
+
}, l.native)));
|
|
263
|
+
}
|
|
264
|
+
//#endregion
|
|
265
|
+
export { AutoScale, ForLanguages, LanguageBanner, LanguageSwitcher, Localized, __T, __TD, __useT, useFormat, useLanguage, useLanguageSuggestion, useLocalized };
|