@l10nmonster/helpers-android 1.0.1 → 3.0.0-alpha.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/README.md +235 -11
- package/filter.js +30 -7
- package/index.js +6 -7
- package/package.json +17 -16
package/README.md
CHANGED
|
@@ -1,18 +1,242 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @l10nmonster/helpers-android
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
L10n Monster helper for Android XML resource files, providing filters, decoders, and encoders for Android app localization.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @l10nmonster/helpers-android
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Components
|
|
12
|
+
|
|
13
|
+
|Component|Export|Description|
|
|
4
14
|
|---|---|---|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
15
|
+
|**Filter**|`Filter`|Resource filter for Android XML files|
|
|
16
|
+
|**Decoders**|||
|
|
17
|
+
||`escapesDecoder`|Decoder for escaped chars like `\n` and `\u00a0`|
|
|
18
|
+
||`spaceCollapser`|Decoder to convert multiple whitespace into single space|
|
|
19
|
+
||`phDecoder`|Decoder for `%d` style placeholders|
|
|
20
|
+
|**Encoders**|||
|
|
21
|
+
||`escapesEncoder`|Encoder for escaped chars as required by Android|
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
10
24
|
|
|
25
|
+
### Android XML Filter
|
|
11
26
|
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
|
|
27
|
+
```javascript
|
|
28
|
+
import { Filter } from '@l10nmonster/helpers-android';
|
|
29
|
+
|
|
30
|
+
const androidFilter = new Filter({
|
|
31
|
+
comment: 'pre' // Options: 'pre', 'post', 'right'
|
|
15
32
|
});
|
|
16
33
|
```
|
|
17
34
|
|
|
18
|
-
|
|
35
|
+
The filter processes Android XML resource files (`strings.xml`, `plurals.xml`, etc.) and handles:
|
|
36
|
+
- String resources with proper escaping
|
|
37
|
+
- Plural forms (quantity="one", "other", etc.)
|
|
38
|
+
- Comments and developer notes positioning
|
|
39
|
+
- CDATA sections for complex text
|
|
40
|
+
- Resource attributes (`translatable`, `formatted`, etc.)
|
|
41
|
+
|
|
42
|
+
### Comment Positioning
|
|
43
|
+
|
|
44
|
+
- **`pre`**: Places developer comments before the string element
|
|
45
|
+
- **`post`**: Places comments after the string element
|
|
46
|
+
- **`right`**: Places comments on the same line as the string
|
|
47
|
+
|
|
48
|
+
### Decoders and Encoders
|
|
49
|
+
|
|
50
|
+
```javascript
|
|
51
|
+
import {
|
|
52
|
+
escapesDecoder,
|
|
53
|
+
spaceCollapser,
|
|
54
|
+
phDecoder,
|
|
55
|
+
escapesEncoder
|
|
56
|
+
} from '@l10nmonster/helpers-android';
|
|
57
|
+
|
|
58
|
+
// Use in content type configuration
|
|
59
|
+
const contentType = {
|
|
60
|
+
name: 'android-strings',
|
|
61
|
+
resourceFilter: androidFilter,
|
|
62
|
+
decoders: [escapesDecoder, spaceCollapser, phDecoder],
|
|
63
|
+
textEncoders: [escapesEncoder]
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Supported Android Features
|
|
68
|
+
|
|
69
|
+
### String Resources
|
|
70
|
+
- Basic string elements: `<string name="key">value</string>`
|
|
71
|
+
- Formatted strings with placeholders: `%s`, `%d`, `%1$s`
|
|
72
|
+
- Non-translatable strings: `translatable="false"`
|
|
73
|
+
- Formatted attribute: `formatted="false"`
|
|
74
|
+
|
|
75
|
+
### Plurals
|
|
76
|
+
- Quantity-based plurals: `zero`, `one`, `two`, `few`, `many`, `other`
|
|
77
|
+
- Proper plural form handling per Android guidelines
|
|
78
|
+
|
|
79
|
+
### Text Formatting
|
|
80
|
+
- Escape sequences: `\n`, `\t`, `\u0020`, etc.
|
|
81
|
+
- HTML tags in strings (preserved as placeholders)
|
|
82
|
+
- CDATA sections for complex markup
|
|
83
|
+
- Apostrophe and quote escaping
|
|
84
|
+
|
|
85
|
+
### XML Features
|
|
86
|
+
- XML entities (`<`, `>`, `&`, etc.)
|
|
87
|
+
- Comments and developer notes
|
|
88
|
+
- Resource arrays (basic support)
|
|
89
|
+
|
|
90
|
+
## Configuration Examples
|
|
91
|
+
|
|
92
|
+
### Basic Android Project
|
|
93
|
+
|
|
94
|
+
```javascript
|
|
95
|
+
// l10nmonster.config.mjs
|
|
96
|
+
import { FsSource, FsTarget } from '@l10nmonster/core';
|
|
97
|
+
import { Filter } from '@l10nmonster/helpers-android';
|
|
98
|
+
|
|
99
|
+
export default {
|
|
100
|
+
channels: [{
|
|
101
|
+
source: new FsSource({
|
|
102
|
+
globs: ['app/src/main/res/values/strings.xml']
|
|
103
|
+
}),
|
|
104
|
+
target: new FsTarget({
|
|
105
|
+
targetPath: (lang, resourceId) =>
|
|
106
|
+
resourceId.replace('/values/', `/values-${lang}/`)
|
|
107
|
+
})
|
|
108
|
+
}],
|
|
109
|
+
|
|
110
|
+
contentTypes: [{
|
|
111
|
+
name: 'android-xml',
|
|
112
|
+
resourceFilter: new Filter({ comment: 'pre' })
|
|
113
|
+
}]
|
|
114
|
+
};
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Multi-Module Android Project
|
|
118
|
+
|
|
119
|
+
```javascript
|
|
120
|
+
export default {
|
|
121
|
+
channels: [{
|
|
122
|
+
source: new FsSource({
|
|
123
|
+
globs: [
|
|
124
|
+
'app/src/main/res/values/strings.xml',
|
|
125
|
+
'feature-*/src/main/res/values/strings.xml',
|
|
126
|
+
'library-*/src/main/res/values/strings.xml'
|
|
127
|
+
]
|
|
128
|
+
}),
|
|
129
|
+
target: new FsTarget({
|
|
130
|
+
targetPath: (lang, resourceId) => {
|
|
131
|
+
// Handle different module structures
|
|
132
|
+
if (resourceId.includes('/values/')) {
|
|
133
|
+
return resourceId.replace('/values/', `/values-${lang}/`);
|
|
134
|
+
}
|
|
135
|
+
return resourceId;
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
}]
|
|
139
|
+
};
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## File Structure Support
|
|
143
|
+
|
|
144
|
+
L10n Monster supports standard Android resource directory structure:
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
app/src/main/res/
|
|
148
|
+
├── values/ # Default (English) strings
|
|
149
|
+
│ ├── strings.xml
|
|
150
|
+
│ ├── plurals.xml
|
|
151
|
+
│ └── arrays.xml
|
|
152
|
+
├── values-es/ # Spanish translations
|
|
153
|
+
│ └── strings.xml
|
|
154
|
+
├── values-zh-rCN/ # Chinese (China) translations
|
|
155
|
+
│ └── strings.xml
|
|
156
|
+
└── values-b+es+419/ # Spanish (Latin America) - BCP 47
|
|
157
|
+
└── strings.xml
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Android-Specific Features
|
|
161
|
+
|
|
162
|
+
### Placeholder Handling
|
|
163
|
+
|
|
164
|
+
The helper properly handles Android string formatting:
|
|
165
|
+
|
|
166
|
+
```xml
|
|
167
|
+
<!-- Source -->
|
|
168
|
+
<string name="welcome">Hello %1$s, you have %2$d messages</string>
|
|
169
|
+
|
|
170
|
+
<!-- Maintains placeholder order and type -->
|
|
171
|
+
<string name="welcome">Hola %1$s, tienes %2$d mensajes</string>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Escape Sequence Processing
|
|
175
|
+
|
|
176
|
+
```xml
|
|
177
|
+
<!-- Input -->
|
|
178
|
+
<string name="multiline">First line\nSecond line\tTabbed</string>
|
|
179
|
+
|
|
180
|
+
<!-- Properly escaped output -->
|
|
181
|
+
<string name="multiline">Primera línea\nSegunda línea\tTabulada</string>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Plural Forms
|
|
185
|
+
|
|
186
|
+
```xml
|
|
187
|
+
<!-- Source -->
|
|
188
|
+
<plurals name="items">
|
|
189
|
+
<item quantity="one">%d item</item>
|
|
190
|
+
<item quantity="other">%d items</item>
|
|
191
|
+
</plurals>
|
|
192
|
+
|
|
193
|
+
<!-- Translated with proper quantity handling -->
|
|
194
|
+
<plurals name="items">
|
|
195
|
+
<item quantity="one">%d elemento</item>
|
|
196
|
+
<item quantity="other">%d elementos</item>
|
|
197
|
+
</plurals>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Testing
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
npm test
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The test suite covers:
|
|
207
|
+
- XML parsing and generation
|
|
208
|
+
- Escape sequence handling
|
|
209
|
+
- Placeholder preservation
|
|
210
|
+
- Plural form processing
|
|
211
|
+
- Comment positioning
|
|
212
|
+
- CDATA section handling
|
|
213
|
+
|
|
214
|
+
## Integration with L10n Monster
|
|
215
|
+
|
|
216
|
+
This helper integrates seamlessly with L10n Monster's provider system:
|
|
217
|
+
|
|
218
|
+
```javascript
|
|
219
|
+
import { providers } from '@l10nmonster/core';
|
|
220
|
+
import { GptAgent } from '@l10nmonster/helpers-openai';
|
|
221
|
+
|
|
222
|
+
export default {
|
|
223
|
+
providers: [{
|
|
224
|
+
id: 'ai-translator',
|
|
225
|
+
provider: new GptAgent({
|
|
226
|
+
model: 'gpt-4',
|
|
227
|
+
systemPrompt: 'Translate Android app strings, preserving all placeholders and formatting.'
|
|
228
|
+
})
|
|
229
|
+
}]
|
|
230
|
+
};
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Requirements
|
|
234
|
+
|
|
235
|
+
- Node.js >= 20.12.0
|
|
236
|
+
- @l10nmonster/core (peer dependency)
|
|
237
|
+
|
|
238
|
+
## Related Documentation
|
|
239
|
+
|
|
240
|
+
- [Android String Resources](https://developer.android.com/guide/topics/resources/string-resource)
|
|
241
|
+
- [Android Localization](https://developer.android.com/guide/topics/resources/localization)
|
|
242
|
+
- [L10n Monster Core Documentation](../core/README.md)
|
package/filter.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// and preserve the source but the downside is that we miss automatic CDATA handling, whitespace trimming, entity management.
|
|
4
4
|
// TODO: double quotes are meant to preserve newlines but we treat double quotes like CDATA (which doesn't)
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
|
|
7
|
+
import { default as formatXml } from 'xml-formatter';
|
|
8
|
+
import { logVerbose } from '@l10nmonster/core';
|
|
8
9
|
|
|
9
10
|
function collapseTextNodes(node) {
|
|
10
11
|
return node.map(e => e['#text']).join('').trim();
|
|
@@ -16,11 +17,26 @@ function isTranslatableNode(resNode, str) {
|
|
|
16
17
|
return resNode[':@'].translatable !== 'false' && !resNode[':@']['/'] && !resourceReferenceRegex.test(str);
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Class representing an AndroidFilter for parsing and translating Android resource files.
|
|
22
|
+
*/
|
|
23
|
+
export class AndroidXMLFilter {
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create an AndroidXMLFilter.
|
|
27
|
+
* @param {Object} [options] - Configuration options for the filter.
|
|
28
|
+
* @param {string} [options.indentation='\t'] - The indentation character(s) to use in the output XML.
|
|
29
|
+
*/
|
|
30
|
+
constructor({ indentation } = {}) {
|
|
21
31
|
this.indentation = indentation || '\t';
|
|
22
32
|
}
|
|
23
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Parse an Android resource file and extract translatable segments.
|
|
36
|
+
* @param {Object} params - Parameters for parsing the resource.
|
|
37
|
+
* @param {string} params.resource - The XML content of the Android resource file.
|
|
38
|
+
* @returns {Promise<Object>} An object containing the extracted segments.
|
|
39
|
+
*/
|
|
24
40
|
async parseResource({ resource }) {
|
|
25
41
|
const segments = [];
|
|
26
42
|
const parsingOptions = {
|
|
@@ -70,8 +86,8 @@ module.exports = class AndroidFilter {
|
|
|
70
86
|
}
|
|
71
87
|
lastComment = null;
|
|
72
88
|
} else {
|
|
73
|
-
|
|
74
|
-
|
|
89
|
+
logVerbose`Unexpected child node in resources`;
|
|
90
|
+
logVerbose`${JSON.stringify(resNode)}`;
|
|
75
91
|
}
|
|
76
92
|
}
|
|
77
93
|
}
|
|
@@ -81,6 +97,13 @@ module.exports = class AndroidFilter {
|
|
|
81
97
|
};
|
|
82
98
|
}
|
|
83
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Translate an Android resource file using the provided translator function.
|
|
102
|
+
* @param {Object} params - Parameters for translating the resource.
|
|
103
|
+
* @param {string} params.resource - The XML content of the Android resource file.
|
|
104
|
+
* @param {Function} params.translator - A function that translates a string given its ID and source text.
|
|
105
|
+
* @returns {Promise<string|null>} The translated XML content, or null if no translations were made.
|
|
106
|
+
*/
|
|
84
107
|
async translateResource({ resource, translator }) {
|
|
85
108
|
const parsingOptions = {
|
|
86
109
|
ignoreAttributes: false,
|
|
@@ -150,6 +173,6 @@ module.exports = class AndroidFilter {
|
|
|
150
173
|
const builder = new XMLBuilder(parsingOptions);
|
|
151
174
|
const roughXML = builder.build(parsedResource);
|
|
152
175
|
// eslint-disable-next-line prefer-template
|
|
153
|
-
return
|
|
176
|
+
return formatXml(roughXML, { collapseContent: true, indentation: this.indentation, lineSeparator: '\n' }) + '\n';
|
|
154
177
|
}
|
|
155
178
|
}
|
package/index.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.Filter = require('./filter');
|
|
1
|
+
import { regex } from '@l10nmonster/core';
|
|
2
|
+
export { AndroidXMLFilter } from './filter.js';
|
|
4
3
|
|
|
5
4
|
const androidControlCharsToDecode = {
|
|
6
5
|
n: '\n',
|
|
7
6
|
t: '\t',
|
|
8
7
|
};
|
|
9
|
-
|
|
8
|
+
export const escapesDecoder = regex.decoderMaker(
|
|
10
9
|
'androidEscapesDecoder',
|
|
11
10
|
/(?<node>\\(?<escapedChar>[@?\\'"])|\\(?<escapedControl>[nt])|\\u(?<codePoint>[0-9A-Za-z]{4}))/g,
|
|
12
11
|
(groups) => (groups.escapedChar ??
|
|
@@ -18,7 +17,7 @@ exports.escapesDecoder = regex.decoderMaker(
|
|
|
18
17
|
);
|
|
19
18
|
|
|
20
19
|
// Android lint doesn't accept % but accepts %%. % should be replaced with '\u0025' but %% shouldn't
|
|
21
|
-
|
|
20
|
+
export const escapesEncoder = (str, flags = {}) => {
|
|
22
21
|
let escapedStr = str.replaceAll(/[@\\'"]/g, '\\$&').replaceAll('\t', '\\t').replaceAll('\n', '\\n').replaceAll(/(?<!%)%(?!%)/g, '\\u0025');
|
|
23
22
|
// eslint-disable-next-line prefer-template
|
|
24
23
|
flags.isFirst && escapedStr[0] === ' ' && (escapedStr = '\\u0020' + escapedStr.substring(1));
|
|
@@ -27,10 +26,10 @@ exports.escapesEncoder = (str, flags = {}) => {
|
|
|
27
26
|
return escapedStr;
|
|
28
27
|
};
|
|
29
28
|
|
|
30
|
-
|
|
29
|
+
export const spaceCollapser = (parts) => parts.map(p => (p.t === 's' ? { ...p, v: p.v.replaceAll(/[ \f\n\r\t\v\u2028\u2029]+/g, ' ')} : p));
|
|
31
30
|
|
|
32
31
|
// C-style placeholders (based on the ios one)
|
|
33
|
-
|
|
32
|
+
export const phDecoder = regex.decoderMaker(
|
|
34
33
|
'iosPHDecoder',
|
|
35
34
|
/(?<tag>%(?:\d\$)?[0#+-]?[0-9*]*\.?\d*[hl]{0,2}[jztL]?[diuoxXeEfgGaAcpsSn])/g,
|
|
36
35
|
(groups) => ({ t: 'x', v: groups.tag })
|
package/package.json
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
|
|
2
|
+
"name": "@l10nmonster/helpers-android",
|
|
3
|
+
"version": "3.0.0-alpha.2",
|
|
4
|
+
"description": "Helpers to deal with Android file formats",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node --test"
|
|
9
|
+
},
|
|
10
|
+
"author": "Diego Lagunas",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"fast-xml-parser": "^5",
|
|
14
|
+
"xml-formatter": "^3"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"@l10nmonster/core": "^3.0.0-alpha.0"
|
|
18
|
+
}
|
|
18
19
|
}
|