@chesterbrains/translify-cli 0.5.0 → 0.6.0
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 +46 -2
- package/dist/commands/pull.js +7 -4
- package/dist/commands/push.js +4 -0
- package/dist/config.js +8 -1
- package/dist/render.js +3 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/schema/translify.schema.json +13 -2
package/README.md
CHANGED
|
@@ -157,6 +157,49 @@ i18next rather than ICU. Give
|
|
|
157
157
|
such a key its own element so the ICU is the whole message, or move it to a JSON rule. A complete example is the
|
|
158
158
|
Angular app in [translify-examples](https://github.com/chesterbrains/translify-examples).
|
|
159
159
|
|
|
160
|
+
## Xcode (XLIFF)
|
|
161
|
+
|
|
162
|
+
Xcode exports and imports **XLIFF 1.2** (`<trans-unit>`), keeps printf placeholders (`%@`, `%lld`) literal, and spreads one
|
|
163
|
+
String Catalog entry over several units for plurals and substitutions. Set `xliffDialect: "xcode"` and `xliffOriginal` on the
|
|
164
|
+
rule and the server converts both ways; Translify keeps ICU, so the same catalog serves React, Flutter, Angular and iOS, and
|
|
165
|
+
iOS translations get placeholder validation (Xcode's own import accepts a target that lost its `%@` without a word).
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{ "files": [{ "pattern": "Localization/{locale}.xliff", "format": "xliff", "namespace": "ios",
|
|
169
|
+
"xliffDialect": "xcode", "xliffOriginal": "Sources/App/Localizable.xcstrings" }] }
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Workflow:
|
|
173
|
+
|
|
174
|
+
1. Seed once: `xcodebuild -exportLocalizations -localizationPath out -exportLanguage en`, copy
|
|
175
|
+
`out/en.xcloc/Localized Contents/en.xliff` to `Localization/en.xliff`, `translify push`.
|
|
176
|
+
2. Translate in Translify, then `translify pull`.
|
|
177
|
+
3. `xcodebuild -importLocalizations -localizationPath Localization/it.xliff` (a bare `.xliff` works; `<file original>`
|
|
178
|
+
comes from `xliffOriginal`).
|
|
179
|
+
|
|
180
|
+
What `pull` writes (arguments are positional, numbered by first appearance in the source, so a reordered translation keeps
|
|
181
|
+
its meaning; the printf type comes from the push that created the key, else `%@` for text, `%lld` for a plural's count, `%f`
|
|
182
|
+
for a fraction):
|
|
183
|
+
|
|
184
|
+
| Translify value | Units |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `Hello, {name}` | `key` → `Hello, %1$@` |
|
|
187
|
+
| `{count, plural, one {# item} other {# items}}` | `key\|==\|plural.one` → `%1$lld item`, `key\|==\|plural.other` → `%1$lld items` |
|
|
188
|
+
| `You have {count, plural, one {# item} other {# items}} in your cart, {name}` | `key` → `You have %1$#@count@ in your cart, %2$@`, plus `key\|==\|substitutions.count.plural.one` / `.other` |
|
|
189
|
+
| untranslated | the unit has a `<source>` and no `<target>`, which Xcode shows as untranslated |
|
|
190
|
+
|
|
191
|
+
What `push` reads: `%@` / `%1$@` → `{arg1}` (renamed to the key's own argument when the key already exists), `%lld` →
|
|
192
|
+
`{arg2}` or `#` inside a plural, `%.2f` → `{arg, number}`, `%%` → `%`; `|==|plural.*` units → one ICU plural named `count`;
|
|
193
|
+
a `%#@name@` substitution → the plural inside the sentence. The printf types are kept in the key's metadata for the next
|
|
194
|
+
pull. Device variations (`|==|device.*`), unknown unit kinds and legacy `.strings`/`.stringsdict` exports are reported as
|
|
195
|
+
`unsupportedVariation` and skipped.
|
|
196
|
+
|
|
197
|
+
Not supported in this version (the pull is refused with `XLIFF_DIALECT_UNSUPPORTED`): `select` and `selectordinal`, exact
|
|
198
|
+
matches (`=0`) and `offset`, two plurals in one message, nested plurals, number formats without a known printf type (e.g.
|
|
199
|
+
currency), a translation whose shape differs from the source (plain text for a plural), and keys whose
|
|
200
|
+
placeholders are printf or i18next rather than ICU. A complete example is `examples/ios-xcstrings` in
|
|
201
|
+
[translify-examples](https://github.com/chesterbrains/translify-examples).
|
|
202
|
+
|
|
160
203
|
## `translify.json` reference
|
|
161
204
|
|
|
162
205
|
`translify.json` lives in the repository root; every command reads it from the current directory. The
|
|
@@ -172,7 +215,8 @@ fields are refused, and so is any field whose name contains `key`, `secret` or `
|
|
|
172
215
|
| `files[].format` | `json` \| `xliff` \| `arb` | yes | File format: i18next JSON, XLIFF or Flutter ARB. |
|
|
173
216
|
| `files[].namespace` | string | when the pattern has no `{namespace}`, and always for `arb` | Fixed namespace for every file the rule matches. Lowercase kebab-case, at most 64 characters. |
|
|
174
217
|
| `files[].jsonStyle` | `nested` \| `flat` | no | Only for `json`. `nested` (the default) writes `{ "a": { "b": "…" } }`, `flat` writes `{ "a.b": "…" }`. |
|
|
175
|
-
| `files[].xliffDialect` | `angular` | no | Only for `xliff`.
|
|
218
|
+
| `files[].xliffDialect` | `angular` \| `xcode` | no | Only for `xliff`. `angular` writes `@angular/localize` placeholders on pull and reads `ng extract-i18n` output on push ([Angular (XLIFF)](#angular-xliff)); `xcode` writes XLIFF 1.2 for String Catalogs and reads `xcodebuild -exportLocalizations` output ([Xcode (XLIFF)](#xcode-xliff)). |
|
|
219
|
+
| `files[].xliffOriginal` | string | with `xliffDialect: "xcode"` | The String Catalog path Xcode knows, e.g. `Sources/App/Localizable.xcstrings`; written to `<file original>`. Xcode imports nothing when it does not match the project. Only with `xcode`. |
|
|
176
220
|
| `locales` | object | no | Maps a locale name on disk to a Translify locale code, e.g. `{ "en_US": "en-US" }`. Unmapped names pass through unchanged. Two names may not map to the same code, and a target may not itself be a mapped name. |
|
|
177
221
|
|
|
178
222
|
Each file must be matched by one rule only, and each (locale, namespace) pair must come from one file only;
|
|
@@ -358,7 +402,7 @@ The server's error `code` decides the exit code first; the HTTP status is only u
|
|
|
358
402
|
| `REVIEW_DISABLED` | 1 | `push --status draft` or `--status needs-review` on a project without review. Turn on **Require review** in the project settings, or drop `--status`. Nothing was written. |
|
|
359
403
|
| `APPROVED_ONLY_REQUIRES_WORKING` | 1 | `--only-approved` was combined with `--from env:<slug>`. Pull an environment without it; it already holds only what its gate allowed. |
|
|
360
404
|
| `INVALID_CELLS` | 6 | `publish` refused: translations that would go live have placeholder errors. Nothing was published; the output lists them like `lint` does. Fix them in Translify, or re-run with `--allow-errors` to publish anyway (recorded in the activity log). |
|
|
361
|
-
| `XLIFF_DIALECT_UNSUPPORTED` | 6 | `pull` refused: a translation cannot be written
|
|
405
|
+
| `XLIFF_DIALECT_UNSUPPORTED` | 6 | `pull` refused: a translation cannot be written in the rule's XLIFF dialect (Angular: text beside a plural, nested plurals, number/date formats; Xcode: `select`, `=0`/`offset`, a translation of a different shape; both: printf/i18next keys). Nothing was written; the output lists the cells and why. Split the text, or move the key to a JSON rule. See [Angular (XLIFF)](#angular-xliff) and [Xcode (XLIFF)](#xcode-xliff). |
|
|
362
406
|
| `TRANSACTION_CONFLICT` | 4 | Another write collided with yours. Retry. |
|
|
363
407
|
| `RATE_LIMITED` | 4 | The CLI already retried 3 times, honouring `Retry-After`. Wait a moment and retry. |
|
|
364
408
|
| `INTERNAL_ERROR` / `DATABASE_ERROR` / 5xx | 4 | A server problem. Retry shortly; if it persists, re-run with `--debug` and report it. |
|
package/dist/commands/pull.js
CHANGED
|
@@ -7,10 +7,10 @@ const readOrUndefined = async (path) => readFile(path, 'utf8').catch(() => undef
|
|
|
7
7
|
const NESTED_CONFLICT = /nested JSON cannot hold both/;
|
|
8
8
|
const isDialectUnsupported = (details) => typeof details === 'object' && details !== null && details.code === 'XLIFF_DIALECT_UNSUPPORTED';
|
|
9
9
|
/** The server's one-line refusal, the cells grouped like `lint`, and the way out. */
|
|
10
|
-
const dialectRefusal = (body, config) => {
|
|
10
|
+
const dialectRefusal = (body, config, dialect) => {
|
|
11
11
|
const cells = Array.isArray(body.cells) ? body.cells : [];
|
|
12
12
|
const lines = [
|
|
13
|
-
`Pull refused: ${body.total} translation(s) cannot be written as
|
|
13
|
+
`Pull refused: ${body.total} translation(s) cannot be written as XLIFF for ${dialect}. Nothing was written.`,
|
|
14
14
|
...renderReasonCells(cells, config),
|
|
15
15
|
];
|
|
16
16
|
if (body.total > cells.length)
|
|
@@ -67,6 +67,8 @@ export async function runPull(opts, deps) {
|
|
|
67
67
|
query.jsonStyle = rule.jsonStyle;
|
|
68
68
|
if (rule.format === 'xliff' && rule.xliffDialect !== undefined)
|
|
69
69
|
query.dialect = rule.xliffDialect;
|
|
70
|
+
if (rule.format === 'xliff' && rule.xliffOriginal !== undefined)
|
|
71
|
+
query.original = rule.xliffOriginal;
|
|
70
72
|
if (opts.from !== undefined)
|
|
71
73
|
query.from = opts.from;
|
|
72
74
|
if (opts.onlyApproved)
|
|
@@ -77,8 +79,9 @@ export async function runPull(opts, deps) {
|
|
|
77
79
|
if (namespace !== undefined)
|
|
78
80
|
query.namespaces = namespace;
|
|
79
81
|
const res = await deps.api.get('/cli/v1/pull', query).catch((error) => {
|
|
80
|
-
if (error instanceof CliError && isDialectUnsupported(error.details))
|
|
81
|
-
throw dialectRefusal(error.details, deps.config);
|
|
82
|
+
if (error instanceof CliError && isDialectUnsupported(error.details)) {
|
|
83
|
+
throw dialectRefusal(error.details, deps.config, rule.xliffDialect ?? 'angular');
|
|
84
|
+
}
|
|
82
85
|
throw error;
|
|
83
86
|
});
|
|
84
87
|
responses.push(res);
|
package/dist/commands/push.js
CHANGED
|
@@ -21,6 +21,7 @@ const buildForm = (uploads, opts, dryRun, prune, pruneDigest) => {
|
|
|
21
21
|
locale: match.locale,
|
|
22
22
|
namespace: match.namespace,
|
|
23
23
|
...(match.rule.format === 'xliff' && match.rule.xliffDialect !== undefined ? { dialect: match.rule.xliffDialect } : {}),
|
|
24
|
+
...(match.rule.format === 'xliff' && match.rule.xliffOriginal !== undefined ? { original: match.rule.xliffOriginal } : {}),
|
|
24
25
|
}))));
|
|
25
26
|
form.set('dryRun', String(dryRun));
|
|
26
27
|
form.set('overwriteTargets', String(opts.overwriteTargets === true));
|
|
@@ -74,6 +75,9 @@ export async function runPush(opts, deps) {
|
|
|
74
75
|
bytes: new Uint8Array(await readFile(join(deps.cwd, match.path))),
|
|
75
76
|
})));
|
|
76
77
|
const show = (res, preview) => {
|
|
78
|
+
if (opts.json !== true)
|
|
79
|
+
for (const warning of res.warnings ?? [])
|
|
80
|
+
deps.out(`warning: ${warning}`);
|
|
77
81
|
deps.out(opts.json === true
|
|
78
82
|
? JSON.stringify({ preview, response: res, summary: summarizePush(res) })
|
|
79
83
|
: renderPush(res, { prune: opts.prune === true }));
|
package/dist/config.js
CHANGED
|
@@ -17,7 +17,8 @@ const fileRule = z
|
|
|
17
17
|
format: z.enum(['json', 'xliff', 'arb']),
|
|
18
18
|
namespace: z.string().regex(NAMESPACE, 'must be lowercase kebab-case').max(64).optional(),
|
|
19
19
|
jsonStyle: z.enum(['nested', 'flat']).optional(),
|
|
20
|
-
xliffDialect: z.enum(['angular']).optional(),
|
|
20
|
+
xliffDialect: z.enum(['angular', 'xcode']).optional(),
|
|
21
|
+
xliffOriginal: z.string().min(1).max(512).optional(),
|
|
21
22
|
})
|
|
22
23
|
.strict()
|
|
23
24
|
.superRefine((rule, ctx) => {
|
|
@@ -34,6 +35,12 @@ const fileRule = z
|
|
|
34
35
|
if (rule.xliffDialect !== undefined && rule.format !== 'xliff') {
|
|
35
36
|
ctx.addIssue({ code: 'custom', path: ['xliffDialect'], message: 'only applies to format "xliff"' });
|
|
36
37
|
}
|
|
38
|
+
if (rule.xliffOriginal !== undefined && rule.xliffDialect !== 'xcode') {
|
|
39
|
+
ctx.addIssue({ code: 'custom', path: ['xliffOriginal'], message: 'only applies to xliffDialect "xcode"' });
|
|
40
|
+
}
|
|
41
|
+
if (rule.xliffDialect === 'xcode' && rule.xliffOriginal === undefined) {
|
|
42
|
+
ctx.addIssue({ code: 'custom', path: ['xliffOriginal'], message: 'required with xliffDialect "xcode"' });
|
|
43
|
+
}
|
|
37
44
|
})
|
|
38
45
|
.transform((rule) => (rule.format === 'json' ? { ...rule, jsonStyle: rule.jsonStyle ?? 'nested' } : rule));
|
|
39
46
|
const configSchema = z
|
package/dist/render.js
CHANGED
|
@@ -143,6 +143,9 @@ const REASON_TEXT = {
|
|
|
143
143
|
formattedArg: 'a number, date or time format has no Angular placeholder',
|
|
144
144
|
literalBrace: "a quoted literal brace ('{') cannot be loaded by Angular",
|
|
145
145
|
syntax: 'the key uses printf or i18next placeholders, not ICU',
|
|
146
|
+
exactMatch: 'an exact match (=0, =1) or offset has no Xcode plural category',
|
|
147
|
+
shapeMismatch: 'the translation is not the same shape as the source (plural vs plain text, or a different plural argument)',
|
|
148
|
+
unsupportedVariation: 'a device variation or an unknown |==| unit (not supported)',
|
|
146
149
|
};
|
|
147
150
|
/** Like renderIssueCells: namespace, key, then one line per cell with the locale as named on disk. */
|
|
148
151
|
export function renderReasonCells(cells, config) {
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -30,8 +30,14 @@
|
|
|
30
30
|
},
|
|
31
31
|
"jsonStyle": { "enum": ["nested", "flat"], "description": "Only for format json. Defaults to nested." },
|
|
32
32
|
"xliffDialect": {
|
|
33
|
-
"enum": ["angular"],
|
|
34
|
-
"description": "Only for format xliff. angular writes @angular/localize placeholders (<ph equiv=\"INTERPOLATION\"/>, VAR_PLURAL) on pull and reads ng extract-i18n output on push."
|
|
33
|
+
"enum": ["angular", "xcode"],
|
|
34
|
+
"description": "Only for format xliff. angular writes @angular/localize placeholders (<ph equiv=\"INTERPOLATION\"/>, VAR_PLURAL) on pull and reads ng extract-i18n output on push. xcode writes XLIFF 1.2 for String Catalogs (positional printf, |==| plural and substitution units) and reads xcodebuild -exportLocalizations output."
|
|
35
|
+
},
|
|
36
|
+
"xliffOriginal": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"minLength": 1,
|
|
39
|
+
"maxLength": 512,
|
|
40
|
+
"description": "Only with xliffDialect xcode, and required with it: the String Catalog path Xcode knows (e.g. Sources/App/Localizable.xcstrings), written to <file original>. Xcode imports nothing when it does not match."
|
|
35
41
|
}
|
|
36
42
|
},
|
|
37
43
|
"allOf": [
|
|
@@ -50,6 +56,11 @@
|
|
|
50
56
|
{
|
|
51
57
|
"if": { "properties": { "format": { "enum": ["json", "arb"] } }, "required": ["format"] },
|
|
52
58
|
"then": { "not": { "required": ["xliffDialect"] } }
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"if": { "properties": { "xliffDialect": { "const": "xcode" } }, "required": ["xliffDialect"] },
|
|
62
|
+
"then": { "required": ["xliffOriginal"] },
|
|
63
|
+
"else": { "not": { "required": ["xliffOriginal"] } }
|
|
53
64
|
}
|
|
54
65
|
]
|
|
55
66
|
}
|