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 +21 -0
- package/README.md +179 -2
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +111 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +136 -0
- package/dist/redirect-CKNY4ru5.js +26 -0
- package/dist/vite.d.ts +16 -0
- package/dist/vite.js +1242 -0
- package/package.json +71 -4
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
|
-
#
|
|
1
|
+
# react-autolocale
|
|
2
2
|
|
|
3
|
-
|
|
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
|
package/dist/index.d.ts
ADDED
|
@@ -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 };
|