i18n-keyless-react 1.8.0 → 1.9.1

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.
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { TranslationOptions } from "i18n-keyless-core";
2
+ import { type TranslationOptions } from "i18n-keyless-core";
3
3
  export interface I18nKeylessTextProps {
4
4
  /**
5
5
  * The `children` prop must be a string.
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React, { useEffect, useMemo } from "react";
3
+ import { getTranslation } from "i18n-keyless-core";
3
4
  import { useI18nKeyless } from "./store";
4
5
  const warnAboutWhitespace = (text) => {
5
6
  if (process.env.NODE_ENV === "development" && text !== text.trim()) {
@@ -11,15 +12,14 @@ export const I18nKeylessText = ({ children, replace, context, debug = false, for
11
12
  const translations = useI18nKeyless((store) => store.translations);
12
13
  const currentLanguage = useI18nKeyless((store) => store.currentLanguage);
13
14
  const config = useI18nKeyless((store) => store.config);
14
- const translateKey = useI18nKeyless((store) => store.translateKey);
15
15
  // Trim the source text immediately
16
16
  const sourceText = children.trim();
17
17
  useEffect(() => {
18
18
  warnAboutWhitespace(children);
19
19
  }, [children]);
20
20
  useEffect(() => {
21
- translateKey(sourceText, { context, debug, forceTemporary });
22
- // eslint-disable-next-line react-hooks/exhaustive-deps
21
+ const store = useI18nKeyless.getState();
22
+ getTranslation(sourceText, store, { context, debug, forceTemporary });
23
23
  }, [sourceText, currentLanguage, context, debug, forceTemporary]);
24
24
  const translatedText = currentLanguage === config.languages.primary
25
25
  ? sourceText
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,144 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { I18nKeylessText } from "../I18nKeylessText";
4
+ import { vi, beforeEach, describe, it, expect, afterEach } from "vitest";
5
+ // Create a mock store before vi.mock call using vi.hoisted
6
+ const mockStore = vi.hoisted(() => {
7
+ const store = {
8
+ config: null,
9
+ currentLanguage: "en",
10
+ translations: {},
11
+ uniqueId: null,
12
+ lastRefresh: null,
13
+ setTranslations: vi.fn(),
14
+ setLanguage: vi.fn((lang) => {
15
+ store.currentLanguage = lang;
16
+ }),
17
+ };
18
+ // Create a function that supports the selector pattern
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ const useI18nKeylessMock = (selectorOrStore) => {
21
+ // If it's a function (selector), call it with the store
22
+ if (typeof selectorOrStore === "function") {
23
+ return selectorOrStore(store);
24
+ }
25
+ // Otherwise return the store
26
+ return store;
27
+ };
28
+ // Add the getState and setState methods to the mock function
29
+ useI18nKeylessMock.getState = vi.fn(() => store);
30
+ useI18nKeylessMock.setState = vi.fn((newState) => Object.assign(store, newState));
31
+ return useI18nKeylessMock;
32
+ });
33
+ // Mock the store module - this is hoisted to the top of the file
34
+ vi.mock("../store", () => {
35
+ return {
36
+ useI18nKeyless: mockStore,
37
+ // Add any other exports from the store that might be needed
38
+ getTranslation: vi.fn(),
39
+ };
40
+ });
41
+ // Mock utils module to avoid errors with getTranslation
42
+ vi.mock("../utils", () => ({
43
+ getTranslation: vi.fn(),
44
+ validateLanguage: vi.fn((lang) => lang),
45
+ }));
46
+ describe("I18nKeylessText", () => {
47
+ // Save original console methods
48
+ const originalConsoleWarn = console.warn;
49
+ const originalConsoleLog = console.log;
50
+ beforeEach(() => {
51
+ vi.clearAllMocks();
52
+ // Mock console methods for testing
53
+ console.warn = vi.fn();
54
+ console.log = vi.fn();
55
+ // Reset store to default state
56
+ mockStore.setState({
57
+ translations: {},
58
+ currentLanguage: "en",
59
+ config: {
60
+ languages: {
61
+ primary: "en",
62
+ supported: ["en", "fr"],
63
+ },
64
+ },
65
+ });
66
+ });
67
+ afterEach(() => {
68
+ // Restore original console methods
69
+ console.warn = originalConsoleWarn;
70
+ console.log = originalConsoleLog;
71
+ });
72
+ it("renders the original text when language is primary", () => {
73
+ render(_jsx(I18nKeylessText, { children: "Hello World" }));
74
+ expect(screen.getByText("Hello World")).toBeInTheDocument();
75
+ });
76
+ it("renders translated text when available", () => {
77
+ mockStore.setState({
78
+ currentLanguage: "fr",
79
+ translations: {
80
+ "Hello World": "Bonjour le monde",
81
+ },
82
+ });
83
+ render(_jsx(I18nKeylessText, { children: "Hello World" }));
84
+ expect(screen.getByText("Bonjour le monde")).toBeInTheDocument();
85
+ });
86
+ it("handles whitespace trimming and warns in development", () => {
87
+ const originalNodeEnv = process.env.NODE_ENV;
88
+ process.env.NODE_ENV = "development";
89
+ render(_jsx(I18nKeylessText, { children: " Hello World " }));
90
+ expect(screen.getByText("Hello World")).toBeInTheDocument();
91
+ expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("received text with leading/trailing whitespace"));
92
+ process.env.NODE_ENV = originalNodeEnv;
93
+ });
94
+ it("handles text replacement", () => {
95
+ render(_jsx(I18nKeylessText, { replace: { "{{name}}": "John" }, children: `Hello {{name}}` }));
96
+ expect(screen.getByText("Hello John")).toBeInTheDocument();
97
+ });
98
+ it("handles context-specific translations", () => {
99
+ mockStore.setState({
100
+ currentLanguage: "fr",
101
+ translations: {
102
+ Welcome__header: "Bienvenue",
103
+ },
104
+ });
105
+ render(_jsx(I18nKeylessText, { context: "header", children: "Welcome" }));
106
+ expect(screen.getByText("Bienvenue")).toBeInTheDocument();
107
+ });
108
+ it("logs debug information when debug is true", () => {
109
+ render(_jsx(I18nKeylessText, { debug: true, children: "Hello World" }));
110
+ expect(console.log).toHaveBeenCalledWith(expect.objectContaining({
111
+ children: "Hello World",
112
+ sourceText: "Hello World",
113
+ currentLanguage: "en",
114
+ }));
115
+ });
116
+ it("handles force temporary translations", () => {
117
+ const forceTemp = {
118
+ fr: "Bonjour temporaire",
119
+ };
120
+ render(_jsx(I18nKeylessText, { forceTemporary: forceTemp, children: "Hello World" }));
121
+ expect(screen.getByText("Hello World")).toBeInTheDocument();
122
+ });
123
+ it("falls back to source text when translation is missing", () => {
124
+ mockStore.setState({
125
+ currentLanguage: "fr",
126
+ translations: {}, // Empty translations
127
+ });
128
+ render(_jsx(I18nKeylessText, { children: "Hello World" }));
129
+ expect(screen.getByText("Hello World")).toBeInTheDocument();
130
+ });
131
+ it("handles multiple replacements in text", () => {
132
+ render(_jsx(I18nKeylessText, { replace: {
133
+ "{{name}}": "John",
134
+ "{{age}}": "30",
135
+ }, children: `{{name}} is {{age}} years old` }));
136
+ expect(screen.getByText("John is 30 years old")).toBeInTheDocument();
137
+ });
138
+ it("preserves special characters in replacements", () => {
139
+ render(_jsx(I18nKeylessText, { replace: {
140
+ "{{special}}": "$@#!",
141
+ }, children: `Special chars: {{special}}` }));
142
+ expect(screen.getByText("Special chars: $@#!")).toBeInTheDocument();
143
+ });
144
+ });
@@ -0,0 +1,9 @@
1
+ import { type TranslationStore } from "i18n-keyless-core";
2
+ export declare const store: TranslationStore;
3
+ export declare const mockStore: any;
4
+ export declare const mockStorage: {
5
+ getItem: import("vitest").Mock<(...args: any[]) => any>;
6
+ setItem: import("vitest").Mock<(...args: any[]) => any>;
7
+ removeItem: import("vitest").Mock<(...args: any[]) => any>;
8
+ clearAll: import("vitest").Mock<(...args: any[]) => any>;
9
+ };
@@ -0,0 +1,33 @@
1
+ import { validateLanguage } from "i18n-keyless-core";
2
+ import { vi } from "vitest";
3
+ export const store = {
4
+ config: null,
5
+ currentLanguage: "fr",
6
+ translations: {},
7
+ uniqueId: null,
8
+ lastRefresh: null,
9
+ setTranslations: vi.fn(),
10
+ setLanguage: vi.fn((lang) => {
11
+ const validated = validateLanguage(lang, store.config);
12
+ store.currentLanguage = validated;
13
+ }),
14
+ };
15
+ // Create a function that supports the selector pattern
16
+ const useI18nKeylessMock = ((selectorOrStore) => {
17
+ // If it's a function (selector), call it with the store
18
+ if (typeof selectorOrStore === "function") {
19
+ return selectorOrStore(store);
20
+ }
21
+ // Otherwise return the store
22
+ return store;
23
+ });
24
+ // Add the getState and setState methods to the mock function
25
+ useI18nKeylessMock.getState = vi.fn(() => store);
26
+ useI18nKeylessMock.setState = vi.fn((newState) => Object.assign(store, newState));
27
+ export const mockStore = useI18nKeylessMock;
28
+ export const mockStorage = {
29
+ getItem: vi.fn(),
30
+ setItem: vi.fn(),
31
+ removeItem: vi.fn(),
32
+ clearAll: vi.fn(),
33
+ };
File without changes
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ // // __mocks__/zustand.ts
3
+ // import { act } from '@testing-library/react'
4
+ // import type * as ZustandExportedTypes from 'zustand'
5
+ // export * from 'zustand'
6
+ // const { create: actualCreate, createStore: actualCreateStore } =
7
+ // await vi.importActual<typeof ZustandExportedTypes>('zustand')
8
+ // // a variable to hold reset functions for all stores declared in the app
9
+ // export const storeResetFns = new Set<() => void>()
10
+ // const createUncurried = <T>(
11
+ // stateCreator: ZustandExportedTypes.StateCreator<T>,
12
+ // ) => {
13
+ // const store = actualCreate(stateCreator)
14
+ // const initialState = store.getInitialState()
15
+ // storeResetFns.add(() => {
16
+ // store.setState(initialState, true)
17
+ // })
18
+ // return store
19
+ // }
20
+ // // when creating a store, we get its initial state, create a reset function and add it in the set
21
+ // export const create = (<T>(
22
+ // stateCreator: ZustandExportedTypes.StateCreator<T>,
23
+ // ) => {
24
+ // console.log('zustand create mock')
25
+ // // to support curried version of create
26
+ // return typeof stateCreator === 'function'
27
+ // ? createUncurried(stateCreator)
28
+ // : createUncurried
29
+ // }) as typeof ZustandExportedTypes.create
30
+ // const createStoreUncurried = <T>(
31
+ // stateCreator: ZustandExportedTypes.StateCreator<T>,
32
+ // ) => {
33
+ // const store = actualCreateStore(stateCreator)
34
+ // const initialState = store.getInitialState()
35
+ // storeResetFns.add(() => {
36
+ // store.setState(initialState, true)
37
+ // })
38
+ // return store
39
+ // }
40
+ // // when creating a store, we get its initial state, create a reset function and add it in the set
41
+ // export const createStore = (<T>(
42
+ // stateCreator: ZustandExportedTypes.StateCreator<T>,
43
+ // ) => {
44
+ // console.log('zustand createStore mock')
45
+ // // to support curried version of createStore
46
+ // return typeof stateCreator === 'function'
47
+ // ? createStoreUncurried(stateCreator)
48
+ // : createStoreUncurried
49
+ // }) as typeof ZustandExportedTypes.createStore
50
+ // // reset all stores after each test run
51
+ // afterEach(() => {
52
+ // act(() => {
53
+ // storeResetFns.forEach((resetFn) => {
54
+ // resetFn()
55
+ // })
56
+ // })
57
+ // })
58
+ // export const createStore = (<T>(stateCreator: ZustandExportedTypes.StateCreator<T>) => {
59
+ // console.log("zustand createStore mock");
60
+ // // to support curried version of createStore
61
+ // return typeof stateCreator === "function" ? createStoreUncurried(stateCreator) : createStoreUncurried;
62
+ // }) as typeof ZustandExportedTypes.createStore;
63
+ // // reset all stores after each test run
64
+ // afterEach(() => {
65
+ // act(() => {
66
+ // storeResetFns.forEach((resetFn) => {
67
+ // resetFn();
68
+ // });
69
+ // });
70
+ // });
@@ -0,0 +1 @@
1
+ import "@testing-library/jest-dom";
@@ -0,0 +1,3 @@
1
+ import "@testing-library/jest-dom";
2
+ import { vi } from "vitest";
3
+ vi.mock("zustand");
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,361 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { act } from "@testing-library/react";
3
+ import { mockStore, mockStorage } from "./__mocks__/store";
4
+ import { useI18nKeyless, init } from "../store";
5
+ import { getTranslation, queue } from "i18n-keyless-core";
6
+ // These vi.mock calls must be at the top level, outside of any function or block
7
+ vi.mock("zustand", () => ({
8
+ create: () => ({
9
+ getState: mockStore.getState,
10
+ setState: mockStore.setState,
11
+ subscribe: mockStore.subscribe,
12
+ }),
13
+ }));
14
+ vi.mock("../store", async () => {
15
+ const actual = await vi.importActual("../store");
16
+ return {
17
+ useI18nKeyless: mockStore,
18
+ useCurrentLanguage: vi.fn(),
19
+ getTranslation: vi.fn(),
20
+ setCurrentLanguage: vi.fn(),
21
+ fetchAllTranslations: vi.fn(),
22
+ clearI18nKeylessStorage: vi.fn(),
23
+ init: actual.init,
24
+ };
25
+ });
26
+ // Mock fetch for API calls
27
+ global.fetch = vi.fn();
28
+ describe("i18n-keyless store", () => {
29
+ // Save original console methods
30
+ const originalConsoleError = console.error;
31
+ const originalConsoleWarn = console.warn;
32
+ const originalConsoleLog = console.log;
33
+ beforeEach(() => {
34
+ vi.clearAllMocks();
35
+ console.error = vi.fn();
36
+ console.warn = vi.fn();
37
+ console.log = vi.fn();
38
+ useI18nKeyless.setState({ translations: {} });
39
+ // The store is automatically reset by the zustand mock
40
+ });
41
+ afterEach(() => {
42
+ // Restore original console methods
43
+ console.error = originalConsoleError;
44
+ console.warn = originalConsoleWarn;
45
+ console.log = originalConsoleLog;
46
+ });
47
+ describe("init function", () => {
48
+ it("should throw an error if languages is not provided", async () => {
49
+ // @ts-expect-error Testing invalid config
50
+ await expect(init({ storage: mockStorage })).rejects.toThrow("i18n-keyless: languages is required");
51
+ });
52
+ it("should throw an error if primary language is not provided", async () => {
53
+ await expect(
54
+ // @ts-expect-error Testing invalid config
55
+ init({ languages: { supported: ["en", "fr"] }, storage: mockStorage })).rejects.toThrow("i18n-keyless: primary is required");
56
+ });
57
+ it("should throw an error if storage is not provided", async () => {
58
+ await expect(
59
+ // @ts-expect-error Testing invalid config
60
+ init({ languages: { primary: "en", supported: ["en", "fr"] } })).rejects.toThrow("i18n-keyless: storage is required");
61
+ });
62
+ it("should throw an error if no API or custom handlers are provided", async () => {
63
+ await expect(
64
+ // @ts-expect-error Testing invalid config
65
+ init({
66
+ languages: { primary: "en", supported: ["en", "fr"] },
67
+ storage: mockStorage,
68
+ })).rejects.toThrow("i18n-keyless: you didn't provide an API_KEY nor an API_URL nor a handleTranslate + getAllTranslations function");
69
+ });
70
+ it("should initialize with API_KEY correctly", async () => {
71
+ await init({
72
+ languages: {
73
+ primary: "en",
74
+ supported: ["en", "fr"],
75
+ },
76
+ storage: mockStorage,
77
+ API_KEY: "test-api-key",
78
+ });
79
+ expect(useI18nKeyless.getState().config).toEqual({
80
+ addMissingTranslations: true,
81
+ languages: {
82
+ fallback: "en",
83
+ initWithDefault: "en",
84
+ primary: "en",
85
+ supported: ["en", "fr"],
86
+ },
87
+ storage: mockStorage,
88
+ API_KEY: "test-api-key",
89
+ });
90
+ });
91
+ it("should initialize with custom handlers correctly", async () => {
92
+ const handleTranslate = vi.fn();
93
+ const getAllTranslations = vi.fn();
94
+ await init({
95
+ languages: {
96
+ primary: "en",
97
+ supported: ["en", "fr"],
98
+ },
99
+ API_KEY: "test-api-key",
100
+ storage: mockStorage,
101
+ handleTranslate,
102
+ getAllTranslations,
103
+ });
104
+ expect(useI18nKeyless.getState().config).toEqual({
105
+ API_KEY: "test-api-key",
106
+ addMissingTranslations: true,
107
+ languages: {
108
+ fallback: "en",
109
+ initWithDefault: "en",
110
+ primary: "en",
111
+ supported: ["en", "fr"],
112
+ },
113
+ storage: mockStorage,
114
+ handleTranslate,
115
+ getAllTranslations,
116
+ });
117
+ });
118
+ it("should add initWithDefault to supported languages if not already included", async () => {
119
+ await init({
120
+ languages: {
121
+ primary: "en",
122
+ supported: ["fr"],
123
+ initWithDefault: "en",
124
+ },
125
+ API_KEY: "test-api-key",
126
+ storage: mockStorage,
127
+ });
128
+ expect(useI18nKeyless.getState().config?.languages.supported).toContain("en");
129
+ });
130
+ it("should call onInit callback with current language", async () => {
131
+ const onInit = vi.fn().mockImplementation((lang) => lang);
132
+ const onInitReturned = onInit("fr");
133
+ await act(async () => {
134
+ await init({
135
+ languages: {
136
+ primary: "en",
137
+ supported: ["en", "fr"],
138
+ },
139
+ storage: mockStorage,
140
+ API_KEY: "test-api-key",
141
+ onInit,
142
+ });
143
+ });
144
+ expect(onInit.mock.calls[0][0]).toBe("fr");
145
+ expect(onInitReturned).toBe("fr");
146
+ });
147
+ });
148
+ describe("hydration", () => {
149
+ it("should hydrate from storage correctly", async () => {
150
+ mockStorage.getItem.mockImplementation((key) => {
151
+ if (key === "i18n-keyless-current-language")
152
+ return "fr";
153
+ if (key === "i18n-keyless-translations")
154
+ return JSON.stringify({ Hello: { fr: "Bonjour" } });
155
+ return Promise.resolve(null);
156
+ });
157
+ await init({
158
+ languages: {
159
+ primary: "en",
160
+ supported: ["en", "fr"],
161
+ },
162
+ storage: mockStorage,
163
+ API_KEY: "test-api-key",
164
+ });
165
+ expect(useI18nKeyless.getState().currentLanguage).toBe("fr");
166
+ expect(useI18nKeyless.getState().translations).toEqual({ Hello: { fr: "Bonjour" } });
167
+ });
168
+ it("should use initWithDefault when no language is stored", async () => {
169
+ mockStorage.getItem.mockResolvedValue(null);
170
+ await init({
171
+ languages: {
172
+ primary: "en",
173
+ supported: ["en", "fr"],
174
+ initWithDefault: "fr",
175
+ },
176
+ storage: mockStorage,
177
+ API_KEY: "test-api-key",
178
+ });
179
+ expect(useI18nKeyless.getState().currentLanguage).toBe("fr");
180
+ });
181
+ });
182
+ describe("Translation functionality", () => {
183
+ beforeEach(async () => {
184
+ // Reset store and initialize with basic config
185
+ await init({
186
+ languages: {
187
+ primary: "en",
188
+ supported: ["en", "fr", "es"],
189
+ },
190
+ storage: mockStorage,
191
+ API_KEY: "test-api-key",
192
+ });
193
+ });
194
+ it("should return original text when current language is primary language", () => {
195
+ useI18nKeyless.setState({ currentLanguage: "en" });
196
+ const store = useI18nKeyless.getState();
197
+ const result = getTranslation("Hello World", store);
198
+ expect(result).toBe("Hello World");
199
+ });
200
+ it("should return original text when current language is primary language whatever context there is", () => {
201
+ useI18nKeyless.setState({ currentLanguage: "en" });
202
+ const store = useI18nKeyless.getState();
203
+ const result = getTranslation("Hello World again", store, { context: "whatever" });
204
+ expect(result).toBe("Hello World again");
205
+ });
206
+ it("should handle translations with context", () => {
207
+ useI18nKeyless.setState({
208
+ currentLanguage: "fr",
209
+ translations: {
210
+ Welcome__header: "Bienvenue",
211
+ "Good bye__footer": "Au revoir",
212
+ },
213
+ });
214
+ const store = useI18nKeyless.getState();
215
+ const headerResult = getTranslation("Welcome", store, { context: "header" });
216
+ const footerResult = getTranslation("Good bye", store, { context: "footer" });
217
+ expect(headerResult).toBe("Bienvenue");
218
+ expect(footerResult).toBe("Au revoir");
219
+ });
220
+ it("forceTemporary should works when no translation is available", () => {
221
+ useI18nKeyless.setState({ currentLanguage: "fr" });
222
+ const store = useI18nKeyless.getState();
223
+ // Mock fetch for API calls
224
+ global.fetch = vi.fn().mockResolvedValue({
225
+ json: () => Promise.resolve({ ok: true, data: { translations: {} } }),
226
+ });
227
+ const result = getTranslation("Hungry", store, {
228
+ forceTemporary: {
229
+ fr: "J'ai faim",
230
+ },
231
+ });
232
+ // return Hello because translation is not available in fr
233
+ expect(result).toBe("Hungry");
234
+ // expect fetch to have been called with the correct params
235
+ expect(fetch).toHaveBeenCalledWith("https://api.i18n-keyless.com/translate", {
236
+ body: JSON.stringify({
237
+ key: "Hungry",
238
+ forceTemporary: {
239
+ fr: "J'ai faim",
240
+ },
241
+ languages: ["en", "fr", "es"],
242
+ primaryLanguage: "en",
243
+ }),
244
+ headers: {
245
+ Authorization: "Bearer test-api-key",
246
+ "Content-Type": "application/json",
247
+ Version: "1.9.1",
248
+ unique_id: "",
249
+ },
250
+ method: "POST",
251
+ });
252
+ });
253
+ it("forceTemporary should works when translation is available", () => {
254
+ useI18nKeyless.setState({ currentLanguage: "fr", translations: { Happiness: "Joie" } });
255
+ const store = useI18nKeyless.getState();
256
+ // Mock fetch for API calls
257
+ global.fetch = vi.fn().mockResolvedValue({
258
+ json: () => Promise.resolve({ ok: true }),
259
+ });
260
+ const result = getTranslation("Happiness", store, {
261
+ forceTemporary: {
262
+ fr: "Joie temporaire",
263
+ },
264
+ });
265
+ // return Joie because translation forced is not there yet
266
+ expect(result).toBe("Joie");
267
+ // expect fetch to have been called with the correct params
268
+ expect(fetch).toHaveBeenCalledWith("https://api.i18n-keyless.com/translate", {
269
+ body: JSON.stringify({
270
+ key: "Happiness",
271
+ forceTemporary: {
272
+ fr: "Joie temporaire",
273
+ },
274
+ languages: ["en", "fr", "es"],
275
+ primaryLanguage: "en",
276
+ }),
277
+ headers: {
278
+ Authorization: "Bearer test-api-key",
279
+ "Content-Type": "application/json",
280
+ Version: "1.9.1",
281
+ unique_id: "",
282
+ },
283
+ method: "POST",
284
+ });
285
+ });
286
+ it("should queue translation requests when translation is missing", () => {
287
+ useI18nKeyless.setState({ currentLanguage: "fr" });
288
+ const store = useI18nKeyless.getState();
289
+ const queueSpy = vi.spyOn(queue, "add");
290
+ getTranslation("Missing Translation", store);
291
+ expect(queueSpy).toHaveBeenCalled();
292
+ });
293
+ it("should handle API translation errors gracefully", async () => {
294
+ useI18nKeyless.setState({ currentLanguage: "fr" });
295
+ const store = useI18nKeyless.getState();
296
+ // Mock a failed API call
297
+ global.fetch = vi.fn().mockRejectedValue(new Error("API Error"));
298
+ useI18nKeyless.setState({ currentLanguage: "fr" });
299
+ getTranslation("Test Error", store);
300
+ // Wait for async queue to process
301
+ await new Promise((resolve) => setTimeout(resolve, 0));
302
+ expect(console.error).toHaveBeenCalledWith(expect.stringContaining("i18n-keyless: fetch all translations error:"), new Error("API Error"));
303
+ });
304
+ it("should not translate empty strings", () => {
305
+ useI18nKeyless.setState({ currentLanguage: "fr" });
306
+ const store = useI18nKeyless.getState();
307
+ const queueSpy = vi.spyOn(queue, "add");
308
+ getTranslation("", store);
309
+ expect(queueSpy).not.toHaveBeenCalled();
310
+ });
311
+ it("should handle debug mode logging", async () => {
312
+ // Clear any previous async operations
313
+ vi.clearAllMocks();
314
+ useI18nKeyless.setState({ currentLanguage: "fr" });
315
+ const store = useI18nKeyless.getState();
316
+ getTranslation("Debug Test", store, { debug: true });
317
+ // Wait for any async operations to complete
318
+ await new Promise((resolve) => setTimeout(resolve, 0));
319
+ expect(console.log).toHaveBeenCalled();
320
+ });
321
+ });
322
+ describe("Storage operations", () => {
323
+ it("should handle malformed JSON in storage", async () => {
324
+ vi.clearAllMocks();
325
+ mockStorage.getItem.mockImplementation((key) => {
326
+ if (key === "i18n-keyless-translations")
327
+ return "invalid json";
328
+ return null;
329
+ });
330
+ await init({
331
+ languages: {
332
+ primary: "en",
333
+ supported: ["en", "fr"],
334
+ },
335
+ storage: mockStorage,
336
+ API_KEY: "test-api-key",
337
+ });
338
+ expect(useI18nKeyless.getState().translations).toEqual({});
339
+ });
340
+ });
341
+ describe("Language fallback behavior", () => {
342
+ it("should use fallback language when current language is not supported", () => {
343
+ useI18nKeyless.setState({
344
+ currentLanguage: "fr",
345
+ translations: {},
346
+ config: {
347
+ languages: {
348
+ primary: "en",
349
+ supported: ["en", "fr", "es"],
350
+ fallback: "es",
351
+ },
352
+ API_KEY: "test-api-key",
353
+ storage: mockStorage,
354
+ },
355
+ });
356
+ useI18nKeyless.getState().setLanguage("pt"); // pt is not supported
357
+ const store = useI18nKeyless.getState();
358
+ expect(store.currentLanguage).toBe("es");
359
+ });
360
+ });
361
+ });
package/dist/api.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export declare const api: {
2
+ fetchTranslations: (url: string, options: RequestInit) => Promise<any>;
3
+ fetchTranslation: (url: string, options: RequestInit) => Promise<any>;
4
+ };