react-autolocale 0.0.0-stage → 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Williams Joseph
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,180 @@
1
- # Temporary Holding Version
1
+ # react-autolocale
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Wrap your React + Vite app in one component. `vite build` produces a fully translated static page for every language (`/en/`, `/fr/`, `/es/`, ...) using a free local translation model. No `t()`, no keys, no translation files in your repo, and your source code stays untouched.
4
+
5
+ ## Quick start
6
+
7
+ **1. Install** (npm, yarn or pnpm)
8
+
9
+ ```bash
10
+ npm install react-autolocale # or: yarn add react-autolocale | pnpm add react-autolocale
11
+ ```
12
+
13
+ **2. Add the plugin to your Vite config**
14
+
15
+ ```bash
16
+ npx react-autolocale init # or: yarn react-autolocale init | pnpm exec react-autolocale init
17
+ ```
18
+
19
+ **3. Wrap your app once**
20
+
21
+ ```tsx
22
+ // main.tsx
23
+ import { AutoScale } from "react-autolocale";
24
+
25
+ createRoot(document.getElementById("root")!).render(
26
+ <AutoScale original="en" languages={["fr", "es", "de", "pt"]} exclude={["Acme Cloud"]}>
27
+ <App />
28
+ </AutoScale>,
29
+ );
30
+ ```
31
+
32
+ **4. Build**
33
+
34
+ ```bash
35
+ npm run build # or: yarn build | pnpm build
36
+ npm run preview # optional: check the built site locally
37
+ ```
38
+
39
+ The output folder has one page per language and route (`dist/fr/`, `dist/fr/about/`, ...), deployable anywhere that serves static files. The first build takes a few minutes: it installs the free local translation engine (about 440 MB, once, into `~/.cache/react-autolocale/engine`, shared by all your projects) and downloads the models. Later builds reuse the cache. Prefer to manage the engine yourself? `npm install -D @huggingface/transformers` and it will be used instead.
40
+
41
+ Optional: `npx react-autolocale add switcher` copies an editable language switcher into your project (see below).
42
+
43
+ Write normal JSX; there is no `t()` to call and no translation file to maintain.
44
+
45
+ ## Multi-page apps (React Router)
46
+
47
+ Every page is generated, with nothing to configure. The build finds pages two ways: by following the links in each page, and by reading the fixed routes in your router code (`<Route path="/thank-you">`, nested routes, and `createBrowserRouter` objects). Pages written as `/fr/about/index.html` etc. are created for each language.
48
+
49
+ Dynamic routes (`/users/:id`) can't be known from code, so list the real pages you want with the `routes` option, which can also be a function that fetches them:
50
+
51
+ ```ts
52
+ reactAutolocale({ routes: async () => (await getUsers()).map((u) => `/users/${u.id}`) })
53
+ ```
54
+
55
+ Give your router the language as its basename, and remount it when the language changes:
56
+
57
+ ```tsx
58
+ function Shell() {
59
+ const { basePath, language } = useLanguage();
60
+ return (
61
+ <BrowserRouter basename={basePath} key={language}>
62
+ <Routes>...</Routes>
63
+ </BrowserRouter>
64
+ );
65
+ }
66
+
67
+ <AutoScale original="en" languages={["fr", "es"]}><Shell /></AutoScale>
68
+ ```
69
+
70
+ Your host must serve `/fr/about/index.html` for `/fr/about`, which static hosts do by default.
71
+
72
+ ## Fixing a translation
73
+
74
+ Machine translation sometimes gets short UI words wrong (a nav link "Home" may come back as "Sommaire"). Correct any string in the plugin options; overrides always win:
75
+
76
+ ```ts
77
+ reactAutolocale({ overrides: { fr: { Home: "Accueil" }, es: { Home: "Inicio" } } })
78
+ ```
79
+
80
+ ## Language switcher
81
+
82
+ Switching is just navigation to `/fr/` (instant, no reload; a page load when `seo` is on). You choose the UI:
83
+
84
+ ```bash
85
+ npx react-autolocale add switcher # copies an editable LanguageSwitcher into src/components
86
+ ```
87
+
88
+ or import the ready-made dropdown: `import { LanguageSwitcher } from "react-autolocale"`.
89
+
90
+ Build your own with the hook:
91
+
92
+ ```tsx
93
+ const { language, languages, setLanguage, basePath } = useLanguage();
94
+ ```
95
+
96
+ ## What gets translated
97
+
98
+ Static JSX text, plus `placeholder`, `title`, `alt`, `aria-label` on HTML elements. `exclude` phrases (brand names) are kept as-is, even inside longer sentences.
99
+
100
+ ## Supported languages
101
+
102
+ Use these codes in `languages={[...]}`. The source (`original`) language can be any of them.
103
+
104
+ | Code | Language | Model |
105
+ | --- | --- | --- |
106
+ | `fr` | French | OPUS-MT |
107
+ | `es` | Spanish | OPUS-MT |
108
+ | `de` | German | OPUS-MT |
109
+ | `it` | Italian | OPUS-MT |
110
+ | `nl` | Dutch | OPUS-MT |
111
+ | `ru` | Russian | OPUS-MT |
112
+ | `zh` | Chinese (Simplified) | OPUS-MT |
113
+ | `ar` | Arabic (RTL) | OPUS-MT |
114
+ | `hi` | Hindi | OPUS-MT |
115
+ | `sv` | Swedish | OPUS-MT |
116
+ | `da` | Danish | OPUS-MT |
117
+ | `fi` | Finnish | OPUS-MT |
118
+ | `cs` | Czech | OPUS-MT |
119
+ | `hu` | Hungarian | OPUS-MT |
120
+ | `ro` | Romanian | OPUS-MT |
121
+ | `uk` | Ukrainian | OPUS-MT |
122
+ | `vi` | Vietnamese | OPUS-MT |
123
+ | `id` | Indonesian | OPUS-MT |
124
+ | `pt` | Portuguese | NLLB |
125
+ | `ja` | Japanese | NLLB |
126
+ | `ko` | Korean | NLLB |
127
+ | `tr` | Turkish | NLLB |
128
+ | `pl` | Polish | NLLB |
129
+ | `no` | Norwegian | NLLB |
130
+ | `el` | Greek | NLLB |
131
+ | `he` | Hebrew (RTL) | NLLB |
132
+ | `th` | Thai | NLLB |
133
+ | `bn` | Bengali | NLLB |
134
+ | `fa` | Persian (RTL) | NLLB |
135
+ | `ur` | Urdu (RTL) | NLLB |
136
+ | `bg` | Bulgarian | NLLB |
137
+ | `hr` | Croatian | NLLB |
138
+ | `sk` | Slovak | NLLB |
139
+ | `ca` | Catalan | NLLB |
140
+ | `ms` | Malay | NLLB |
141
+ | `sw` | Swahili | NLLB |
142
+ | `tl` | Tagalog | NLLB |
143
+ | `en` | English (default source) | NLLB when used as a target |
144
+
145
+ OPUS-MT models are small (about 75 MB each) and are used when the source language is English. NLLB-200 is one larger download (about 600 MB) shared by all the languages marked NLLB, and is also used for any non-English source. Right-to-left languages automatically get `dir="rtl"` on the page.
146
+
147
+ ## Translations and caching
148
+
149
+ Translations are generated on first build into `node_modules/.cache/react-autolocale/` and reused; only new or changed strings are translated afterwards. Cache that folder in CI. Models download once into `~/.cache/react-autolocale` (about 75 MB per OPUS-MT language, about 600 MB for NLLB: Japanese, Korean, Portuguese and others). The dev server translates text you add while it runs and reloads.
150
+
151
+ ## Automatic language navigation
152
+
153
+ Whenever a visitor lands on a URL without a language prefix (like `/` or `/about`), they are sent to the page for their saved language, or otherwise their browser language, if you support it: a French browser lands on `/fr/`. A language chosen in the switcher is remembered and wins on later visits; the browser language itself is not saved, it is simply checked each time. Explicit URLs such as `/fr/about/` or `/en/` are never redirected, and unsupported browser languages stay on the original.
154
+
155
+ ## SEO (opt-in)
156
+
157
+ By default react-autolocale does not touch your SEO: the only changes to `<head>` are one inline script holding that page's translations, plus `lang` and `dir` on `<html>`.
158
+
159
+ Turn it on with `seo` on the provider:
160
+
161
+ ```tsx
162
+ <AutoScale original="en" languages={["fr", "es"]} seo siteUrl="https://example.com">
163
+ <App />
164
+ </AutoScale>
165
+ ```
166
+
167
+ With `seo` the build also:
168
+ - translates `<title>` and the description / Open Graph / Twitter meta tags,
169
+ - adds `<link rel="canonical">` and `hreflang` alternates (plus `x-default`) to every page,
170
+ - writes `sitemap.xml` with the alternates (needs `siteUrl`; reference it from your `robots.txt`).
171
+
172
+ With `seo`, switching language navigates to the real page (so its head tags are correct) instead of swapping in place. Brand names in the title are translated like any text, so add them to `exclude`.
173
+
174
+ ## v1 limits
175
+
176
+ - Dynamic content (`Hello {name}`, API data) is not translated; planned for v1.5. Text next to `{expressions}` stays in the original language.
177
+ - Dynamic routes (`/users/:id`) only get pages you list in `routes`, or that are linked.
178
+ - `<AutoScale>` props must be literals so the plugin can read them at build time.
179
+ - Use Vite's default `base: "/"`.
180
+ - Machine translation is draft quality.
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {}
package/dist/cli.js ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ //#region src/cli/viteConfig.ts
5
+ const IMPORT_LINE = `import reactAutolocale from "react-autolocale/vite";`;
6
+ /** Adds the react-autolocale plugin to a vite.config source string. Returns null if it can't be done safely. */
7
+ function patchViteConfig(source) {
8
+ if (source.includes("react-autolocale/vite")) return {
9
+ code: source,
10
+ changed: false
11
+ };
12
+ let code = source;
13
+ if (/plugins\s*:\s*\[/.test(code)) code = code.replace(/plugins\s*:\s*\[/, "plugins: [reactAutolocale(), ");
14
+ else if (/defineConfig\(\s*\{/.test(code)) code = code.replace(/defineConfig\(\s*\{/, "defineConfig({\n plugins: [reactAutolocale()],");
15
+ else if (/export\s+default\s*\{/.test(code)) code = code.replace(/export\s+default\s*\{/, "export default {\n plugins: [reactAutolocale()],");
16
+ else return null;
17
+ const imports = [...code.matchAll(/^import[^\n]*$/gm)];
18
+ const last = imports[imports.length - 1];
19
+ if (last) {
20
+ const at = last.index + last[0].length;
21
+ code = code.slice(0, at) + "\n" + IMPORT_LINE + code.slice(at);
22
+ } else code = IMPORT_LINE + "\n" + code;
23
+ return {
24
+ code,
25
+ changed: true
26
+ };
27
+ }
28
+ //#endregion
29
+ //#region src/cli/index.ts
30
+ const HELP = `react-autolocale
31
+
32
+ Usage:
33
+ react-autolocale init
34
+ Adds the plugin to your vite.config for you.
35
+ react-autolocale add switcher [--dir src/components]
36
+ Copies an editable <LanguageSwitcher /> into your project.
37
+ `;
38
+ const TSX = `import { useLanguage } from "react-autolocale";
39
+
40
+ // This file is yours: change the markup, style it, or replace it entirely.
41
+ export function LanguageSwitcher() {
42
+ const { language, languages, setLanguage } = useLanguage();
43
+ return (
44
+ <div role="group" aria-label="Language">
45
+ {languages.map((l) => (
46
+ <button
47
+ key={l.code}
48
+ lang={l.code}
49
+ aria-pressed={l.code === language}
50
+ onClick={() => setLanguage(l.code)}
51
+ >
52
+ {l.native}
53
+ </button>
54
+ ))}
55
+ </div>
56
+ );
57
+ }
58
+ `;
59
+ const JSX = TSX;
60
+ function flag(args, name) {
61
+ const i = args.indexOf(name);
62
+ return i >= 0 ? args[i + 1] : void 0;
63
+ }
64
+ const VITE_CONFIGS = [
65
+ "vite.config.ts",
66
+ "vite.config.mts",
67
+ "vite.config.js",
68
+ "vite.config.mjs"
69
+ ];
70
+ const NEXT_STEP = `
71
+ Next, wrap your app once (for example in main.tsx):
72
+
73
+ import { AutoScale } from "react-autolocale";
74
+
75
+ <AutoScale original="en" languages={["fr", "es"]}>
76
+ <App />
77
+ </AutoScale>
78
+
79
+ Then run your build.`;
80
+ function init(root) {
81
+ const file = VITE_CONFIGS.map((f) => path.join(root, f)).find((f) => fs.existsSync(f));
82
+ if (!file) throw new Error("No vite.config found. This package works with Vite projects.");
83
+ const name = path.basename(file);
84
+ const patched = patchViteConfig(fs.readFileSync(file, "utf8"));
85
+ if (!patched) throw new Error(`Could not edit ${name} automatically. Add this yourself:\n import reactAutolocale from "react-autolocale/vite";\n plugins: [reactAutolocale(), ...]`);
86
+ if (patched.changed) fs.writeFileSync(file, patched.code);
87
+ console.log(patched.changed ? `Added the react-autolocale plugin to ${name}.` : `${name} already uses react-autolocale.`);
88
+ console.log(NEXT_STEP);
89
+ }
90
+ function addSwitcher(root, args) {
91
+ const dir = path.resolve(root, flag(args, "--dir") ?? (fs.existsSync(path.join(root, "src")) ? "src/components" : "components"));
92
+ const ts = fs.existsSync(path.join(root, "tsconfig.json"));
93
+ const file = path.join(dir, ts ? "LanguageSwitcher.tsx" : "LanguageSwitcher.jsx");
94
+ if (fs.existsSync(file)) throw new Error(`${path.relative(root, file)} already exists.`);
95
+ fs.mkdirSync(dir, { recursive: true });
96
+ fs.writeFileSync(file, ts ? TSX : JSX);
97
+ console.log(`Created ${path.relative(root, file)}\nImport it anywhere: import { LanguageSwitcher } from "./components/LanguageSwitcher";`);
98
+ }
99
+ try {
100
+ const [cmd, what, ...rest] = process.argv.slice(2);
101
+ if (cmd === "init") init(process.cwd());
102
+ else if (cmd === "add" && what === "switcher") addSwitcher(process.cwd(), rest);
103
+ else {
104
+ console.log(HELP);
105
+ if (cmd) process.exitCode = 1;
106
+ }
107
+ } catch (err) {
108
+ console.error(`react-autolocale: ${err.message}`);
109
+ process.exit(1);
110
+ }
111
+ //#endregion
@@ -0,0 +1,61 @@
1
+ import { CSSProperties, ReactElement, ReactNode } from "react";
2
+ //#region src/runtime/index.d.ts
3
+ type Dict = Record<string, string>;
4
+ declare global {
5
+ interface Window {
6
+ /** Written into every generated page so the first render already has its translations. */
7
+ __AUTOSCALE__?: {
8
+ lang: string;
9
+ dict: Dict;
10
+ };
11
+ }
12
+ }
13
+ export interface AutoScaleProps {
14
+ /** Language the app is written in, e.g. "en". Must be a literal so the build can read it. */
15
+ original: string;
16
+ /** Languages to generate, e.g. ["fr", "es"]. Must be a literal. */
17
+ languages: string[];
18
+ /** Phrases that must never be translated, such as brand names. Must be a literal. */
19
+ exclude?: string[];
20
+ /**
21
+ * Opt in to SEO changes at build time: translated title/meta tags, canonical and hreflang links,
22
+ * and a sitemap. Off by default: nothing in <head> is touched. Must be a literal.
23
+ */
24
+ seo?: boolean | "true";
25
+ /** Public site URL (e.g. "https://example.com"); used by `seo` for absolute links and the sitemap. */
26
+ siteUrl?: string;
27
+ children?: ReactNode;
28
+ }
29
+ /**
30
+ * Wrap your app once. Language comes from the URL (/fr/...), so every language is a real, static page.
31
+ * The props are read at build time by the Vite plugin.
32
+ */
33
+ export declare function AutoScale({ children }: AutoScaleProps): ReactElement;
34
+ /** @internal Inserted by the Vite plugin in place of static JSX text. */
35
+ export declare function __T({ s }: {
36
+ s: string;
37
+ }): ReactElement;
38
+ /** @internal Inserted by the Vite plugin for translated attributes (placeholder, alt, ...). */
39
+ export declare function __useT(): (s: string) => string;
40
+ export interface LanguageApi {
41
+ language: string;
42
+ /** Switches language and updates the URL; resolves once translations are loaded. */
43
+ setLanguage: (code: string) => Promise<void>;
44
+ /** All languages, original first. */
45
+ languages: {
46
+ code: string;
47
+ native: string;
48
+ }[];
49
+ /** "/fr" - pass to your router's basename if you use client-side routing. */
50
+ basePath: string;
51
+ }
52
+ export declare function useLanguage(): LanguageApi;
53
+ export interface LanguageSwitcherProps {
54
+ className?: string;
55
+ style?: CSSProperties;
56
+ /** Accessible label for the dropdown. */
57
+ label?: string;
58
+ }
59
+ /** A dropdown that switches the whole app's language instantly, without a reload. */
60
+ export declare function LanguageSwitcher({ className, style, label }: LanguageSwitcherProps): ReactElement;
61
+ //#endregion
package/dist/index.js ADDED
@@ -0,0 +1,136 @@
1
+ import { r as redirectTarget, t as STORAGE_KEY } from "./redirect-CKNY4ru5.js";
2
+ import { Fragment, createContext, createElement, useCallback, useContext, useEffect, useMemo, useState } from "react";
3
+ import { 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
+ const language = boot?.lang ?? prefixOf(window.location.pathname) ?? original;
36
+ if (boot) cache.set(boot.lang, boot.dict);
37
+ return {
38
+ language,
39
+ dict: language === original ? {} : cache.get(language) ?? {}
40
+ };
41
+ }
42
+ /**
43
+ * Wrap your app once. Language comes from the URL (/fr/...), so every language is a real, static page.
44
+ * The props are read at build time by the Vite plugin.
45
+ */
46
+ function AutoScale({ children }) {
47
+ const [state, setState] = useState(initialState);
48
+ const apply = useCallback(async (code, push) => {
49
+ if (!codes.has(code)) return;
50
+ if (push && hasDom) {
51
+ try {
52
+ window.localStorage.setItem(STORAGE_KEY, code);
53
+ } catch {}
54
+ if (seo) {
55
+ window.location.assign(pathFor(code));
56
+ return;
57
+ }
58
+ }
59
+ const dict = code === original ? {} : await load(code);
60
+ setState({
61
+ language: code,
62
+ dict
63
+ });
64
+ if (push && hasDom) window.history.pushState(null, "", pathFor(code));
65
+ }, []);
66
+ const setLanguage = useCallback((code) => apply(code, true), [apply]);
67
+ useEffect(() => {
68
+ document.documentElement.lang = state.language;
69
+ document.documentElement.dir = languages.find((l) => l.code === state.language)?.rtl ? "rtl" : "ltr";
70
+ }, [state.language]);
71
+ useEffect(() => {
72
+ let saved = null;
73
+ try {
74
+ saved = window.localStorage.getItem(STORAGE_KEY);
75
+ } catch {}
76
+ const to = redirectTarget({
77
+ pathname: window.location.pathname,
78
+ search: window.location.search,
79
+ hash: window.location.hash,
80
+ saved,
81
+ browser: window.navigator.languages?.length ? window.navigator.languages : [window.navigator.language],
82
+ codes: [...codes],
83
+ original
84
+ });
85
+ if (to) window.location.replace(to);
86
+ }, []);
87
+ useEffect(() => {
88
+ if (state.language !== original && !cache.has(state.language)) apply(state.language, false);
89
+ const onPop = () => void apply(prefixOf(window.location.pathname) ?? original, false);
90
+ window.addEventListener("popstate", onPop);
91
+ return () => window.removeEventListener("popstate", onPop);
92
+ }, [apply]);
93
+ const value = useMemo(() => ({
94
+ ...state,
95
+ setLanguage
96
+ }), [state, setLanguage]);
97
+ return createElement(Context.Provider, { value }, children);
98
+ }
99
+ /** @internal Inserted by the Vite plugin in place of static JSX text. */
100
+ function __T({ s }) {
101
+ const { dict } = useContext(Context);
102
+ return createElement(Fragment, null, dict[s] ?? s);
103
+ }
104
+ /** @internal Inserted by the Vite plugin for translated attributes (placeholder, alt, ...). */
105
+ function __useT() {
106
+ const { dict } = useContext(Context);
107
+ return useCallback((s) => dict[s] ?? s, [dict]);
108
+ }
109
+ function useLanguage() {
110
+ const { language, setLanguage } = useContext(Context);
111
+ return useMemo(() => ({
112
+ language,
113
+ setLanguage,
114
+ languages: [{
115
+ code: original,
116
+ native: originalNative
117
+ }, ...languages],
118
+ basePath: `/${language}`
119
+ }), [language, setLanguage]);
120
+ }
121
+ /** A dropdown that switches the whole app's language instantly, without a reload. */
122
+ function LanguageSwitcher({ className, style, label = "Language" }) {
123
+ const { language, languages: all, setLanguage } = useLanguage();
124
+ return createElement("select", {
125
+ className,
126
+ style,
127
+ value: language,
128
+ "aria-label": label,
129
+ onChange: (e) => void setLanguage(e.target.value)
130
+ }, all.map((l) => createElement("option", {
131
+ key: l.code,
132
+ value: l.code
133
+ }, l.native)));
134
+ }
135
+ //#endregion
136
+ export { AutoScale, LanguageSwitcher, __T, __useT, useLanguage };
@@ -0,0 +1,26 @@
1
+ //#region src/core/redirect.ts
2
+ /**
3
+ * Where should a visitor on an un-prefixed URL (like "/" or "/about") be sent?
4
+ * Saved choice wins, then the browser's language list. Returns null to stay put.
5
+ */
6
+ function redirectTarget(opts) {
7
+ const { pathname, codes, original } = opts;
8
+ const prefix = /^\/([a-z]{2,3})(?=\/|$)/i.exec(pathname)?.[1]?.toLowerCase();
9
+ if (prefix && codes.includes(prefix)) return null;
10
+ let target = opts.saved && codes.includes(opts.saved) ? opts.saved : null;
11
+ for (const tag of opts.browser) {
12
+ if (target) break;
13
+ const lower = String(tag).toLowerCase();
14
+ if (codes.includes(lower)) target = lower;
15
+ else if (codes.includes(lower.split("-")[0])) target = lower.split("-")[0];
16
+ }
17
+ if (!target || target === original) return null;
18
+ return `/${target}${pathname}${opts.search ?? ""}${opts.hash ?? ""}`;
19
+ }
20
+ const STORAGE_KEY = "react-autolocale:language";
21
+ /** The same logic as a tiny inline script, so the root page redirects before anything paints. */
22
+ function redirectScript(codes, original) {
23
+ return `(function(){try{var C=${JSON.stringify(codes)},O=${JSON.stringify(original)},p=location.pathname,m=/^\\/([a-z]{2,3})(?=\\/|$)/i.exec(p);if(m&&C.indexOf(m[1].toLowerCase())>-1)return;var t=null,s=localStorage.getItem(${JSON.stringify(STORAGE_KEY)});if(s&&C.indexOf(s)>-1)t=s;var l=navigator.languages||[navigator.language];for(var i=0;i<l.length&&!t;i++){var c=String(l[i]).toLowerCase();if(C.indexOf(c)>-1)t=c;else if(C.indexOf(c.split("-")[0])>-1)t=c.split("-")[0]}if(t&&t!==O)location.replace("/"+t+p+location.search+location.hash)}catch(e){}})()`;
24
+ }
25
+ //#endregion
26
+ export { redirectScript as n, redirectTarget as r, STORAGE_KEY as t };
package/dist/vite.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { Plugin } from "vite";
2
+ //#region src/vite/index.d.ts
3
+ export interface ReactAutolocaleOptions {
4
+ /** Folders (relative to the project root) scanned for JSX. Defaults to "src". */
5
+ include?: string[];
6
+ /** Fix individual translations by hand, e.g. { fr: { Contact: "Contact" } }. */
7
+ overrides?: Record<string, Record<string, string>>;
8
+ /**
9
+ * Extra pages to build, mainly dynamic ones such as "/users/1" (a function can fetch them). Pages reached
10
+ * by links and fixed routes written in your router (<Route path="/thank-you">) are found automatically.
11
+ */
12
+ routes?: string[] | (() => string[] | Promise<string[]>);
13
+ }
14
+ export default function reactAutolocale(options?: ReactAutolocaleOptions): Plugin;
15
+ //#endregion
16
+ export { reactAutolocale };