@l10nmonster/helpers-android 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 CHANGED
@@ -20,7 +20,7 @@
20
20
  },
21
21
  {
22
22
  "path": "@semantic-release/npm",
23
- "npmPublish": true
23
+ "npmPublish": false
24
24
  },
25
25
  {
26
26
  "path": "@semantic-release/git",
package/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## @l10nmonster/helpers-android [3.0.1](https://public-github/l10nmonster/l10nmonster/compare/@l10nmonster/helpers-android@3.0.0...@l10nmonster/helpers-android@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/filter.js CHANGED
@@ -35,9 +35,10 @@ export class AndroidXMLFilter {
35
35
  * Parse an Android resource file and extract translatable segments.
36
36
  * @param {Object} params - Parameters for parsing the resource.
37
37
  * @param {string} params.resource - The XML content of the Android resource file.
38
+ * @param {string[]} [params.targetPluralForms] - Array of plural forms required for target languages.
38
39
  * @returns {Promise<Object>} An object containing the extracted segments.
39
40
  */
40
- async parseResource({ resource }) {
41
+ async parseResource({ resource, targetPluralForms }) {
41
42
  const segments = [];
42
43
  const parsingOptions = {
43
44
  ignoreAttributes: false,
@@ -52,6 +53,7 @@ export class AndroidXMLFilter {
52
53
  parseTagValue: false,
53
54
  trimValues: true,
54
55
  };
56
+
55
57
  const parser = new XMLParser(parsingOptions);
56
58
  for (const rootNode of parser.parse(resource)) {
57
59
  if ('resources' in rootNode) {
@@ -71,17 +73,42 @@ export class AndroidXMLFilter {
71
73
  lastComment = null;
72
74
  }
73
75
  } else if ('plurals' in resNode) { // TODO: support string-array
76
+ const pluralName = resNode[':@'].name;
77
+ const pluralForms = new Map(); // quantity -> { seg, notes }
78
+ let pluralComment = lastComment;
79
+
80
+ // Collect existing plural forms
74
81
  for (const itemNode of resNode.plurals) {
75
82
  if ('#comment' in itemNode) {
76
- lastComment = itemNode['#comment'].map(e => e['#text']).join('').trim();
83
+ pluralComment = itemNode['#comment'].map(e => e['#text']).join('').trim();
77
84
  } else if ('item' in itemNode) {
85
+ const quantity = itemNode[':@'].quantity;
78
86
  const seg = {
79
- sid: `${resNode[':@'].name}_${itemNode[':@'].quantity}`,
80
- isSuffixPluralized: true,
87
+ sid: `${pluralName}_${quantity}`,
88
+ pluralForm: quantity,
81
89
  str: collapseTextNodes(itemNode.item)
82
90
  };
83
- lastComment && (seg.notes = lastComment);
84
- segments.push(seg);
91
+ pluralComment && (seg.notes = pluralComment);
92
+ pluralForms.set(quantity, seg);
93
+ }
94
+ }
95
+
96
+ // Android <plurals> element explicitly defines plural rules
97
+ // Expansion can happen as long as 'other' form is present
98
+ const otherForm = pluralForms.get('other');
99
+
100
+ // Add forms in natural plural order (existing or generated from 'other')
101
+ for (const form of targetPluralForms) {
102
+ if (pluralForms.has(form)) {
103
+ segments.push(pluralForms.get(form));
104
+ } else if (otherForm) {
105
+ // Generate missing form from 'other'
106
+ segments.push({
107
+ sid: `${pluralName}_${form}`,
108
+ pluralForm: form,
109
+ str: otherForm.str,
110
+ ...(otherForm.notes && { notes: otherForm.notes })
111
+ });
85
112
  }
86
113
  }
87
114
  lastComment = null;
@@ -102,9 +129,10 @@ export class AndroidXMLFilter {
102
129
  * @param {Object} params - Parameters for translating the resource.
103
130
  * @param {string} params.resource - The XML content of the Android resource file.
104
131
  * @param {Function} params.translator - A function that translates a string given its ID and source text.
132
+ * @param {string[]} [params.targetPluralForms] - Array of plural forms required for the target language.
105
133
  * @returns {Promise<string|null>} The translated XML content, or null if no translations were made.
106
134
  */
107
- async translateResource({ resource, translator }) {
135
+ async translateResource({ resource, translator, targetPluralForms }) {
108
136
  const parsingOptions = {
109
137
  ignoreAttributes: false,
110
138
  processEntities: true,
@@ -118,6 +146,7 @@ export class AndroidXMLFilter {
118
146
  parseTagValue: false,
119
147
  trimValues: true,
120
148
  };
149
+
121
150
  const parser = new XMLParser(parsingOptions);
122
151
  const parsedResource = parser.parse(resource);
123
152
  const nodesToDelete = [];
@@ -138,26 +167,51 @@ export class AndroidXMLFilter {
138
167
  } else {
139
168
  nodesToDelete.push(resNode);
140
169
  }
141
- } else if ('plurals' in resNode) { // TODO: deal with plurals of the target language, not the source
142
- let dropPlural = false;
143
- const itemNodesToDelete = []
170
+ } else if ('plurals' in resNode) {
171
+ const pluralName = resNode[':@'].name;
172
+
173
+ // Collect source plural forms
174
+ const sourceForms = new Map(); // quantity -> { text, itemNode }
144
175
  for (const itemNode of resNode.plurals) {
145
- if ('#comment' in itemNode) {
146
- itemNodesToDelete.push(itemNode)
147
- // eslint-disable-next-line no-continue
148
- continue;
176
+ if ('item' in itemNode) {
177
+ sourceForms.set(itemNode[':@'].quantity, {
178
+ text: collapseTextNodes(itemNode.item),
179
+ itemNode
180
+ });
149
181
  }
150
- const translation = await translator(`${resNode[':@'].name}_${itemNode[':@'].quantity}`, collapseTextNodes(itemNode.item));
182
+ }
183
+
184
+ // Get 'other' form for generating missing target forms
185
+ // Android <plurals> is explicitly a plural, so we can expand as long as 'other' exists
186
+ const otherForm = sourceForms.get('other');
187
+
188
+ // Build new plurals node with only required target forms in CLDR order
189
+ const newPluralItems = [];
190
+ let dropPlural = false;
191
+
192
+ for (const form of targetPluralForms) {
193
+ const sourceForm = sourceForms.get(form) ?? otherForm;
194
+ if (!sourceForm) {
195
+ // Can't generate this required form - no source and no fallback
196
+ dropPlural = true;
197
+ break;
198
+ }
199
+ const translation = await translator(`${pluralName}_${form}`, sourceForm.text);
151
200
  if (translation === undefined) {
152
201
  dropPlural = true;
153
- } else {
154
- itemNode.item = [ { '#text': translation } ];
202
+ break;
155
203
  }
204
+ newPluralItems.push({
205
+ item: [{ '#text': translation }],
206
+ ':@': { quantity: form }
207
+ });
156
208
  }
157
- resNode.plurals = resNode.plurals.filter(n => !itemNodesToDelete.includes(n))
158
- if (dropPlural) {
209
+
210
+ if (dropPlural || newPluralItems.length === 0) {
159
211
  nodesToDelete.push(resNode);
160
212
  } else {
213
+ // Replace plurals with new items containing only target forms
214
+ resNode.plurals = newPluralItems;
161
215
  translated++;
162
216
  }
163
217
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@l10nmonster/helpers-android",
3
- "version": "3.0.0-alpha.8",
3
+ "version": "3.0.1",
4
4
  "description": "Helpers to deal with Android file formats",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -14,6 +14,6 @@
14
14
  "xml-formatter": "^3"
15
15
  },
16
16
  "peerDependencies": {
17
- "@l10nmonster/core": "^3.0.0-alpha.0"
17
+ "@l10nmonster/core": "3.1.0"
18
18
  }
19
19
  }