@l10nmonster/helpers-json 3.0.0-alpha.8 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.releaserc.json +1 -1
- package/CHANGELOG.md +9 -0
- package/i18next.js +181 -113
- package/package.json +2 -2
- package/utils.js +65 -0
package/.releaserc.json
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## @l10nmonster/helpers-json [3.0.1](https://public-github/l10nmonster/l10nmonster/compare/@l10nmonster/helpers-json@3.0.0...@l10nmonster/helpers-json@3.0.1) (2025-12-20)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* Add proper pluralization expansion support ([3d062bb](https://public-github/l10nmonster/l10nmonster/commit/3d062bbb3272c61e969b419a56a7a5e347ab96c6))
|
|
7
|
+
* Pluralization improvements ([5964250](https://public-github/l10nmonster/l10nmonster/commit/596425092c425cc8d6c312ef58509c4c3c537431))
|
|
8
|
+
* **server:** Fix cart cleanup ([9bbcab9](https://public-github/l10nmonster/l10nmonster/commit/9bbcab93e1fd20aeb09f59c828665159f091f37c))
|
|
9
|
+
|
|
1
10
|
# Changelog
|
|
2
11
|
|
|
3
12
|
All notable changes to this project will be documented in this file.
|
package/i18next.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
/* eslint-disable no-eq-null, eqeqeq */
|
|
3
3
|
|
|
4
4
|
// i18next v4 json format defined at https://www.i18next.com/misc/json-format
|
|
5
|
-
import {
|
|
5
|
+
import { unflatten } from 'flat';
|
|
6
6
|
import { regex } from '@l10nmonster/core';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
arbPlaceholderHandler,
|
|
9
|
+
parseResourceAnnotations,
|
|
10
|
+
} from './utils.js';
|
|
8
11
|
|
|
9
|
-
const isArbAnnotations = e => e[0].split('.').some(segment => segment.startsWith(ARB_ANNOTATION_MARKER));
|
|
10
|
-
const validPluralSuffixes = new Set(['one', 'other', 'zero', 'two', 'few', 'many']);
|
|
11
|
-
const extractArbGroupsRegex = /(?<prefix>.+?\.)?@(?<key>[^.]+)\.(?<attribute>.+)/;
|
|
12
12
|
const defaultArbAnnotationHandlers = {
|
|
13
13
|
description: (_, data) => (data == null ? undefined : data),
|
|
14
14
|
placeholders: (_, data) => (data == null ? undefined : arbPlaceholderHandler(data)),
|
|
@@ -16,139 +16,207 @@ const defaultArbAnnotationHandlers = {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* @description
|
|
22
|
-
* Parse resource annotations according to the given configuration.
|
|
23
|
-
*
|
|
24
|
-
* @param {object} resource - The resource to parse.
|
|
25
|
-
* @param {boolean} enableArbAnnotations - Whether to enable annotations
|
|
26
|
-
* @param {object} arbAnnotationHandlers - An object mapping annotation names to a function which takes an annotation name and its value and returns a string.
|
|
27
|
-
*
|
|
28
|
-
* @returns {array} An array with two elements. The first element is an array of key-value pairs for the translatable segments. The second element is an object with the parsed annotations.
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* const resource = {
|
|
32
|
-
* "key": "value",
|
|
33
|
-
* "@key": {
|
|
34
|
-
* "description": "description for key",
|
|
35
|
-
* "placeholders": {
|
|
36
|
-
* "placeholder": {
|
|
37
|
-
* "example": "example for placeholder",
|
|
38
|
-
* "description": "description for placeholder",
|
|
39
|
-
* }
|
|
40
|
-
* }
|
|
41
|
-
* }
|
|
42
|
-
* };
|
|
43
|
-
* const [segments, notes] = parseResourceAnnotations(resource, true, {
|
|
44
|
-
* description: (_, data) => (data == null ? undefined : data),
|
|
45
|
-
* placeholders: (_, data) => (data == null ? undefined : arbPlaceholderHandler(data)),
|
|
46
|
-
* DEFAULT: (name, data) => (data == null ? undefined : `${name}: ${data}`),
|
|
47
|
-
* });
|
|
48
|
-
* // segments is [["key", "value"]]
|
|
49
|
-
* // notes is { "key": "description for key\nplaceholder: example for placeholder - description for placeholder" }
|
|
19
|
+
* Filter for i18next v4 JSON format.
|
|
20
|
+
* @see https://www.i18next.com/misc/json-format
|
|
50
21
|
*/
|
|
51
|
-
function parseResourceAnnotations(resource, enableArbAnnotations, arbAnnotationHandlers) {
|
|
52
|
-
if (!enableArbAnnotations) {
|
|
53
|
-
return [ Object.entries(flatten(resource)), {} ]
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const { res, notes } = flattenAndSplitResources([], resource)
|
|
57
|
-
const parsedNotes = {}
|
|
58
|
-
for (const [key, arbAnnotations] of Object.entries(notes)) {
|
|
59
|
-
if (typeof arbAnnotations === "object") {
|
|
60
|
-
const notes = []
|
|
61
|
-
for (const [annotation, data] of Object.entries(arbAnnotations)) {
|
|
62
|
-
const handler = arbAnnotationHandlers[annotation] ?? arbAnnotationHandlers.DEFAULT
|
|
63
|
-
if (handler != null) {
|
|
64
|
-
const val = handler(annotation, data)
|
|
65
|
-
if (val !== undefined) {
|
|
66
|
-
notes.push(val)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
parsedNotes[key] = notes.join("\n")
|
|
71
|
-
} else {
|
|
72
|
-
parsedNotes[key] = arbAnnotations
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return [ Object.entries(res), parsedNotes ];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
22
|
export class I18nextFilter {
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {Object} [params] - Configuration options
|
|
26
|
+
* @param {boolean} [params.enableArbAnnotations=false] - Enable parsing of ARB-style annotations (@key objects)
|
|
27
|
+
* @param {boolean} [params.enableArrays=false] - Enable array syntax in generated output (vs object notation)
|
|
28
|
+
* @param {boolean} [params.emitArbAnnotations=false] - Emit ARB annotations in generated output
|
|
29
|
+
* @param {Object} [params.arbAnnotationHandlers] - Custom handlers for ARB annotation types.
|
|
30
|
+
* Each handler is a function(name, data) returning a string or undefined.
|
|
31
|
+
* Built-in handlers: description, placeholders, DEFAULT.
|
|
32
|
+
*/
|
|
79
33
|
constructor(params) {
|
|
80
34
|
this.enableArbAnnotations = params?.enableArbAnnotations || false;
|
|
81
|
-
this.enablePluralSuffixes = params?.enablePluralSuffixes || false;
|
|
82
35
|
this.enableArrays = params?.enableArrays || false;
|
|
83
36
|
this.emitArbAnnotations = params?.emitArbAnnotations || false;
|
|
84
37
|
this.arbAnnotationHandlers = {
|
|
85
38
|
...defaultArbAnnotationHandlers,
|
|
86
39
|
...(params?.arbAnnotationHandlers ?? {})
|
|
87
|
-
}
|
|
40
|
+
};
|
|
88
41
|
}
|
|
89
42
|
|
|
90
|
-
async parseResource({ resource }) {
|
|
91
|
-
const response = {
|
|
92
|
-
|
|
43
|
+
async parseResource({ resource, sourcePluralForms, targetPluralForms }) {
|
|
44
|
+
const response = { segments: [] };
|
|
45
|
+
if (!resource) return response;
|
|
46
|
+
|
|
47
|
+
const unParsedResource = JSON.parse(resource);
|
|
48
|
+
const targetLangs = unParsedResource['@@targetLocales'];
|
|
49
|
+
Array.isArray(targetLangs) && (response.targetLangs = targetLangs);
|
|
50
|
+
|
|
51
|
+
const [parsedResource, notes] = parseResourceAnnotations(
|
|
52
|
+
unParsedResource,
|
|
53
|
+
this.enableArbAnnotations,
|
|
54
|
+
this.arbAnnotationHandlers,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const targetFormsSet = new Set(targetPluralForms);
|
|
58
|
+
|
|
59
|
+
// Collect all segments and group potential plurals by baseKey
|
|
60
|
+
const potentialPluralGroups = new Map(); // baseKey -> Map(suffix -> segment)
|
|
61
|
+
|
|
62
|
+
for (const [key, value] of parsedResource) {
|
|
63
|
+
const seg = {
|
|
64
|
+
sid: key,
|
|
65
|
+
str: value,
|
|
66
|
+
...(notes[key] && { notes: notes[key] }),
|
|
67
|
+
};
|
|
68
|
+
response.segments.push(seg);
|
|
69
|
+
|
|
70
|
+
// Check if key is a plural form (e.g., "key_one", "key_other")
|
|
71
|
+
const underscoreIdx = key.lastIndexOf('_');
|
|
72
|
+
if (underscoreIdx !== -1) {
|
|
73
|
+
const suffix = key.slice(underscoreIdx + 1);
|
|
74
|
+
if (targetFormsSet.has(suffix)) {
|
|
75
|
+
const baseKey = key.slice(0, underscoreIdx);
|
|
76
|
+
if (!potentialPluralGroups.has(baseKey)) {
|
|
77
|
+
potentialPluralGroups.set(baseKey, new Map());
|
|
78
|
+
}
|
|
79
|
+
potentialPluralGroups.get(baseKey).set(suffix, seg);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
93
82
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
|
|
83
|
+
|
|
84
|
+
// For groups with all required forms: set pluralForm and generate missing forms
|
|
85
|
+
// We need to reorder segments so all forms of a plural group are together in CLDR order
|
|
86
|
+
const pluralGroupsToReorder = new Map(); // baseKey -> { firstIndex, forms: Map(suffix -> seg) }
|
|
87
|
+
|
|
88
|
+
for (const [baseKey, forms] of potentialPluralGroups) {
|
|
89
|
+
const hasAllForms = sourcePluralForms.every(form => forms.has(form));
|
|
90
|
+
if (!hasAllForms) continue;
|
|
91
|
+
|
|
92
|
+
// Find the first index of this plural group in segments
|
|
93
|
+
let firstIndex = -1;
|
|
94
|
+
for (let i = 0; i < response.segments.length; i++) {
|
|
95
|
+
if (forms.has(response.segments[i].sid?.split('_').pop()) &&
|
|
96
|
+
response.segments[i].sid?.startsWith(baseKey + '_')) {
|
|
97
|
+
firstIndex = i;
|
|
98
|
+
break;
|
|
108
99
|
}
|
|
109
|
-
response.segments.push(seg);
|
|
110
100
|
}
|
|
101
|
+
|
|
102
|
+
// Set pluralForm on existing segments
|
|
103
|
+
for (const [suffix, seg] of forms) {
|
|
104
|
+
seg.pluralForm = suffix;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Generate missing forms from _other
|
|
108
|
+
const other = forms.get('other');
|
|
109
|
+
for (const suffix of targetPluralForms) {
|
|
110
|
+
if (!forms.has(suffix)) {
|
|
111
|
+
forms.set(suffix, {
|
|
112
|
+
sid: `${baseKey}_${suffix}`,
|
|
113
|
+
str: other.str,
|
|
114
|
+
pluralForm: suffix,
|
|
115
|
+
...(other.notes && { notes: other.notes })
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
pluralGroupsToReorder.set(baseKey, { firstIndex, forms });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Rebuild segments with plural groups in correct order
|
|
124
|
+
if (pluralGroupsToReorder.size > 0) {
|
|
125
|
+
const newSegments = [];
|
|
126
|
+
const processedPluralKeys = new Set();
|
|
127
|
+
|
|
128
|
+
for (const seg of response.segments) {
|
|
129
|
+
const underscoreIdx = seg.sid.lastIndexOf('_');
|
|
130
|
+
const suffix = underscoreIdx !== -1 ? seg.sid.slice(underscoreIdx + 1) : null;
|
|
131
|
+
const baseKey = underscoreIdx !== -1 ? seg.sid.slice(0, underscoreIdx) : null;
|
|
132
|
+
|
|
133
|
+
if (baseKey && pluralGroupsToReorder.has(baseKey) && !processedPluralKeys.has(baseKey)) {
|
|
134
|
+
// Insert all forms of this plural group in CLDR order
|
|
135
|
+
processedPluralKeys.add(baseKey);
|
|
136
|
+
const { forms } = pluralGroupsToReorder.get(baseKey);
|
|
137
|
+
for (const form of targetPluralForms) {
|
|
138
|
+
if (forms.has(form)) {
|
|
139
|
+
newSegments.push(forms.get(form));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} else if (!baseKey || !pluralGroupsToReorder.has(baseKey)) {
|
|
143
|
+
// Non-plural segment
|
|
144
|
+
newSegments.push(seg);
|
|
145
|
+
}
|
|
146
|
+
// Skip plural segments that were already added via the group
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
response.segments = newSegments;
|
|
111
150
|
}
|
|
151
|
+
|
|
112
152
|
return response;
|
|
113
153
|
}
|
|
114
154
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Generate a resource file from segments
|
|
157
|
+
* @param {Object} params
|
|
158
|
+
* @param {Array} params.segments - Array of segment objects with sid, str, etc.
|
|
159
|
+
* @param {Function} params.translator - Translator function(seg) that returns translated string
|
|
160
|
+
* @param {Array} [params.targetPluralForms] - Array of plural forms required for target language
|
|
161
|
+
* @returns {Promise<string>} JSON string of the generated resource
|
|
162
|
+
*/
|
|
163
|
+
async generateResource({ segments, translator, targetPluralForms }) {
|
|
164
|
+
const targetFormsSet = targetPluralForms ? new Set(targetPluralForms) : null;
|
|
165
|
+
|
|
166
|
+
// Collect translations
|
|
167
|
+
const translations = new Map(); // sid -> translatedStr
|
|
168
|
+
const pluralGroups = new Map(); // baseKey -> Set of sids
|
|
169
|
+
|
|
170
|
+
for (const seg of segments) {
|
|
171
|
+
// Skip plural forms not needed for target language
|
|
172
|
+
if (seg.pluralForm && targetFormsSet && !targetFormsSet.has(seg.pluralForm)) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const translatedStr = await translator(seg);
|
|
176
|
+
if (translatedStr != null) {
|
|
177
|
+
translations.set(seg.sid, translatedStr.str);
|
|
178
|
+
|
|
179
|
+
// Track plural groups
|
|
180
|
+
if (seg.pluralForm) {
|
|
181
|
+
const underscoreIdx = seg.sid.lastIndexOf('_');
|
|
182
|
+
if (underscoreIdx !== -1) {
|
|
183
|
+
const baseKey = seg.sid.slice(0, underscoreIdx);
|
|
184
|
+
if (!pluralGroups.has(baseKey)) {
|
|
185
|
+
pluralGroups.set(baseKey, new Set());
|
|
186
|
+
}
|
|
187
|
+
pluralGroups.get(baseKey).add(seg.sid);
|
|
188
|
+
}
|
|
125
189
|
}
|
|
126
190
|
}
|
|
127
191
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
192
|
+
|
|
193
|
+
// Build flatResource with plural forms grouped and ordered
|
|
194
|
+
const flatResource = {};
|
|
195
|
+
const processedPluralKeys = new Set();
|
|
196
|
+
|
|
197
|
+
for (const seg of segments) {
|
|
198
|
+
if (!translations.has(seg.sid)) continue;
|
|
199
|
+
|
|
200
|
+
if (seg.pluralForm) {
|
|
201
|
+
const underscoreIdx = seg.sid.lastIndexOf('_');
|
|
202
|
+
const baseKey = underscoreIdx !== -1 ? seg.sid.slice(0, underscoreIdx) : null;
|
|
203
|
+
|
|
204
|
+
if (baseKey && pluralGroups.has(baseKey) && !processedPluralKeys.has(baseKey)) {
|
|
205
|
+
// Output all forms of this plural group in CLDR order
|
|
206
|
+
processedPluralKeys.add(baseKey);
|
|
207
|
+
for (const form of targetPluralForms) {
|
|
208
|
+
const sid = `${baseKey}_${form}`;
|
|
209
|
+
if (translations.has(sid)) {
|
|
210
|
+
flatResource[sid] = translations.get(sid);
|
|
211
|
+
}
|
|
145
212
|
}
|
|
146
|
-
} else {
|
|
147
|
-
// No regex match, can't determine corresponding translation, so delete
|
|
148
|
-
delete flatResource[key];
|
|
149
213
|
}
|
|
214
|
+
// Skip individual plural forms - they were added via the group
|
|
215
|
+
} else {
|
|
216
|
+
flatResource[seg.sid] = translations.get(seg.sid);
|
|
150
217
|
}
|
|
151
218
|
}
|
|
219
|
+
|
|
152
220
|
return `${JSON.stringify(unflatten(flatResource, { object: !this.enableArrays }), null, 2)}\n`;
|
|
153
221
|
}
|
|
154
222
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@l10nmonster/helpers-json",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Helpers to deal with JSON file formats",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"flat": "^6"
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
|
-
"@l10nmonster/core": "
|
|
16
|
+
"@l10nmonster/core": "3.1.0"
|
|
17
17
|
},
|
|
18
18
|
"engines": {
|
|
19
19
|
"node": ">=22.11.0"
|
package/utils.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// Intentionally uses `== null` to handle undefined and null
|
|
2
|
+
/* eslint-disable no-eq-null, eqeqeq */
|
|
3
|
+
|
|
4
|
+
import { flatten } from 'flat';
|
|
5
|
+
|
|
1
6
|
export const ARB_ANNOTATION_MARKER = "@";
|
|
2
7
|
export const FLATTEN_SEPARATOR = ".";
|
|
3
8
|
|
|
@@ -105,3 +110,63 @@ export function arbPlaceholderHandler(placeholders) {
|
|
|
105
110
|
}
|
|
106
111
|
return phs.join("\n")
|
|
107
112
|
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @function parseResourceAnnotations
|
|
116
|
+
*
|
|
117
|
+
* @description
|
|
118
|
+
* Parse resource annotations according to the given configuration.
|
|
119
|
+
*
|
|
120
|
+
* @param {object} resource - The resource to parse.
|
|
121
|
+
* @param {boolean} enableArbAnnotations - Whether to enable annotations
|
|
122
|
+
* @param {object} arbAnnotationHandlers - An object mapping annotation names to a function which takes an annotation name and its value and returns a string.
|
|
123
|
+
*
|
|
124
|
+
* @returns {array} An array with two elements. The first element is an array of key-value pairs for the translatable segments. The second element is an object with the parsed annotations.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* const resource = {
|
|
128
|
+
* "key": "value",
|
|
129
|
+
* "@key": {
|
|
130
|
+
* "description": "description for key",
|
|
131
|
+
* "placeholders": {
|
|
132
|
+
* "placeholder": {
|
|
133
|
+
* "example": "example for placeholder",
|
|
134
|
+
* "description": "description for placeholder",
|
|
135
|
+
* }
|
|
136
|
+
* }
|
|
137
|
+
* }
|
|
138
|
+
* };
|
|
139
|
+
* const [segments, notes] = parseResourceAnnotations(resource, true, {
|
|
140
|
+
* description: (_, data) => (data == null ? undefined : data),
|
|
141
|
+
* placeholders: (_, data) => (data == null ? undefined : arbPlaceholderHandler(data)),
|
|
142
|
+
* DEFAULT: (name, data) => (data == null ? undefined : `${name}: ${data}`),
|
|
143
|
+
* });
|
|
144
|
+
* // segments is [["key", "value"]]
|
|
145
|
+
* // notes is { "key": "description for key\nplaceholder: example for placeholder - description for placeholder" }
|
|
146
|
+
*/
|
|
147
|
+
export function parseResourceAnnotations(resource, enableArbAnnotations, arbAnnotationHandlers) {
|
|
148
|
+
if (!enableArbAnnotations) {
|
|
149
|
+
return [ Object.entries(flatten(resource)), {} ]
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const { res, notes } = flattenAndSplitResources([], resource)
|
|
153
|
+
const parsedNotes = {}
|
|
154
|
+
for (const [key, arbAnnotations] of Object.entries(notes)) {
|
|
155
|
+
if (typeof arbAnnotations === "object") {
|
|
156
|
+
const notesList = []
|
|
157
|
+
for (const [annotation, data] of Object.entries(arbAnnotations)) {
|
|
158
|
+
const handler = arbAnnotationHandlers[annotation] ?? arbAnnotationHandlers.DEFAULT
|
|
159
|
+
if (handler != null) {
|
|
160
|
+
const val = handler(annotation, data)
|
|
161
|
+
if (val !== undefined) {
|
|
162
|
+
notesList.push(val)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
parsedNotes[key] = notesList.join("\n")
|
|
167
|
+
} else {
|
|
168
|
+
parsedNotes[key] = arbAnnotations
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return [ Object.entries(res), parsedNotes ];
|
|
172
|
+
}
|