@open-xchange/rolldown-plugin-i18next-gettext 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## `1.2.0` – 2026-Apr-15
4
+
5
+ - added: switched back to direct imports of PO files
6
+ - removed: virtual modules for namespaces
7
+
3
8
  ## `1.1.0` – 2026-Apr-15
4
9
 
5
10
  - removed: direct imports of PO files
package/README.md CHANGED
@@ -4,9 +4,19 @@ A [rolldown](https://rolldown.rs/) plugin that allows building libraries or appl
4
4
 
5
5
  This plugin has the following responsibilities:
6
6
 
7
- - Virtual module imports provide translation catalogs from selected `.po` files in a single namespace dictionary.
7
+ - All `.po` files imported in source code will be converted to i18next translation catalogs.
8
8
  - The source code will be scanned for translation strings (`t` function calls), and `.pot` files containing all strings will be generated into the output directory. Supports i18next namespaces (one `.pot` file per namespace).
9
9
 
10
+ ## Installation
11
+
12
+ ```sh
13
+ npm install -D @open-xchange/rolldown-plugin-i18next-gettext
14
+ # or
15
+ pnpm add -D @open-xchange/rolldown-plugin-i18next-gettext
16
+ # or
17
+ yarn add -D @open-xchange/rolldown-plugin-i18next-gettext
18
+ ```
19
+
10
20
  ## Configuration
11
21
 
12
22
  Add the plugin to your rolldown configuration (or compatible configurations like Vite or tsdown):
@@ -19,10 +29,6 @@ import i18nextPlugin from '@open-xchange/rolldown-plugin-i18next-gettext'
19
29
  export default defineConfig(() => {
20
30
  plugins: [
21
31
  i18nextPlugin({
22
- poFiles: {
23
- default: 'i18n/*.po',
24
- custom: 'i18n/custom/*.po',
25
- },
26
32
  srcFiles: 'src/**/*.{js,jsx,ts,tsx}',
27
33
  potFiles: '[NAMESPACE].pot',
28
34
  projectName: 'My Project',
@@ -33,40 +39,34 @@ export default defineConfig(() => {
33
39
 
34
40
  ### Options
35
41
 
36
- Type `Globs` can be a single glob pattern as string, or an array of glob patterns.
37
-
38
- ```ts
39
- type Globs = string | readonly string[]
40
- ```
41
-
42
42
  | Name | Type | Default | Description |
43
43
  | - | - | - | - |
44
- | `poFiles` | `Record<string,Globs>` | _required_ | Glob pattern(s) for all PO files to be converted to i18next translation catalogs, mapped by i18next namespace identifiers. |
45
- | `srcFiles` | `Globs` | `src/**/*.{js,jsx,ts,tsx}` | Glob pattern(s) for all source files to be scanned for UI strings for the POT file generator. |
44
+ | `poFiles` | `string\|string[]` | `**/*.po` | Glob pattern(s) for all PO files to be converted when imported in source code. |
45
+ | `srcFiles` | `string\|string[]` | `src/**/*.{js,jsx,ts,tsx}` | Glob pattern(s) for all source files to be scanned for UI strings for the POT file generator. |
46
46
  | `potFiles` | `string` | `[NAMESPACE].pot` | Path to and filename of the generated POT files, relative to the build output directory. Must contain the placeholder `[NAMESPACE]` if the project contains translation strings in different i18next namespaces. |
47
47
  | `projectName` | `string` | _required_ | The project name to be inserted into the POT file under the key 'Project-Id-Version'. May contain the placeholder `[NAMESPACE]` |
48
48
 
49
49
  ## Usage
50
50
 
51
- Register TypeScript type definitions for the virtual module imports:
51
+ Register TypeScript type definitions for the PO file imports:
52
52
 
53
53
  ```json
54
54
  // tsconfig.json
55
55
  {
56
56
  "types": [
57
- "@open-xchange/rolldown-plugin-i18next-gettext/virtual"
57
+ "@open-xchange/rolldown-plugin-i18next-gettext/client"
58
58
  ],
59
59
  }
60
60
  ```
61
61
 
62
- Import translation catalogs in source code:
62
+ Load translation catalogs in source code on demand via i18next backend plugin:
63
63
 
64
64
  ```ts
65
- // use namespace specifiers as configured in plugin's option `poFiles`
66
- import translations from 'virtual:i18next-namespace:default'
65
+ import i18next from 'i18next'
66
+ import resourcesToBackend from 'i18next-resources-to-backend'
67
67
 
68
- i18next.init({ resources: {} });
69
- for (const [lang, catalog] of Object.entries(translations)) {
70
- i18next.addResources(lang, 'translation', catalog)
71
- }
68
+ i18next.use(resourcesToBackend(async (lang: string, namespace: string) => {
69
+ // build appropriate path to the PO files
70
+ return await import(`../i18n/${namespace}/${lang}.po`)
71
+ }))
72
72
  ```
@@ -0,0 +1,12 @@
1
+
2
+ declare module '*.po' {
3
+
4
+ /**
5
+ * Shape of an i18next JSON translation catalog (mapping from source keys to
6
+ * translations) for a single language, converted from a gettext PO file.
7
+ */
8
+ export type Catalog = Record<string, string>
9
+
10
+ const catalog: Catalog
11
+ export default catalog
12
+ }
package/dist/index.d.mts CHANGED
@@ -1,16 +1,18 @@
1
1
  import { Plugin } from "rolldown";
2
2
 
3
3
  //#region src/index.d.ts
4
- type Globs = string | readonly string[];
5
4
  /**
6
5
  * Configuration options for the plugin '@open-xchange/rolldown-plugin-i18next-gettext'.
7
6
  */
8
7
  interface PluginI18nextGettextOptions {
9
8
  /**
10
- * Glob pattern(s) for all PO files to be converted to i18next translation
11
- * catalogs, mapped by i18next namespace identifiers.
9
+ * Glob pattern(s) for all PO files to be converted when imported in source
10
+ * code.
11
+ *
12
+ * @default
13
+ * '**\/*.po'
12
14
  */
13
- poFiles: Record<string, Globs>;
15
+ poFiles?: string | readonly string[];
14
16
  /**
15
17
  * Glob pattern(s) for all source files to be scanned for UI strings for the
16
18
  * POT file generator.
@@ -18,7 +20,7 @@ interface PluginI18nextGettextOptions {
18
20
  * @default
19
21
  * 'src/**\/*.{js,jsx,ts,tsx}'
20
22
  */
21
- srcFiles?: Globs;
23
+ srcFiles?: string | readonly string[];
22
24
  /**
23
25
  * Path to and filename of the generated POT files, relative to the build
24
26
  * output directory. Must contain the placeholder `[NAMESPACE]` if the
@@ -49,5 +51,5 @@ declare const PROJECT_NAME = "@open-xchange/rolldown-plugin-i18next-gettext";
49
51
  */
50
52
  declare function pluginI18nextGettext(options: PluginI18nextGettextOptions): Plugin;
51
53
  //#endregion
52
- export { Globs, PROJECT_NAME, PluginI18nextGettextOptions, pluginI18nextGettext as default };
54
+ export { PROJECT_NAME, PluginI18nextGettextOptions, pluginI18nextGettext as default };
53
55
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;KAYY,KAAA;;AAAZ;;UAKiB,2BAAA;EALA;;AAKjB;;EAME,OAAA,EAAS,MAAA,SAAe,KAAA;EAAA;;;;;;;EASxB,QAAA,GAAW,KAAA;EAAX;;;;;;AAwBF;;EAdE,QAAA;EAcuB;;AAAkD;;EARzE,WAAA;AAAA;;;;cAQW,YAAA;;;;;;;;;;iBAaW,oBAAA,CAAqB,OAAA,EAAS,2BAAA,GAA8B,MAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;AAUA;UAAiB,2BAAA;;;;;;;;EASf,OAAA;EAiCW;;;;;AAA8D;;EAxBzE,QAAA;EAqCwF;;;;;;;;EA3BxF,QAAA;;;;;EAMA,WAAA;AAAA;;;;cAQW,YAAA;;;;;;;;;;iBAaW,oBAAA,CAAqB,OAAA,EAAS,2BAAA,GAA8B,MAAA"}
package/dist/index.mjs CHANGED
@@ -1,6 +1,4 @@
1
- import * as path from "node:path";
2
- import { createJsonModule, getRemainder, prefixRegex } from "@open-xchange/rolldown-utils";
3
- import { glob } from "tinyglobby";
1
+ import { createJsonModule } from "@open-xchange/rolldown-utils";
4
2
  import { parsePoFile, parseSourceFiles } from "@open-xchange/i18next-po-parser";
5
3
  //#region src/index.ts
6
4
  /**
@@ -17,31 +15,16 @@ const PROJECT_NAME = "@open-xchange/rolldown-plugin-i18next-gettext";
17
15
  * The rolldown plugin object.
18
16
  */
19
17
  function pluginI18nextGettext(options) {
20
- const VIRTUAL_MODULE_PREFIX = "virtual:i18next-namespace:";
18
+ const { poFiles = "**/*.po", srcFiles = "src/**/*.{js,jsx,ts,tsx}", potFiles = "[NAMESPACE].pot", projectName } = options;
21
19
  return {
22
20
  name: PROJECT_NAME,
23
- resolveId: {
24
- filter: { id: prefixRegex(VIRTUAL_MODULE_PREFIX) },
25
- handler: (id) => `\0${id}`
26
- },
27
21
  load: {
28
- filter: { id: prefixRegex(`\0${VIRTUAL_MODULE_PREFIX}`) },
22
+ filter: { id: poFiles },
29
23
  async handler(id) {
30
- const key = getRemainder(id, `\0${VIRTUAL_MODULE_PREFIX}`);
31
- if (!key) this.error("missing namespace specifier");
32
- const globs = options.poFiles[key];
33
- if (!globs) this.error(`invalid namespace specifier: ${key}`);
34
- const catalogs = Object.create(null);
35
- const filePaths = await glob(globs);
36
- await Promise.all(filePaths.map(async (filePath) => {
37
- const basename = path.parse(filePath).name.replaceAll("_", "-");
38
- catalogs[basename] = await parsePoFile(filePath);
39
- }));
40
- return createJsonModule(catalogs);
24
+ return createJsonModule(await parsePoFile(id));
41
25
  }
42
26
  },
43
27
  async generateBundle() {
44
- const { srcFiles = "src/**/*.{js,jsx,ts,tsx}", potFiles = "[NAMESPACE].pot", projectName } = options;
45
28
  const catalogs = await parseSourceFiles({
46
29
  files: srcFiles,
47
30
  project: projectName
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["\nimport * as path from 'node:path'\n\nimport type { Plugin } from 'rolldown'\nimport { prefixRegex, getRemainder, createJsonModule } from '@open-xchange/rolldown-utils'\nimport { glob } from 'tinyglobby'\nimport { parsePoFile, parseSourceFiles } from '@open-xchange/i18next-po-parser'\n\nimport type { Namespace } from 'virtual:i18next-namespace:*'\n\n// types ======================================================================\n\nexport type Globs = string | readonly string[]\n\n/**\n * Configuration options for the plugin '@open-xchange/rolldown-plugin-i18next-gettext'.\n */\nexport interface PluginI18nextGettextOptions {\n\n /**\n * Glob pattern(s) for all PO files to be converted to i18next translation\n * catalogs, mapped by i18next namespace identifiers.\n */\n poFiles: Record<string, Globs>\n\n /**\n * Glob pattern(s) for all source files to be scanned for UI strings for the\n * POT file generator.\n *\n * @default\n * 'src/**\\/*.{js,jsx,ts,tsx}'\n */\n srcFiles?: Globs\n\n /**\n * Path to and filename of the generated POT files, relative to the build\n * output directory. Must contain the placeholder `[NAMESPACE]` if the\n * project contains translation strings in different i18next namespaces.\n *\n * @default\n * '[NAMESPACE].pot'\n */\n potFiles?: string\n\n /**\n * The project name to be inserted into the POT files under the key\n * 'Project-Id-Version'. May contain the placeholder `[NAMESPACE]`.\n */\n projectName: string\n}\n\n// constants ==================================================================\n\n/**\n * Identifier of this plugin.\n */\nexport const PROJECT_NAME = '@open-xchange/rolldown-plugin-i18next-gettext'\n\n// plugin =====================================================================\n\n/**\n * Rolldown plugin for using i18next with gettext `.po` files under the hood.\n *\n * @param options\n * Plugin configuration.\n *\n * @returns\n * The rolldown plugin object.\n */\nexport default function pluginI18nextGettext(options: PluginI18nextGettextOptions): Plugin {\n\n // prefix for virtual modules generated by this plugin\n const VIRTUAL_MODULE_PREFIX = 'virtual:i18next-namespace:'\n\n return {\n name: PROJECT_NAME,\n\n // handle virtual module identifiers\n resolveId: {\n filter: { id: prefixRegex(VIRTUAL_MODULE_PREFIX) },\n handler: id => `\\0${id}`,\n },\n\n // convert PO files to i18next namespace records\n load: {\n filter: { id: prefixRegex(`\\0${VIRTUAL_MODULE_PREFIX}`) },\n async handler(id) {\n const key = getRemainder(id, `\\0${VIRTUAL_MODULE_PREFIX}`)\n if (!key) this.error('missing namespace specifier')\n const globs = options.poFiles[key]\n if (!globs) this.error(`invalid namespace specifier: ${key}`)\n const catalogs = Object.create(null) as Namespace\n const filePaths = await glob(globs)\n await Promise.all(filePaths.map(async filePath => {\n const basename = path.parse(filePath).name.replaceAll('_', '-')\n catalogs[basename] = await parsePoFile(filePath)\n }))\n return createJsonModule(catalogs)\n },\n },\n\n // parse source files and create POT files per namespace\n async generateBundle() {\n\n // resolve default options\n const { srcFiles = 'src/**/*.{js,jsx,ts,tsx}', potFiles = '[NAMESPACE].pot', projectName } = options\n\n // parse all source files and extract translations\n const catalogs = await parseSourceFiles({ files: srcFiles, project: projectName })\n\n // warn when writing multiple POT files to same file location\n if ((catalogs.size > 1) && !potFiles.includes('[NAMESPACE]')) {\n this.error('multiple POT files written to same file location (missing placeholder `[NAMESPACE]` in option potFile)')\n }\n\n // add all POT files as assets to the bundle\n for (const [namespace, source] of catalogs) {\n const fileName = potFiles.replaceAll('[NAMESPACE]', namespace)\n this.emitFile({ type: 'asset', fileName, source })\n }\n },\n }\n}\n"],"mappings":";;;;;;;;AAwDA,MAAa,eAAe;;;;;;;;;;AAa5B,SAAwB,qBAAqB,SAA8C;CAGzF,MAAM,wBAAwB;AAE9B,QAAO;EACL,MAAM;EAGN,WAAW;GACT,QAAQ,EAAE,IAAI,YAAY,sBAAsB,EAAE;GAClD,UAAS,OAAM,KAAK;GACrB;EAGD,MAAM;GACJ,QAAQ,EAAE,IAAI,YAAY,KAAK,wBAAwB,EAAE;GACzD,MAAM,QAAQ,IAAI;IAChB,MAAM,MAAM,aAAa,IAAI,KAAK,wBAAwB;AAC1D,QAAI,CAAC,IAAK,MAAK,MAAM,8BAA8B;IACnD,MAAM,QAAQ,QAAQ,QAAQ;AAC9B,QAAI,CAAC,MAAO,MAAK,MAAM,gCAAgC,MAAM;IAC7D,MAAM,WAAW,OAAO,OAAO,KAAK;IACpC,MAAM,YAAY,MAAM,KAAK,MAAM;AACnC,UAAM,QAAQ,IAAI,UAAU,IAAI,OAAM,aAAY;KAChD,MAAM,WAAW,KAAK,MAAM,SAAS,CAAC,KAAK,WAAW,KAAK,IAAI;AAC/D,cAAS,YAAY,MAAM,YAAY,SAAS;MAChD,CAAC;AACH,WAAO,iBAAiB,SAAS;;GAEpC;EAGD,MAAM,iBAAiB;GAGrB,MAAM,EAAE,WAAW,4BAA4B,WAAW,mBAAmB,gBAAgB;GAG7F,MAAM,WAAW,MAAM,iBAAiB;IAAE,OAAO;IAAU,SAAS;IAAa,CAAC;AAGlF,OAAK,SAAS,OAAO,KAAM,CAAC,SAAS,SAAS,cAAc,CAC1D,MAAK,MAAM,yGAAyG;AAItH,QAAK,MAAM,CAAC,WAAW,WAAW,UAAU;IAC1C,MAAM,WAAW,SAAS,WAAW,eAAe,UAAU;AAC9D,SAAK,SAAS;KAAE,MAAM;KAAS;KAAU;KAAQ,CAAC;;;EAGvD"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["\nimport type { Plugin, GeneralHookFilter } from 'rolldown'\nimport { createJsonModule } from '@open-xchange/rolldown-utils'\nimport { parsePoFile, parseSourceFiles } from '@open-xchange/i18next-po-parser'\n\n// types ======================================================================\n\n/**\n * Configuration options for the plugin '@open-xchange/rolldown-plugin-i18next-gettext'.\n */\nexport interface PluginI18nextGettextOptions {\n\n /**\n * Glob pattern(s) for all PO files to be converted when imported in source\n * code.\n *\n * @default\n * '**\\/*.po'\n */\n poFiles?: string | readonly string[]\n\n /**\n * Glob pattern(s) for all source files to be scanned for UI strings for the\n * POT file generator.\n *\n * @default\n * 'src/**\\/*.{js,jsx,ts,tsx}'\n */\n srcFiles?: string | readonly string[]\n\n /**\n * Path to and filename of the generated POT files, relative to the build\n * output directory. Must contain the placeholder `[NAMESPACE]` if the\n * project contains translation strings in different i18next namespaces.\n *\n * @default\n * '[NAMESPACE].pot'\n */\n potFiles?: string\n\n /**\n * The project name to be inserted into the POT files under the key\n * 'Project-Id-Version'. May contain the placeholder `[NAMESPACE]`.\n */\n projectName: string\n}\n\n// constants ==================================================================\n\n/**\n * Identifier of this plugin.\n */\nexport const PROJECT_NAME = '@open-xchange/rolldown-plugin-i18next-gettext'\n\n// plugin =====================================================================\n\n/**\n * Rolldown plugin for using i18next with gettext `.po` files under the hood.\n *\n * @param options\n * Plugin configuration.\n *\n * @returns\n * The rolldown plugin object.\n */\nexport default function pluginI18nextGettext(options: PluginI18nextGettextOptions): Plugin {\n\n // resolve default options\n const {\n poFiles = '**/*.po',\n srcFiles = 'src/**/*.{js,jsx,ts,tsx}',\n potFiles = '[NAMESPACE].pot',\n projectName,\n } = options\n\n return {\n name: PROJECT_NAME,\n\n // convert PO files to i18next namespace records\n load: {\n filter: { id: poFiles as GeneralHookFilter },\n async handler(id) {\n return createJsonModule(await parsePoFile(id))\n },\n },\n\n // parse source files and create POT files per namespace\n async generateBundle() {\n\n // parse all source files and extract translations\n const catalogs = await parseSourceFiles({ files: srcFiles, project: projectName })\n\n // warn when writing multiple POT files to same file location\n if ((catalogs.size > 1) && !potFiles.includes('[NAMESPACE]')) {\n this.error('multiple POT files written to same file location (missing placeholder `[NAMESPACE]` in option potFile)')\n }\n\n // add all POT files as assets to the bundle\n for (const [namespace, source] of catalogs) {\n const fileName = potFiles.replaceAll('[NAMESPACE]', namespace)\n this.emitFile({ type: 'asset', fileName, source })\n }\n },\n }\n}\n"],"mappings":";;;;;;AAoDA,MAAa,eAAe;;;;;;;;;;AAa5B,SAAwB,qBAAqB,SAA8C;CAGzF,MAAM,EACJ,UAAU,WACV,WAAW,4BACX,WAAW,mBACX,gBACE;AAEJ,QAAO;EACL,MAAM;EAGN,MAAM;GACJ,QAAQ,EAAE,IAAI,SAA8B;GAC5C,MAAM,QAAQ,IAAI;AAChB,WAAO,iBAAiB,MAAM,YAAY,GAAG,CAAC;;GAEjD;EAGD,MAAM,iBAAiB;GAGrB,MAAM,WAAW,MAAM,iBAAiB;IAAE,OAAO;IAAU,SAAS;IAAa,CAAC;AAGlF,OAAK,SAAS,OAAO,KAAM,CAAC,SAAS,SAAS,cAAc,CAC1D,MAAK,MAAM,yGAAyG;AAItH,QAAK,MAAM,CAAC,WAAW,WAAW,UAAU;IAC1C,MAAM,WAAW,SAAS,WAAW,eAAe,UAAU;AAC9D,SAAK,SAAS;KAAE,MAAM;KAAS;KAAU;KAAQ,CAAC;;;EAGvD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-xchange/rolldown-plugin-i18next-gettext",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Rolldown integration of i18next using gettext PO files",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,8 +15,8 @@
15
15
  "exports": {
16
16
  ".": "./dist/index.mjs",
17
17
  "./package.json": "./package.json",
18
- "./virtual": {
19
- "types": "./dist/virtual.d.ts"
18
+ "./client": {
19
+ "types": "./dist/client.d.ts"
20
20
  }
21
21
  },
22
22
  "files": [
@@ -25,22 +25,31 @@
25
25
  ],
26
26
  "dependencies": {
27
27
  "@open-xchange/i18next-po-parser": "^1.4.0",
28
- "tinyglobby": "^0.2.16",
29
28
  "@open-xchange/rolldown-utils": "^1.0.0"
30
29
  },
31
30
  "devDependencies": {
31
+ "@open-xchange/rolldown-plugin-dynamic-import-vars": "^1.0.2",
32
32
  "@vitest/coverage-v8": "^4.1.3",
33
33
  "i18next": "^26.0.4",
34
34
  "rolldown": "^1.0.0-rc.15",
35
- "tsdown": "^0.21.7",
35
+ "tsdown": "^0.21.8",
36
36
  "vitest": "^4.1.3",
37
+ "@open-xchange/vitest-plugin-extended": "^1.6.0",
38
+ "@open-xchange/vitest-plugin-utils": "^1.1.1",
37
39
  "@open-xchange/linter-presets": "^1.22.0",
38
- "@open-xchange/tsconfig": "^0.0.3",
39
- "@open-xchange/vitest-plugin-utils": "^1.0.0",
40
- "@open-xchange/vitest-plugin-extended": "^1.6.0"
40
+ "@open-xchange/tsconfig": "^0.0.3"
41
41
  },
42
42
  "peerDependencies": {
43
- "rolldown": "*"
43
+ "rolldown": "*",
44
+ "tsdown": "*"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "rolldown": {
48
+ "optional": true
49
+ },
50
+ "tsdown": {
51
+ "optional": true
52
+ }
44
53
  },
45
54
  "scripts": {
46
55
  "build": "tsdown",
package/dist/virtual.d.ts DELETED
@@ -1,18 +0,0 @@
1
-
2
- declare module 'virtual:i18next-namespace:*' {
3
-
4
- /**
5
- * Shape of an i18next JSON translation catalog (mapping from source keys to
6
- * translations) for a single language, converted from a gettext PO file.
7
- */
8
- export type Catalog = Record<string, string>
9
-
10
- /**
11
- * Shape of an i18next JSON translation namespace (mapping from language
12
- * codes to translation catalogs), converted from multiple gettext PO files.
13
- */
14
- export type Namespace = Record<string, Catalog>
15
-
16
- const namespace: Namespace
17
- export default namespace
18
- }