i18n-keyless-core 2.3.2 → 2.4.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.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { AVAILABLE_LANGS } from "./types.ts";
1
+ export { AVAILABLE_LANGS, DEFAULT_NAMESPACE } from "./types.ts";
2
2
  export type { Lang, PrimaryLang, Translations, TranslationsUsage, HandleTranslateFunction, GetAllTranslationsFunction, SendTranslationsUsageFunction, GetAllTranslationsForAllLanguagesFunction, LanguagesConfig, LastRefresh, UniqueId, I18nKeylessRequestBody, I18nKeylessResponse, I18nKeylessTranslationsUsageRequestBody, I18nKeylessAllTranslationsResponse, FetchTranslationParams, TranslationOptions } from "./types.ts";
3
- export { getTranslationCore, getAllTranslationsFromLanguage, sendTranslationsUsageToI18nKeyless, queue } from "./service.ts";
3
+ export { getTranslationCore, getAllTranslationsFromLanguage, sendTranslationsUsageToI18nKeyless, getNamespacesToFetchAfterTranslationFinished, resolveNamespace, queue } from "./service.ts";
4
4
  export { api } from "./api.ts";
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { AVAILABLE_LANGS } from "./types.js";
2
- export { getTranslationCore, getAllTranslationsFromLanguage, sendTranslationsUsageToI18nKeyless, queue } from "./service.js";
1
+ export { AVAILABLE_LANGS, DEFAULT_NAMESPACE } from "./types.js";
2
+ export { getTranslationCore, getAllTranslationsFromLanguage, sendTranslationsUsageToI18nKeyless, getNamespacesToFetchAfterTranslationFinished, resolveNamespace, queue } from "./service.js";
3
3
  export { api } from "./api.js";
package/dist/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "i18n-keyless-core",
3
3
  "private": false,
4
- "version": "2.3.2",
4
+ "version": "2.4.1",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
package/dist/service.d.ts CHANGED
@@ -1,6 +1,19 @@
1
1
  import type { Lang, TranslationOptions, I18nKeylessResponse, FetchTranslationParams, TranslationsUsage } from "./types.ts";
2
2
  import MyPQueue from "./my-pqueue.ts";
3
3
  export declare const queue: MyPQueue;
4
+ /**
5
+ * Resolves the effective namespace for a translation call: an explicit per-call
6
+ * `namespace` wins, then the config-level `defaultNamespace`, then `DEFAULT_NAMESPACE`.
7
+ */
8
+ export declare function resolveNamespace(options: TranslationOptions | undefined, config: FetchTranslationParams["config"]): string;
9
+ /**
10
+ * Returns the namespaces queued since the last call (with their `unpersisted` flag) and
11
+ * clears the map.
12
+ */
13
+ export declare function getNamespacesToFetchAfterTranslationFinished(): Array<{
14
+ namespace: string;
15
+ unpersisted: boolean;
16
+ }>;
4
17
  /**
5
18
  * Gets a translation for the specified key from the store
6
19
  * @param key - The translation key (text in primary language)
@@ -24,7 +37,7 @@ export declare function translateKey(key: string, store: FetchTranslationParams,
24
37
  * @param store - The translation store
25
38
  * @returns Promise resolving to the translation response or void if failed
26
39
  */
27
- export declare function getAllTranslationsFromLanguage(targetLanguage: Lang, store: FetchTranslationParams): Promise<I18nKeylessResponse | void>;
40
+ export declare function getAllTranslationsFromLanguage(targetLanguage: Lang, store: FetchTranslationParams, namespace?: string): Promise<I18nKeylessResponse | void>;
28
41
  /**
29
42
  * Send the translations usage to i18n-keyless API
30
43
  *
@@ -33,11 +46,11 @@ export declare function getAllTranslationsFromLanguage(targetLanguage: Lang, sto
33
46
  *
34
47
  * It's called on lib initialization
35
48
  * and everytime the language is set
36
- * @param translationsUsage - The translations usage to send to the API
49
+ * @param translationsUsageByNamespace - Usage keyed by namespace (default under "default")
37
50
  * @param store - The translation store
38
51
  * @returns Promise resolving to the translation response or void if failed
39
52
  */
40
- export declare function sendTranslationsUsageToI18nKeyless(translationsUsage: TranslationsUsage, store: FetchTranslationParams): Promise<{
53
+ export declare function sendTranslationsUsageToI18nKeyless(translationsUsageByNamespace: Record<string, TranslationsUsage>, store: FetchTranslationParams): Promise<{
41
54
  ok: boolean;
42
55
  message: string;
43
56
  } | void>;
package/dist/service.js CHANGED
@@ -1,7 +1,35 @@
1
+ import { DEFAULT_NAMESPACE } from "./types.js";
1
2
  import MyPQueue from "./my-pqueue.js";
2
3
  import packageJson from "./package.json" with { type: "json" };
3
4
  import { api } from "./api.js";
4
5
  export const queue = new MyPQueue({ concurrency: 30 });
6
+ /**
7
+ * Resolves the effective namespace for a translation call: an explicit per-call
8
+ * `namespace` wins, then the config-level `defaultNamespace`, then `DEFAULT_NAMESPACE`.
9
+ */
10
+ export function resolveNamespace(options, config) {
11
+ return options?.namespace || config.defaultNamespace || DEFAULT_NAMESPACE;
12
+ }
13
+ /**
14
+ * Scratchpad of namespaces that had at least one missing key queued for translation since
15
+ * the last bulk fetch (mapped to whether that namespace is `unpersisted`). The queue's
16
+ * "empty" handler (in the react store / node service) reads this to know which namespaces
17
+ * to bulk-fetch — so we only re-download the namespaces that were actually rendered, never
18
+ * the whole project — and whether to persist the result.
19
+ */
20
+ const namespacesToFetchAfterTranslationFinished = new Map();
21
+ /**
22
+ * Returns the namespaces queued since the last call (with their `unpersisted` flag) and
23
+ * clears the map.
24
+ */
25
+ export function getNamespacesToFetchAfterTranslationFinished() {
26
+ const namespaces = Array.from(namespacesToFetchAfterTranslationFinished, ([namespace, unpersisted]) => ({
27
+ namespace,
28
+ unpersisted,
29
+ }));
30
+ namespacesToFetchAfterTranslationFinished.clear();
31
+ return namespaces;
32
+ }
5
33
  /**
6
34
  * Gets a translation for the specified key from the store
7
35
  * @param key - The translation key (text in primary language)
@@ -61,6 +89,7 @@ export function translateKey(key, store, options) {
61
89
  }
62
90
  const context = options?.context;
63
91
  const debug = options?.debug;
92
+ const namespace = resolveNamespace(options, config);
64
93
  // if (key.length > 280) {
65
94
  // console.error("i18n-keyless: Key length exceeds 280 characters limit:", key);
66
95
  // return;
@@ -69,7 +98,7 @@ export function translateKey(key, store, options) {
69
98
  return;
70
99
  }
71
100
  if (debug) {
72
- console.log("translateKey", key, context, debug);
101
+ console.log("translateKey", key, context, namespace, debug);
73
102
  }
74
103
  const forceTemporaryLang = options?.forceTemporary?.[currentLanguage];
75
104
  const translation = context ? translations[`${key}__${context}`] : translations[key];
@@ -79,13 +108,19 @@ export function translateKey(key, store, options) {
79
108
  }
80
109
  return;
81
110
  }
111
+ // Remember this namespace (and whether it's unpersisted) so the queue's "empty" handler
112
+ // bulk-fetches it (and only it) and persists the result accordingly.
113
+ namespacesToFetchAfterTranslationFinished.set(namespace, !!options?.unpersistedNamespace);
114
+ // Dedup/guard per namespace so the same source text can be queued independently under
115
+ // different namespaces.
116
+ const queueId = `${namespace}:${key}`;
82
117
  queue.add(async () => {
83
118
  try {
84
- if (translating[key]) {
119
+ if (translating[queueId]) {
85
120
  return;
86
121
  }
87
122
  else {
88
- translating[key] = true;
123
+ translating[queueId] = true;
89
124
  }
90
125
  if (config.handleTranslate) {
91
126
  await config.handleTranslate?.(key);
@@ -94,6 +129,9 @@ export function translateKey(key, store, options) {
94
129
  const body = {
95
130
  key,
96
131
  context,
132
+ // Omit the default namespace so the wire format is unchanged for projects that
133
+ // don't use namespaces (the backend treats "no namespace" as the default).
134
+ namespace: namespace === DEFAULT_NAMESPACE ? undefined : namespace,
97
135
  forceTemporary: options?.forceTemporary,
98
136
  languages: config.languages.supported,
99
137
  primaryLanguage: config.languages.primary,
@@ -122,14 +160,14 @@ export function translateKey(key, store, options) {
122
160
  console.warn("i18n-keyless: ", response.message);
123
161
  }
124
162
  }
125
- translating[key] = false;
163
+ translating[queueId] = false;
126
164
  return;
127
165
  }
128
166
  catch (error) {
129
167
  console.error("i18n-keyless: Error translating key:", error);
130
- translating[key] = false;
168
+ translating[queueId] = false;
131
169
  }
132
- }, { priority: 1, id: key });
170
+ }, { priority: 1, id: queueId });
133
171
  }
134
172
  /**
135
173
  * Fetches all translations for a target language
@@ -137,7 +175,7 @@ export function translateKey(key, store, options) {
137
175
  * @param store - The translation store
138
176
  * @returns Promise resolving to the translation response or void if failed
139
177
  */
140
- export async function getAllTranslationsFromLanguage(targetLanguage, store) {
178
+ export async function getAllTranslationsFromLanguage(targetLanguage, store, namespace) {
141
179
  const config = store.config;
142
180
  const lastRefresh = store.lastRefresh;
143
181
  const uniqueId = store.uniqueId;
@@ -148,11 +186,14 @@ export async function getAllTranslationsFromLanguage(targetLanguage, store) {
148
186
  // if (config.languages.primary === targetLanguage) {
149
187
  // return;
150
188
  // }
189
+ // Omit the default namespace from the query so existing (non-namespaced) installs keep
190
+ // hitting the exact same URL.
191
+ const namespaceQuery = namespace && namespace !== DEFAULT_NAMESPACE ? `&namespace=${encodeURIComponent(namespace)}` : "";
151
192
  try {
152
193
  const response = config.getAllTranslations
153
194
  ? await config.getAllTranslations()
154
195
  : await api
155
- .fetchTranslationsForOneLanguage(`${config.API_URL || "https://api.i18n-keyless.com"}/translate/${targetLanguage}?last_refresh=${lastRefresh}`, {
196
+ .fetchTranslationsForOneLanguage(`${config.API_URL || "https://api.i18n-keyless.com"}/translate/${targetLanguage}?last_refresh=${lastRefresh}${namespaceQuery}`, {
156
197
  method: "GET",
157
198
  headers: {
158
199
  "Content-Type": "application/json",
@@ -182,22 +223,27 @@ export async function getAllTranslationsFromLanguage(targetLanguage, store) {
182
223
  *
183
224
  * It's called on lib initialization
184
225
  * and everytime the language is set
185
- * @param translationsUsage - The translations usage to send to the API
226
+ * @param translationsUsageByNamespace - Usage keyed by namespace (default under "default")
186
227
  * @param store - The translation store
187
228
  * @returns Promise resolving to the translation response or void if failed
188
229
  */
189
- export async function sendTranslationsUsageToI18nKeyless(translationsUsage, store) {
230
+ export async function sendTranslationsUsageToI18nKeyless(translationsUsageByNamespace, store) {
190
231
  const config = store.config;
191
232
  if (!config.API_KEY) {
192
233
  console.error("i18n-keyless: No config found");
193
234
  return;
194
235
  }
195
- if (Object.keys(translationsUsage).length === 0) {
236
+ if (Object.keys(translationsUsageByNamespace).length === 0) {
196
237
  return;
197
238
  }
239
+ const requestBody = {
240
+ primaryLanguage: config.languages.primary,
241
+ translationsUsageByNamespace,
242
+ };
198
243
  try {
199
244
  const response = config.sendTranslationsUsage
200
- ? await config.sendTranslationsUsage(translationsUsage)
245
+ ? // custom handlers keep their flat signature: hand them the default-namespace bucket
246
+ await config.sendTranslationsUsage(translationsUsageByNamespace.default ?? {})
201
247
  : await api
202
248
  .postLastUsedTranslations(`${config.API_URL || "https://api.i18n-keyless.com"}/translate/last-used-translations`, {
203
249
  method: "POST",
@@ -206,10 +252,7 @@ export async function sendTranslationsUsageToI18nKeyless(translationsUsage, stor
206
252
  Authorization: `Bearer ${config.API_KEY}`,
207
253
  Version: packageJson.version,
208
254
  },
209
- body: JSON.stringify({
210
- primaryLanguage: config.languages.primary,
211
- translationsUsage,
212
- }),
255
+ body: JSON.stringify(requestBody),
213
256
  })
214
257
  .then((res) => res);
215
258
  if (response.message) {
package/dist/types.d.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  export type PrimaryLang = "fr" | "en";
2
2
  export declare const AVAILABLE_LANGS: readonly ["fr", "en", "nl", "it", "de", "es", "pl", "pt", "ro", "hu", "sv", "tr", "ja", "cn", "cz", "ru", "ko", "ar"];
3
3
  export type Lang = (typeof AVAILABLE_LANGS)[number];
4
+ /**
5
+ * The namespace used when none is provided (per call or via `defaultNamespace` in config).
6
+ * The default namespace reuses the legacy storage keys (`i18n-keyless-translations` and
7
+ * `i18n-keyless-last-refresh`) so existing installs keep working without any migration.
8
+ */
9
+ export declare const DEFAULT_NAMESPACE = "default";
4
10
  /**
5
11
  * The translations for a key
6
12
  * { "un text": "a text" }
@@ -63,6 +69,23 @@ export type TranslationOptions = {
63
69
  * You'll find it useful when it occurs to you, don't worry :)
64
70
  */
65
71
  context?: string;
72
+ /**
73
+ * The namespace this translation belongs to.
74
+ * Translations are fetched and persisted per namespace, so splitting a large project
75
+ * into several namespaces keeps each storage item small (avoids the localStorage quota
76
+ * error) and lets the app download only the namespaces it actually renders.
77
+ * Defaults to `defaultNamespace` from the config, or "default" if neither is set.
78
+ */
79
+ namespace?: string;
80
+ /**
81
+ * When true, this namespace's translations live in memory only: they are never written
82
+ * to storage, never added to the persisted namespaces index, and never reloaded at boot
83
+ * or refetched on language change from storage. Use it for high-cardinality, transient
84
+ * namespaces (e.g. one namespace per discussion) so they add zero storage weight and zero
85
+ * boot / language-switch cost. Defaults to false (persisted).
86
+ * Only affects the client (i18n-keyless-react); the node lib is in-memory regardless.
87
+ */
88
+ unpersistedNamespace?: boolean;
66
89
  /**
67
90
  * Could be helpful if something weird happens with this particular key.
68
91
  */
@@ -84,13 +107,22 @@ export type TranslationOptions = {
84
107
  export interface I18nKeylessRequestBody {
85
108
  key: string;
86
109
  context?: string;
110
+ namespace?: string;
87
111
  forceTemporary?: TranslationOptions["forceTemporary"];
88
112
  languages: LanguagesConfig["supported"];
89
113
  primaryLanguage: LanguagesConfig["primary"];
90
114
  }
91
115
  export interface I18nKeylessTranslationsUsageRequestBody {
92
116
  primaryLanguage: LanguagesConfig["primary"];
93
- translationsUsage: TranslationsUsage;
117
+ /**
118
+ * Usage keyed by namespace: `{ "<namespace>": { "key__context": "YYYY-MM-DD" } }`. The
119
+ * default namespace is included under the key "default". The backend marks `last_used` on
120
+ * the exact `(key, context, namespace)` row.
121
+ *
122
+ * (Clients < 2.4.0 instead send a flat `translationsUsage` with no namespace; the backend
123
+ * treats that as the "default" namespace.) `unpersistedNamespace` namespaces are excluded.
124
+ */
125
+ translationsUsageByNamespace: Record<string, TranslationsUsage>;
94
126
  }
95
127
  export interface I18nKeylessResponse {
96
128
  ok: boolean;
@@ -120,6 +152,7 @@ export type FetchTranslationParams = {
120
152
  API_KEY: string;
121
153
  API_URL?: string;
122
154
  languages: LanguagesConfig;
155
+ defaultNamespace?: string;
123
156
  addMissingTranslations?: boolean;
124
157
  debug?: boolean;
125
158
  handleTranslate?: HandleTranslateFunction;
package/dist/types.js CHANGED
@@ -18,3 +18,9 @@ export const AVAILABLE_LANGS = [
18
18
  "ko",
19
19
  "ar"
20
20
  ];
21
+ /**
22
+ * The namespace used when none is provided (per call or via `defaultNamespace` in config).
23
+ * The default namespace reuses the legacy storage keys (`i18n-keyless-translations` and
24
+ * `i18n-keyless-last-refresh`) so existing installs keep working without any migration.
25
+ */
26
+ export const DEFAULT_NAMESPACE = "default";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "i18n-keyless-core",
3
3
  "private": false,
4
- "version": "2.3.2",
4
+ "version": "2.4.1",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",