@vaadin/hilla-react-i18n 24.8.0-beta2 → 24.8.0-beta3
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/backend.d.ts +2 -2
- package/backend.js +3 -2
- package/backend.js.map +1 -1
- package/index.d.ts +16 -0
- package/index.js +100 -15
- package/index.js.map +1 -1
- package/package.json +3 -3
package/backend.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { TranslationsResult } from "./types.js";
|
|
2
2
|
export interface I18nBackend {
|
|
3
|
-
loadTranslations(language: string, chunks?: readonly string[]): Promise<TranslationsResult>;
|
|
3
|
+
loadTranslations(language: string, chunks?: readonly string[], keys?: readonly string[]): Promise<TranslationsResult>;
|
|
4
4
|
}
|
|
5
5
|
export declare class DefaultBackend implements I18nBackend {
|
|
6
|
-
loadTranslations(language: string, chunks?: readonly string[]): Promise<TranslationsResult>;
|
|
6
|
+
loadTranslations(language: string, chunks?: readonly string[], keys?: readonly string[]): Promise<TranslationsResult>;
|
|
7
7
|
}
|
package/backend.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export class DefaultBackend {
|
|
2
|
-
async loadTranslations(language, chunks) {
|
|
2
|
+
async loadTranslations(language, chunks, keys) {
|
|
3
3
|
const params = new URLSearchParams([
|
|
4
4
|
["v-r", "i18n"],
|
|
5
5
|
["langtag", language],
|
|
6
|
-
...(chunks ?? []).map((chunk) => ["chunks", chunk])
|
|
6
|
+
...(chunks ?? []).map((chunk) => ["chunks", chunk]),
|
|
7
|
+
...(keys ?? []).map((key) => ["keys", key])
|
|
7
8
|
]);
|
|
8
9
|
const response = await fetch(`./?${params.toString()}`);
|
|
9
10
|
if (!response.ok) {
|
package/backend.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"AAMA,OAAO,MAAM,eAAsC;CACjD,MAAM,
|
|
1
|
+
{"mappings":"AAMA,OAAO,MAAM,eAAsC;CACjD,MAAM,iBACJA,UACAC,QACAC,MAC6B;EAC7B,MAAM,SAAS,IAAI,gBAAgB;GACjC,CAAC,OAAO,MAAO;GACf,CAAC,WAAW,QAAS;GACrB,GAAG,CAAC,UAAU,CAAE,GAAE,IAAI,CAAC,UAAU,CAAC,UAAU,KAAM,EAAC;GACnD,GAAG,CAAC,QAAQ,CAAE,GAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,GAAI,EAAC;EAC5C;EACD,MAAM,WAAW,MAAM,OAAO,KAAK,OAAO,UAAU,CAAC,EAAE;AACvD,OAAK,SAAS,IAAI;AAChB,SAAM,IAAI,MAAM;EACjB;EACD,MAAM,kBAAkB,SAAS,QAAQ,IAAI,4BAA4B;EACzE,MAAMC,eAA6B,MAAM,SAAS,MAAM;AACxD,SAAO;GACL;GACA,kBAAkB,mBAAmB;EACtC;CACF;AACF","names":["language: string","chunks?: readonly string[]","keys?: readonly string[]","translations: Translations"],"sources":["/opt/agent/work/1af72d8adc613024/hilla/packages/ts/react-i18n/src/backend.ts"],"sourcesContent":["import type { Translations, TranslationsResult } from './types.js';\n\nexport interface I18nBackend {\n loadTranslations(language: string, chunks?: readonly string[], keys?: readonly string[]): Promise<TranslationsResult>;\n}\n\nexport class DefaultBackend implements I18nBackend {\n async loadTranslations(\n language: string,\n chunks?: readonly string[],\n keys?: readonly string[],\n ): Promise<TranslationsResult> {\n const params = new URLSearchParams([\n ['v-r', 'i18n'],\n ['langtag', language],\n ...(chunks ?? []).map((chunk) => ['chunks', chunk]),\n ...(keys ?? []).map((key) => ['keys', key]),\n ]);\n const response = await fetch(`./?${params.toString()}`);\n if (!response.ok) {\n throw new Error('Failed fetching translations.');\n }\n const retrievedLocale = response.headers.get('X-Vaadin-Retrieved-Locale');\n const translations: Translations = await response.json();\n return {\n translations,\n resolvedLanguage: retrievedLocale ?? undefined,\n };\n }\n}\n"],"version":3}
|
package/index.d.ts
CHANGED
|
@@ -80,6 +80,7 @@ export declare class I18n {
|
|
|
80
80
|
* @param newLanguage - a valid IETF BCP 47 language tag, such as `en` or `en-US`
|
|
81
81
|
*/
|
|
82
82
|
setLanguage(newLanguage: string): Promise<void>;
|
|
83
|
+
private requestKeys;
|
|
83
84
|
private updateLanguage;
|
|
84
85
|
/**
|
|
85
86
|
* Reloads all translations for the current language. This method should only
|
|
@@ -109,6 +110,21 @@ export declare class I18n {
|
|
|
109
110
|
* @param params - Optional object with placeholder values
|
|
110
111
|
*/
|
|
111
112
|
translate(k: I18nKey, params?: Record<string, unknown>): string;
|
|
113
|
+
/**
|
|
114
|
+
* Creates a computed signal with translated string for to the given key.
|
|
115
|
+
* This method uses dynamic loading and does not guarantee immediate
|
|
116
|
+
* availability of the translation.
|
|
117
|
+
*
|
|
118
|
+
* If the translation for the given key has not been loaded yet at the time
|
|
119
|
+
* of the call, loads the translation for the key and updates the returned
|
|
120
|
+
* signal value.
|
|
121
|
+
*
|
|
122
|
+
* When given an `undefined` key, returns empty string signal value.
|
|
123
|
+
*
|
|
124
|
+
* @param k - The translation key to translate
|
|
125
|
+
* @param params - Optional object with placeholder values
|
|
126
|
+
*/
|
|
127
|
+
translateDynamic(k: string | undefined, params?: Record<string, unknown>): ReadonlySignal<string>;
|
|
112
128
|
}
|
|
113
129
|
/**
|
|
114
130
|
* The global I18n instance that is used to initialize translations, change the
|
package/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { batch, signal } from "@vaadin/hilla-react-signals";
|
|
1
|
+
import { batch, computed, signal } from "@vaadin/hilla-react-signals";
|
|
2
2
|
import { DefaultBackend } from "./backend.js";
|
|
3
3
|
import { FormatCache } from "./FormatCache.js";
|
|
4
4
|
import { getLanguageSettings, updateLanguageSettings } from "./settings.js";
|
|
@@ -20,7 +20,11 @@ export class I18n {
|
|
|
20
20
|
#translations = signal({});
|
|
21
21
|
#resolvedLanguage = signal(undefined);
|
|
22
22
|
#chunks = new Set();
|
|
23
|
+
#alreadyRequestedKeys = signal(new Set());
|
|
24
|
+
#batchedKeys = new Set();
|
|
25
|
+
#batchedKeysPromise = undefined;
|
|
23
26
|
#formatCache = new FormatCache(navigator.language);
|
|
27
|
+
#translationSignalCache = new Map();
|
|
24
28
|
constructor() {
|
|
25
29
|
if (import.meta.hot) {
|
|
26
30
|
import.meta.hot.on("translations-update", () => {
|
|
@@ -82,8 +86,8 @@ export class I18n {
|
|
|
82
86
|
* @param options - Optional options object to specify the initial language.
|
|
83
87
|
*/
|
|
84
88
|
async configure(options) {
|
|
85
|
-
const
|
|
86
|
-
await this.updateLanguage(
|
|
89
|
+
const language = determineInitialLanguage(options);
|
|
90
|
+
await this.updateLanguage({ language });
|
|
87
91
|
}
|
|
88
92
|
/**
|
|
89
93
|
* Changes the current language and loads translations for the new language.
|
|
@@ -107,7 +111,10 @@ export class I18n {
|
|
|
107
111
|
* @param newLanguage - a valid IETF BCP 47 language tag, such as `en` or `en-US`
|
|
108
112
|
*/
|
|
109
113
|
async setLanguage(newLanguage) {
|
|
110
|
-
await this.updateLanguage(
|
|
114
|
+
await this.updateLanguage({
|
|
115
|
+
language: newLanguage,
|
|
116
|
+
updateSettings: true
|
|
117
|
+
});
|
|
111
118
|
}
|
|
112
119
|
/**
|
|
113
120
|
* Registers the chunk name for loading translations, and loads the
|
|
@@ -123,32 +130,70 @@ export class I18n {
|
|
|
123
130
|
}
|
|
124
131
|
this.#chunks.add(chunkName);
|
|
125
132
|
if (this.#language.value) {
|
|
126
|
-
await this.updateLanguage(
|
|
133
|
+
await this.updateLanguage({ chunkName });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async requestKeys(keys) {
|
|
137
|
+
if (this.#batchedKeysPromise) {
|
|
138
|
+
for (const k of keys) {
|
|
139
|
+
this.#batchedKeys.add(k);
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const nonBatchedKeys = keys.filter((k) => !this.#batchedKeys.has(k));
|
|
144
|
+
if (nonBatchedKeys.length === 0) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
this.#batchedKeys.clear();
|
|
148
|
+
for (const k of nonBatchedKeys) {
|
|
149
|
+
this.#batchedKeys.add(k);
|
|
127
150
|
}
|
|
151
|
+
this.#batchedKeysPromise = Promise.resolve(this.#batchedKeys);
|
|
152
|
+
await this.#batchedKeysPromise;
|
|
153
|
+
this.#batchedKeysPromise = undefined;
|
|
154
|
+
const batchedKeys = [...this.#batchedKeys];
|
|
155
|
+
return this.updateLanguage({ keys: batchedKeys }).then(() => {
|
|
156
|
+
console.warn(["A server call was made to translate keys those were not loaded:", ...batchedKeys].join("\n - "));
|
|
157
|
+
});
|
|
128
158
|
}
|
|
129
|
-
async updateLanguage(
|
|
130
|
-
|
|
159
|
+
async updateLanguage(options) {
|
|
160
|
+
const { language, updateSettings, chunkName, keys } = {
|
|
161
|
+
language: this.#language.value,
|
|
162
|
+
updateSettings: false,
|
|
163
|
+
chunkName: undefined,
|
|
164
|
+
keys: undefined,
|
|
165
|
+
...options
|
|
166
|
+
};
|
|
167
|
+
const partialLoad = !!chunkName || !!keys?.length;
|
|
168
|
+
if (language === undefined || language === this.#language.value && !partialLoad) {
|
|
131
169
|
return;
|
|
132
170
|
}
|
|
133
|
-
const chunks =
|
|
171
|
+
const chunks = chunkName ? [chunkName] : this.#chunks.size && !keys?.length ? [...this.#chunks.values()] : undefined;
|
|
134
172
|
let translationsResult;
|
|
135
173
|
try {
|
|
136
|
-
translationsResult = await this.#backend.loadTranslations(
|
|
174
|
+
translationsResult = await this.#backend.loadTranslations(language, chunks, keys);
|
|
137
175
|
} catch (e) {
|
|
138
|
-
console.error(`Failed to load translations for language: ${
|
|
176
|
+
console.error(`Failed to load translations for language: ${language}`, e);
|
|
139
177
|
return;
|
|
140
178
|
}
|
|
141
179
|
batch(() => {
|
|
142
|
-
this.#translations.value =
|
|
180
|
+
this.#translations.value = partialLoad ? {
|
|
143
181
|
...this.#translations.value,
|
|
144
182
|
...translationsResult.translations
|
|
145
183
|
} : translationsResult.translations;
|
|
146
|
-
this.#language.value = newLanguage;
|
|
147
184
|
this.#resolvedLanguage.value = translationsResult.resolvedLanguage;
|
|
148
|
-
|
|
149
|
-
|
|
185
|
+
if (language !== this.#language.value) {
|
|
186
|
+
this.#language.value = language;
|
|
187
|
+
this.#formatCache = new FormatCache(language);
|
|
188
|
+
}
|
|
189
|
+
this.#initialized.value = !!language;
|
|
190
|
+
if (!partialLoad) {
|
|
191
|
+
this.#alreadyRequestedKeys.value = new Set();
|
|
192
|
+
} else if (keys?.length) {
|
|
193
|
+
this.#alreadyRequestedKeys.value = new Set([...this.#alreadyRequestedKeys.value, ...keys]);
|
|
194
|
+
}
|
|
150
195
|
if (updateSettings) {
|
|
151
|
-
updateLanguageSettings({ language
|
|
196
|
+
updateLanguageSettings({ language });
|
|
152
197
|
}
|
|
153
198
|
});
|
|
154
199
|
}
|
|
@@ -204,6 +249,46 @@ export class I18n {
|
|
|
204
249
|
const format = this.#formatCache.getFormat(translation);
|
|
205
250
|
return format.format(params);
|
|
206
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Creates a computed signal with translated string for to the given key.
|
|
254
|
+
* This method uses dynamic loading and does not guarantee immediate
|
|
255
|
+
* availability of the translation.
|
|
256
|
+
*
|
|
257
|
+
* If the translation for the given key has not been loaded yet at the time
|
|
258
|
+
* of the call, loads the translation for the key and updates the returned
|
|
259
|
+
* signal value.
|
|
260
|
+
*
|
|
261
|
+
* When given an `undefined` key, returns empty string signal value.
|
|
262
|
+
*
|
|
263
|
+
* @param k - The translation key to translate
|
|
264
|
+
* @param params - Optional object with placeholder values
|
|
265
|
+
*/
|
|
266
|
+
translateDynamic(k, params) {
|
|
267
|
+
if (this.#translationSignalCache.has(k ?? "")) {
|
|
268
|
+
return this.#translationSignalCache.get(k ?? "");
|
|
269
|
+
}
|
|
270
|
+
if (!k) {
|
|
271
|
+
const translationSignal = computed(() => "");
|
|
272
|
+
this.#translationSignalCache.set("", translationSignal);
|
|
273
|
+
return translationSignal;
|
|
274
|
+
}
|
|
275
|
+
const translationSignal = computed(() => {
|
|
276
|
+
const translation = this.#translations.value[k];
|
|
277
|
+
if (!translation) {
|
|
278
|
+
if (this.#alreadyRequestedKeys.value.has(k)) {
|
|
279
|
+
return k;
|
|
280
|
+
}
|
|
281
|
+
if (this.#language.value) {
|
|
282
|
+
void this.requestKeys([k]);
|
|
283
|
+
}
|
|
284
|
+
return "";
|
|
285
|
+
}
|
|
286
|
+
const format = this.#formatCache.getFormat(translation);
|
|
287
|
+
return format.format(params);
|
|
288
|
+
});
|
|
289
|
+
this.#translationSignalCache.set(k, translationSignal);
|
|
290
|
+
return translationSignal;
|
|
291
|
+
}
|
|
207
292
|
}
|
|
208
293
|
/**
|
|
209
294
|
* The global I18n instance that is used to initialize translations, change the
|
package/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"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","names":["options?: I18nOptions","keyLiteralMarker: unique symbol","#backend","#initialized","#language","#translations","#resolvedLanguage","#chunks","newLanguage: string","chunkName: string","newChunk?: string","translationsResult: TranslationsResult","#formatCache","k: I18nKey","params?: Record<string, unknown>","i18n: I18n","strings: readonly string[]"],"sources":["/opt/agent/work/1af72d8adc613024/hilla/packages/ts/react-i18n/src/index.ts"],"sourcesContent":["import { batch, type ReadonlySignal, signal, type Signal } from '@vaadin/hilla-react-signals';\nimport { DefaultBackend, type I18nBackend } from './backend.js';\nimport { FormatCache } from './FormatCache.js';\nimport { getLanguageSettings, updateLanguageSettings } from './settings.js';\nimport type { I18nOptions, Translations, TranslationsResult } from './types.js';\n\nfunction determineInitialLanguage(options?: I18nOptions): string {\n // Use explicitly configured language if defined\n if (options?.language) {\n return options.language;\n }\n // Use last used language as fallback\n const settings = getLanguageSettings();\n if (settings?.language) {\n return settings.language;\n }\n // Otherwise use browser language\n return navigator.language;\n}\n\nconst keyLiteralMarker: unique symbol = Symbol('keyMarker');\n\n/**\n * A type for translation keys. It is a string with a special marker.\n */\nexport type I18nKey = string & { [keyLiteralMarker]: unknown };\n\nexport class I18n {\n readonly #backend: I18nBackend = new DefaultBackend();\n\n readonly #initialized: Signal<boolean> = signal(false);\n readonly #language: Signal<string | undefined> = signal(undefined);\n readonly #translations: Signal<Translations> = signal({});\n readonly #resolvedLanguage: Signal<string | undefined> = signal(undefined);\n readonly #chunks = new Set<string>();\n\n #formatCache: FormatCache = new FormatCache(navigator.language);\n\n constructor() {\n // @ts-expect-error import.meta.hot does not have TS definitions\n if (import.meta.hot) {\n // @ts-expect-error import.meta.hot does not have TS definitions\n // eslint-disable-next-line\n import.meta.hot.on('translations-update', () => {\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this.reloadTranslations();\n });\n }\n }\n\n /**\n * Returns a signal indicating whether the I18n instance has been initialized.\n * The instance is considered initialized after `configure` has been called\n * and translations for the initial language have been loaded. Can be used to\n * show a placeholder or loading indicator until the translations are ready.\n *\n * Subscribers to this signal will be notified when initialization is complete\n * and translations are ready to be used.\n */\n get initialized(): ReadonlySignal<boolean> {\n return this.#initialized;\n }\n\n /**\n * Returns a signal with the currently configured language.\n *\n * Subscribers to this signal will be notified when the language has changed\n * and new translations have been loaded.\n */\n get language(): ReadonlySignal<string | undefined> {\n return this.#language;\n }\n\n /**\n * Returns a signal with the resolved language. The resolved language is the\n * language that was actually used to load translations. It may differ from\n * the configured language if there are no translations available for the\n * configured language. For example, when setting the language to \"de-DE\" but\n * translations are only available for \"de\", the resolved language will be\n * \"de\".\n */\n get resolvedLanguage(): ReadonlySignal<string | undefined> {\n return this.#resolvedLanguage;\n }\n\n /**\n * Initializes the I18n instance. This method should be called once to load\n * translations for the initial language. The `translate` API will not return\n * properly translated strings until the initializations has completed.\n *\n * The initialization runs asynchronously as translations are loaded from the\n * backend. The method returns a promise that resolves when the translations\n * have been loaded, after which the `translate` API can safely be used.\n *\n * The initial language is determined as follows:\n * 1. If a user opens the app for the first time, the browser language is used.\n * 2. If the language has been changed in a previous usage of the app using\n * `setLanguage`, the last used language is used. The last used language is\n * automatically stored in local storage.\n *\n * Alternatively, the initial language can be explicitly configured using the\n * `language` option. The language should be a valid IETF BCP 47 language tag,\n * such as `en` or `en-US`.\n *\n * @param options - Optional options object to specify the initial language.\n */\n async configure(options?: I18nOptions): Promise<void> {\n const initialLanguage = determineInitialLanguage(options);\n await this.updateLanguage(initialLanguage);\n }\n\n /**\n * Changes the current language and loads translations for the new language.\n * Components using the `translate` API will automatically re-render, and\n * subscribers to the `language` signal will be notified, when the new\n * translations have been loaded.\n *\n * The language should be a valid IETF BCP 47 language tag, such as `en` or\n * `en-US`.\n *\n * If there is no translation file for that specific language tag, the backend\n * will try to load the translation file for the parent language tag. For\n * example, if there is no translation file for `en-US`, the backend will try\n * to load the translation file for `en`. Otherwise, it will fall back to the\n * default translation file `translations.properties`.\n *\n * Changing the language is an asynchronous operation. The method returns a\n * promise that resolves when the translations for the new language have been\n * loaded.\n *\n * @param newLanguage - a valid IETF BCP 47 language tag, such as `en` or `en-US`\n */\n async setLanguage(newLanguage: string): Promise<void> {\n await this.updateLanguage(newLanguage, true);\n }\n\n /**\n * Registers the chunk name for loading translations, and loads the\n * translations for the specified chunk.\n *\n * @internal only for automatic internal calls from production JS bundles\n *\n * @param chunkName - the production JS bundle chunk name\n */\n async registerChunk(chunkName: string): Promise<void> {\n if (this.#chunks.has(chunkName)) {\n return;\n }\n\n this.#chunks.add(chunkName);\n\n if (this.#language.value) {\n await this.updateLanguage(this.#language.value, false, chunkName);\n }\n }\n\n private async updateLanguage(newLanguage: string, updateSettings = false, newChunk?: string) {\n if (this.#language.value === newLanguage && !newChunk) {\n return;\n }\n\n const chunks = newChunk\n ? [newChunk] // New chunk is registered, load only that\n : this.#chunks.size > 0\n ? [...this.#chunks.values()] // Load the new language for all chunks registered so far\n : undefined; // Load the new language without specifying chunks, assuming dev. mode\n\n let translationsResult: TranslationsResult;\n try {\n translationsResult = await this.#backend.loadTranslations(newLanguage, chunks);\n } catch (e) {\n console.error(`Failed to load translations for language: ${newLanguage}`, e);\n return;\n }\n\n // Update all signals together to avoid triggering side effects multiple times\n batch(() => {\n this.#translations.value = newChunk\n ? { ...this.#translations.value, ...translationsResult.translations }\n : translationsResult.translations;\n this.#language.value = newLanguage;\n this.#resolvedLanguage.value = translationsResult.resolvedLanguage;\n this.#formatCache = new FormatCache(newLanguage);\n this.#initialized.value = true;\n\n if (updateSettings) {\n updateLanguageSettings({\n language: newLanguage,\n });\n }\n });\n }\n\n /**\n * Reloads all translations for the current language. This method should only\n * be used for HMR in development mode.\n */\n private async reloadTranslations() {\n const currentLanguage = this.#language.value;\n if (!currentLanguage) {\n return;\n }\n\n let translationsResult: TranslationsResult;\n try {\n translationsResult = await this.#backend.loadTranslations(currentLanguage);\n } catch (e) {\n console.error(`Failed to reload translations for language: ${currentLanguage}`, e);\n return;\n }\n\n // Update all signals together to avoid triggering side effects multiple times\n batch(() => {\n this.#translations.value = translationsResult.translations;\n this.#resolvedLanguage.value = translationsResult.resolvedLanguage;\n this.#formatCache = new FormatCache(currentLanguage);\n });\n }\n\n /**\n * Returns a translated string for the given translation key. The key should\n * match a key in the loaded translations. If no translation is found for the\n * key, the key itself is returned.\n *\n * Translations may contain placeholders, following the ICU MessageFormat\n * syntax. They can be replaced by passing a `params` object with placeholder\n * values, where keys correspond to the placeholder names and values are the\n * replacement value. Values should be basic types such as strings, numbers,\n * or dates that match the placeholder format configured in the translation\n * string. For example, when using a placeholder `{count, number}`, the value\n * should be a number, when using `{date, date}`, the value should be a Date\n * object, and so on.\n *\n * This method internally accesses a signal, meaning that React components\n * that use it will automatically re-render when translations change.\n * Likewise, signal effects automatically subscribe to translation changes\n * when calling this method.\n *\n * @param k - The translation key to translate\n * @param params - Optional object with placeholder values\n */\n translate(k: I18nKey, params?: Record<string, unknown>): string {\n const translation = this.#translations.value[k];\n if (!translation) {\n return k.toString();\n }\n const format = this.#formatCache.getFormat(translation);\n return format.format(params) as string;\n }\n}\n\n/**\n * The global I18n instance that is used to initialize translations, change the\n * current language, and translate strings.\n */\nconst i18n: I18n = new I18n();\n\n/**\n * A tagged template literal function to create translation keys.\n * The {@link translate} function requires using this tag.\n * E.g.:\n * translate(key`my.translation.key`)\n */\nexport function key(strings: readonly string[], ..._values: never[]): I18nKey {\n return Object.assign(strings[0], { [keyLiteralMarker]: undefined }) as I18nKey;\n}\n\n/**\n * Returns a translated string for the given translation key. The key should\n * match a key in the loaded translations. If no translation is found for the\n * key, the key itself is returned.\n *\n * Translations may contain placeholders, following the ICU MessageFormat\n * syntax. They can be replaced by passing a `params` object with placeholder\n * values, where keys correspond to the placeholder names and values are the\n * replacement value. Values should be basic types such as strings, numbers,\n * or dates that match the placeholder format configured in the translation\n * string. For example, when using a placeholder `{count, number}`, the value\n * should be a number, when using `{date, date}`, the value should be a Date\n * object, and so on.\n *\n * This method internally accesses a signal, meaning that React components\n * that use it will automatically re-render when translations change.\n * Likewise, signal effects automatically subscribe to translation changes\n * when calling this method.\n *\n * This function is a shorthand for `i18n.translate` of the global I18n instance.\n *\n * @param k - The translation key to translate\n * @param params - Optional object with placeholder values\n */\nexport function translate(k: I18nKey, params?: Record<string, unknown>): string {\n return i18n.translate(k, params);\n}\n\nexport { i18n };\n"],"version":3}
|
|
1
|
+
{"mappings":"AAAA,SAAS,OAAO,UAA+B,2CAAyD;AACxG,SAAS,oCAAuD;AAChE,SAAS,qCAAsC;AAC/C,SAAS,qBAAqB,6CAA8C;AAG5E,SAAS,yBAAyBA,SAA+B;AAE/D,KAAI,SAAS,UAAU;AACrB,SAAO,QAAQ;CAChB;CAED,MAAM,WAAW,qBAAqB;AACtC,KAAI,UAAU,UAAU;AACtB,SAAO,SAAS;CACjB;AAED,QAAO,UAAU;AAClB;AAED,MAAMC,mBAAkC,OAAO,YAAY;AAO3D,OAAO,MAAM,KAAK;CAChB,AAASC,WAAwB,IAAI;CAErC,AAASC,eAAgC,OAAO,MAAM;CACtD,AAASC,YAAwC,OAAO,UAAU;CAClE,AAASC,gBAAsC,OAAO,CAAE,EAAC;CACzD,AAASC,oBAAgD,OAAO,UAAU;CAC1E,AAASC,UAAU,IAAI;CACvB,AAASC,wBAAqD,OAAO,IAAI,MAAM;CAC/E,AAASC,eAAe,IAAI;CAC5B,sBAAgE;CAEhE,eAA4B,IAAI,YAAY,UAAU;CACtD,AAASC,0BAA0B,IAAI;CAEvC,cAAc;AAEZ,MAAI,OAAO,KAAK,KAAK;AAGnB,UAAO,KAAK,IAAI,GAAG,uBAAuB,MAAM;AAE9C,SAAK,oBAAoB;GAC1B,EAAC;EACH;CACF;;;;;;;;;;CAWD,IAAI,cAAuC;AACzC,SAAO,KAAKP;CACb;;;;;;;CAQD,IAAI,WAA+C;AACjD,SAAO,KAAKC;CACb;;;;;;;;;CAUD,IAAI,mBAAuD;AACzD,SAAO,KAAKE;CACb;;;;;;;;;;;;;;;;;;;;;;CAuBD,MAAM,UAAUN,SAAsC;EACpD,MAAM,WAAW,yBAAyB,QAAQ;AAClD,QAAM,KAAK,eAAe,EAAE,SAAU,EAAC;CACxC;;;;;;;;;;;;;;;;;;;;;;CAuBD,MAAM,YAAYW,aAAoC;AACpD,QAAM,KAAK,eAAe;GAAE,UAAU;GAAa,gBAAgB;EAAM,EAAC;CAC3E;;;;;;;;;CAUD,MAAM,cAAcC,WAAkC;AACpD,MAAI,KAAKL,QAAQ,IAAI,UAAU,EAAE;AAC/B;EACD;AAED,OAAKA,QAAQ,IAAI,UAAU;AAE3B,MAAI,KAAKH,UAAU,OAAO;AACxB,SAAM,KAAK,eAAe,EAAE,UAAW,EAAC;EACzC;CACF;CAED,MAAc,YAAYS,MAAwC;AAChE,MAAI,KAAKC,qBAAqB;AAE5B,QAAK,MAAM,KAAK,MAAM;AACpB,SAAKL,aAAa,IAAI,EAAE;GACzB;AACD;EACD;EAED,MAAM,iBAAiB,KAAK,OAAO,CAAC,OAAO,KAAKA,aAAa,IAAI,EAAE,CAAC;AACpE,MAAI,eAAe,WAAW,GAAG;AAE/B;EACD;AAGD,OAAKA,aAAa,OAAO;AACzB,OAAK,MAAM,KAAK,gBAAgB;AAC9B,QAAKA,aAAa,IAAI,EAAE;EACzB;AAED,OAAKK,sBAAsB,QAAQ,QAAQ,KAAKL,aAAa;AAC7D,QAAM,KAAKK;AACX,OAAKA,sBAAsB;EAC3B,MAAM,cAAc,CAAC,GAAG,KAAKL,YAAa;AAC1C,SAAO,KAAK,eAAe,EAAE,MAAM,YAAa,EAAC,CAAC,KAAK,MAAM;AAC3D,WAAQ,KAAK,CAAC,mEAAmE,GAAG,WAAY,EAAC,KAAK,SAAS,CAAC;EACjH,EAAC;CACH;CAED,MAAc,eACZM,SAIA;EACA,MAAM,EAAE,UAAU,gBAAgB,WAAW,MAAM,GAAG;GACpD,UAAU,KAAKX,UAAU;GACzB,gBAAgB;GAChB,WAAW;GACX,MAAM;GACN,GAAG;EACJ;EAED,MAAM,gBAAgB,eAAe,MAAM;AAE3C,MAAI,aAAa,aAAc,aAAa,KAAKA,UAAU,UAAU,aAAc;AACjF;EACD;EAED,MAAM,SAAS,YACX,CAAC,SAAU,IACX,KAAKG,QAAQ,SAAS,MAAM,SAC1B,CAAC,GAAG,KAAKA,QAAQ,QAAQ,AAAC,IAC1B;EAEN,IAAIS;AACJ,MAAI;AACF,wBAAqB,MAAM,KAAKd,SAAS,iBAAiB,UAAU,QAAQ,KAAK;EAClF,SAAQ,GAAG;AACV,WAAQ,OAAO,4CAA4C,SAAS,GAAG,EAAE;AACzE;EACD;AAGD,QAAM,MAAM;AACV,QAAKG,cAAc,QAAQ,cACvB;IAAE,GAAG,KAAKA,cAAc;IAAO,GAAG,mBAAmB;GAAc,IACnE,mBAAmB;AACvB,QAAKC,kBAAkB,QAAQ,mBAAmB;AAClD,OAAI,aAAa,KAAKF,UAAU,OAAO;AACrC,SAAKA,UAAU,QAAQ;AACvB,SAAKa,eAAe,IAAI,YAAY;GACrC;AACD,QAAKd,aAAa,UAAU;AAE5B,QAAK,aAAa;AAChB,SAAKK,sBAAsB,QAAQ,IAAI;GACxC,WAAU,MAAM,QAAQ;AACvB,SAAKA,sBAAsB,QAAQ,IAAI,IAAI,CAAC,GAAG,KAAKA,sBAAsB,OAAO,GAAG,IAAK;GAC1F;AAED,OAAI,gBAAgB;AAClB,2BAAuB,EAAE,SAAU,EAAC;GACrC;EACF,EAAC;CACH;;;;;CAMD,MAAc,qBAAqB;EACjC,MAAM,kBAAkB,KAAKJ,UAAU;AACvC,OAAK,iBAAiB;AACpB;EACD;EAED,IAAIY;AACJ,MAAI;AACF,wBAAqB,MAAM,KAAKd,SAAS,iBAAiB,gBAAgB;EAC3E,SAAQ,GAAG;AACV,WAAQ,OAAO,8CAA8C,gBAAgB,GAAG,EAAE;AAClF;EACD;AAGD,QAAM,MAAM;AACV,QAAKG,cAAc,QAAQ,mBAAmB;AAC9C,QAAKC,kBAAkB,QAAQ,mBAAmB;AAClD,QAAKW,eAAe,IAAI,YAAY;EACrC,EAAC;CACH;;;;;;;;;;;;;;;;;;;;;;;CAwBD,UAAUC,GAAYC,QAA0C;EAC9D,MAAM,cAAc,KAAKd,cAAc,MAAM;AAC7C,OAAK,aAAa;AAChB,UAAO,EAAE,UAAU;EACpB;EACD,MAAM,SAAS,KAAKY,aAAa,UAAU,YAAY;AACvD,SAAO,OAAO,OAAO,OAAO;CAC7B;;;;;;;;;;;;;;;CAgBD,iBAAiBG,GAAuBD,QAA0D;AAIhG,MAAI,KAAKT,wBAAwB,IAAI,KAAK,GAAG,EAAE;AAC7C,UAAO,KAAKA,wBAAwB,IAAI,KAAK,GAAG;EACjD;AAED,OAAK,GAAG;GACN,MAAM,oBAAoB,SAAS,MAAM,GAAG;AAC5C,QAAKA,wBAAwB,IAAI,IAAI,kBAAkB;AACvD,UAAO;EACR;EAED,MAAM,oBAAoB,SAAS,MAAM;GACvC,MAAM,cAAc,KAAKL,cAAc,MAAM;AAE7C,QAAK,aAAa;AAChB,QAAI,KAAKG,sBAAsB,MAAM,IAAI,EAAE,EAAE;AAE3C,YAAO;IACR;AAED,QAAI,KAAKJ,UAAU,OAAO;AAExB,UAAK,KAAK,YAAY,CAAC,CAAE,EAAC;IAC3B;AAGD,WAAO;GACR;GAED,MAAM,SAAS,KAAKa,aAAa,UAAU,YAAY;AACvD,UAAO,OAAO,OAAO,OAAO;EAC7B,EAAC;AAEF,OAAKP,wBAAwB,IAAI,GAAG,kBAAkB;AACtD,SAAO;CACR;AACF;;;;;AAMD,MAAMW,OAAa,IAAI;;;;;;;AAQvB,OAAO,SAAS,IAAIC,SAA4B,GAAG,SAA2B;AAC5E,QAAO,OAAO,OAAO,QAAQ,IAAI,GAAG,mBAAmB,UAAW,EAAC;AACpE;;;;;;;;;;;;;;;;;;;;;;;;;AA0BD,OAAO,SAAS,UAAUJ,GAAYC,QAA0C;AAC9E,QAAO,KAAK,UAAU,GAAG,OAAO;AACjC;AAED,SAAS","names":["options?: I18nOptions","keyLiteralMarker: unique symbol","#backend","#initialized","#language","#translations","#resolvedLanguage","#chunks","#alreadyRequestedKeys","#batchedKeys","#translationSignalCache","newLanguage: string","chunkName: string","keys: readonly string[]","#batchedKeysPromise","options:\n | { readonly language: string; updateSettings?: boolean }\n | { readonly chunkName: string }\n | { readonly keys: string[] }","translationsResult: TranslationsResult","#formatCache","k: I18nKey","params?: Record<string, unknown>","k: string | undefined","i18n: I18n","strings: readonly string[]"],"sources":["/opt/agent/work/1af72d8adc613024/hilla/packages/ts/react-i18n/src/index.ts"],"sourcesContent":["import { batch, computed, type ReadonlySignal, signal, type Signal } from '@vaadin/hilla-react-signals';\nimport { DefaultBackend, type I18nBackend } from './backend.js';\nimport { FormatCache } from './FormatCache.js';\nimport { getLanguageSettings, updateLanguageSettings } from './settings.js';\nimport type { I18nOptions, Translations, TranslationsResult } from './types.js';\n\nfunction determineInitialLanguage(options?: I18nOptions): string {\n // Use explicitly configured language if defined\n if (options?.language) {\n return options.language;\n }\n // Use last used language as fallback\n const settings = getLanguageSettings();\n if (settings?.language) {\n return settings.language;\n }\n // Otherwise use browser language\n return navigator.language;\n}\n\nconst keyLiteralMarker: unique symbol = Symbol('keyMarker');\n\n/**\n * A type for translation keys. It is a string with a special marker.\n */\nexport type I18nKey = string & { [keyLiteralMarker]: unknown };\n\nexport class I18n {\n readonly #backend: I18nBackend = new DefaultBackend();\n\n readonly #initialized: Signal<boolean> = signal(false);\n readonly #language: Signal<string | undefined> = signal(undefined);\n readonly #translations: Signal<Translations> = signal({});\n readonly #resolvedLanguage: Signal<string | undefined> = signal(undefined);\n readonly #chunks = new Set<string>();\n readonly #alreadyRequestedKeys: Signal<ReadonlySet<string>> = signal(new Set());\n readonly #batchedKeys = new Set<string>();\n #batchedKeysPromise: Promise<ReadonlySet<string>> | undefined = undefined;\n\n #formatCache: FormatCache = new FormatCache(navigator.language);\n readonly #translationSignalCache = new Map<string, ReadonlySignal<string>>();\n\n constructor() {\n // @ts-expect-error import.meta.hot does not have TS definitions\n if (import.meta.hot) {\n // @ts-expect-error import.meta.hot does not have TS definitions\n // eslint-disable-next-line\n import.meta.hot.on('translations-update', () => {\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this.reloadTranslations();\n });\n }\n }\n\n /**\n * Returns a signal indicating whether the I18n instance has been initialized.\n * The instance is considered initialized after `configure` has been called\n * and translations for the initial language have been loaded. Can be used to\n * show a placeholder or loading indicator until the translations are ready.\n *\n * Subscribers to this signal will be notified when initialization is complete\n * and translations are ready to be used.\n */\n get initialized(): ReadonlySignal<boolean> {\n return this.#initialized;\n }\n\n /**\n * Returns a signal with the currently configured language.\n *\n * Subscribers to this signal will be notified when the language has changed\n * and new translations have been loaded.\n */\n get language(): ReadonlySignal<string | undefined> {\n return this.#language;\n }\n\n /**\n * Returns a signal with the resolved language. The resolved language is the\n * language that was actually used to load translations. It may differ from\n * the configured language if there are no translations available for the\n * configured language. For example, when setting the language to \"de-DE\" but\n * translations are only available for \"de\", the resolved language will be\n * \"de\".\n */\n get resolvedLanguage(): ReadonlySignal<string | undefined> {\n return this.#resolvedLanguage;\n }\n\n /**\n * Initializes the I18n instance. This method should be called once to load\n * translations for the initial language. The `translate` API will not return\n * properly translated strings until the initializations has completed.\n *\n * The initialization runs asynchronously as translations are loaded from the\n * backend. The method returns a promise that resolves when the translations\n * have been loaded, after which the `translate` API can safely be used.\n *\n * The initial language is determined as follows:\n * 1. If a user opens the app for the first time, the browser language is used.\n * 2. If the language has been changed in a previous usage of the app using\n * `setLanguage`, the last used language is used. The last used language is\n * automatically stored in local storage.\n *\n * Alternatively, the initial language can be explicitly configured using the\n * `language` option. The language should be a valid IETF BCP 47 language tag,\n * such as `en` or `en-US`.\n *\n * @param options - Optional options object to specify the initial language.\n */\n async configure(options?: I18nOptions): Promise<void> {\n const language = determineInitialLanguage(options);\n await this.updateLanguage({ language });\n }\n\n /**\n * Changes the current language and loads translations for the new language.\n * Components using the `translate` API will automatically re-render, and\n * subscribers to the `language` signal will be notified, when the new\n * translations have been loaded.\n *\n * The language should be a valid IETF BCP 47 language tag, such as `en` or\n * `en-US`.\n *\n * If there is no translation file for that specific language tag, the backend\n * will try to load the translation file for the parent language tag. For\n * example, if there is no translation file for `en-US`, the backend will try\n * to load the translation file for `en`. Otherwise, it will fall back to the\n * default translation file `translations.properties`.\n *\n * Changing the language is an asynchronous operation. The method returns a\n * promise that resolves when the translations for the new language have been\n * loaded.\n *\n * @param newLanguage - a valid IETF BCP 47 language tag, such as `en` or `en-US`\n */\n async setLanguage(newLanguage: string): Promise<void> {\n await this.updateLanguage({ language: newLanguage, updateSettings: true });\n }\n\n /**\n * Registers the chunk name for loading translations, and loads the\n * translations for the specified chunk.\n *\n * @internal only for automatic internal calls from production JS bundles\n *\n * @param chunkName - the production JS bundle chunk name\n */\n async registerChunk(chunkName: string): Promise<void> {\n if (this.#chunks.has(chunkName)) {\n return;\n }\n\n this.#chunks.add(chunkName);\n\n if (this.#language.value) {\n await this.updateLanguage({ chunkName });\n }\n }\n\n private async requestKeys(keys: readonly string[]): Promise<void> {\n if (this.#batchedKeysPromise) {\n // Keys request is being queued - add keys to the batch.\n for (const k of keys) {\n this.#batchedKeys.add(k);\n }\n return;\n }\n\n const nonBatchedKeys = keys.filter((k) => !this.#batchedKeys.has(k));\n if (nonBatchedKeys.length === 0) {\n // Keys request for these is already in progress - skip another request.\n return;\n }\n\n // New request\n this.#batchedKeys.clear();\n for (const k of nonBatchedKeys) {\n this.#batchedKeys.add(k);\n }\n // Wait to possibly collect other synchronously requested keys\n this.#batchedKeysPromise = Promise.resolve(this.#batchedKeys);\n await this.#batchedKeysPromise;\n this.#batchedKeysPromise = undefined;\n const batchedKeys = [...this.#batchedKeys];\n return this.updateLanguage({ keys: batchedKeys }).then(() => {\n console.warn(['A server call was made to translate keys those were not loaded:', ...batchedKeys].join('\\n - '));\n });\n }\n\n private async updateLanguage(\n options:\n | { readonly language: string; updateSettings?: boolean }\n | { readonly chunkName: string }\n | { readonly keys: string[] },\n ) {\n const { language, updateSettings, chunkName, keys } = {\n language: this.#language.value,\n updateSettings: false,\n chunkName: undefined,\n keys: undefined,\n ...options,\n };\n\n const partialLoad = !!chunkName || !!keys?.length;\n\n if (language === undefined || (language === this.#language.value && !partialLoad)) {\n return;\n }\n\n const chunks = chunkName\n ? [chunkName] // New chunk is registered, load only that\n : this.#chunks.size && !keys?.length\n ? [...this.#chunks.values()] // Load the new language for all chunks registered so far\n : undefined; // Load the new language without specifying chunks, assuming dev. mode or keys requested\n\n let translationsResult: TranslationsResult;\n try {\n translationsResult = await this.#backend.loadTranslations(language, chunks, keys);\n } catch (e) {\n console.error(`Failed to load translations for language: ${language}`, e);\n return;\n }\n\n // Update all signals together to avoid triggering side effects multiple times\n batch(() => {\n this.#translations.value = partialLoad\n ? { ...this.#translations.value, ...translationsResult.translations }\n : translationsResult.translations;\n this.#resolvedLanguage.value = translationsResult.resolvedLanguage;\n if (language !== this.#language.value) {\n this.#language.value = language;\n this.#formatCache = new FormatCache(language);\n }\n this.#initialized.value = !!language;\n\n if (!partialLoad) {\n this.#alreadyRequestedKeys.value = new Set<string>();\n } else if (keys?.length) {\n this.#alreadyRequestedKeys.value = new Set([...this.#alreadyRequestedKeys.value, ...keys]);\n }\n\n if (updateSettings) {\n updateLanguageSettings({ language });\n }\n });\n }\n\n /**\n * Reloads all translations for the current language. This method should only\n * be used for HMR in development mode.\n */\n private async reloadTranslations() {\n const currentLanguage = this.#language.value;\n if (!currentLanguage) {\n return;\n }\n\n let translationsResult: TranslationsResult;\n try {\n translationsResult = await this.#backend.loadTranslations(currentLanguage);\n } catch (e) {\n console.error(`Failed to reload translations for language: ${currentLanguage}`, e);\n return;\n }\n\n // Update all signals together to avoid triggering side effects multiple times\n batch(() => {\n this.#translations.value = translationsResult.translations;\n this.#resolvedLanguage.value = translationsResult.resolvedLanguage;\n this.#formatCache = new FormatCache(currentLanguage);\n });\n }\n\n /**\n * Returns a translated string for the given translation key. The key should\n * match a key in the loaded translations. If no translation is found for the\n * key, the key itself is returned.\n *\n * Translations may contain placeholders, following the ICU MessageFormat\n * syntax. They can be replaced by passing a `params` object with placeholder\n * values, where keys correspond to the placeholder names and values are the\n * replacement value. Values should be basic types such as strings, numbers,\n * or dates that match the placeholder format configured in the translation\n * string. For example, when using a placeholder `{count, number}`, the value\n * should be a number, when using `{date, date}`, the value should be a Date\n * object, and so on.\n *\n * This method internally accesses a signal, meaning that React components\n * that use it will automatically re-render when translations change.\n * Likewise, signal effects automatically subscribe to translation changes\n * when calling this method.\n *\n * @param k - The translation key to translate\n * @param params - Optional object with placeholder values\n */\n translate(k: I18nKey, params?: Record<string, unknown>): string {\n const translation = this.#translations.value[k];\n if (!translation) {\n return k.toString();\n }\n const format = this.#formatCache.getFormat(translation);\n return format.format(params) as string;\n }\n\n /**\n * Creates a computed signal with translated string for to the given key.\n * This method uses dynamic loading and does not guarantee immediate\n * availability of the translation.\n *\n * If the translation for the given key has not been loaded yet at the time\n * of the call, loads the translation for the key and updates the returned\n * signal value.\n *\n * When given an `undefined` key, returns empty string signal value.\n *\n * @param k - The translation key to translate\n * @param params - Optional object with placeholder values\n */\n translateDynamic(k: string | undefined, params?: Record<string, unknown>): ReadonlySignal<string> {\n // Return a signal that depends on #translations and #language signals.\n // If the key is not found, it will wait for a language to be defined\n // and then try to load the key from the server.\n if (this.#translationSignalCache.has(k ?? '')) {\n return this.#translationSignalCache.get(k ?? '')!;\n }\n\n if (!k) {\n const translationSignal = computed(() => '');\n this.#translationSignalCache.set('', translationSignal);\n return translationSignal;\n }\n\n const translationSignal = computed(() => {\n const translation = this.#translations.value[k];\n\n if (!translation) {\n if (this.#alreadyRequestedKeys.value.has(k)) {\n // No hope to load this key, return it as is\n return k;\n }\n\n if (this.#language.value) {\n // eslint-disable-next-line no-void\n void this.requestKeys([k]);\n }\n\n // Prevent flashing the key in the UI\n return '';\n }\n\n const format = this.#formatCache.getFormat(translation);\n return format.format(params) as string;\n });\n\n this.#translationSignalCache.set(k, translationSignal);\n return translationSignal;\n }\n}\n\n/**\n * The global I18n instance that is used to initialize translations, change the\n * current language, and translate strings.\n */\nconst i18n: I18n = new I18n();\n\n/**\n * A tagged template literal function to create translation keys.\n * The {@link translate} function requires using this tag.\n * E.g.:\n * translate(key`my.translation.key`)\n */\nexport function key(strings: readonly string[], ..._values: never[]): I18nKey {\n return Object.assign(strings[0], { [keyLiteralMarker]: undefined }) as I18nKey;\n}\n\n/**\n * Returns a translated string for the given translation key. The key should\n * match a key in the loaded translations. If no translation is found for the\n * key, the key itself is returned.\n *\n * Translations may contain placeholders, following the ICU MessageFormat\n * syntax. They can be replaced by passing a `params` object with placeholder\n * values, where keys correspond to the placeholder names and values are the\n * replacement value. Values should be basic types such as strings, numbers,\n * or dates that match the placeholder format configured in the translation\n * string. For example, when using a placeholder `{count, number}`, the value\n * should be a number, when using `{date, date}`, the value should be a Date\n * object, and so on.\n *\n * This method internally accesses a signal, meaning that React components\n * that use it will automatically re-render when translations change.\n * Likewise, signal effects automatically subscribe to translation changes\n * when calling this method.\n *\n * This function is a shorthand for `i18n.translate` of the global I18n instance.\n *\n * @param k - The translation key to translate\n * @param params - Optional object with placeholder values\n */\nexport function translate(k: I18nKey, params?: Record<string, unknown>): string {\n return i18n.translate(k, params);\n}\n\nexport { i18n };\n"],"version":3}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vaadin/hilla-react-i18n",
|
|
3
|
-
"version": "24.8.0-
|
|
3
|
+
"version": "24.8.0-beta3",
|
|
4
4
|
"description": "Hilla I18n utils for React",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.js",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@vaadin/hilla-frontend": "24.8.0-
|
|
48
|
-
"@vaadin/hilla-react-signals": "24.8.0-
|
|
47
|
+
"@vaadin/hilla-frontend": "24.8.0-beta3",
|
|
48
|
+
"@vaadin/hilla-react-signals": "24.8.0-beta3",
|
|
49
49
|
"intl-messageformat": "10.7.11"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|