@lingo.dev/react 1.0.1 → 1.0.2

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/dist/index.cjs ADDED
@@ -0,0 +1,343 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let _lingo_dev_spec = require("@lingo.dev/spec");
25
+ let react = require("react");
26
+ let intl_messageformat = require("intl-messageformat");
27
+ intl_messageformat = __toESM(intl_messageformat);
28
+ let react_jsx_runtime = require("react/jsx-runtime");
29
+ //#region src/format.ts
30
+ /**
31
+ * Locale-aware formatting methods wrapping native Intl APIs.
32
+ * Near-zero bundle cost - each method delegates to built-in browser/Node Intl formatters.
33
+ * All methods are pure functions that take a locale string and return formatted strings.
34
+ */
35
+ const FILE_SIZE_UNITS = [
36
+ "B",
37
+ "KB",
38
+ "MB",
39
+ "GB",
40
+ "TB",
41
+ "PB"
42
+ ];
43
+ /**
44
+ * Creates all formatting methods bound to a specific locale.
45
+ */
46
+ function createFormatMethods(locale) {
47
+ return {
48
+ num: (value, options) => new Intl.NumberFormat(locale, options).format(value),
49
+ currency: (value, currency, options) => new Intl.NumberFormat(locale, {
50
+ ...options,
51
+ style: "currency",
52
+ currency
53
+ }).format(value),
54
+ percent: (value, options) => new Intl.NumberFormat(locale, {
55
+ ...options,
56
+ style: "percent"
57
+ }).format(value),
58
+ unit: (value, unit, options) => new Intl.NumberFormat(locale, {
59
+ ...options,
60
+ style: "unit",
61
+ unit
62
+ }).format(value),
63
+ compact: (value, options) => new Intl.NumberFormat(locale, {
64
+ ...options,
65
+ notation: "compact"
66
+ }).format(value),
67
+ date: (value, options) => new Intl.DateTimeFormat(locale, {
68
+ dateStyle: "medium",
69
+ ...options
70
+ }).format(value),
71
+ time: (value, options) => new Intl.DateTimeFormat(locale, {
72
+ timeStyle: "short",
73
+ ...options
74
+ }).format(value),
75
+ datetime: (value, options) => new Intl.DateTimeFormat(locale, {
76
+ dateStyle: "medium",
77
+ timeStyle: "short",
78
+ ...options
79
+ }).format(value),
80
+ relative: (value, unit, options) => new Intl.RelativeTimeFormat(locale, options).format(value, unit),
81
+ list: (items, options) => new Intl.ListFormat(locale, {
82
+ type: "conjunction",
83
+ ...options
84
+ }).format(items),
85
+ displayName: (code, type) => {
86
+ try {
87
+ return new Intl.DisplayNames(locale, { type }).of(code);
88
+ } catch {
89
+ return;
90
+ }
91
+ },
92
+ sort: (items, options) => {
93
+ const collator = new Intl.Collator(locale, options);
94
+ return [...items].sort(collator.compare);
95
+ },
96
+ segment: (text, granularity = "grapheme") => [...new Intl.Segmenter(locale, { granularity }).segment(text)].map((s) => s.segment),
97
+ fileSize: (bytes) => {
98
+ if (bytes === 0) return `0 ${FILE_SIZE_UNITS[0]}`;
99
+ const exp = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), FILE_SIZE_UNITS.length - 1);
100
+ const value = bytes / 1024 ** exp;
101
+ return `${new Intl.NumberFormat(locale, { maximumFractionDigits: exp === 0 ? 0 : 1 }).format(value)} ${FILE_SIZE_UNITS[exp]}`;
102
+ }
103
+ };
104
+ }
105
+ //#endregion
106
+ //#region src/text.ts
107
+ const ICU_COMPLEX = /\{[^}]+,\s*(?:plural|select|selectordinal|number|date|time)\s*[,}]/;
108
+ const formatCache = /* @__PURE__ */ new Map();
109
+ const warnedKeys = /* @__PURE__ */ new Set();
110
+ function warnOnce(id, message, level = "warn") {
111
+ if (process.env.NODE_ENV === "production") return;
112
+ if (warnedKeys.has(id)) return;
113
+ warnedKeys.add(id);
114
+ console[level](`[lingo] ${message}`);
115
+ }
116
+ function simpleInterpolate(template, values) {
117
+ return template.replace(/\{(\w+)\}/g, (_, key) => {
118
+ const val = values[key];
119
+ return val !== void 0 ? String(val) : `{${key}}`;
120
+ });
121
+ }
122
+ function getFormatter(template, locale) {
123
+ const cacheKey = `${locale}\0${template}`;
124
+ let formatter = formatCache.get(cacheKey);
125
+ if (!formatter) {
126
+ formatter = new intl_messageformat.default(template, locale);
127
+ formatCache.set(cacheKey, formatter);
128
+ }
129
+ return formatter;
130
+ }
131
+ /**
132
+ * Looks up a translation by hash key with dev-mode diagnostics.
133
+ * Falls back to source text when translation is missing or empty.
134
+ */
135
+ function lookupTemplate(source, messages, locale, context) {
136
+ const key = (0, _lingo_dev_spec.computeKey)(source, context);
137
+ const raw = messages[key];
138
+ if (process.env.NODE_ENV !== "production") {
139
+ if (raw === void 0) warnOnce(`missing:${key}:${locale}`, `Missing translation for "${source}"\n Key: ${key}\n Locale: ${locale}\n → Run \`lingo extract\` to add this string to your locale files`);
140
+ else if (raw === "") warnOnce(`empty:${key}:${locale}`, `Empty translation for "${source}"\n Key: ${key}\n Locale: ${locale}\n → This key exists in your locale file but has no translation`);
141
+ }
142
+ return raw || source;
143
+ }
144
+ /**
145
+ * Resolves a translated string from messages by hash, falling back to source text.
146
+ * Uses simple regex for {placeholder} substitution; loads ICU parser only for complex syntax.
147
+ */
148
+ function resolveText(source, messages, locale, options) {
149
+ const template = lookupTemplate(source, messages, locale, options?.context);
150
+ if (!options?.values) return template;
151
+ if (!ICU_COMPLEX.test(template)) return simpleInterpolate(template, options.values);
152
+ try {
153
+ return getFormatter(template, locale).format(options.values);
154
+ } catch (error) {
155
+ if (process.env.NODE_ENV !== "production") {
156
+ const key = (0, _lingo_dev_spec.computeKey)(source, options?.context);
157
+ warnOnce(`icu:${key}:${locale}`, `Failed to format "${template}"\n Key: ${key}\n Locale: ${locale}\n Error: ${error instanceof Error ? error.message : String(error)}\n → Check the ICU MessageFormat syntax in your locale file`, "error");
158
+ }
159
+ return template;
160
+ }
161
+ }
162
+ //#endregion
163
+ //#region src/rich.ts
164
+ const TAG_RE = /<(\w+)>([\s\S]*?)<\/\1>|<(\w+)\s*\/>/g;
165
+ function renderTemplate(template, tags, values) {
166
+ const parts = [];
167
+ let lastIndex = 0;
168
+ let keyCounter = 0;
169
+ for (const match of template.matchAll(TAG_RE)) {
170
+ if (match.index > lastIndex) parts.push(simpleInterpolate(template.slice(lastIndex, match.index), values));
171
+ const tagName = match[1] ?? match[3];
172
+ const content = match[2];
173
+ const tagFn = tags[tagName];
174
+ if (tagFn) {
175
+ const children = content != null ? renderTemplate(content, tags, values) : null;
176
+ parts.push((0, react.createElement)(react.Fragment, { key: keyCounter++ }, tagFn(children)));
177
+ } else parts.push(match[0]);
178
+ lastIndex = match.index + match[0].length;
179
+ }
180
+ if (lastIndex < template.length) parts.push(simpleInterpolate(template.slice(lastIndex), values));
181
+ if (parts.length === 0) return "";
182
+ if (parts.length === 1) return parts[0];
183
+ return (0, react.createElement)(react.Fragment, null, ...parts);
184
+ }
185
+ /**
186
+ * Resolves a translated string with rich text tag interpolation.
187
+ * Parses <tag>children</tag> and <tag/> patterns, mapping them to React components.
188
+ */
189
+ function resolveRich(source, messages, locale, options) {
190
+ return renderTemplate(lookupTemplate(source, messages, locale, options?.context), options?.tags ?? {}, options?.values ?? {});
191
+ }
192
+ //#endregion
193
+ //#region src/lingo.ts
194
+ const RTL_LANGS = new Set([
195
+ "ar",
196
+ "arc",
197
+ "dv",
198
+ "fa",
199
+ "ha",
200
+ "he",
201
+ "khw",
202
+ "ks",
203
+ "ku",
204
+ "ps",
205
+ "ur",
206
+ "yi"
207
+ ]);
208
+ function fallbackDirection(locale) {
209
+ const lang = locale.split("-")[0].toLowerCase();
210
+ return RTL_LANGS.has(lang) ? "rtl" : "ltr";
211
+ }
212
+ function resolveDirection(locale) {
213
+ try {
214
+ const direction = new Intl.Locale(locale).textInfo?.direction;
215
+ if (direction === "rtl" || direction === "ltr") return direction;
216
+ } catch {}
217
+ return fallbackDirection(locale);
218
+ }
219
+ function resolveLocaleInfo(locale) {
220
+ try {
221
+ const loc = new Intl.Locale(locale).maximize();
222
+ return {
223
+ script: loc.script,
224
+ region: loc.region
225
+ };
226
+ } catch {
227
+ return {
228
+ script: void 0,
229
+ region: void 0
230
+ };
231
+ }
232
+ }
233
+ /**
234
+ * Creates a `Lingo` object for translating text outside of React context.
235
+ * Used by `@lingo.dev/react-next` for Server Components and internally by `LingoProvider`.
236
+ *
237
+ * @param locale - BCP-47 locale string (e.g., "en", "es", "ar-SA")
238
+ * @param messages - Hash-keyed translations from JSONC locale files
239
+ *
240
+ * @example
241
+ * ```ts
242
+ * const l = createLingo("es", messages);
243
+ * l.text("Hello", { context: "Hero greeting" });
244
+ * l.direction; // "ltr"
245
+ * l.script; // "Latn"
246
+ * ```
247
+ */
248
+ function createLingo(locale, messages = {}) {
249
+ const direction = resolveDirection(locale);
250
+ const { script, region } = resolveLocaleInfo(locale);
251
+ return {
252
+ locale,
253
+ direction,
254
+ script,
255
+ region,
256
+ text: (source, options) => resolveText(source, messages, locale, options),
257
+ rich: (source, options) => resolveRich(source, messages, locale, options),
258
+ plural: (count, forms, options) => {
259
+ return resolveText((0, _lingo_dev_spec.buildIcuPlural)(forms), messages, locale, {
260
+ context: options?.context ?? "",
261
+ values: { count }
262
+ });
263
+ },
264
+ select: (value, forms, options) => {
265
+ return resolveText((0, _lingo_dev_spec.buildIcuSelect)(forms), messages, locale, {
266
+ context: options?.context ?? "",
267
+ values: { value }
268
+ });
269
+ },
270
+ ...createFormatMethods(locale)
271
+ };
272
+ }
273
+ //#endregion
274
+ //#region src/provider.tsx
275
+ const LingoContext = (0, react.createContext)(null);
276
+ /**
277
+ * Provides locale and translations to all descendant components via `useLingo()`.
278
+ *
279
+ * Nested providers with the **same locale** merge messages (child overrides parent,
280
+ * missing keys fall through). Nested providers with **different locales** are standalone.
281
+ *
282
+ * @example
283
+ * ```tsx
284
+ * // Root layout: shared messages
285
+ * <LingoProvider locale="es" messages={sharedMessages}>
286
+ * {/* Dashboard: adds route-specific messages *​/}
287
+ * <LingoProvider locale="es" messages={dashboardMessages}>
288
+ * <App />
289
+ * </LingoProvider>
290
+ * </LingoProvider>
291
+ * ```
292
+ */
293
+ function LingoProvider({ locale, messages = {}, children }) {
294
+ const parent = (0, react.useContext)(LingoContext);
295
+ const parentMessages = parent?.messages;
296
+ const parentLocale = parent?.lingo.locale;
297
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LingoContext, {
298
+ value: (0, react.useMemo)(() => {
299
+ const merged = parentMessages && parentLocale === locale ? {
300
+ ...parentMessages,
301
+ ...messages
302
+ } : messages;
303
+ return {
304
+ lingo: createLingo(locale, merged),
305
+ messages: merged
306
+ };
307
+ }, [
308
+ parentMessages,
309
+ parentLocale,
310
+ locale,
311
+ messages
312
+ ]),
313
+ children
314
+ });
315
+ }
316
+ /**
317
+ * Access the `Lingo` translation object from the nearest `LingoProvider`.
318
+ *
319
+ * @throws If called outside a `<LingoProvider>`.
320
+ *
321
+ * @example
322
+ * ```tsx
323
+ * function Greeting() {
324
+ * const l = useLingo();
325
+ * return <h1>{l.text("Hello")}</h1>;
326
+ * }
327
+ * ```
328
+ */
329
+ function useLingo() {
330
+ const ctx = (0, react.useContext)(LingoContext);
331
+ if (ctx === null) throw new Error("useLingo() called outside <LingoProvider>. Wrap your component tree with <LingoProvider locale=\"en\"> to fix this.");
332
+ return ctx.lingo;
333
+ }
334
+ //#endregion
335
+ exports.LingoProvider = LingoProvider;
336
+ Object.defineProperty(exports, "computeKey", {
337
+ enumerable: true,
338
+ get: function() {
339
+ return _lingo_dev_spec.computeKey;
340
+ }
341
+ });
342
+ exports.createLingo = createLingo;
343
+ exports.useLingo = useLingo;