@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 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
- // filter for iOS .strings file (in utf-8)
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.0-alpha.9",
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": "^3.0.0-alpha.0"
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
- }