@vaadin/hilla-react-i18n 24.5.0-alpha9 → 24.5.0-beta2

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 CHANGED
@@ -1,8 +1,8 @@
1
1
  import type { TranslationsResult } from './types.js';
2
2
  export interface I18nBackend {
3
- loadTranslations(language: string): Promise<TranslationsResult>;
3
+ loadTranslations(language: string, chunks?: readonly string[]): Promise<TranslationsResult>;
4
4
  }
5
5
  export declare class DefaultBackend implements I18nBackend {
6
- loadTranslations(language: string): Promise<TranslationsResult>;
6
+ loadTranslations(language: string, chunks?: readonly string[]): Promise<TranslationsResult>;
7
7
  }
8
8
  //# sourceMappingURL=backend.d.ts.map
package/backend.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["src/backend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgB,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEnE,MAAM,WAAW,WAAW;IAC1B,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACjE;AAED,qBAAa,cAAe,YAAW,WAAW;IAC1C,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAYtE"}
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["src/backend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgB,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEnE,MAAM,WAAW,WAAW;IAC1B,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;CAC7F;AAED,qBAAa,cAAe,YAAW,WAAW;IAC1C,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAiBlG"}
package/backend.js CHANGED
@@ -1,6 +1,11 @@
1
1
  class DefaultBackend {
2
- async loadTranslations(language) {
3
- const response = await fetch(`./?v-r=i18n&langtag=${language}`);
2
+ async loadTranslations(language, chunks) {
3
+ const params = new URLSearchParams([
4
+ ["v-r", "i18n"],
5
+ ["langtag", language],
6
+ ...(chunks ?? []).map((chunk) => ["chunks", chunk])
7
+ ]);
8
+ const response = await fetch(`./?${params.toString()}`);
4
9
  if (!response.ok) {
5
10
  throw new Error("Failed fetching translations.");
6
11
  }
package/backend.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["src/backend.ts"],
4
- "sourcesContent": ["import type { Translations, TranslationsResult } from './types.js';\n\nexport interface I18nBackend {\n loadTranslations(language: string): Promise<TranslationsResult>;\n}\n\nexport class DefaultBackend implements I18nBackend {\n async loadTranslations(language: string): Promise<TranslationsResult> {\n const response = await fetch(`./?v-r=i18n&langtag=${language}`);\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"],
5
- "mappings": "AAMO,MAAM,eAAsC;AAAA,EACjD,MAAM,iBAAiB,UAA+C;AACpE,UAAM,WAAW,MAAM,MAAM,uBAAuB,QAAQ,EAAE;AAC9D,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI,MAAM,+BAA+B;AAAA,IACjD;AACA,UAAM,kBAAkB,SAAS,QAAQ,IAAI,2BAA2B;AACxE,UAAM,eAA6B,MAAM,SAAS,KAAK;AACvD,WAAO;AAAA,MACL;AAAA,MACA,kBAAkB,mBAAmB;AAAA,IACvC;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type { Translations, TranslationsResult } from './types.js';\n\nexport interface I18nBackend {\n loadTranslations(language: string, chunks?: readonly string[]): Promise<TranslationsResult>;\n}\n\nexport class DefaultBackend implements I18nBackend {\n async loadTranslations(language: string, chunks?: readonly string[]): Promise<TranslationsResult> {\n const params = new URLSearchParams([\n ['v-r', 'i18n'],\n ['langtag', language],\n ...(chunks ?? []).map((chunk) => ['chunks', chunk]),\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"],
5
+ "mappings": "AAMO,MAAM,eAAsC;AAAA,EACjD,MAAM,iBAAiB,UAAkB,QAAyD;AAChG,UAAM,SAAS,IAAI,gBAAgB;AAAA,MACjC,CAAC,OAAO,MAAM;AAAA,MACd,CAAC,WAAW,QAAQ;AAAA,MACpB,IAAI,UAAU,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,UAAU,KAAK,CAAC;AAAA,IACpD,CAAC;AACD,UAAM,WAAW,MAAM,MAAM,MAAM,OAAO,SAAS,CAAC,EAAE;AACtD,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI,MAAM,+BAA+B;AAAA,IACjD;AACA,UAAM,kBAAkB,SAAS,QAAQ,IAAI,2BAA2B;AACxE,UAAM,eAA6B,MAAM,SAAS,KAAK;AACvD,WAAO;AAAA,MACL;AAAA,MACA,kBAAkB,mBAAmB;AAAA,IACvC;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
package/index.d.ts CHANGED
@@ -73,6 +73,15 @@ export declare class I18n {
73
73
  * @param newLanguage - a valid IETF BCP 47 language tag, such as `en` or `en-US`
74
74
  */
75
75
  setLanguage(newLanguage: string): Promise<void>;
76
+ /**
77
+ * Registers the chunk name for loading translations, and loads the
78
+ * translations for the specified chunk.
79
+ *
80
+ * @internal only for automatic internal calls from production JS bundles
81
+ *
82
+ * @param chunkName - the production JS bundle chunk name
83
+ */
84
+ registerChunk(chunkName: string): Promise<void>;
76
85
  private updateLanguage;
77
86
  /**
78
87
  * Returns a translated string for the given translation key. The key should
package/index.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,cAAc,EAAuB,MAAM,6BAA6B,CAAC;AAI9F,OAAO,KAAK,EAAE,WAAW,EAAoC,MAAM,YAAY,CAAC;AAgBhF,qBAAa,IAAI;;;IAmBf;;;;;;;;OAQG;IACH,IAAI,WAAW,IAAI,cAAc,CAAC,OAAO,CAAC,CAEzC;IAED;;;;;OAKG;IACH,IAAI,QAAQ,IAAI,cAAc,CAAC,MAAM,GAAG,SAAS,CAAC,CAEjD;IAED;;;;;;;OAOG;IACH,IAAI,gBAAgB,IAAI,cAAc,CAAC,MAAM,GAAG,SAAS,CAAC,CAEzD;IAED;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,SAAS,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAKrD;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAIvC,cAAc;IA6B5B;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM;CAQjE;AAED;;;GAGG;AACH,eAAO,MAAM,IAAI,MAAa,CAAC;AAE/B;;;;;;;;;;;;;;;;;;;;;;;GAuBG,CAAC,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAEnF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,cAAc,EAAuB,MAAM,6BAA6B,CAAC;AAI9F,OAAO,KAAK,EAAE,WAAW,EAAoC,MAAM,YAAY,CAAC;AAgBhF,qBAAa,IAAI;;;IAqBf;;;;;;;;OAQG;IACH,IAAI,WAAW,IAAI,cAAc,CAAC,OAAO,CAAC,CAEzC;IAED;;;;;OAKG;IACH,IAAI,QAAQ,IAAI,cAAc,CAAC,MAAM,GAAG,SAAS,CAAC,CAEjD;IAED;;;;;;;OAOG;IACH,IAAI,gBAAgB,IAAI,cAAc,CAAC,MAAM,GAAG,SAAS,CAAC,CAEzD;IAED;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,SAAS,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAKrD;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrD;;;;;;;OAOG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAYvC,cAAc;IAqC5B;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM;CAQjE;AAED;;;GAGG;AACH,eAAO,MAAM,IAAI,MAAa,CAAC;AAE/B;;;;;;;;;;;;;;;;;;;;;;;GAuBG,CAAC,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAEnF"}
package/index.js CHANGED
@@ -18,6 +18,7 @@ class I18n {
18
18
  #language = signal(void 0);
19
19
  #translations = signal({});
20
20
  #resolvedLanguage = signal(void 0);
21
+ #chunks = /* @__PURE__ */ new Set();
21
22
  #formatCache = new FormatCache(navigator.language);
22
23
  constructor() {
23
24
  if (!window.Vaadin?.featureFlags?.hillaI18n) {
@@ -107,19 +108,37 @@ class I18n {
107
108
  async setLanguage(newLanguage) {
108
109
  await this.updateLanguage(newLanguage, true);
109
110
  }
110
- async updateLanguage(newLanguage, updateSettings = false) {
111
- if (this.#language.value === newLanguage) {
111
+ /**
112
+ * Registers the chunk name for loading translations, and loads the
113
+ * translations for the specified chunk.
114
+ *
115
+ * @internal only for automatic internal calls from production JS bundles
116
+ *
117
+ * @param chunkName - the production JS bundle chunk name
118
+ */
119
+ async registerChunk(chunkName) {
120
+ if (this.#chunks.has(chunkName)) {
121
+ return;
122
+ }
123
+ this.#chunks.add(chunkName);
124
+ if (this.#language.value) {
125
+ await this.updateLanguage(this.#language.value, false, chunkName);
126
+ }
127
+ }
128
+ async updateLanguage(newLanguage, updateSettings = false, newChunk) {
129
+ if (this.#language.value === newLanguage && !newChunk) {
112
130
  return;
113
131
  }
132
+ const chunks = newChunk ? [newChunk] : this.#chunks.size > 0 ? [...this.#chunks.values()] : void 0;
114
133
  let translationsResult;
115
134
  try {
116
- translationsResult = await this.#backend.loadTranslations(newLanguage);
135
+ translationsResult = await this.#backend.loadTranslations(newLanguage, chunks);
117
136
  } catch (e) {
118
137
  console.error(`Failed to load translations for language: ${newLanguage}`, e);
119
138
  return;
120
139
  }
121
140
  batch(() => {
122
- this.#translations.value = translationsResult.translations;
141
+ this.#translations.value = newChunk ? { ...this.#translations.value, ...translationsResult.translations } : translationsResult.translations;
123
142
  this.#language.value = newLanguage;
124
143
  this.#resolvedLanguage.value = translationsResult.resolvedLanguage;
125
144
  this.#formatCache = new FormatCache(newLanguage);
package/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["src/index.ts"],
4
- "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\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 #formatCache: FormatCache = new FormatCache(navigator.language);\n\n constructor() {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access\n if (!(window as any).Vaadin?.featureFlags?.hillaI18n) {\n // Remove when removing feature flag\n throw new Error(\n `The Hilla I18n API is currently considered experimental and may change in the future. To use it you need to explicitly enable it in Copilot or by adding com.vaadin.experimental.hillaI18n=true to vaadin-featureflags.properties`,\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 private async updateLanguage(newLanguage: string, updateSettings = false) {\n if (this.#language.value === newLanguage) {\n return;\n }\n\n let translationsResult: TranslationsResult;\n try {\n translationsResult = await this.#backend.loadTranslations(newLanguage);\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 = 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 * 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 key - The translation key to translate\n * @param params - Optional object with placeholder values\n */\n translate(key: string, params?: Record<string, unknown>): string {\n const translation = this.#translations.value[key];\n if (!translation) {\n return key;\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 */\nexport const i18n = new I18n();\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 key - The translation key to translate\n * @param params - Optional object with placeholder values\n */ export function translate(key: string, params?: Record<string, unknown>): string {\n return i18n.translate(key, params);\n}\n"],
5
- "mappings": "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",
4
+ "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\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 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access\n if (!(window as any).Vaadin?.featureFlags?.hillaI18n) {\n // Remove when removing feature flag\n throw new Error(\n `The Hilla I18n API is currently considered experimental and may change in the future. To use it you need to explicitly enable it in Copilot or by adding com.vaadin.experimental.hillaI18n=true to vaadin-featureflags.properties`,\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 * 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 key - The translation key to translate\n * @param params - Optional object with placeholder values\n */\n translate(key: string, params?: Record<string, unknown>): string {\n const translation = this.#translations.value[key];\n if (!translation) {\n return key;\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 */\nexport const i18n = new I18n();\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 key - The translation key to translate\n * @param params - Optional object with placeholder values\n */ export function translate(key: string, params?: Record<string, unknown>): string {\n return i18n.translate(key, params);\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/hilla-react-i18n",
3
- "version": "24.5.0-alpha9",
3
+ "version": "24.5.0-beta2",
4
4
  "description": "Hilla I18n utils for React",
5
5
  "main": "index.js",
6
6
  "module": "index.js",
@@ -46,8 +46,8 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@vaadin/hilla-frontend": "24.5.0-alpha9",
50
- "@vaadin/hilla-react-signals": "24.5.0-alpha9",
49
+ "@vaadin/hilla-frontend": "24.5.0-beta2",
50
+ "@vaadin/hilla-react-signals": "24.5.0-beta2",
51
51
  "intl-messageformat": "^10.5.11"
52
52
  },
53
53
  "peerDependencies": {
@@ -71,6 +71,6 @@
71
71
  "fetch-mock": "^9.11.0",
72
72
  "sinon": "^16.0.0",
73
73
  "sinon-chai": "^3.7.0",
74
- "typescript": "5.5.2"
74
+ "typescript": "5.6.2"
75
75
  }
76
76
  }
package/types.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- export type Translations = Record<string, string>;
2
- export type TranslationsResult = {
1
+ export type Translations = Readonly<Record<string, string>>;
2
+ export type TranslationsResult = Readonly<{
3
3
  translations: Translations;
4
4
  resolvedLanguage?: string;
5
- };
5
+ }>;
6
6
  export interface I18nOptions {
7
7
  /**
8
8
  * Allows to explicitly set the initial language. Should be a valid
package/types.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAElD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,YAAY,EAAE,YAAY,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAE5D,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,YAAY,EAAE,YAAY,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC,CAAC;AAEH,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}