solid-translate 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.
@@ -0,0 +1,239 @@
1
+ import { JSX } from 'solid-js';
2
+ import { LanguageModelV1 } from 'ai';
3
+
4
+ /** Configuration for the solid-translate Vite plugin */
5
+ interface SolidTranslatePluginConfig {
6
+ /** Source locale code (default: "en") */
7
+ sourceLocale?: string;
8
+ /** Target locale codes to translate into */
9
+ targetLocales: string[];
10
+ /** Directory containing locale JSON files, relative to project root (default: "./src/locales") */
11
+ localesDir?: string;
12
+ /** AI model to use for translations (any Vercel AI SDK LanguageModelV1) */
13
+ model: LanguageModelV1;
14
+ /** Custom system prompt for the AI translator */
15
+ systemPrompt?: string;
16
+ /** Max keys per API call (default: 50) */
17
+ batchSize?: number;
18
+ /**
19
+ * Automatically extract translatable strings from source files.
20
+ * Scans for `<T>` components and `msg()` calls, then writes
21
+ * discovered strings into the source locale JSON file.
22
+ */
23
+ autoExtract?: boolean;
24
+ /**
25
+ * Glob patterns for files to scan during auto-extraction.
26
+ * Default: `["src/**\/*.tsx", "src/**\/*.ts", "src/**\/*.jsx"]`
27
+ */
28
+ include?: string[];
29
+ }
30
+ /** A flat dictionary mapping keys to translated strings */
31
+ type TranslationDictionary = Record<string, string>;
32
+ /** All translations keyed by locale code */
33
+ type Translations = Record<string, TranslationDictionary>;
34
+
35
+ interface TranslationContextValue {
36
+ /** Current locale as a reactive signal */
37
+ locale: () => string;
38
+ /** Switch to a different locale */
39
+ setLocale: (locale: string) => void;
40
+ /** Translate a key with optional interpolation params */
41
+ t: (key: string, params?: Record<string, string | number>) => string;
42
+ /** The source locale code */
43
+ sourceLocale: string;
44
+ /** All available locale codes (reactive) */
45
+ availableLocales: () => string[];
46
+ /** Raw translations object */
47
+ translations: Translations;
48
+ }
49
+
50
+ interface VarProps {
51
+ /** Optional name for the variable (used as placeholder in templates) */
52
+ name?: string;
53
+ children: JSX.Element;
54
+ }
55
+ /**
56
+ * Marks content as untranslatable. When used inside `<T>`, the content
57
+ * is preserved as-is while surrounding text is translated.
58
+ *
59
+ * ```tsx
60
+ * <T>Hello <Var>{userName()}</Var>, welcome!</T>
61
+ * ```
62
+ */
63
+ declare function Var(props: VarProps): JSX.Element;
64
+ interface NumProps {
65
+ /** The number to format */
66
+ children: number;
67
+ /** Intl.NumberFormat options */
68
+ options?: Intl.NumberFormatOptions;
69
+ }
70
+ /**
71
+ * Formats a number according to the current locale using `Intl.NumberFormat`.
72
+ *
73
+ * ```tsx
74
+ * <Num>{1000000}</Num> // "1,000,000" in en, "1.000.000" in de
75
+ * <Num options={{ style: "percent" }}>{0.42}</Num> // "42%"
76
+ * ```
77
+ */
78
+ declare function Num(props: NumProps): JSX.Element;
79
+ interface CurrencyProps {
80
+ /** The numeric value */
81
+ children: number;
82
+ /** ISO 4217 currency code (e.g. "USD", "EUR") */
83
+ currency: string;
84
+ /** Additional Intl.NumberFormat options */
85
+ options?: Intl.NumberFormatOptions;
86
+ }
87
+ /**
88
+ * Formats a number as currency according to the current locale.
89
+ *
90
+ * ```tsx
91
+ * <Currency currency="USD">{29.99}</Currency> // "$29.99" in en-US
92
+ * <Currency currency="EUR">{29.99}</Currency> // "29,99 €" in de
93
+ * ```
94
+ */
95
+ declare function Currency(props: CurrencyProps): JSX.Element;
96
+ interface DateTimeProps {
97
+ /** The date to format (Date object, timestamp, or ISO string) */
98
+ children: Date | number | string;
99
+ /** Intl.DateTimeFormat options */
100
+ options?: Intl.DateTimeFormatOptions;
101
+ }
102
+ /**
103
+ * Formats a date/time according to the current locale using `Intl.DateTimeFormat`.
104
+ *
105
+ * ```tsx
106
+ * <DateTime>{new Date()}</DateTime>
107
+ * <DateTime options={{ dateStyle: "long" }}>{new Date()}</DateTime>
108
+ * ```
109
+ */
110
+ declare function DateTime(props: DateTimeProps): JSX.Element;
111
+ interface PluralProps {
112
+ /** The count value to determine which plural form to use */
113
+ n: number;
114
+ /** Form for zero items */
115
+ zero?: JSX.Element;
116
+ /** Form for exactly one item */
117
+ one?: JSX.Element;
118
+ /** Form for exactly two items */
119
+ two?: JSX.Element;
120
+ /** Form for "few" items (language-dependent) */
121
+ few?: JSX.Element;
122
+ /** Form for "many" items (language-dependent) */
123
+ many?: JSX.Element;
124
+ /** Default/fallback form */
125
+ other: JSX.Element;
126
+ }
127
+ /**
128
+ * Renders the appropriate plural form based on CLDR plural rules for the current locale.
129
+ *
130
+ * ```tsx
131
+ * <Plural n={count()}
132
+ * zero="No items"
133
+ * one="1 item"
134
+ * other={`${count()} items`}
135
+ * />
136
+ * ```
137
+ */
138
+ declare function Plural(props: PluralProps): JSX.Element;
139
+ interface LocaleSelectorProps {
140
+ /** Override which locales to show (defaults to all available) */
141
+ locales?: string[];
142
+ /** Map locale codes to display names, e.g. { en: "English", es: "Español" } */
143
+ labels?: Record<string, string>;
144
+ /** Additional CSS class */
145
+ class?: string;
146
+ }
147
+ /**
148
+ * A ready-to-use locale selector dropdown.
149
+ *
150
+ * ```tsx
151
+ * <LocaleSelector labels={{ en: "English", es: "Español", fr: "Français" }} />
152
+ * ```
153
+ */
154
+ declare function LocaleSelector(props: LocaleSelectorProps): JSX.Element;
155
+
156
+ /**
157
+ * Mark a string for translation extraction.
158
+ *
159
+ * At build time, the Vite plugin and CLI scan for `msg()` calls and add
160
+ * the strings to the source locale file for AI translation.
161
+ *
162
+ * At runtime, `msg()` is a no-op — it returns the source text as-is.
163
+ * Use `t()` from `useTranslation()` for runtime translation.
164
+ *
165
+ * ```ts
166
+ * // Marks "Save changes" for extraction
167
+ * const label = msg("Save changes");
168
+ *
169
+ * // With interpolation template
170
+ * const greeting = msg("Hello {{name}}", { name: "World" });
171
+ *
172
+ * // In a component, translate at runtime:
173
+ * const { t } = useTranslation();
174
+ * <button>{t(label)}</button>
175
+ * ```
176
+ */
177
+ declare function msg(text: string, _params?: Record<string, string | number>): string;
178
+
179
+ /**
180
+ * Detect the user's preferred locale from browser settings.
181
+ *
182
+ * Checks `navigator.languages` (and falls back to `navigator.language`)
183
+ * then matches against the list of available locales. Tries exact match
184
+ * first, then language-only match (e.g. "en-US" → "en").
185
+ */
186
+ declare function detectLocale(availableLocales?: string[]): string;
187
+
188
+ interface TranslationProviderProps {
189
+ /**
190
+ * Initial locale. If omitted, auto-detects from the browser's
191
+ * `navigator.languages` header, falling back to `sourceLocale`.
192
+ */
193
+ locale?: string;
194
+ /** Source locale code (default: "en") */
195
+ sourceLocale?: string;
196
+ /** Translation dictionaries keyed by locale */
197
+ translations: Translations;
198
+ children: JSX.Element;
199
+ }
200
+ declare function TranslationProvider(props: TranslationProviderProps): JSX.Element;
201
+ /** Access the full translation context. Must be inside a TranslationProvider. */
202
+ declare function useTranslation(): TranslationContextValue;
203
+ /** Access just the current locale and setter. */
204
+ declare function useLocale(): {
205
+ locale: () => string;
206
+ setLocale: (locale: string) => void;
207
+ sourceLocale: string;
208
+ availableLocales: () => string[];
209
+ };
210
+ interface TProps {
211
+ /** Explicit translation key. If omitted, children text is used as the key. */
212
+ id?: string;
213
+ /** Interpolation parameters */
214
+ params?: Record<string, string | number>;
215
+ /**
216
+ * AI context hint — tells the AI translator about the meaning of this text.
217
+ * Only used at build time for disambiguation; has no runtime effect.
218
+ *
219
+ * ```tsx
220
+ * <T context="Button to save a document, not save money">Save</T>
221
+ * ```
222
+ */
223
+ context?: string;
224
+ /** Source text / JSX content */
225
+ children?: JSX.Element;
226
+ }
227
+ /**
228
+ * Translatable content component.
229
+ *
230
+ * ```tsx
231
+ * <T>Hello world</T>
232
+ * <T id="greeting" params={{ name: "Alice" }}>Hello {{name}}</T>
233
+ * <T context="the physical bank">Bank</T>
234
+ * <T>Welcome <Var>{userName()}</Var>, you have <Num>{count()}</Num> items</T>
235
+ * ```
236
+ */
237
+ declare function T(props: TProps): JSX.Element;
238
+
239
+ export { Currency, type CurrencyProps, DateTime, type DateTimeProps, LocaleSelector, type LocaleSelectorProps, Num, type NumProps, Plural, type PluralProps, type SolidTranslatePluginConfig, T, type TProps, type TranslationContextValue, type TranslationDictionary, TranslationProvider, type TranslationProviderProps, type Translations, Var, type VarProps, detectLocale, msg, useLocale, useTranslation };
package/dist/index.js ADDED
@@ -0,0 +1,238 @@
1
+ // src/index.ts
2
+ import {
3
+ createComponent,
4
+ useContext as useContext2,
5
+ createSignal,
6
+ createMemo as createMemo2,
7
+ children as resolveChildren
8
+ } from "solid-js";
9
+
10
+ // src/context.ts
11
+ import { createContext } from "solid-js";
12
+ var TranslationContext = createContext();
13
+
14
+ // src/locale-detect.ts
15
+ function detectLocale(availableLocales) {
16
+ if (typeof navigator === "undefined") return "en";
17
+ const browserLocales = navigator.languages ? [...navigator.languages] : [navigator.language || "en"];
18
+ if (!availableLocales || availableLocales.length === 0) {
19
+ return normalizeLocale(browserLocales[0] || "en");
20
+ }
21
+ for (const bl of browserLocales) {
22
+ const normalized = normalizeLocale(bl);
23
+ if (availableLocales.includes(normalized)) return normalized;
24
+ }
25
+ for (const bl of browserLocales) {
26
+ const lang = bl.split("-")[0].toLowerCase();
27
+ if (availableLocales.includes(lang)) return lang;
28
+ }
29
+ return availableLocales[0] || "en";
30
+ }
31
+ function normalizeLocale(locale) {
32
+ return locale.toLowerCase().replace("_", "-");
33
+ }
34
+
35
+ // src/components.tsx
36
+ import { useContext, For, createMemo } from "solid-js";
37
+ function Var(props) {
38
+ return (() => props.children);
39
+ }
40
+ Var.__st_var = true;
41
+ function Num(props) {
42
+ const ctx = useContext(TranslationContext);
43
+ return createMemo(() => {
44
+ const locale = ctx?.locale() || "en";
45
+ return new Intl.NumberFormat(locale, props.options).format(props.children);
46
+ });
47
+ }
48
+ function Currency(props) {
49
+ const ctx = useContext(TranslationContext);
50
+ return createMemo(() => {
51
+ const locale = ctx?.locale() || "en";
52
+ return new Intl.NumberFormat(locale, {
53
+ style: "currency",
54
+ currency: props.currency,
55
+ ...props.options
56
+ }).format(props.children);
57
+ });
58
+ }
59
+ function DateTime(props) {
60
+ const ctx = useContext(TranslationContext);
61
+ return createMemo(() => {
62
+ const locale = ctx?.locale() || "en";
63
+ const date = props.children instanceof Date ? props.children : new Date(props.children);
64
+ return new Intl.DateTimeFormat(locale, props.options).format(date);
65
+ });
66
+ }
67
+ function Plural(props) {
68
+ const ctx = useContext(TranslationContext);
69
+ return createMemo(() => {
70
+ const locale = ctx?.locale() || "en";
71
+ const rules = new Intl.PluralRules(locale);
72
+ const category = rules.select(props.n);
73
+ const forms = {
74
+ zero: props.zero,
75
+ one: props.one,
76
+ two: props.two,
77
+ few: props.few,
78
+ many: props.many,
79
+ other: props.other
80
+ };
81
+ return forms[category] ?? props.other;
82
+ });
83
+ }
84
+ function LocaleSelector(props) {
85
+ const ctx = useContext(TranslationContext);
86
+ if (!ctx) {
87
+ throw new Error(
88
+ "<LocaleSelector> must be used within a <TranslationProvider>"
89
+ );
90
+ }
91
+ const locales = createMemo(() => props.locales || ctx.availableLocales());
92
+ const displayName = (code) => {
93
+ if (props.labels?.[code]) return props.labels[code];
94
+ try {
95
+ const dn = new Intl.DisplayNames([code], { type: "language" });
96
+ return dn.of(code) || code;
97
+ } catch {
98
+ return code;
99
+ }
100
+ };
101
+ return <select
102
+ class={props.class}
103
+ value={ctx.locale()}
104
+ onChange={(e) => ctx.setLocale(e.currentTarget.value)}
105
+ >
106
+ <For each={locales()}>
107
+ {(code) => <option value={code}>{displayName(code)}</option>}
108
+ </For>
109
+ </select>;
110
+ }
111
+
112
+ // src/msg.ts
113
+ function msg(text, _params) {
114
+ return text;
115
+ }
116
+
117
+ // src/index.ts
118
+ function TranslationProvider(props) {
119
+ const sourceLocale = props.sourceLocale || "en";
120
+ const availableLocales = createMemo2(() => Object.keys(props.translations));
121
+ const initialLocale = props.locale || detectLocale(availableLocales()) || sourceLocale;
122
+ const [locale, setLocale] = createSignal(initialLocale);
123
+ const t = (key, params) => {
124
+ const cur = locale();
125
+ let text = key;
126
+ const dict = props.translations[cur];
127
+ if (dict && key in dict) {
128
+ text = dict[key];
129
+ }
130
+ if (params) {
131
+ for (const [k, v] of Object.entries(params)) {
132
+ text = text.replace(
133
+ new RegExp(`\\{\\{${k}\\}\\}|\\{${k}\\}`, "g"),
134
+ String(v)
135
+ );
136
+ }
137
+ }
138
+ return text;
139
+ };
140
+ const value = {
141
+ locale,
142
+ setLocale,
143
+ t,
144
+ sourceLocale,
145
+ availableLocales,
146
+ translations: props.translations
147
+ };
148
+ return createComponent(TranslationContext.Provider, {
149
+ value,
150
+ get children() {
151
+ return props.children;
152
+ }
153
+ });
154
+ }
155
+ function useTranslation() {
156
+ const ctx = useContext2(TranslationContext);
157
+ if (!ctx) {
158
+ throw new Error(
159
+ "useTranslation() must be used within a <TranslationProvider>"
160
+ );
161
+ }
162
+ return ctx;
163
+ }
164
+ function useLocale() {
165
+ const ctx = useTranslation();
166
+ return {
167
+ locale: ctx.locale,
168
+ setLocale: ctx.setLocale,
169
+ sourceLocale: ctx.sourceLocale,
170
+ availableLocales: ctx.availableLocales
171
+ };
172
+ }
173
+ function T(props) {
174
+ const ctx = useContext2(TranslationContext);
175
+ const resolved = resolveChildren(() => props.children);
176
+ return createMemo2(() => {
177
+ const kids = resolved.toArray();
178
+ if (!ctx) return kids.length === 1 ? kids[0] : kids;
179
+ if (kids.length === 1 && typeof kids[0] === "string") {
180
+ const key = props.id || kids[0];
181
+ return ctx.t(key, props.params);
182
+ }
183
+ if (props.id) {
184
+ const translated2 = ctx.t(props.id, props.params);
185
+ if (!/{(\d+)}/.test(translated2)) return translated2;
186
+ const slots2 = [];
187
+ for (const kid of kids) {
188
+ if (typeof kid !== "string" && typeof kid !== "number") {
189
+ slots2.push(kid);
190
+ }
191
+ }
192
+ return interpolateSlots(translated2, slots2);
193
+ }
194
+ const slots = [];
195
+ let template = "";
196
+ for (const kid of kids) {
197
+ if (typeof kid === "string") {
198
+ template += kid;
199
+ } else if (typeof kid === "number") {
200
+ template += String(kid);
201
+ } else {
202
+ template += `{${slots.length}}`;
203
+ slots.push(kid);
204
+ }
205
+ }
206
+ const translated = ctx.t(template, props.params);
207
+ if (slots.length === 0) return translated;
208
+ return interpolateSlots(translated, slots);
209
+ });
210
+ }
211
+ function interpolateSlots(text, slots) {
212
+ const parts = text.split(/\{(\d+)\}/);
213
+ const result = [];
214
+ for (let i = 0; i < parts.length; i++) {
215
+ if (i % 2 === 0) {
216
+ if (parts[i]) result.push(parts[i]);
217
+ } else {
218
+ const idx = parseInt(parts[i], 10);
219
+ if (slots[idx] !== void 0) result.push(slots[idx]);
220
+ }
221
+ }
222
+ return result;
223
+ }
224
+ export {
225
+ Currency,
226
+ DateTime,
227
+ LocaleSelector,
228
+ Num,
229
+ Plural,
230
+ T,
231
+ TranslationProvider,
232
+ Var,
233
+ detectLocale,
234
+ msg,
235
+ useLocale,
236
+ useTranslation
237
+ };
238
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/context.ts","../src/locale-detect.ts","../src/components.tsx","../src/msg.ts"],"sourcesContent":["import {\n createComponent,\n useContext,\n createSignal,\n createMemo,\n children as resolveChildren,\n type JSX,\n} from \"solid-js\";\nimport {\n TranslationContext,\n type TranslationContextValue,\n} from \"./context.js\";\nimport { detectLocale } from \"./locale-detect.js\";\nimport type { TranslationDictionary, Translations } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport type { TranslationContextValue } from \"./context.js\";\nexport type { TranslationDictionary, Translations } from \"./types.js\";\nexport type { SolidTranslatePluginConfig } from \"./types.js\";\nexport { Var, Num, Currency, DateTime, Plural, LocaleSelector } from \"./components.js\";\nexport type {\n VarProps,\n NumProps,\n CurrencyProps,\n DateTimeProps,\n PluralProps,\n LocaleSelectorProps,\n} from \"./components.js\";\nexport { msg } from \"./msg.js\";\nexport { detectLocale } from \"./locale-detect.js\";\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\nexport interface TranslationProviderProps {\n /**\n * Initial locale. If omitted, auto-detects from the browser's\n * `navigator.languages` header, falling back to `sourceLocale`.\n */\n locale?: string;\n /** Source locale code (default: \"en\") */\n sourceLocale?: string;\n /** Translation dictionaries keyed by locale */\n translations: Translations;\n children: JSX.Element;\n}\n\nexport function TranslationProvider(props: TranslationProviderProps) {\n const sourceLocale = props.sourceLocale || \"en\";\n const availableLocales = createMemo(() => Object.keys(props.translations));\n\n // Auto-detect locale from browser if not explicitly provided\n const initialLocale =\n props.locale || detectLocale(availableLocales()) || sourceLocale;\n const [locale, setLocale] = createSignal(initialLocale);\n\n const t = (\n key: string,\n params?: Record<string, string | number>,\n ): string => {\n const cur = locale();\n let text = key;\n\n // Look up in translation dictionary (works for both source and target locales)\n const dict = props.translations[cur];\n if (dict && key in dict) {\n text = dict[key]!;\n }\n\n // Interpolate {{variable}} and {variable} placeholders\n if (params) {\n for (const [k, v] of Object.entries(params)) {\n text = text.replace(\n new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}|\\\\{${k}\\\\}`, \"g\"),\n String(v),\n );\n }\n }\n\n return text;\n };\n\n const value: TranslationContextValue = {\n locale,\n setLocale,\n t,\n sourceLocale,\n availableLocales,\n translations: props.translations,\n };\n\n return createComponent(TranslationContext.Provider, {\n value,\n get children() {\n return props.children;\n },\n });\n}\n\n// ---------------------------------------------------------------------------\n// Hooks\n// ---------------------------------------------------------------------------\n\n/** Access the full translation context. Must be inside a TranslationProvider. */\nexport function useTranslation(): TranslationContextValue {\n const ctx = useContext(TranslationContext);\n if (!ctx) {\n throw new Error(\n \"useTranslation() must be used within a <TranslationProvider>\",\n );\n }\n return ctx;\n}\n\n/** Access just the current locale and setter. */\nexport function useLocale(): {\n locale: () => string;\n setLocale: (locale: string) => void;\n sourceLocale: string;\n availableLocales: () => string[];\n} {\n const ctx = useTranslation();\n return {\n locale: ctx.locale,\n setLocale: ctx.setLocale,\n sourceLocale: ctx.sourceLocale,\n availableLocales: ctx.availableLocales,\n };\n}\n\n// ---------------------------------------------------------------------------\n// <T> Component\n// ---------------------------------------------------------------------------\n\nexport interface TProps {\n /** Explicit translation key. If omitted, children text is used as the key. */\n id?: string;\n /** Interpolation parameters */\n params?: Record<string, string | number>;\n /**\n * AI context hint — tells the AI translator about the meaning of this text.\n * Only used at build time for disambiguation; has no runtime effect.\n *\n * ```tsx\n * <T context=\"Button to save a document, not save money\">Save</T>\n * ```\n */\n context?: string;\n /** Source text / JSX content */\n children?: JSX.Element;\n}\n\n/**\n * Translatable content component.\n *\n * ```tsx\n * <T>Hello world</T>\n * <T id=\"greeting\" params={{ name: \"Alice\" }}>Hello {{name}}</T>\n * <T context=\"the physical bank\">Bank</T>\n * <T>Welcome <Var>{userName()}</Var>, you have <Num>{count()}</Num> items</T>\n * ```\n */\nexport function T(props: TProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n const resolved = resolveChildren(() => props.children);\n\n return createMemo(() => {\n const kids = resolved.toArray();\n\n // No context — just render children\n if (!ctx) return kids.length === 1 ? kids[0] : kids;\n\n // Simple case: single text child\n if (kids.length === 1 && typeof kids[0] === \"string\") {\n const key = props.id || (kids[0] as string);\n return ctx.t(key, props.params);\n }\n\n // Explicit id with non-text children — translate via id\n if (props.id) {\n const translated = ctx.t(props.id, props.params);\n\n // If translation is just text (no slot placeholders), return it\n if (!/{(\\d+)}/.test(translated)) return translated;\n\n // Collect non-text children (Var, Num, etc.) as ordered slots\n const slots: JSX.Element[] = [];\n for (const kid of kids) {\n if (typeof kid !== \"string\" && typeof kid !== \"number\") {\n slots.push(kid as JSX.Element);\n }\n }\n\n return interpolateSlots(translated, slots);\n }\n\n // Mixed children without explicit id — build a template key\n const slots: JSX.Element[] = [];\n let template = \"\";\n for (const kid of kids) {\n if (typeof kid === \"string\") {\n template += kid;\n } else if (typeof kid === \"number\") {\n template += String(kid);\n } else {\n template += `{${slots.length}}`;\n slots.push(kid as JSX.Element);\n }\n }\n\n const translated = ctx.t(template, props.params);\n if (slots.length === 0) return translated;\n return interpolateSlots(translated, slots);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Split a translated string by `{0}`, `{1}`, etc. and interleave with slots */\nfunction interpolateSlots(\n text: string,\n slots: JSX.Element[],\n): (string | JSX.Element)[] {\n const parts = text.split(/\\{(\\d+)\\}/);\n const result: (string | JSX.Element)[] = [];\n for (let i = 0; i < parts.length; i++) {\n if (i % 2 === 0) {\n if (parts[i]) result.push(parts[i]!);\n } else {\n const idx = parseInt(parts[i]!, 10);\n if (slots[idx] !== undefined) result.push(slots[idx]!);\n }\n }\n return result;\n}\n","import { createContext } from \"solid-js\";\nimport type { Translations } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Context value type\n// ---------------------------------------------------------------------------\n\nexport interface TranslationContextValue {\n /** Current locale as a reactive signal */\n locale: () => string;\n /** Switch to a different locale */\n setLocale: (locale: string) => void;\n /** Translate a key with optional interpolation params */\n t: (key: string, params?: Record<string, string | number>) => string;\n /** The source locale code */\n sourceLocale: string;\n /** All available locale codes (reactive) */\n availableLocales: () => string[];\n /** Raw translations object */\n translations: Translations;\n}\n\n// ---------------------------------------------------------------------------\n// Shared context instance\n// ---------------------------------------------------------------------------\n\nexport const TranslationContext = createContext<TranslationContextValue>();\n","/**\n * Detect the user's preferred locale from browser settings.\n *\n * Checks `navigator.languages` (and falls back to `navigator.language`)\n * then matches against the list of available locales. Tries exact match\n * first, then language-only match (e.g. \"en-US\" → \"en\").\n */\nexport function detectLocale(availableLocales?: string[]): string {\n if (typeof navigator === \"undefined\") return \"en\";\n\n const browserLocales = navigator.languages\n ? [...navigator.languages]\n : [navigator.language || \"en\"];\n\n if (!availableLocales || availableLocales.length === 0) {\n return normalizeLocale(browserLocales[0] || \"en\");\n }\n\n // Exact match\n for (const bl of browserLocales) {\n const normalized = normalizeLocale(bl);\n if (availableLocales.includes(normalized)) return normalized;\n }\n\n // Language-only match (e.g. \"en-US\" → \"en\")\n for (const bl of browserLocales) {\n const lang = bl.split(\"-\")[0]!.toLowerCase();\n if (availableLocales.includes(lang)) return lang;\n }\n\n return availableLocales[0] || \"en\";\n}\n\nfunction normalizeLocale(locale: string): string {\n return locale.toLowerCase().replace(\"_\", \"-\");\n}\n","import { useContext, type JSX, For, createMemo } from \"solid-js\";\nimport { TranslationContext } from \"./context.js\";\n\n// ---------------------------------------------------------------------------\n// <Var> — protect dynamic content from translation\n// ---------------------------------------------------------------------------\n\nexport interface VarProps {\n /** Optional name for the variable (used as placeholder in templates) */\n name?: string;\n children: JSX.Element;\n}\n\n/**\n * Marks content as untranslatable. When used inside `<T>`, the content\n * is preserved as-is while surrounding text is translated.\n *\n * ```tsx\n * <T>Hello <Var>{userName()}</Var>, welcome!</T>\n * ```\n */\nexport function Var(props: VarProps): JSX.Element {\n return (() => props.children) as unknown as JSX.Element;\n}\n\n// Mark Var for identification by T component\n(Var as any).__st_var = true;\n\n// ---------------------------------------------------------------------------\n// <Num> — locale-aware number formatting\n// ---------------------------------------------------------------------------\n\nexport interface NumProps {\n /** The number to format */\n children: number;\n /** Intl.NumberFormat options */\n options?: Intl.NumberFormatOptions;\n}\n\n/**\n * Formats a number according to the current locale using `Intl.NumberFormat`.\n *\n * ```tsx\n * <Num>{1000000}</Num> // \"1,000,000\" in en, \"1.000.000\" in de\n * <Num options={{ style: \"percent\" }}>{0.42}</Num> // \"42%\"\n * ```\n */\nexport function Num(props: NumProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n return new Intl.NumberFormat(locale, props.options).format(props.children);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <Currency> — locale-aware currency formatting\n// ---------------------------------------------------------------------------\n\nexport interface CurrencyProps {\n /** The numeric value */\n children: number;\n /** ISO 4217 currency code (e.g. \"USD\", \"EUR\") */\n currency: string;\n /** Additional Intl.NumberFormat options */\n options?: Intl.NumberFormatOptions;\n}\n\n/**\n * Formats a number as currency according to the current locale.\n *\n * ```tsx\n * <Currency currency=\"USD\">{29.99}</Currency> // \"$29.99\" in en-US\n * <Currency currency=\"EUR\">{29.99}</Currency> // \"29,99 €\" in de\n * ```\n */\nexport function Currency(props: CurrencyProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n return new Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: props.currency,\n ...props.options,\n }).format(props.children);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <DateTime> — locale-aware date/time formatting\n// ---------------------------------------------------------------------------\n\nexport interface DateTimeProps {\n /** The date to format (Date object, timestamp, or ISO string) */\n children: Date | number | string;\n /** Intl.DateTimeFormat options */\n options?: Intl.DateTimeFormatOptions;\n}\n\n/**\n * Formats a date/time according to the current locale using `Intl.DateTimeFormat`.\n *\n * ```tsx\n * <DateTime>{new Date()}</DateTime>\n * <DateTime options={{ dateStyle: \"long\" }}>{new Date()}</DateTime>\n * ```\n */\nexport function DateTime(props: DateTimeProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n const date =\n props.children instanceof Date\n ? props.children\n : new Date(props.children);\n return new Intl.DateTimeFormat(locale, props.options).format(date);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <Plural> — CLDR plural rules\n// ---------------------------------------------------------------------------\n\nexport interface PluralProps {\n /** The count value to determine which plural form to use */\n n: number;\n /** Form for zero items */\n zero?: JSX.Element;\n /** Form for exactly one item */\n one?: JSX.Element;\n /** Form for exactly two items */\n two?: JSX.Element;\n /** Form for \"few\" items (language-dependent) */\n few?: JSX.Element;\n /** Form for \"many\" items (language-dependent) */\n many?: JSX.Element;\n /** Default/fallback form */\n other: JSX.Element;\n}\n\n/**\n * Renders the appropriate plural form based on CLDR plural rules for the current locale.\n *\n * ```tsx\n * <Plural n={count()}\n * zero=\"No items\"\n * one=\"1 item\"\n * other={`${count()} items`}\n * />\n * ```\n */\nexport function Plural(props: PluralProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n const rules = new Intl.PluralRules(locale);\n const category = rules.select(props.n);\n\n const forms: Record<string, JSX.Element | undefined> = {\n zero: props.zero,\n one: props.one,\n two: props.two,\n few: props.few,\n many: props.many,\n other: props.other,\n };\n\n return forms[category] ?? props.other;\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <LocaleSelector> — drop-in locale picker\n// ---------------------------------------------------------------------------\n\nexport interface LocaleSelectorProps {\n /** Override which locales to show (defaults to all available) */\n locales?: string[];\n /** Map locale codes to display names, e.g. { en: \"English\", es: \"Español\" } */\n labels?: Record<string, string>;\n /** Additional CSS class */\n class?: string;\n}\n\n/**\n * A ready-to-use locale selector dropdown.\n *\n * ```tsx\n * <LocaleSelector labels={{ en: \"English\", es: \"Español\", fr: \"Français\" }} />\n * ```\n */\nexport function LocaleSelector(props: LocaleSelectorProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n if (!ctx) {\n throw new Error(\n \"<LocaleSelector> must be used within a <TranslationProvider>\",\n );\n }\n\n const locales = createMemo(() => props.locales || ctx.availableLocales());\n\n const displayName = (code: string): string => {\n if (props.labels?.[code]) return props.labels[code]!;\n try {\n const dn = new Intl.DisplayNames([code], { type: \"language\" });\n return dn.of(code) || code;\n } catch {\n return code;\n }\n };\n\n return (\n <select\n class={props.class}\n value={ctx.locale()}\n onChange={(e) => ctx.setLocale(e.currentTarget.value)}\n >\n <For each={locales()}>\n {(code) => <option value={code}>{displayName(code)}</option>}\n </For>\n </select>\n ) as JSX.Element;\n}\n","/**\n * Mark a string for translation extraction.\n *\n * At build time, the Vite plugin and CLI scan for `msg()` calls and add\n * the strings to the source locale file for AI translation.\n *\n * At runtime, `msg()` is a no-op — it returns the source text as-is.\n * Use `t()` from `useTranslation()` for runtime translation.\n *\n * ```ts\n * // Marks \"Save changes\" for extraction\n * const label = msg(\"Save changes\");\n *\n * // With interpolation template\n * const greeting = msg(\"Hello {{name}}\", { name: \"World\" });\n *\n * // In a component, translate at runtime:\n * const { t } = useTranslation();\n * <button>{t(label)}</button>\n * ```\n */\nexport function msg(\n text: string,\n _params?: Record<string, string | number>,\n): string {\n return text;\n}\n"],"mappings":"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","names":["useContext","createMemo","createMemo","useContext","translated","slots"]}
package/dist/vite.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { Plugin } from 'vite';
2
+ import { LanguageModelV1 } from 'ai';
3
+
4
+ /** Configuration for the solid-translate Vite plugin */
5
+ interface SolidTranslatePluginConfig {
6
+ /** Source locale code (default: "en") */
7
+ sourceLocale?: string;
8
+ /** Target locale codes to translate into */
9
+ targetLocales: string[];
10
+ /** Directory containing locale JSON files, relative to project root (default: "./src/locales") */
11
+ localesDir?: string;
12
+ /** AI model to use for translations (any Vercel AI SDK LanguageModelV1) */
13
+ model: LanguageModelV1;
14
+ /** Custom system prompt for the AI translator */
15
+ systemPrompt?: string;
16
+ /** Max keys per API call (default: 50) */
17
+ batchSize?: number;
18
+ /**
19
+ * Automatically extract translatable strings from source files.
20
+ * Scans for `<T>` components and `msg()` calls, then writes
21
+ * discovered strings into the source locale JSON file.
22
+ */
23
+ autoExtract?: boolean;
24
+ /**
25
+ * Glob patterns for files to scan during auto-extraction.
26
+ * Default: `["src/**\/*.tsx", "src/**\/*.ts", "src/**\/*.jsx"]`
27
+ */
28
+ include?: string[];
29
+ }
30
+
31
+ /**
32
+ * Vite plugin for solid-translate.
33
+ *
34
+ * Handles:
35
+ * 1. Optional extraction of <T>, msg() strings from source files
36
+ * 2. AI translation of source locale to target locales (with context support)
37
+ * 3. Lock file management for efficient re-translation
38
+ * 4. Virtual module serving translations at runtime
39
+ */
40
+ declare function solidTranslate(config: SolidTranslatePluginConfig): Plugin;
41
+
42
+ export { type SolidTranslatePluginConfig, solidTranslate as default, solidTranslate };