@l10nmonster/helpers-java 3.0.0-alpha.9 → 3.0.2

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,3 +1,27 @@
1
+ ## @l10nmonster/helpers-java [3.0.2](https://public-github/l10nmonster/l10nmonster/compare/@l10nmonster/helpers-java@3.0.1...@l10nmonster/helpers-java@3.0.2) (2025-12-23)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Improve type definitions and checks ([826b412](https://public-github/l10nmonster/l10nmonster/commit/826b412f0f7e761d404165a243b0c2b26c416ac1))
7
+
8
+
9
+
10
+
11
+
12
+ ### Dependencies
13
+
14
+ * **@l10nmonster/core:** upgraded to 3.1.1
15
+
16
+ ## @l10nmonster/helpers-java [3.0.1](https://public-github/l10nmonster/l10nmonster/compare/@l10nmonster/helpers-java@3.0.0...@l10nmonster/helpers-java@3.0.1) (2025-12-20)
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * Add proper pluralization expansion support ([3d062bb](https://public-github/l10nmonster/l10nmonster/commit/3d062bbb3272c61e969b419a56a7a5e347ab96c6))
22
+ * Pluralization improvements ([5964250](https://public-github/l10nmonster/l10nmonster/commit/596425092c425cc8d6c312ef58509c4c3c537431))
23
+ * **server:** Fix cart cleanup ([9bbcab9](https://public-github/l10nmonster/l10nmonster/commit/9bbcab93e1fd20aeb09f59c828665159f091f37c))
24
+
1
25
  # Changelog
2
26
 
3
27
  All notable changes to this project will be documented in this file.
package/filter.js CHANGED
@@ -1,10 +1,27 @@
1
1
  import { parseToEntries, stringifyFromEntries } from '@js.properties/properties';
2
2
 
3
+ /** @typedef {import('@l10nmonster/core').ResourceFilter} ResourceFilter */
4
+
5
+ /**
6
+ * Filter for Java .properties files.
7
+ * @implements {ResourceFilter}
8
+ */
3
9
  export default class JavaPropertiesFilter {
4
- async parseResource({ resource }) {
10
+
11
+ /**
12
+ * @param {Object} [params] - Configuration options
13
+ * @param {boolean} [params.enablePluralizationSuffixes=false] - Enable detection of plural forms via key suffixes (_one, _other, etc.)
14
+ */
15
+ constructor(params) {
16
+ this.enablePluralizationSuffixes = params?.enablePluralizationSuffixes || false;
17
+ }
18
+
19
+ async parseResource({ resource, sourcePluralForms, targetPluralForms }) {
5
20
  const parsedResource = parseToEntries(resource, { sep: true, eol: true, all: true, original: true, location: true });
6
21
  const segments = [];
7
22
  let previousComments = [];
23
+
24
+ // First pass: collect all segments
8
25
  for (const e of parsedResource) {
9
26
  if (e.key && e.sep.trim() === '=') {
10
27
  const location = {startLine: e.location.start.line, endLine: e.location.end.line}
@@ -30,24 +47,192 @@ export default class JavaPropertiesFilter {
30
47
  e.original.trim().length > 0 && previousComments.push(e.original);
31
48
  }
32
49
  }
50
+
51
+ // Second pass: detect and mark plural forms (only if enabled)
52
+ if (this.enablePluralizationSuffixes) {
53
+ const targetFormsSet = new Set(targetPluralForms);
54
+ const potentialPluralGroups = new Map(); // baseKey -> Map(suffix -> segment)
55
+
56
+ for (const seg of segments) {
57
+ const underscoreIdx = seg.sid.lastIndexOf('_');
58
+ if (underscoreIdx !== -1) {
59
+ const suffix = seg.sid.slice(underscoreIdx + 1);
60
+ if (targetFormsSet.has(suffix)) {
61
+ const baseKey = seg.sid.slice(0, underscoreIdx);
62
+ if (!potentialPluralGroups.has(baseKey)) {
63
+ potentialPluralGroups.set(baseKey, new Map());
64
+ }
65
+ potentialPluralGroups.get(baseKey).set(suffix, seg);
66
+ }
67
+ }
68
+ }
69
+
70
+ // For groups with all required source forms: set pluralForm and generate missing forms
71
+ // We need to reorder segments so all forms of a plural group are together in CLDR order
72
+ const pluralGroupsToReorder = new Map(); // baseKey -> forms Map
73
+
74
+ for (const [baseKey, forms] of potentialPluralGroups) {
75
+ const hasAllForms = sourcePluralForms.every(form => forms.has(form));
76
+ if (!hasAllForms) continue;
77
+
78
+ // Set pluralForm on existing segments
79
+ for (const [suffix, seg] of forms) {
80
+ seg.pluralForm = suffix;
81
+ }
82
+
83
+ // Generate missing forms from _other
84
+ const other = forms.get('other');
85
+ for (const suffix of targetPluralForms) {
86
+ if (!forms.has(suffix)) {
87
+ forms.set(suffix, {
88
+ sid: `${baseKey}_${suffix}`,
89
+ str: other.str,
90
+ pluralForm: suffix,
91
+ ...(other.notes && { notes: other.notes }),
92
+ ...(other.location && { location: other.location })
93
+ });
94
+ }
95
+ }
96
+
97
+ pluralGroupsToReorder.set(baseKey, forms);
98
+ }
99
+
100
+ // Rebuild segments with plural groups in correct order
101
+ if (pluralGroupsToReorder.size > 0) {
102
+ const newSegments = [];
103
+ const processedPluralKeys = new Set();
104
+
105
+ for (const seg of segments) {
106
+ const underscoreIdx = seg.sid.lastIndexOf('_');
107
+ const suffix = underscoreIdx !== -1 ? seg.sid.slice(underscoreIdx + 1) : null;
108
+ const baseKey = underscoreIdx !== -1 ? seg.sid.slice(0, underscoreIdx) : null;
109
+
110
+ if (baseKey && pluralGroupsToReorder.has(baseKey) && !processedPluralKeys.has(baseKey)) {
111
+ // Insert all forms of this plural group in CLDR order
112
+ processedPluralKeys.add(baseKey);
113
+ const forms = pluralGroupsToReorder.get(baseKey);
114
+ for (const form of targetPluralForms) {
115
+ if (forms.has(form)) {
116
+ newSegments.push(forms.get(form));
117
+ }
118
+ }
119
+ } else if (!baseKey || !pluralGroupsToReorder.has(baseKey)) {
120
+ // Non-plural segment
121
+ newSegments.push(seg);
122
+ }
123
+ // Skip plural segments that were already added via the group
124
+ }
125
+
126
+ return { segments: newSegments };
127
+ }
128
+ }
129
+
33
130
  return {
34
131
  segments,
35
132
  };
36
133
  }
37
134
 
38
- async translateResource({ resource, translator }) {
135
+ async translateResource({ resource, translator, sourcePluralForms, targetPluralForms }) {
39
136
  const parsedResource = parseToEntries(resource, { sep: true, eol: true, all: true, original: true });
40
137
  const translatedEntries = [];
41
- for (const entry of parsedResource) {
42
- if (entry.key) {
43
- const translation = await translator(entry.key, entry.element);
44
- if (translation !== undefined) {
45
- // eslint-disable-next-line no-unused-vars
46
- const { original, element, ...rest } = entry;
47
- translatedEntries.push({
48
- ...rest,
49
- element: translation,
50
- })
138
+
139
+ if (this.enablePluralizationSuffixes) {
140
+ const targetFormsSet = new Set(targetPluralForms);
141
+
142
+ // First pass: identify plural groups and collect entries
143
+ const potentialPluralGroups = new Map(); // baseKey -> Map(suffix -> entry)
144
+
145
+ for (const entry of parsedResource) {
146
+ if (entry.key) {
147
+ const underscoreIdx = entry.key.lastIndexOf('_');
148
+ if (underscoreIdx !== -1) {
149
+ const suffix = entry.key.slice(underscoreIdx + 1);
150
+ if (targetFormsSet.has(suffix)) {
151
+ const baseKey = entry.key.slice(0, underscoreIdx);
152
+ if (!potentialPluralGroups.has(baseKey)) {
153
+ potentialPluralGroups.set(baseKey, new Map());
154
+ }
155
+ potentialPluralGroups.get(baseKey).set(suffix, entry);
156
+ }
157
+ }
158
+ }
159
+ }
160
+
161
+ // Identify valid plural groups (those with all required source forms)
162
+ const validPluralBaseKeys = new Set();
163
+ for (const [baseKey, forms] of potentialPluralGroups) {
164
+ const hasAllForms = sourcePluralForms.every(form => forms.has(form));
165
+ if (hasAllForms) {
166
+ validPluralBaseKeys.add(baseKey);
167
+ }
168
+ }
169
+
170
+ // Second pass: translate entries and handle plurals
171
+ const processedPluralGroups = new Set();
172
+
173
+ for (const entry of parsedResource) {
174
+ if (entry.key) {
175
+ const underscoreIdx = entry.key.lastIndexOf('_');
176
+ const suffix = underscoreIdx !== -1 ? entry.key.slice(underscoreIdx + 1) : null;
177
+ const baseKey = underscoreIdx !== -1 ? entry.key.slice(0, underscoreIdx) : null;
178
+
179
+ // Check if this is part of a valid plural group
180
+ if (baseKey && validPluralBaseKeys.has(baseKey) && targetFormsSet.has(suffix)) {
181
+ // Skip if we already processed this plural group
182
+ if (processedPluralGroups.has(baseKey)) {
183
+ continue;
184
+ }
185
+ processedPluralGroups.add(baseKey);
186
+
187
+ // Get the _other form as template for generating missing forms
188
+ const otherEntry = potentialPluralGroups.get(baseKey).get('other');
189
+ const templateEntry = otherEntry || entry;
190
+
191
+ // Generate all required target forms
192
+ for (const targetSuffix of targetPluralForms) {
193
+ const targetKey = `${baseKey}_${targetSuffix}`;
194
+ const sourceEntry = potentialPluralGroups.get(baseKey).get(targetSuffix) || otherEntry;
195
+
196
+ if (sourceEntry) {
197
+ const translation = await translator(targetKey, sourceEntry.element);
198
+ if (translation !== undefined) {
199
+ // eslint-disable-next-line no-unused-vars
200
+ const { original, element, key, ...rest } = templateEntry;
201
+ translatedEntries.push({
202
+ ...rest,
203
+ key: targetKey,
204
+ element: translation,
205
+ });
206
+ }
207
+ }
208
+ }
209
+ } else {
210
+ // Regular (non-plural) entry
211
+ const translation = await translator(entry.key, entry.element);
212
+ if (translation !== undefined) {
213
+ // eslint-disable-next-line no-unused-vars
214
+ const { original, element, ...rest } = entry;
215
+ translatedEntries.push({
216
+ ...rest,
217
+ element: translation,
218
+ });
219
+ }
220
+ }
221
+ }
222
+ }
223
+ } else {
224
+ // Simple translation without plural handling
225
+ for (const entry of parsedResource) {
226
+ if (entry.key) {
227
+ const translation = await translator(entry.key, entry.element);
228
+ if (translation !== undefined) {
229
+ // eslint-disable-next-line no-unused-vars
230
+ const { original, element, ...rest } = entry;
231
+ translatedEntries.push({
232
+ ...rest,
233
+ element: translation,
234
+ });
235
+ }
51
236
  }
52
237
  }
53
238
  }
package/index.js CHANGED
@@ -9,6 +9,8 @@ const javaControlCharsToDecode = {
9
9
  r: '\r',
10
10
  f: '\f',
11
11
  };
12
+
13
+ /** @type {import('@l10nmonster/core').DecoderFunction} */
12
14
  export const escapesDecoder = regex.decoderMaker(
13
15
  'javaEscapesDecoder',
14
16
  /(?<node>\\(?<escapedChar>['"\\])|\\(?<escapedControl>[tbnrf])|\\u(?<codePoint>[0-9A-Za-z]{4}))/g,
@@ -21,6 +23,7 @@ export const escapesDecoder = regex.decoderMaker(
21
23
  );
22
24
 
23
25
  // TODO: do we need to escape also those escapedChar that we decoded?
26
+ /** @type {import('@l10nmonster/core').TextEncoderFunction} */
24
27
  export const escapesEncoder = regex.encoderMaker(
25
28
  'javaEscapesEncoder',
26
29
  // eslint-disable-next-line prefer-named-capture-group
@@ -34,6 +37,7 @@ export const escapesEncoder = regex.encoderMaker(
34
37
  }
35
38
  );
36
39
 
40
+ /** @type {import('@l10nmonster/core').DecoderFunction} */
37
41
  export const MFQuotesDecoder = regex.decoderMaker(
38
42
  'javaMFQuotesDecoder',
39
43
  /(?:(?<quote>')'|(?:'(?<quoted>[^']+)'))/g,
@@ -41,6 +45,7 @@ export const MFQuotesDecoder = regex.decoderMaker(
41
45
  );
42
46
 
43
47
  // need to be smart about detecting whether MessageFormat was used or not based on presence of {vars}
48
+ /** @type {import('@l10nmonster/core').TextEncoderFunction} */
44
49
  export const MFQuotesEncoder = regex.encoderMaker(
45
50
  'javaMFQuotesEncoder',
46
51
  // eslint-disable-next-line prefer-named-capture-group
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@l10nmonster/helpers-java",
3
- "version": "3.0.0-alpha.9",
3
+ "version": "3.0.2",
4
4
  "description": "Helpers to deal with Java file formats",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "scripts": {
8
- "test": "node --test"
8
+ "test": "node --test",
9
+ "typecheck": "tsc --noEmit"
9
10
  },
10
11
  "author": "Diego Lagunas",
11
12
  "license": "MIT",
@@ -13,6 +14,6 @@
13
14
  "@js.properties/properties": "^0.5.4"
14
15
  },
15
16
  "peerDependencies": {
16
- "@l10nmonster/core": "^3.0.0-alpha.0"
17
+ "@l10nmonster/core": "3.1.1"
17
18
  }
18
19
  }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "extends": "../tsconfig.base.json",
3
+ "include": [
4
+ "*.js",
5
+ "**/*.js"
6
+ ],
7
+ "exclude": [
8
+ "node_modules",
9
+ "**/node_modules",
10
+ "test/**",
11
+ "tests/**",
12
+ "**/*.test.js",
13
+ "**/*.spec.js",
14
+ "dist/**",
15
+ "ui/**",
16
+ "types/**"
17
+ ]
18
+ }
package/.releaserc.json DELETED
@@ -1,31 +0,0 @@
1
- {
2
- "branches": [
3
- "main",
4
- {
5
- "name": "next",
6
- "prerelease": "alpha"
7
- },
8
- {
9
- "name": "beta",
10
- "prerelease": "beta"
11
- }
12
- ],
13
- "tagFormat": "@l10nmonster/helpers-java@${version}",
14
- "plugins": [
15
- "@semantic-release/commit-analyzer",
16
- "@semantic-release/release-notes-generator",
17
- {
18
- "path": "@semantic-release/changelog",
19
- "changelogFile": "CHANGELOG.md"
20
- },
21
- {
22
- "path": "@semantic-release/npm",
23
- "npmPublish": true
24
- },
25
- {
26
- "path": "@semantic-release/git",
27
- "assets": ["CHANGELOG.md", "package.json"],
28
- "message": "chore(release): @l10nmonster/helpers-java@${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
29
- }
30
- ]
31
- }