@l10nmonster/helpers-ios 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 +23 -0
- package/index.js +12 -1
- package/package.json +6 -4
- package/stringsdict.js +151 -0
- package/tsconfig.json +18 -0
- package/.releaserc.json +0 -31
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
## @l10nmonster/helpers-ios [3.0.2](https://public-github/l10nmonster/l10nmonster/compare/@l10nmonster/helpers-ios@3.0.1...@l10nmonster/helpers-ios@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-ios [3.0.1](https://public-github/l10nmonster/l10nmonster/compare/@l10nmonster/helpers-ios@3.0.0...@l10nmonster/helpers-ios@3.0.1) (2025-12-20)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* Pluralization improvements ([5964250](https://public-github/l10nmonster/l10nmonster/commit/596425092c425cc8d6c312ef58509c4c3c537431))
|
|
22
|
+
* **server:** Fix cart cleanup ([9bbcab9](https://public-github/l10nmonster/l10nmonster/commit/9bbcab93e1fd20aeb09f59c828665159f091f37c))
|
|
23
|
+
|
|
1
24
|
# Changelog
|
|
2
25
|
|
|
3
26
|
All notable changes to this project will be documented in this file.
|
package/index.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import i18nStringsFiles from '@l10nmonster/i18n-strings-files';
|
|
2
2
|
import { regex } from '@l10nmonster/core';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/** @typedef {import('@l10nmonster/core').ResourceFilter} ResourceFilter */
|
|
5
|
+
|
|
6
|
+
export { StringsdictFilter } from './stringsdict.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Filter for iOS .strings files (in utf-8).
|
|
10
|
+
* @implements {ResourceFilter}
|
|
11
|
+
*/
|
|
5
12
|
export class StringsFilter {
|
|
6
13
|
constructor(params) {
|
|
7
14
|
this.emitComments = params?.emitComments || false;
|
|
@@ -43,6 +50,8 @@ const iosControlCharsToDecode = {
|
|
|
43
50
|
r: '\r',
|
|
44
51
|
f: '\f',
|
|
45
52
|
};
|
|
53
|
+
|
|
54
|
+
/** @type {import('@l10nmonster/core').DecoderFunction} */
|
|
46
55
|
export const escapesDecoder = regex.decoderMaker(
|
|
47
56
|
'iosEscapesDecoder',
|
|
48
57
|
/(?<node>\\(?<escapedChar>['"\\])|\\(?<escapedControl>[tbnrf])|\\U(?<codePoint>[0-9A-Za-z]{4}))/g, // note that in ios the \U is uppercase!
|
|
@@ -54,6 +63,7 @@ export const escapesDecoder = regex.decoderMaker(
|
|
|
54
63
|
)
|
|
55
64
|
);
|
|
56
65
|
|
|
66
|
+
/** @type {import('@l10nmonster/core').TextEncoderFunction} */
|
|
57
67
|
export const escapesEncoder = regex.encoderMaker(
|
|
58
68
|
'iosEscapesEncoder',
|
|
59
69
|
// eslint-disable-next-line prefer-named-capture-group
|
|
@@ -71,6 +81,7 @@ export const escapesEncoder = regex.encoderMaker(
|
|
|
71
81
|
// and https://pubs.opengroup.org/onlinepubs/009695399/functions/printf.html
|
|
72
82
|
// loosely based on https://stackoverflow.com/questions/45215648/regex-capture-type-specifiers-in-format-string
|
|
73
83
|
// space and quote tags have been omitted to avoid matching unexpected combinations
|
|
84
|
+
/** @type {import('@l10nmonster/core').DecoderFunction} */
|
|
74
85
|
export const phDecoder = regex.decoderMaker(
|
|
75
86
|
'iosPHDecoder',
|
|
76
87
|
/(?<tag>%(?:\d\$)?[0#+-]?[0-9*]*\.?\d*[hl]{0,2}[jztL]?[diuoxXeEfgGaAcpsSn@])/g,
|
package/package.json
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@l10nmonster/helpers-ios",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "Helpers to deal with iOS 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",
|
|
12
13
|
"dependencies": {
|
|
13
|
-
"@l10nmonster/i18n-strings-files": "^3.0.0"
|
|
14
|
+
"@l10nmonster/i18n-strings-files": "^3.0.0",
|
|
15
|
+
"plist": "^3.1.0"
|
|
14
16
|
},
|
|
15
17
|
"peerDependencies": {
|
|
16
|
-
"@l10nmonster/core": "
|
|
18
|
+
"@l10nmonster/core": "3.1.1"
|
|
17
19
|
}
|
|
18
20
|
}
|
package/stringsdict.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import plist from 'plist';
|
|
2
|
+
|
|
3
|
+
/** @typedef {import('@l10nmonster/core').ResourceFilter} ResourceFilter */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Filter for iOS .stringsdict files (plist format for pluralization).
|
|
7
|
+
* @see https://developer.apple.com/documentation/xcode/localizing-strings-that-contain-plurals
|
|
8
|
+
* @implements {ResourceFilter}
|
|
9
|
+
*/
|
|
10
|
+
export class StringsdictFilter {
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a StringsdictFilter.
|
|
14
|
+
* @param {Object} [params] - Configuration options for the filter.
|
|
15
|
+
*/
|
|
16
|
+
constructor(params) {
|
|
17
|
+
this.emitComments = params?.emitComments || false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parse a stringsdict file and extract translatable segments.
|
|
22
|
+
* @param {Object} params - Parameters for parsing the resource.
|
|
23
|
+
* @param {string} params.resource - The XML/plist content of the stringsdict file.
|
|
24
|
+
* @param {string[]} [params.sourcePluralForms] - Array of plural forms required in source language.
|
|
25
|
+
* @param {string[]} [params.targetPluralForms] - Array of plural forms required for target languages.
|
|
26
|
+
* @returns {Promise<Object>} An object containing the extracted segments.
|
|
27
|
+
*/
|
|
28
|
+
async parseResource({ resource, sourcePluralForms, targetPluralForms }) {
|
|
29
|
+
const segments = [];
|
|
30
|
+
const parsedPlist = plist.parse(resource);
|
|
31
|
+
|
|
32
|
+
for (const [key, value] of Object.entries(parsedPlist)) {
|
|
33
|
+
if (typeof value !== 'object' || !value.NSStringLocalizedFormatKey) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const formatKey = value.NSStringLocalizedFormatKey;
|
|
38
|
+
// Extract variable names from format key (e.g., "%#@items@" -> "items")
|
|
39
|
+
const variableMatches = formatKey.matchAll(/%#@(\w+)@/g);
|
|
40
|
+
|
|
41
|
+
for (const match of variableMatches) {
|
|
42
|
+
const varName = match[1];
|
|
43
|
+
const varConfig = value[varName];
|
|
44
|
+
|
|
45
|
+
if (!varConfig ||
|
|
46
|
+
varConfig.NSStringFormatSpecTypeKey !== 'NSStringPluralRuleType') {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// stringsdict NSStringPluralRuleType explicitly defines plural rules
|
|
51
|
+
// Expansion can happen as long as 'other' form is present
|
|
52
|
+
const otherStr = varConfig.other;
|
|
53
|
+
|
|
54
|
+
// Add forms in natural plural order (existing or generated from 'other')
|
|
55
|
+
for (const form of targetPluralForms) {
|
|
56
|
+
const str = varConfig[form] ?? otherStr;
|
|
57
|
+
if (str !== undefined) {
|
|
58
|
+
segments.push({
|
|
59
|
+
sid: `${key}/${varName}_${form}`,
|
|
60
|
+
pluralForm: form,
|
|
61
|
+
str,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { segments };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Translate a stringsdict file using the provided translator function.
|
|
73
|
+
* @param {Object} params - Parameters for translating the resource.
|
|
74
|
+
* @param {string} params.resource - The XML/plist content of the stringsdict file.
|
|
75
|
+
* @param {Function} params.translator - A function that translates a string given its ID and source text.
|
|
76
|
+
* @param {string[]} [params.sourcePluralForms] - Array of plural forms in the source language.
|
|
77
|
+
* @param {string[]} [params.targetPluralForms] - Array of plural forms required for the target language.
|
|
78
|
+
* @returns {Promise<string|null>} The translated stringsdict content, or null if no translations were made.
|
|
79
|
+
*/
|
|
80
|
+
async translateResource({ resource, translator, sourcePluralForms, targetPluralForms }) {
|
|
81
|
+
const parsedPlist = plist.parse(resource);
|
|
82
|
+
|
|
83
|
+
const translatedPlist = {};
|
|
84
|
+
let translated = 0;
|
|
85
|
+
|
|
86
|
+
for (const [key, value] of Object.entries(parsedPlist)) {
|
|
87
|
+
if (typeof value !== 'object' || !value.NSStringLocalizedFormatKey) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const formatKey = value.NSStringLocalizedFormatKey;
|
|
92
|
+
const variableMatches = [...formatKey.matchAll(/%#@(\w+)@/g)];
|
|
93
|
+
|
|
94
|
+
const translatedEntry = {
|
|
95
|
+
NSStringLocalizedFormatKey: formatKey,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
let entryHasTranslations = true;
|
|
99
|
+
|
|
100
|
+
for (const match of variableMatches) {
|
|
101
|
+
const varName = match[1];
|
|
102
|
+
const varConfig = value[varName];
|
|
103
|
+
|
|
104
|
+
if (!varConfig ||
|
|
105
|
+
varConfig.NSStringFormatSpecTypeKey !== 'NSStringPluralRuleType') {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const translatedVarConfig = {
|
|
110
|
+
NSStringFormatSpecTypeKey: varConfig.NSStringFormatSpecTypeKey,
|
|
111
|
+
NSStringFormatValueTypeKey: varConfig.NSStringFormatValueTypeKey || 'd',
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// stringsdict NSStringPluralRuleType explicitly defines plural rules
|
|
115
|
+
// Expansion can happen as long as 'other' form is present
|
|
116
|
+
const otherStr = varConfig.other;
|
|
117
|
+
|
|
118
|
+
// Translate each required target form in CLDR order
|
|
119
|
+
for (const form of targetPluralForms) {
|
|
120
|
+
const sourceText = varConfig[form] ?? otherStr;
|
|
121
|
+
if (sourceText === undefined) {
|
|
122
|
+
// Can't generate this required form - no source and no fallback
|
|
123
|
+
entryHasTranslations = false;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
const translation = await translator(`${key}/${varName}_${form}`, sourceText);
|
|
127
|
+
if (translation === undefined) {
|
|
128
|
+
entryHasTranslations = false;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
translatedVarConfig[form] = translation;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!entryHasTranslations) break;
|
|
135
|
+
|
|
136
|
+
translatedEntry[varName] = translatedVarConfig;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (entryHasTranslations && variableMatches.length > 0) {
|
|
140
|
+
translatedPlist[key] = translatedEntry;
|
|
141
|
+
translated++;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (translated === 0) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return plist.build(translatedPlist);
|
|
150
|
+
}
|
|
151
|
+
}
|
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-ios@${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-ios@${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
|
29
|
-
}
|
|
30
|
-
]
|
|
31
|
-
}
|