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