koto-react 1.0.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/dist/index.js ADDED
@@ -0,0 +1,931 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var React = require('react');
6
+
7
+ const instanceOfAny = (object, constructors) => constructors.some((c) => object instanceof c);
8
+
9
+ let idbProxyableTypes;
10
+ let cursorAdvanceMethods;
11
+ // This is a function to prevent it throwing up in node environments.
12
+ function getIdbProxyableTypes() {
13
+ return (idbProxyableTypes ||
14
+ (idbProxyableTypes = [
15
+ IDBDatabase,
16
+ IDBObjectStore,
17
+ IDBIndex,
18
+ IDBCursor,
19
+ IDBTransaction,
20
+ ]));
21
+ }
22
+ // This is a function to prevent it throwing up in node environments.
23
+ function getCursorAdvanceMethods() {
24
+ return (cursorAdvanceMethods ||
25
+ (cursorAdvanceMethods = [
26
+ IDBCursor.prototype.advance,
27
+ IDBCursor.prototype.continue,
28
+ IDBCursor.prototype.continuePrimaryKey,
29
+ ]));
30
+ }
31
+ const transactionDoneMap = new WeakMap();
32
+ const transformCache = new WeakMap();
33
+ const reverseTransformCache = new WeakMap();
34
+ function promisifyRequest(request) {
35
+ const promise = new Promise((resolve, reject) => {
36
+ const unlisten = () => {
37
+ request.removeEventListener('success', success);
38
+ request.removeEventListener('error', error);
39
+ };
40
+ const success = () => {
41
+ resolve(wrap(request.result));
42
+ unlisten();
43
+ };
44
+ const error = () => {
45
+ reject(request.error);
46
+ unlisten();
47
+ };
48
+ request.addEventListener('success', success);
49
+ request.addEventListener('error', error);
50
+ });
51
+ // This mapping exists in reverseTransformCache but doesn't exist in transformCache. This
52
+ // is because we create many promises from a single IDBRequest.
53
+ reverseTransformCache.set(promise, request);
54
+ return promise;
55
+ }
56
+ function cacheDonePromiseForTransaction(tx) {
57
+ // Early bail if we've already created a done promise for this transaction.
58
+ if (transactionDoneMap.has(tx))
59
+ return;
60
+ const done = new Promise((resolve, reject) => {
61
+ const unlisten = () => {
62
+ tx.removeEventListener('complete', complete);
63
+ tx.removeEventListener('error', error);
64
+ tx.removeEventListener('abort', error);
65
+ };
66
+ const complete = () => {
67
+ resolve();
68
+ unlisten();
69
+ };
70
+ const error = () => {
71
+ reject(tx.error || new DOMException('AbortError', 'AbortError'));
72
+ unlisten();
73
+ };
74
+ tx.addEventListener('complete', complete);
75
+ tx.addEventListener('error', error);
76
+ tx.addEventListener('abort', error);
77
+ });
78
+ // Cache it for later retrieval.
79
+ transactionDoneMap.set(tx, done);
80
+ }
81
+ let idbProxyTraps = {
82
+ get(target, prop, receiver) {
83
+ if (target instanceof IDBTransaction) {
84
+ // Special handling for transaction.done.
85
+ if (prop === 'done')
86
+ return transactionDoneMap.get(target);
87
+ // Make tx.store return the only store in the transaction, or undefined if there are many.
88
+ if (prop === 'store') {
89
+ return receiver.objectStoreNames[1]
90
+ ? undefined
91
+ : receiver.objectStore(receiver.objectStoreNames[0]);
92
+ }
93
+ }
94
+ // Else transform whatever we get back.
95
+ return wrap(target[prop]);
96
+ },
97
+ set(target, prop, value) {
98
+ target[prop] = value;
99
+ return true;
100
+ },
101
+ has(target, prop) {
102
+ if (target instanceof IDBTransaction &&
103
+ (prop === 'done' || prop === 'store')) {
104
+ return true;
105
+ }
106
+ return prop in target;
107
+ },
108
+ };
109
+ function replaceTraps(callback) {
110
+ idbProxyTraps = callback(idbProxyTraps);
111
+ }
112
+ function wrapFunction(func) {
113
+ // Due to expected object equality (which is enforced by the caching in `wrap`), we
114
+ // only create one new func per func.
115
+ // Cursor methods are special, as the behaviour is a little more different to standard IDB. In
116
+ // IDB, you advance the cursor and wait for a new 'success' on the IDBRequest that gave you the
117
+ // cursor. It's kinda like a promise that can resolve with many values. That doesn't make sense
118
+ // with real promises, so each advance methods returns a new promise for the cursor object, or
119
+ // undefined if the end of the cursor has been reached.
120
+ if (getCursorAdvanceMethods().includes(func)) {
121
+ return function (...args) {
122
+ // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use
123
+ // the original object.
124
+ func.apply(unwrap(this), args);
125
+ return wrap(this.request);
126
+ };
127
+ }
128
+ return function (...args) {
129
+ // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use
130
+ // the original object.
131
+ return wrap(func.apply(unwrap(this), args));
132
+ };
133
+ }
134
+ function transformCachableValue(value) {
135
+ if (typeof value === 'function')
136
+ return wrapFunction(value);
137
+ // This doesn't return, it just creates a 'done' promise for the transaction,
138
+ // which is later returned for transaction.done (see idbObjectHandler).
139
+ if (value instanceof IDBTransaction)
140
+ cacheDonePromiseForTransaction(value);
141
+ if (instanceOfAny(value, getIdbProxyableTypes()))
142
+ return new Proxy(value, idbProxyTraps);
143
+ // Return the same value back if we're not going to transform it.
144
+ return value;
145
+ }
146
+ function wrap(value) {
147
+ // We sometimes generate multiple promises from a single IDBRequest (eg when cursoring), because
148
+ // IDB is weird and a single IDBRequest can yield many responses, so these can't be cached.
149
+ if (value instanceof IDBRequest)
150
+ return promisifyRequest(value);
151
+ // If we've already transformed this value before, reuse the transformed value.
152
+ // This is faster, but it also provides object equality.
153
+ if (transformCache.has(value))
154
+ return transformCache.get(value);
155
+ const newValue = transformCachableValue(value);
156
+ // Not all types are transformed.
157
+ // These may be primitive types, so they can't be WeakMap keys.
158
+ if (newValue !== value) {
159
+ transformCache.set(value, newValue);
160
+ reverseTransformCache.set(newValue, value);
161
+ }
162
+ return newValue;
163
+ }
164
+ const unwrap = (value) => reverseTransformCache.get(value);
165
+
166
+ /**
167
+ * Open a database.
168
+ *
169
+ * @param name Name of the database.
170
+ * @param version Schema version.
171
+ * @param callbacks Additional callbacks.
172
+ */
173
+ function openDB(name, version, { blocked, upgrade, blocking, terminated } = {}) {
174
+ const request = indexedDB.open(name, version);
175
+ const openPromise = wrap(request);
176
+ if (upgrade) {
177
+ request.addEventListener('upgradeneeded', (event) => {
178
+ upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction), event);
179
+ });
180
+ }
181
+ if (blocked) {
182
+ request.addEventListener('blocked', (event) => blocked(
183
+ // Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405
184
+ event.oldVersion, event.newVersion, event));
185
+ }
186
+ openPromise
187
+ .then((db) => {
188
+ if (terminated)
189
+ db.addEventListener('close', () => terminated());
190
+ if (blocking) {
191
+ db.addEventListener('versionchange', (event) => blocking(event.oldVersion, event.newVersion, event));
192
+ }
193
+ })
194
+ .catch(() => { });
195
+ return openPromise;
196
+ }
197
+
198
+ const readMethods = ['get', 'getKey', 'getAll', 'getAllKeys', 'count'];
199
+ const writeMethods = ['put', 'add', 'delete', 'clear'];
200
+ const cachedMethods = new Map();
201
+ function getMethod(target, prop) {
202
+ if (!(target instanceof IDBDatabase &&
203
+ !(prop in target) &&
204
+ typeof prop === 'string')) {
205
+ return;
206
+ }
207
+ if (cachedMethods.get(prop))
208
+ return cachedMethods.get(prop);
209
+ const targetFuncName = prop.replace(/FromIndex$/, '');
210
+ const useIndex = prop !== targetFuncName;
211
+ const isWrite = writeMethods.includes(targetFuncName);
212
+ if (
213
+ // Bail if the target doesn't exist on the target. Eg, getAll isn't in Edge.
214
+ !(targetFuncName in (useIndex ? IDBIndex : IDBObjectStore).prototype) ||
215
+ !(isWrite || readMethods.includes(targetFuncName))) {
216
+ return;
217
+ }
218
+ const method = async function (storeName, ...args) {
219
+ // isWrite ? 'readwrite' : undefined gzipps better, but fails in Edge :(
220
+ const tx = this.transaction(storeName, isWrite ? 'readwrite' : 'readonly');
221
+ let target = tx.store;
222
+ if (useIndex)
223
+ target = target.index(args.shift());
224
+ // Must reject if op rejects.
225
+ // If it's a write operation, must reject if tx.done rejects.
226
+ // Must reject with op rejection first.
227
+ // Must resolve with op value.
228
+ // Must handle both promises (no unhandled rejections)
229
+ return (await Promise.all([
230
+ target[targetFuncName](...args),
231
+ isWrite && tx.done,
232
+ ]))[0];
233
+ };
234
+ cachedMethods.set(prop, method);
235
+ return method;
236
+ }
237
+ replaceTraps((oldTraps) => ({
238
+ ...oldTraps,
239
+ get: (target, prop, receiver) => getMethod(target, prop) || oldTraps.get(target, prop, receiver),
240
+ has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop),
241
+ }));
242
+
243
+ const advanceMethodProps = ['continue', 'continuePrimaryKey', 'advance'];
244
+ const methodMap = {};
245
+ const advanceResults = new WeakMap();
246
+ const ittrProxiedCursorToOriginalProxy = new WeakMap();
247
+ const cursorIteratorTraps = {
248
+ get(target, prop) {
249
+ if (!advanceMethodProps.includes(prop))
250
+ return target[prop];
251
+ let cachedFunc = methodMap[prop];
252
+ if (!cachedFunc) {
253
+ cachedFunc = methodMap[prop] = function (...args) {
254
+ advanceResults.set(this, ittrProxiedCursorToOriginalProxy.get(this)[prop](...args));
255
+ };
256
+ }
257
+ return cachedFunc;
258
+ },
259
+ };
260
+ async function* iterate(...args) {
261
+ // tslint:disable-next-line:no-this-assignment
262
+ let cursor = this;
263
+ if (!(cursor instanceof IDBCursor)) {
264
+ cursor = await cursor.openCursor(...args);
265
+ }
266
+ if (!cursor)
267
+ return;
268
+ cursor = cursor;
269
+ const proxiedCursor = new Proxy(cursor, cursorIteratorTraps);
270
+ ittrProxiedCursorToOriginalProxy.set(proxiedCursor, cursor);
271
+ // Map this double-proxy back to the original, so other cursor methods work.
272
+ reverseTransformCache.set(proxiedCursor, unwrap(cursor));
273
+ while (cursor) {
274
+ yield proxiedCursor;
275
+ // If one of the advancing methods was not called, call continue().
276
+ cursor = await (advanceResults.get(proxiedCursor) || cursor.continue());
277
+ advanceResults.delete(proxiedCursor);
278
+ }
279
+ }
280
+ function isIteratorProp(target, prop) {
281
+ return ((prop === Symbol.asyncIterator &&
282
+ instanceOfAny(target, [IDBIndex, IDBObjectStore, IDBCursor])) ||
283
+ (prop === 'iterate' && instanceOfAny(target, [IDBIndex, IDBObjectStore])));
284
+ }
285
+ replaceTraps((oldTraps) => ({
286
+ ...oldTraps,
287
+ get(target, prop, receiver) {
288
+ if (isIteratorProp(target, prop))
289
+ return iterate;
290
+ return oldTraps.get(target, prop, receiver);
291
+ },
292
+ has(target, prop) {
293
+ return isIteratorProp(target, prop) || oldTraps.has(target, prop);
294
+ },
295
+ }));
296
+
297
+ const DB_NAME = 'koto-translations';
298
+ const DB_VERSION = 3;
299
+ const STORE_NAME = 'translations';
300
+ const CACHE_DURATION = 1000 * 60 * 60; // 1 hour
301
+ class TranslationStorage {
302
+ constructor() {
303
+ this.db = null;
304
+ }
305
+ async init() {
306
+ if (this.db)
307
+ return;
308
+ this.db = await openDB(DB_NAME, DB_VERSION, {
309
+ upgrade(db) {
310
+ // Create or keep translations store
311
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
312
+ db.createObjectStore(STORE_NAME);
313
+ }
314
+ },
315
+ });
316
+ }
317
+ async getTranslations(locale) {
318
+ await this.init();
319
+ if (!this.db)
320
+ return null;
321
+ try {
322
+ const stored = await this.db.get(STORE_NAME, locale);
323
+ if (!stored)
324
+ return null;
325
+ // Check if cache is still valid
326
+ const now = Date.now();
327
+ if (now - stored.timestamp > CACHE_DURATION) {
328
+ // Cache expired
329
+ await this.db.delete(STORE_NAME, locale);
330
+ return null;
331
+ }
332
+ return stored;
333
+ }
334
+ catch (error) {
335
+ console.error('Error getting translations from IndexedDB:', error);
336
+ return null;
337
+ }
338
+ }
339
+ async setTranslations(locale, translations, apiResponse) {
340
+ await this.init();
341
+ if (!this.db)
342
+ return;
343
+ try {
344
+ // Process translations based on API response format
345
+ const processedTranslations = this.processTranslations(translations);
346
+ const stored = {
347
+ locale,
348
+ translations: processedTranslations,
349
+ timestamp: Date.now(),
350
+ version: apiResponse?.version,
351
+ };
352
+ // Include metadata if provided in API response
353
+ if (apiResponse?.localeInfo || apiResponse?.localeName) {
354
+ stored.metadata = {
355
+ name: apiResponse.localeName || apiResponse.localeInfo?.name,
356
+ nativeName: apiResponse.localeNativeName || apiResponse.localeInfo?.nativeName,
357
+ direction: apiResponse.direction || apiResponse.localeInfo?.direction || 'ltr',
358
+ };
359
+ }
360
+ await this.db.put(STORE_NAME, stored, locale);
361
+ }
362
+ catch (error) {
363
+ console.error('Error storing translations in IndexedDB:', error);
364
+ }
365
+ }
366
+ processTranslations(translations) {
367
+ // Handle different response formats
368
+ if (typeof translations === 'object' && translations !== null) {
369
+ // Check if it's already in the right format
370
+ if (this.isNestedStructure(translations)) {
371
+ return translations;
372
+ }
373
+ // Check if it's a flat key-value structure
374
+ if (this.isFlatStructure(translations)) {
375
+ return this.buildNestedFromFlat(translations);
376
+ }
377
+ }
378
+ // Return as-is if we can't determine the structure
379
+ return translations;
380
+ }
381
+ isNestedStructure(obj) {
382
+ // Check if any value is an object (nested structure)
383
+ return Object.values(obj).some(val => typeof val === 'object' && val !== null && !Array.isArray(val));
384
+ }
385
+ isFlatStructure(obj) {
386
+ // Check if all values are strings and keys contain dots
387
+ return Object.entries(obj).every(([, val]) => typeof val === 'string') && Object.keys(obj).some(key => key.includes('.'));
388
+ }
389
+ buildNestedFromFlat(flatData) {
390
+ const result = {};
391
+ for (const [key, value] of Object.entries(flatData)) {
392
+ const keys = key.split('.');
393
+ let current = result;
394
+ for (let i = 0; i < keys.length - 1; i++) {
395
+ const k = keys[i];
396
+ if (!(k in current)) {
397
+ current[k] = {};
398
+ }
399
+ current = current[k];
400
+ }
401
+ current[keys[keys.length - 1]] = value;
402
+ }
403
+ return result;
404
+ }
405
+ async clearTranslations(locale) {
406
+ await this.init();
407
+ if (!this.db)
408
+ return;
409
+ try {
410
+ if (locale) {
411
+ await this.db.delete(STORE_NAME, locale);
412
+ }
413
+ else {
414
+ await this.db.clear(STORE_NAME);
415
+ }
416
+ }
417
+ catch (error) {
418
+ console.error('Error clearing translations from IndexedDB:', error);
419
+ }
420
+ }
421
+ async getAllLocales() {
422
+ await this.init();
423
+ if (!this.db)
424
+ return [];
425
+ try {
426
+ return await this.db.getAllKeys(STORE_NAME);
427
+ }
428
+ catch (error) {
429
+ console.error('Error getting locales from IndexedDB:', error);
430
+ return [];
431
+ }
432
+ }
433
+ async getSupportedLocales() {
434
+ await this.init();
435
+ if (!this.db)
436
+ return [];
437
+ try {
438
+ const allData = await this.db.getAll(STORE_NAME);
439
+ return allData.map(data => ({
440
+ code: data.locale,
441
+ metadata: data.metadata,
442
+ }));
443
+ }
444
+ catch (error) {
445
+ console.error('Error getting supported locales:', error);
446
+ return [];
447
+ }
448
+ }
449
+ async processApiResponse(locale, response) {
450
+ // Handle different response formats dynamically
451
+ let translations = {};
452
+ // Check for translations in various possible locations
453
+ if (response.translations) {
454
+ translations = response.translations;
455
+ }
456
+ else if (response.data) {
457
+ translations = response.data;
458
+ }
459
+ else if (typeof response === 'object' && !response.error) {
460
+ // Direct translation object - must be TranslationData format
461
+ translations = response;
462
+ }
463
+ // Store with API metadata
464
+ await this.setTranslations(locale, translations, response);
465
+ return this.processTranslations(translations);
466
+ }
467
+ }
468
+ const storage = new TranslationStorage();
469
+
470
+ /**
471
+ * Get a nested translation value using dot notation
472
+ * @param translations - The translations object
473
+ * @param key - The dot-notated key (e.g., 'checkout.payment.title')
474
+ * @returns The translation value or null if not found
475
+ */
476
+ function getNestedTranslation(translations, key) {
477
+ const keys = key.split('.');
478
+ let current = translations;
479
+ for (const k of keys) {
480
+ if (current && typeof current === 'object' && k in current) {
481
+ current = current[k];
482
+ }
483
+ else {
484
+ return null;
485
+ }
486
+ }
487
+ return typeof current === 'string' ? current : null;
488
+ }
489
+ /**
490
+ * Flatten nested translation object
491
+ * @param obj - The nested translation object
492
+ * @param prefix - The prefix for keys
493
+ * @returns Flattened object with dot-notated keys
494
+ */
495
+ function flattenTranslations(obj, prefix = '') {
496
+ const flattened = {};
497
+ for (const [key, value] of Object.entries(obj)) {
498
+ const newKey = prefix ? `${prefix}.${key}` : key;
499
+ if (typeof value === 'string') {
500
+ flattened[newKey] = value;
501
+ }
502
+ else if (typeof value === 'object' && value !== null) {
503
+ Object.assign(flattened, flattenTranslations(value, newKey));
504
+ }
505
+ }
506
+ return flattened;
507
+ }
508
+ /**
509
+ * Unflatten dot-notated keys to nested object
510
+ * @param obj - The flattened object
511
+ * @returns Nested translation object
512
+ */
513
+ function unflattenTranslations(obj) {
514
+ const result = {};
515
+ for (const [key, value] of Object.entries(obj)) {
516
+ const keys = key.split('.');
517
+ let current = result;
518
+ for (let i = 0; i < keys.length - 1; i++) {
519
+ const k = keys[i];
520
+ if (!(k in current)) {
521
+ current[k] = {};
522
+ }
523
+ current = current[k];
524
+ }
525
+ current[keys[keys.length - 1]] = value;
526
+ }
527
+ return result;
528
+ }
529
+
530
+ /**
531
+ * Available locales following BCP 47 standard (IETF language tags)
532
+ * Format: language[-script][-region]
533
+ * Examples: en, en-US, zh-CN, zh-Hans-CN
534
+ *
535
+ * These locale codes must match exactly what's in your database
536
+ * Following the same standard as react-i18next
537
+ */
538
+ const AVAILABLE_LOCALES = [
539
+ // English variants
540
+ { code: 'en', name: 'English', nativeName: 'English', direction: 'ltr', enabled: true },
541
+ { code: 'en-US', name: 'English (US)', nativeName: 'English (US)', direction: 'ltr', enabled: true },
542
+ { code: 'en-GB', name: 'English (UK)', nativeName: 'English (UK)', direction: 'ltr', enabled: true },
543
+ // Chinese variants - using script subtags
544
+ { code: 'zh', name: 'Chinese', nativeName: '中文', direction: 'ltr', enabled: true },
545
+ { code: 'zh-CN', name: 'Chinese (Simplified)', nativeName: '简体中文', direction: 'ltr', enabled: true },
546
+ { code: 'zh-TW', name: 'Chinese (Traditional)', nativeName: '繁體中文', direction: 'ltr', enabled: true },
547
+ { code: 'zh-Hans', name: 'Chinese (Simplified)', nativeName: '简体中文', direction: 'ltr', enabled: true },
548
+ { code: 'zh-Hant', name: 'Chinese (Traditional)', nativeName: '繁體中文', direction: 'ltr', enabled: true },
549
+ // Spanish variants
550
+ { code: 'es', name: 'Spanish', nativeName: 'Español', direction: 'ltr', enabled: true },
551
+ { code: 'es-ES', name: 'Spanish (Spain)', nativeName: 'Español (España)', direction: 'ltr', enabled: true },
552
+ { code: 'es-MX', name: 'Spanish (Mexico)', nativeName: 'Español (México)', direction: 'ltr', enabled: true },
553
+ // Portuguese variants
554
+ { code: 'pt', name: 'Portuguese', nativeName: 'Português', direction: 'ltr', enabled: true },
555
+ { code: 'pt-BR', name: 'Portuguese (Brazil)', nativeName: 'Português (Brasil)', direction: 'ltr', enabled: true },
556
+ { code: 'pt-PT', name: 'Portuguese (Portugal)', nativeName: 'Português (Portugal)', direction: 'ltr', enabled: true },
557
+ // French variants
558
+ { code: 'fr', name: 'French', nativeName: 'Français', direction: 'ltr', enabled: true },
559
+ { code: 'fr-FR', name: 'French (France)', nativeName: 'Français (France)', direction: 'ltr', enabled: true },
560
+ { code: 'fr-CA', name: 'French (Canada)', nativeName: 'Français (Canada)', direction: 'ltr', enabled: true },
561
+ // German variants
562
+ { code: 'de', name: 'German', nativeName: 'Deutsch', direction: 'ltr', enabled: true },
563
+ { code: 'de-DE', name: 'German (Germany)', nativeName: 'Deutsch (Deutschland)', direction: 'ltr', enabled: true },
564
+ { code: 'de-AT', name: 'German (Austria)', nativeName: 'Deutsch (Österreich)', direction: 'ltr', enabled: true },
565
+ { code: 'de-CH', name: 'German (Switzerland)', nativeName: 'Deutsch (Schweiz)', direction: 'ltr', enabled: true },
566
+ // Other languages
567
+ { code: 'it', name: 'Italian', nativeName: 'Italiano', direction: 'ltr', enabled: true },
568
+ { code: 'it-IT', name: 'Italian (Italy)', nativeName: 'Italiano (Italia)', direction: 'ltr', enabled: true },
569
+ { code: 'ja', name: 'Japanese', nativeName: '日本語', direction: 'ltr', enabled: true },
570
+ { code: 'ja-JP', name: 'Japanese (Japan)', nativeName: '日本語 (日本)', direction: 'ltr', enabled: true },
571
+ { code: 'ko', name: 'Korean', nativeName: '한국어', direction: 'ltr', enabled: true },
572
+ { code: 'ko-KR', name: 'Korean (Korea)', nativeName: '한국어 (대한민국)', direction: 'ltr', enabled: true },
573
+ { code: 'ru', name: 'Russian', nativeName: 'Русский', direction: 'ltr', enabled: true },
574
+ { code: 'ru-RU', name: 'Russian (Russia)', nativeName: 'Русский (Россия)', direction: 'ltr', enabled: true },
575
+ // RTL languages
576
+ { code: 'ar', name: 'Arabic', nativeName: 'العربية', direction: 'rtl', enabled: true },
577
+ { code: 'ar-SA', name: 'Arabic (Saudi Arabia)', nativeName: 'العربية (المملكة العربية السعودية)', direction: 'rtl', enabled: true },
578
+ { code: 'ar-AE', name: 'Arabic (UAE)', nativeName: 'العربية (الإمارات)', direction: 'rtl', enabled: true },
579
+ { code: 'he', name: 'Hebrew', nativeName: 'עברית', direction: 'rtl', enabled: true },
580
+ { code: 'he-IL', name: 'Hebrew (Israel)', nativeName: 'עברית (ישראל)', direction: 'rtl', enabled: true },
581
+ // Other Asian languages
582
+ { code: 'hi', name: 'Hindi', nativeName: 'हिन्दी', direction: 'ltr', enabled: true },
583
+ { code: 'hi-IN', name: 'Hindi (India)', nativeName: 'हिन्दी (भारत)', direction: 'ltr', enabled: true },
584
+ { code: 'th', name: 'Thai', nativeName: 'ไทย', direction: 'ltr', enabled: true },
585
+ { code: 'th-TH', name: 'Thai (Thailand)', nativeName: 'ไทย (ประเทศไทย)', direction: 'ltr', enabled: true },
586
+ { code: 'vi', name: 'Vietnamese', nativeName: 'Tiếng Việt', direction: 'ltr', enabled: true },
587
+ { code: 'vi-VN', name: 'Vietnamese (Vietnam)', nativeName: 'Tiếng Việt (Việt Nam)', direction: 'ltr', enabled: true },
588
+ { code: 'id', name: 'Indonesian', nativeName: 'Bahasa Indonesia', direction: 'ltr', enabled: true },
589
+ { code: 'id-ID', name: 'Indonesian (Indonesia)', nativeName: 'Bahasa Indonesia (Indonesia)', direction: 'ltr', enabled: true },
590
+ // European languages
591
+ { code: 'nl', name: 'Dutch', nativeName: 'Nederlands', direction: 'ltr', enabled: true },
592
+ { code: 'nl-NL', name: 'Dutch (Netherlands)', nativeName: 'Nederlands (Nederland)', direction: 'ltr', enabled: true },
593
+ { code: 'nl-BE', name: 'Dutch (Belgium)', nativeName: 'Nederlands (België)', direction: 'ltr', enabled: true },
594
+ { code: 'pl', name: 'Polish', nativeName: 'Polski', direction: 'ltr', enabled: true },
595
+ { code: 'pl-PL', name: 'Polish (Poland)', nativeName: 'Polski (Polska)', direction: 'ltr', enabled: true },
596
+ { code: 'tr', name: 'Turkish', nativeName: 'Türkçe', direction: 'ltr', enabled: true },
597
+ { code: 'tr-TR', name: 'Turkish (Turkey)', nativeName: 'Türkçe (Türkiye)', direction: 'ltr', enabled: true },
598
+ { code: 'sv', name: 'Swedish', nativeName: 'Svenska', direction: 'ltr', enabled: true },
599
+ { code: 'sv-SE', name: 'Swedish (Sweden)', nativeName: 'Svenska (Sverige)', direction: 'ltr', enabled: true },
600
+ { code: 'da', name: 'Danish', nativeName: 'Dansk', direction: 'ltr', enabled: true },
601
+ { code: 'da-DK', name: 'Danish (Denmark)', nativeName: 'Dansk (Danmark)', direction: 'ltr', enabled: true },
602
+ { code: 'no', name: 'Norwegian', nativeName: 'Norsk', direction: 'ltr', enabled: true },
603
+ { code: 'nb-NO', name: 'Norwegian Bokmål', nativeName: 'Norsk Bokmål', direction: 'ltr', enabled: true },
604
+ { code: 'fi', name: 'Finnish', nativeName: 'Suomi', direction: 'ltr', enabled: true },
605
+ { code: 'fi-FI', name: 'Finnish (Finland)', nativeName: 'Suomi (Suomi)', direction: 'ltr', enabled: true },
606
+ { code: 'cs', name: 'Czech', nativeName: 'Čeština', direction: 'ltr', enabled: true },
607
+ { code: 'cs-CZ', name: 'Czech (Czech Republic)', nativeName: 'Čeština (Česká republika)', direction: 'ltr', enabled: true },
608
+ { code: 'hu', name: 'Hungarian', nativeName: 'Magyar', direction: 'ltr', enabled: true },
609
+ { code: 'hu-HU', name: 'Hungarian (Hungary)', nativeName: 'Magyar (Magyarország)', direction: 'ltr', enabled: true },
610
+ { code: 'el', name: 'Greek', nativeName: 'Ελληνικά', direction: 'ltr', enabled: true },
611
+ { code: 'el-GR', name: 'Greek (Greece)', nativeName: 'Ελληνικά (Ελλάδα)', direction: 'ltr', enabled: true },
612
+ ];
613
+ /**
614
+ * Get locale info by code
615
+ */
616
+ function getLocaleInfo(code) {
617
+ return AVAILABLE_LOCALES.find(locale => locale.code === code);
618
+ }
619
+ /**
620
+ * Check if a locale code is supported
621
+ */
622
+ function isLocaleSupported(code) {
623
+ return AVAILABLE_LOCALES.some(locale => locale.code === code);
624
+ }
625
+ /**
626
+ * Get fallback locale code
627
+ * For example: en-US -> en, zh-CN -> zh
628
+ */
629
+ function getFallbackLocale(code) {
630
+ // Already a base locale
631
+ if (!code.includes('-')) {
632
+ return undefined;
633
+ }
634
+ // Extract base language code
635
+ const baseCode = code.split('-')[0];
636
+ return isLocaleSupported(baseCode) ? baseCode : undefined;
637
+ }
638
+ /**
639
+ * Resolve locale with fallback
640
+ * Tries the exact locale, then fallback to base language
641
+ */
642
+ function resolveLocale(code) {
643
+ // Exact match
644
+ if (isLocaleSupported(code)) {
645
+ return code;
646
+ }
647
+ // Try fallback
648
+ const fallback = getFallbackLocale(code);
649
+ if (fallback) {
650
+ return fallback;
651
+ }
652
+ // Default to English
653
+ return 'en';
654
+ }
655
+
656
+ const KotoContext = React.createContext(null);
657
+ const LOCALE_STORAGE_KEY = 'koto-selected-locale';
658
+ function KotoProvider({ children, apiKey, projectId, defaultLocale, apiUrl = 'https://api.koto.dev/v1/translations' }) {
659
+ // Initialize locale from localStorage or use defaultLocale
660
+ const getInitialLocale = () => {
661
+ if (typeof window !== 'undefined') {
662
+ const savedLocale = localStorage.getItem(LOCALE_STORAGE_KEY);
663
+ if (savedLocale) {
664
+ return savedLocale;
665
+ }
666
+ }
667
+ return defaultLocale;
668
+ };
669
+ const [translations, setTranslations] = React.useState({});
670
+ const [locale, setLocale] = React.useState(getInitialLocale());
671
+ const [loading, setLoading] = React.useState(true);
672
+ const [error, setError] = React.useState(null);
673
+ // Use hardcoded locales that match the database
674
+ const availableLocales = AVAILABLE_LOCALES;
675
+ // Fetch translations from API
676
+ const fetchTranslations = React.useCallback(async (currentLocale) => {
677
+ try {
678
+ const response = await fetch(`${apiUrl}?locale=${currentLocale}&projectId=${projectId}`, {
679
+ headers: {
680
+ 'x-api-key': apiKey,
681
+ 'Content-Type': 'application/json',
682
+ },
683
+ });
684
+ if (!response.ok) {
685
+ throw new Error(`Failed to fetch translations: ${response.statusText}`);
686
+ }
687
+ const data = await response.json();
688
+ // Let storage handle the response format dynamically
689
+ return await storage.processApiResponse(currentLocale, data);
690
+ }
691
+ catch (err) {
692
+ throw err instanceof Error ? err : new Error('Failed to fetch translations');
693
+ }
694
+ }, [apiKey, projectId, apiUrl]);
695
+ // Load translations (from cache or API)
696
+ const loadTranslations = React.useCallback(async (currentLocale) => {
697
+ setLoading(true);
698
+ setError(null);
699
+ try {
700
+ // Try to get from IndexedDB first
701
+ const cached = await storage.getTranslations(currentLocale);
702
+ if (cached) {
703
+ setTranslations(cached.translations);
704
+ setLoading(false);
705
+ // Fetch fresh data in background
706
+ fetchTranslations(currentLocale)
707
+ .then((freshTranslations) => {
708
+ setTranslations(freshTranslations);
709
+ })
710
+ .catch(console.error);
711
+ }
712
+ else {
713
+ // No cache, fetch from API
714
+ const freshTranslations = await fetchTranslations(currentLocale);
715
+ setTranslations(freshTranslations);
716
+ setLoading(false);
717
+ }
718
+ }
719
+ catch (err) {
720
+ setError(err instanceof Error ? err : new Error('Failed to load translations'));
721
+ setLoading(false);
722
+ }
723
+ }, [fetchTranslations]);
724
+ // Translation function
725
+ const t = React.useCallback((key, fallback) => {
726
+ const translation = getNestedTranslation(translations, key);
727
+ return translation || fallback || key;
728
+ }, [translations]);
729
+ // Function to change locale
730
+ const changeLocale = React.useCallback((newLocale) => {
731
+ if (newLocale !== locale) {
732
+ setLocale(newLocale);
733
+ // Save to localStorage
734
+ if (typeof window !== 'undefined') {
735
+ localStorage.setItem(LOCALE_STORAGE_KEY, newLocale);
736
+ }
737
+ }
738
+ }, [locale]);
739
+ // Get available locales (returns hardcoded list)
740
+ const getAvailableLocales = React.useCallback(() => {
741
+ return availableLocales;
742
+ }, [availableLocales]);
743
+ // Load translations on mount and locale change
744
+ React.useEffect(() => {
745
+ loadTranslations(locale);
746
+ }, [locale, loadTranslations]);
747
+ // Context value
748
+ const value = React.useMemo(() => ({
749
+ translations,
750
+ locale,
751
+ loading,
752
+ error,
753
+ t,
754
+ setLocale: changeLocale,
755
+ availableLocales,
756
+ getAvailableLocales,
757
+ }), [translations, locale, loading, error, t, changeLocale, availableLocales, getAvailableLocales]);
758
+ return (React.createElement(KotoContext.Provider, { value: value }, children));
759
+ }
760
+ // Hook to use Koto context
761
+ function useKoto() {
762
+ const context = React.useContext(KotoContext);
763
+ if (!context) {
764
+ throw new Error('useKoto must be used within a KotoProvider');
765
+ }
766
+ return context;
767
+ }
768
+ // Hook to use translation function
769
+ function useTranslation() {
770
+ const { t, locale, loading, setLocale, availableLocales, getAvailableLocales } = useKoto();
771
+ return { t, locale, loading, setLocale, availableLocales, getAvailableLocales };
772
+ }
773
+
774
+ /**
775
+ * Higher-Order Component for class components
776
+ * Wraps a component and injects translation props
777
+ */
778
+ function withTranslation(Component) {
779
+ const ComponentWithTranslation = (props) => {
780
+ const { t, locale, loading, setLocale, translations, error } = useKoto();
781
+ return (React.createElement(Component, { ...props, t: t, locale: locale, loading: loading, setLocale: setLocale, translations: translations, error: error }));
782
+ };
783
+ ComponentWithTranslation.displayName = `withTranslation(${Component.displayName || Component.name || 'Component'})`;
784
+ return ComponentWithTranslation;
785
+ }
786
+ function Translation({ children }) {
787
+ const kotoContext = useKoto();
788
+ return React.createElement(React.Fragment, null, children(kotoContext));
789
+ }
790
+ /**
791
+ * Consumer component for direct context access
792
+ * Useful for class components that need conditional rendering
793
+ */
794
+ class KotoConsumer extends React.Component {
795
+ render() {
796
+ return React.createElement(Translation, null, this.props.children);
797
+ }
798
+ }
799
+
800
+ // Global cache for translations
801
+ let globalTranslations = {};
802
+ let globalLocale = 'en';
803
+ let isInitialized = false;
804
+ /**
805
+ * Initialize the translation function with a locale
806
+ * This should be called once when the app starts
807
+ */
808
+ async function initTranslations(locale) {
809
+ globalLocale = locale;
810
+ // Try to load from IndexedDB
811
+ const cached = await storage.getTranslations(locale);
812
+ if (cached) {
813
+ globalTranslations = cached.translations;
814
+ isInitialized = true;
815
+ }
816
+ }
817
+ /**
818
+ * Set translations directly (useful when fetched from API)
819
+ */
820
+ function setTranslations(translations, locale) {
821
+ globalTranslations = translations;
822
+ globalLocale = locale;
823
+ isInitialized = true;
824
+ // Store in IndexedDB for future use
825
+ storage.setTranslations(locale, translations);
826
+ }
827
+ /**
828
+ * Get current locale
829
+ */
830
+ function getLocale() {
831
+ return globalLocale;
832
+ }
833
+ /**
834
+ * Translation function
835
+ * @param key - The translation key (e.g., 'checkout.payment.title')
836
+ * @param fallback - Optional fallback value if translation not found
837
+ * @returns The translated string or the key if not found
838
+ */
839
+ function t(key, fallback) {
840
+ if (!isInitialized) {
841
+ console.warn('Translations not initialized. Call initTranslations() first.');
842
+ return fallback || key;
843
+ }
844
+ const translation = getNestedTranslation(globalTranslations, key);
845
+ return translation || fallback || key;
846
+ }
847
+ /**
848
+ * Translation function with interpolation
849
+ * @param key - The translation key
850
+ * @param params - Object with values to interpolate
851
+ * @param fallback - Optional fallback value
852
+ * @returns The translated and interpolated string
853
+ *
854
+ * @example
855
+ * // Translation: "Hello, {{name}}!"
856
+ * ti('greeting', { name: 'John' }) // "Hello, John!"
857
+ */
858
+ function ti(key, params, fallback) {
859
+ let translation = t(key, fallback);
860
+ // Replace placeholders with values
861
+ Object.entries(params).forEach(([paramKey, value]) => {
862
+ const placeholder = new RegExp(`{{\\s*${paramKey}\\s*}}`, 'g');
863
+ translation = translation.replace(placeholder, String(value));
864
+ });
865
+ return translation;
866
+ }
867
+ /**
868
+ * Pluralization helper
869
+ * @param key - The base translation key
870
+ * @param count - The count for pluralization
871
+ * @param params - Optional parameters for interpolation
872
+ * @returns The pluralized translation
873
+ *
874
+ * @example
875
+ * // Translations:
876
+ * // "items.zero": "No items"
877
+ * // "items.one": "One item"
878
+ * // "items.other": "{{count}} items"
879
+ * tp('items', 0) // "No items"
880
+ * tp('items', 1) // "One item"
881
+ * tp('items', 5) // "5 items"
882
+ */
883
+ function tp(key, count, params) {
884
+ let pluralKey = key;
885
+ if (count === 0) {
886
+ pluralKey = `${key}.zero`;
887
+ }
888
+ else if (count === 1) {
889
+ pluralKey = `${key}.one`;
890
+ }
891
+ else {
892
+ pluralKey = `${key}.other`;
893
+ }
894
+ // Try the plural key first, fall back to base key
895
+ let translation = t(pluralKey);
896
+ if (translation === pluralKey) {
897
+ translation = t(key);
898
+ }
899
+ // Apply interpolation with count included
900
+ const allParams = { count, ...params };
901
+ return ti(translation, allParams);
902
+ }
903
+
904
+ // Provider and hooks
905
+
906
+ exports.AVAILABLE_LOCALES = AVAILABLE_LOCALES;
907
+ exports.HermesConsumer = KotoConsumer;
908
+ exports.HermesProvider = KotoProvider;
909
+ exports.KotoConsumer = KotoConsumer;
910
+ exports.KotoProvider = KotoProvider;
911
+ exports.Translation = Translation;
912
+ exports.default = t;
913
+ exports.flattenTranslations = flattenTranslations;
914
+ exports.getFallbackLocale = getFallbackLocale;
915
+ exports.getLocale = getLocale;
916
+ exports.getLocaleInfo = getLocaleInfo;
917
+ exports.getNestedTranslation = getNestedTranslation;
918
+ exports.initTranslations = initTranslations;
919
+ exports.isLocaleSupported = isLocaleSupported;
920
+ exports.resolveLocale = resolveLocale;
921
+ exports.setTranslations = setTranslations;
922
+ exports.storage = storage;
923
+ exports.t = t;
924
+ exports.ti = ti;
925
+ exports.tp = tp;
926
+ exports.unflattenTranslations = unflattenTranslations;
927
+ exports.useHermes = useKoto;
928
+ exports.useKoto = useKoto;
929
+ exports.useTranslation = useTranslation;
930
+ exports.withTranslation = withTranslation;
931
+ //# sourceMappingURL=index.js.map