react-autolocale 0.1.0 → 0.3.0-beta.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.
@@ -0,0 +1,21 @@
1
+ //#region src/core/config.ts
2
+ /** Typed helper for auto-locale.config.ts. */
3
+ function defineConfig(config) {
4
+ return config;
5
+ }
6
+ const PROVIDER_NAMES = [
7
+ "local",
8
+ "ollama",
9
+ "openai",
10
+ "anthropic"
11
+ ];
12
+ const DEFAULT_MODELS = {
13
+ openai: "gpt-6-astra",
14
+ anthropic: "claude-opus-5-5"
15
+ };
16
+ const DEFAULT_KEY_ENV = {
17
+ openai: "OPENAI_API_KEY",
18
+ anthropic: "ANTHROPIC_API_KEY"
19
+ };
20
+ //#endregion
21
+ export { defineConfig as i, DEFAULT_MODELS as n, PROVIDER_NAMES as r, DEFAULT_KEY_ENV as t };
@@ -0,0 +1,56 @@
1
+ import { n as Translator, t as TranslateFn } from "./translate-COrJX1KS.js";
2
+ //#region src/core/config.d.ts
3
+ /** The free local models (OPUS-MT / NLLB-200 through Transformers.js). Downloads happen only on `translate`. */
4
+ interface LocalProvider {
5
+ name: "local";
6
+ }
7
+ /** A local LLM served by Ollama. */
8
+ interface OllamaProvider {
9
+ name: "ollama";
10
+ model: string;
11
+ url?: string;
12
+ }
13
+ /** OpenAI through the official `openai` package (install it in your project). */
14
+ interface OpenAIProvider {
15
+ name: "openai";
16
+ /** Defaults to "gpt-6-astra". */
17
+ model?: string;
18
+ /** Environment variable holding the key. Defaults to OPENAI_API_KEY. */
19
+ apiKeyEnv?: string;
20
+ /** Custom endpoint, e.g. an Azure or self-hosted OpenAI-compatible gateway. */
21
+ baseURL?: string;
22
+ }
23
+ /** Anthropic Claude through the official `@anthropic-ai/sdk` package (install it in your project). */
24
+ interface AnthropicProvider {
25
+ name: "anthropic";
26
+ /** Defaults to "claude-opus-5-5". */
27
+ model?: string;
28
+ /** Environment variable holding the key. Defaults to ANTHROPIC_API_KEY. */
29
+ apiKeyEnv?: string;
30
+ /** How much the model thinks per request. Short UI strings rarely need more than "low" (the default). */
31
+ effort?: "low" | "medium" | "high" | "xhigh" | "max";
32
+ }
33
+ type ProviderConfig = LocalProvider | OllamaProvider | OpenAIProvider | AnthropicProvider;
34
+ interface AutoLocaleConfig {
35
+ /** Language the source code is written in, e.g. "en". */
36
+ sourceLocale?: string;
37
+ /** Every language the site ships in. The source language may be listed too. */
38
+ locales?: string[];
39
+ /**
40
+ * Where catalogs are written, relative to the project root. Defaults to `src/generated/auto-locale` when the
41
+ * project has a root-level `src/`, otherwise `generated/auto-locale`.
42
+ */
43
+ output?: string;
44
+ /** Folders scanned for JSX. Defaults to "src" (or the project root when there is no src/). */
45
+ include?: string[];
46
+ /** Phrases never translated (brand and product names). Adds to `<AutoScale exclude>`. */
47
+ exclude?: string[];
48
+ /** Terms that must always translate a given way: { fr: { Roadmap: "Feuille de route" } }. */
49
+ glossary?: Record<string, Record<string, string>>;
50
+ /** Translation engine used by `react-autolocale translate`. Defaults to the local models. */
51
+ provider?: ProviderConfig | Translator | TranslateFn;
52
+ }
53
+ /** Typed helper for auto-locale.config.ts. */
54
+ export declare function defineConfig(config: AutoLocaleConfig): AutoLocaleConfig;
55
+ //#endregion
56
+ export type { AnthropicProvider, AutoLocaleConfig, LocalProvider, OllamaProvider, OpenAIProvider, ProviderConfig, TranslateFn, Translator };
package/dist/config.js ADDED
@@ -0,0 +1 @@
1
+ export { i as defineConfig } from "./config-CYHyGpdX.js";
package/dist/index.d.ts CHANGED
@@ -31,12 +31,73 @@ export interface AutoScaleProps {
31
31
  * The props are read at build time by the Vite plugin.
32
32
  */
33
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 }: {
34
+ /** @internal Inserted by the Vite plugin in place of static JSX text: message id and source text (the fallback). */
35
+ export declare function __T({ i, s }: {
36
+ i: string;
36
37
  s: string;
37
38
  }): ReactElement;
38
39
  /** @internal Inserted by the Vite plugin for translated attributes (placeholder, alt, ...). */
39
- export declare function __useT(): (s: string) => string;
40
+ export declare function __useT(): (i: string, s: string) => string;
41
+ /** @internal Inserted by the Vite plugin for text with variables, e.g. `Hello {name}`. */
42
+ export declare function __TD({ i, s, v }: {
43
+ i: string;
44
+ s: string;
45
+ v: Record<string, unknown>;
46
+ }): ReactElement;
47
+ /**
48
+ * Pick a value by language: `useLocalized({ en: "/pricing", fr: "/tarifs", default: "/pricing" })`.
49
+ * Falls back to `default`, then to the original language's value.
50
+ */
51
+ export declare function useLocalized<T>(values: Record<string, T | undefined> & {
52
+ default?: T;
53
+ }): T;
54
+ /** Component form of `useLocalized`: `<Localized en={<A />} fr={<B />} default={<A />} />`. */
55
+ export declare function Localized(props: Record<string, ReactNode>): ReactElement;
56
+ export interface ForLanguagesProps {
57
+ /** Show only for these languages. */
58
+ only?: string[];
59
+ /** Show for every language except these. */
60
+ except?: string[];
61
+ children?: ReactNode;
62
+ }
63
+ /** Content that appears for some languages only, such as a local offer or legal notice. */
64
+ export declare function ForLanguages({ only, except, children }: ForLanguagesProps): ReactElement | null;
65
+ export interface Format {
66
+ language: string;
67
+ number: (value: number, options?: Intl.NumberFormatOptions) => string;
68
+ currency: (value: number, currency: string, options?: Intl.NumberFormatOptions) => string;
69
+ percent: (value: number, options?: Intl.NumberFormatOptions) => string;
70
+ date: (value: Date | number | string, options?: Intl.DateTimeFormatOptions) => string;
71
+ }
72
+ /** Numbers, prices and dates formatted for the current language: `format.currency(19.99, "EUR")`. */
73
+ export declare function useFormat(): Format;
74
+ export interface LanguageSuggestion {
75
+ /** Code of the suggested language, e.g. "fr". */
76
+ language: string;
77
+ /** The suggested language's own name, e.g. "Français". */
78
+ native: string;
79
+ /** Full sentence in the visitor's language, e.g. "This page is also available in Français. Switch?" */
80
+ text: string;
81
+ switchLabel: string;
82
+ dismissLabel: string;
83
+ /** Go to this page in the suggested language. */
84
+ accept: () => void;
85
+ /** Hide the suggestion and don't offer this language again. */
86
+ dismiss: () => void;
87
+ }
88
+ /**
89
+ * For building your own banner. Returns null when there is nothing to suggest: the visitor's browser language
90
+ * is the page's language, is not supported, was dismissed, or they already picked a language.
91
+ */
92
+ export declare function useLanguageSuggestion(): LanguageSuggestion | null;
93
+ export interface LanguageBannerProps {
94
+ className?: string;
95
+ style?: CSSProperties;
96
+ /** Replace the markup entirely: `{(s) => <MyBar>{s.text}</MyBar>}`. */
97
+ children?: (suggestion: LanguageSuggestion) => ReactNode;
98
+ }
99
+ /** A ready-made suggestion bar. Style it with `className`/`style`, or pass a render function to build your own. */
100
+ export declare function LanguageBanner({ className, style, children }: LanguageBannerProps): ReactElement | null;
40
101
  export interface LanguageApi {
41
102
  language: string;
42
103
  /** Switches language and updates the URL; resolves once translations are loaded. */
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
- import { r as redirectTarget, t as STORAGE_KEY } from "./redirect-CKNY4ru5.js";
1
+ import { r as redirectTarget, t as STORAGE_KEY } from "./redirect-pDFblLdm.js";
2
+ import { n as DISMISSED_KEY, r as suggestLanguage, t as BANNER_EN } from "./suggestion-CkZQqMvZ.js";
2
3
  import { Fragment, createContext, createElement, useCallback, useContext, useEffect, useMemo, useState } from "react";
3
- import { languages, loaders, original, originalNative, seo } from "virtual:react-autolocale";
4
+ import { banner, languages, loaders, original, originalNative, seo } from "virtual:react-autolocale";
4
5
  //#region src/runtime/index.ts
5
6
  const hasDom = typeof window !== "undefined" && typeof document !== "undefined";
6
7
  const codes = /* @__PURE__ */ new Set([original, ...languages.map((l) => l.code)]);
@@ -32,8 +33,27 @@ function initialState() {
32
33
  dict: {}
33
34
  };
34
35
  const boot = window.__AUTOSCALE__;
35
- const language = boot?.lang ?? prefixOf(window.location.pathname) ?? original;
36
36
  if (boot) cache.set(boot.lang, boot.dict);
37
+ const { pathname, search, hash } = window.location;
38
+ const fromUrl = prefixOf(pathname);
39
+ if (fromUrl === null) {
40
+ let saved = null;
41
+ try {
42
+ saved = window.localStorage.getItem(STORAGE_KEY);
43
+ } catch {}
44
+ const to = redirectTarget({
45
+ pathname,
46
+ search,
47
+ hash,
48
+ saved,
49
+ browser: window.navigator.languages?.length ? window.navigator.languages : [window.navigator.language],
50
+ codes: [...codes],
51
+ original
52
+ });
53
+ if (to) window.location.replace(to);
54
+ else window.history.replaceState(window.history.state, "", `/${original}${pathname}${search}${hash}`);
55
+ }
56
+ const language = fromUrl ?? original;
37
57
  return {
38
58
  language,
39
59
  dict: language === original ? {} : cache.get(language) ?? {}
@@ -68,22 +88,6 @@ function AutoScale({ children }) {
68
88
  document.documentElement.lang = state.language;
69
89
  document.documentElement.dir = languages.find((l) => l.code === state.language)?.rtl ? "rtl" : "ltr";
70
90
  }, [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
91
  useEffect(() => {
88
92
  if (state.language !== original && !cache.has(state.language)) apply(state.language, false);
89
93
  const onPop = () => void apply(prefixOf(window.location.pathname) ?? original, false);
@@ -96,15 +100,141 @@ function AutoScale({ children }) {
96
100
  }), [state, setLanguage]);
97
101
  return createElement(Context.Provider, { value }, children);
98
102
  }
99
- /** @internal Inserted by the Vite plugin in place of static JSX text. */
100
- function __T({ s }) {
103
+ /** @internal Inserted by the Vite plugin in place of static JSX text: message id and source text (the fallback). */
104
+ function __T({ i, s }) {
101
105
  const { dict } = useContext(Context);
102
- return createElement(Fragment, null, dict[s] ?? s);
106
+ return createElement(Fragment, null, dict[i] || s);
103
107
  }
104
108
  /** @internal Inserted by the Vite plugin for translated attributes (placeholder, alt, ...). */
105
109
  function __useT() {
106
110
  const { dict } = useContext(Context);
107
- return useCallback((s) => dict[s] ?? s, [dict]);
111
+ return useCallback((i, s) => dict[i] || s, [dict]);
112
+ }
113
+ /** @internal Inserted by the Vite plugin for text with variables, e.g. `Hello {name}`. */
114
+ function __TD({ i, s, v }) {
115
+ const { dict } = useContext(Context);
116
+ const parts = (dict[i] || s).split(/\{(\w+)\}/g).map((part, i) => i % 2 ? v[part] : part);
117
+ return createElement(Fragment, null, ...parts.map((p, i) => createElement(Fragment, { key: i }, p)));
118
+ }
119
+ /**
120
+ * Pick a value by language: `useLocalized({ en: "/pricing", fr: "/tarifs", default: "/pricing" })`.
121
+ * Falls back to `default`, then to the original language's value.
122
+ */
123
+ function useLocalized(values) {
124
+ const { language } = useContext(Context);
125
+ return values[language] ?? values.default ?? values[original];
126
+ }
127
+ /** Component form of `useLocalized`: `<Localized en={<A />} fr={<B />} default={<A />} />`. */
128
+ function Localized(props) {
129
+ return createElement(Fragment, null, useLocalized(props));
130
+ }
131
+ /** Content that appears for some languages only, such as a local offer or legal notice. */
132
+ function ForLanguages({ only, except, children }) {
133
+ const { language } = useContext(Context);
134
+ if (only && !only.includes(language)) return null;
135
+ if (except?.includes(language)) return null;
136
+ return createElement(Fragment, null, children);
137
+ }
138
+ /** Numbers, prices and dates formatted for the current language: `format.currency(19.99, "EUR")`. */
139
+ function useFormat() {
140
+ const { language } = useContext(Context);
141
+ return useMemo(() => ({
142
+ language,
143
+ number: (value, options) => new Intl.NumberFormat(language, options).format(value),
144
+ currency: (value, currency, options) => new Intl.NumberFormat(language, {
145
+ style: "currency",
146
+ currency,
147
+ ...options
148
+ }).format(value),
149
+ percent: (value, options) => new Intl.NumberFormat(language, {
150
+ style: "percent",
151
+ ...options
152
+ }).format(value),
153
+ date: (value, options) => new Intl.DateTimeFormat(language, options ?? { dateStyle: "medium" }).format(new Date(value))
154
+ }), [language]);
155
+ }
156
+ function readDismissed() {
157
+ try {
158
+ const v = JSON.parse(window.localStorage.getItem("react-autolocale:banner-dismissed") ?? "[]");
159
+ return Array.isArray(v) ? v : [];
160
+ } catch {
161
+ return [];
162
+ }
163
+ }
164
+ /**
165
+ * For building your own banner. Returns null when there is nothing to suggest: the visitor's browser language
166
+ * is the page's language, is not supported, was dismissed, or they already picked a language.
167
+ */
168
+ function useLanguageSuggestion() {
169
+ const { language, setLanguage } = useContext(Context);
170
+ const [code, setCode] = useState(null);
171
+ useEffect(() => {
172
+ let chosen = false;
173
+ try {
174
+ chosen = window.localStorage.getItem(STORAGE_KEY) !== null;
175
+ } catch {}
176
+ setCode(suggestLanguage({
177
+ browser: window.navigator.languages?.length ? window.navigator.languages : [window.navigator.language],
178
+ codes: [...codes],
179
+ current: language,
180
+ dismissed: readDismissed(),
181
+ chosen
182
+ }));
183
+ }, [language]);
184
+ if (!code) return null;
185
+ const text = banner[code] ?? BANNER_EN;
186
+ const native = code === original ? originalNative : languages.find((l) => l.code === code)?.native ?? code;
187
+ return {
188
+ language: code,
189
+ native,
190
+ text: text.prompt.split("{language}").join(native),
191
+ switchLabel: text.switch.split("{language}").join(native),
192
+ dismissLabel: text.dismiss.split("{language}").join(native),
193
+ accept: () => void setLanguage(code),
194
+ dismiss: () => {
195
+ try {
196
+ window.localStorage.setItem(DISMISSED_KEY, JSON.stringify([...readDismissed(), code]));
197
+ } catch {}
198
+ setCode(null);
199
+ }
200
+ };
201
+ }
202
+ const BAR = {
203
+ position: "fixed",
204
+ left: 0,
205
+ right: 0,
206
+ bottom: 0,
207
+ display: "flex",
208
+ gap: 12,
209
+ alignItems: "center",
210
+ justifyContent: "center",
211
+ padding: "10px 16px",
212
+ background: "#111",
213
+ color: "#fff",
214
+ font: "14px system-ui, sans-serif",
215
+ zIndex: 2147483e3
216
+ };
217
+ /** A ready-made suggestion bar. Style it with `className`/`style`, or pass a render function to build your own. */
218
+ function LanguageBanner({ className, style, children }) {
219
+ const s = useLanguageSuggestion();
220
+ if (!s) return null;
221
+ if (children) return createElement(Fragment, null, children(s));
222
+ return createElement("div", {
223
+ role: "region",
224
+ "aria-label": s.native,
225
+ className,
226
+ style: className ? style : {
227
+ ...BAR,
228
+ ...style
229
+ }
230
+ }, createElement("span", null, s.text), createElement("button", {
231
+ type: "button",
232
+ onClick: s.accept,
233
+ lang: s.language
234
+ }, s.switchLabel), createElement("button", {
235
+ type: "button",
236
+ onClick: s.dismiss
237
+ }, s.dismissLabel));
108
238
  }
109
239
  function useLanguage() {
110
240
  const { language, setLanguage } = useContext(Context);
@@ -133,4 +263,4 @@ function LanguageSwitcher({ className, style, label = "Language" }) {
133
263
  }, l.native)));
134
264
  }
135
265
  //#endregion
136
- export { AutoScale, LanguageSwitcher, __T, __useT, useLanguage };
266
+ export { AutoScale, ForLanguages, LanguageBanner, LanguageSwitcher, Localized, __T, __TD, __useT, useFormat, useLanguage, useLanguageSuggestion, useLocalized };