@chesterbrains/translify-cli 0.7.0 → 0.8.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 +15 -11
- package/dist/commands/push.js +10 -10
- package/dist/http.js +1 -1
- package/dist/index.js +1 -1
- package/dist/render.js +22 -22
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -257,34 +257,38 @@ Options:
|
|
|
257
257
|
- **Source locale: your files win.** Existing source translations are overwritten with what is in the file.
|
|
258
258
|
- **Target locales: Translify fills only the gaps.** Keys that already have a translation are left alone and counted as skipped.
|
|
259
259
|
- `--overwrite-targets` also overwrites existing target translations.
|
|
260
|
-
- `--prune`
|
|
260
|
+
- `--prune` moves keys that no longer exist in your source files to the Translify trash, together with their translations. They can be restored for 30 days. See [Prune safety](#prune-safety).
|
|
261
261
|
- `--yes` skips the confirmation prompt for the two destructive flags. Without a terminal and without `--yes`, a destructive push is refused and nothing changes.
|
|
262
262
|
- `--dry-run` shows the effect of any combination and writes nothing.
|
|
263
263
|
|
|
264
264
|
## Prune safety
|
|
265
265
|
|
|
266
|
-
`translify push --prune` is the only command that
|
|
266
|
+
`translify push --prune` is the only command that removes keys. It never deletes on the spot: it archives them. What
|
|
267
|
+
it archives, and what stops it:
|
|
267
268
|
|
|
268
269
|
- **Only the namespaces in this push.** A key is an orphan when its namespace is in the push and its source file no
|
|
269
270
|
longer has it. Namespaces you did not push (another rule, another repository, `--namespace` left them out) are never
|
|
270
271
|
touched.
|
|
271
|
-
- **
|
|
272
|
-
|
|
272
|
+
- **Archived, not deleted.** A pruned key goes to the trash in Translify, with its translations and their history.
|
|
273
|
+
It is deleted for good 30 days later. Until then, restore it from the trash on the project's Translations tab, or
|
|
274
|
+
push a source file that names it again: the push restores it in place and reports it as restored.
|
|
275
|
+
- **Live keys too.** A key that is live in an environment is archived like any other, but the
|
|
276
|
+
environment keeps serving it until its next publish, which drops it. Restore it before then if it should stay.
|
|
273
277
|
- **Never a whole namespace by accident.** If a source file in the push has no entries, the push is refused
|
|
274
|
-
(`VALIDATION_FAILED` with `emptySourcePrune`) instead of
|
|
278
|
+
(`VALIDATION_FAILED` with `emptySourcePrune`) instead of archiving every key in that namespace. Emptying a namespace
|
|
275
279
|
is done in the Translify web app.
|
|
276
|
-
- **Exactly the list you confirmed.** The CLI always runs a dry run first, shows the keys it would
|
|
280
|
+
- **Exactly the list you confirmed.** The CLI always runs a dry run first, shows the keys it would archive and asks
|
|
277
281
|
for confirmation. The real push sends a digest of that list; if the server's list differs by then (keys added,
|
|
278
|
-
removed or
|
|
282
|
+
removed, archived or restored in between), it refuses with `ORPHANS_CHANGED` and writes nothing. A server too old to return
|
|
279
283
|
the digest makes `--prune` refuse.
|
|
280
284
|
- **No silent prune in CI.** Without a terminal, `--prune` is refused unless you pass `--yes`.
|
|
281
|
-
- **Atomic.** The whole push,
|
|
285
|
+
- **Atomic.** The whole push, archives and restores included, is one transaction: if anything fails, nothing is written.
|
|
282
286
|
|
|
283
287
|
`--yes` confirms whatever that run's dry run lists, so in CI review the list before it runs unattended. One way:
|
|
284
288
|
show the orphans on pull requests and prune on `main`.
|
|
285
289
|
|
|
286
290
|
```yaml
|
|
287
|
-
# on pull_request: list what a prune would
|
|
291
|
+
# on pull_request: list what a prune would archive, change nothing
|
|
288
292
|
- run: npx translify push --prune --dry-run
|
|
289
293
|
env:
|
|
290
294
|
TRANSLIFY_SECRET_KEY: ${{ secrets.TRANSLIFY_SECRET_KEY }}
|
|
@@ -364,7 +368,7 @@ The server's error `code` decides the exit code first; the HTTP status is only u
|
|
|
364
368
|
| `VALIDATION_FAILED` | 1 | Nothing was written. The output lists `file:line key: reason`; fix those entries and push again. |
|
|
365
369
|
| `LOCALE_NOT_FOUND` | 1 | A locale in your files is not enabled in the project. Enable it in Translify, or map it with `locales` in `translify.json`. |
|
|
366
370
|
| `NAMESPACE_NOT_FOUND` | 1 | The namespace does not exist in the project. Create it in Translify or fix the `--namespace` / pattern. |
|
|
367
|
-
| `VALIDATION_FAILED` with `emptySourcePrune` | 1 | `--prune` was refused because a source file in the push has no entries (it would
|
|
371
|
+
| `VALIDATION_FAILED` with `emptySourcePrune` | 1 | `--prune` was refused because a source file in the push has no entries (it would archive the whole namespace). Push the real source file, or drop `--prune`; emptying a namespace is done in the Translify web app. |
|
|
368
372
|
| `JSON_STYLE_REQUIRES_JSON` | 1 | `jsonStyle` only applies to rules with `"format": "json"`. Remove it from the other rules. |
|
|
369
373
|
| `BAD_REQUEST` | 1 | The request was malformed. Re-run with `--debug` for the server's message. |
|
|
370
374
|
| `ENVIRONMENT_NOT_FOUND` / `ENVIRONMENT_ARCHIVED` | 1 | Check the environment slug passed to `publish` or `--from env:<slug>`, and that it is not archived. |
|
|
@@ -377,7 +381,7 @@ The server's error `code` decides the exit code first; the HTTP status is only u
|
|
|
377
381
|
| `QUOTA_EXCEEDED` | 3 | Your plan limit was reached (usually translation keys). Remove keys, or push a smaller set. |
|
|
378
382
|
| `PUSH_TOO_LARGE` | 4 | Split the push by namespace: `translify push --namespace <ns>`. |
|
|
379
383
|
| `CLI_TOO_OLD` | 4 | Upgrade: `npm i -g @chesterbrains/translify-cli@latest` (or use `npx translify@latest`). |
|
|
380
|
-
| `ORPHANS_CHANGED` | 1 | The orphan list changed between the dry run and the real push (keys were added, removed or
|
|
384
|
+
| `ORPHANS_CHANGED` | 1 | The orphan list changed between the dry run and the real push (keys were added, removed, archived or restored). Nothing was written; run `translify push --prune` again to review the new list. |
|
|
381
385
|
| `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. |
|
|
382
386
|
| `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. |
|
|
383
387
|
| `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). |
|
package/dist/commands/push.js
CHANGED
|
@@ -42,10 +42,11 @@ const buildForm = (uploads, opts, dryRun, prune, pruneDigest) => {
|
|
|
42
42
|
*/
|
|
43
43
|
const describeDamage = async (preview, opts, api) => {
|
|
44
44
|
const damage = [];
|
|
45
|
-
//
|
|
46
|
-
const
|
|
47
|
-
if (opts.prune === true &&
|
|
48
|
-
damage.push(`
|
|
45
|
+
// Every orphan is archived, live ones included: they keep serving until the next publish.
|
|
46
|
+
const archivable = preview.orphans.length;
|
|
47
|
+
if (opts.prune === true && archivable > 0) {
|
|
48
|
+
damage.push(`move ${archivable} key(s) and their translations to the trash (restorable for 30 days in Translify)`);
|
|
49
|
+
}
|
|
49
50
|
if (opts.overwriteTargets === true) {
|
|
50
51
|
const { project } = await api.get('/cli/v1/whoami');
|
|
51
52
|
const overwritten = Object.entries(preview.locales)
|
|
@@ -90,12 +91,12 @@ export async function runPush(opts, deps) {
|
|
|
90
91
|
}
|
|
91
92
|
const preview = await deps.api.post(PUSH, buildForm(uploads, opts, true, opts.prune === true));
|
|
92
93
|
if (opts.prune === true && typeof preview.pruneDigest !== 'string') {
|
|
93
|
-
deps.err('This server cannot confirm a prune (its dry run returned no pruneDigest), so --prune would
|
|
94
|
+
deps.err('This server cannot confirm a prune (its dry run returned no pruneDigest), so --prune would archive without a bound. ' +
|
|
94
95
|
'Nothing was changed. Upgrade the Translify server, or push without --prune.');
|
|
95
96
|
return EXIT.failed;
|
|
96
97
|
}
|
|
97
98
|
const damage = await describeDamage(preview, opts, deps.api);
|
|
98
|
-
// Nothing would be
|
|
99
|
+
// Nothing would be archived or overwritten: no question to ask (F19).
|
|
99
100
|
if (damage.length > 0) {
|
|
100
101
|
// With --json, stdout is reserved for the final document.
|
|
101
102
|
(opts.json === true ? deps.err : deps.out)(renderPush(preview));
|
|
@@ -111,10 +112,9 @@ export async function runPush(opts, deps) {
|
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
|
-
// With
|
|
115
|
-
//
|
|
116
|
-
const
|
|
117
|
-
const prune = opts.prune === true && deletable > 0;
|
|
115
|
+
// With orphans the real push sends the digest of the list the user confirmed: if the server's orphan
|
|
116
|
+
// set differs it answers 409 ORPHANS_CHANGED and writes nothing. With none, there is nothing to prune.
|
|
117
|
+
const prune = opts.prune === true && preview.orphans.length > 0;
|
|
118
118
|
const res = await deps.api.post(PUSH, buildForm(uploads, opts, false, prune, prune ? preview.pruneDigest : undefined));
|
|
119
119
|
show(res, preview);
|
|
120
120
|
return EXIT.ok;
|
package/dist/http.js
CHANGED
|
@@ -85,7 +85,7 @@ const describeBody = (status, body) => {
|
|
|
85
85
|
case 'NAMESPACE_NOT_FOUND':
|
|
86
86
|
return `Unknown namespace(s): ${list(body.namespaces)}.`;
|
|
87
87
|
case 'ORPHANS_CHANGED':
|
|
88
|
-
return 'The orphans changed since the dry run (keys were added, removed or
|
|
88
|
+
return 'The orphans changed since the dry run (keys were added, removed, archived or restored meanwhile). Nothing was written. Run translify push --prune again to review the new list.';
|
|
89
89
|
case 'REVIEW_DISABLED':
|
|
90
90
|
return 'Review is off for this project, so --status draft or needs-review cannot be used. Turn on "Require review" in the project settings, or drop --status.';
|
|
91
91
|
case 'APPROVED_ONLY_REQUIRES_WORKING':
|
package/dist/index.js
CHANGED
|
@@ -89,7 +89,7 @@ program
|
|
|
89
89
|
.description('Upload source and target files')
|
|
90
90
|
.option('--dry-run', 'show what would change, write nothing')
|
|
91
91
|
.option('--overwrite-targets', 'overwrite existing target-locale translations')
|
|
92
|
-
.option('--prune', '
|
|
92
|
+
.option('--prune', 'move keys missing from your source files to the Translify trash (restorable for 30 days); archives exactly the keys you confirmed, and refuses if anything changed meanwhile (re-run to review)')
|
|
93
93
|
.option('-y, --yes', 'confirm destructive flags without prompting')
|
|
94
94
|
.option('--namespace <ns>', 'only this namespace')
|
|
95
95
|
.addOption(new Option('--status <status>', 'status target-locale cells land in (default: needs-review when the project requires review, approved otherwise)').choices(PUSH_STATUSES))
|
package/dist/render.js
CHANGED
|
@@ -14,8 +14,8 @@ export function summarizePush(res) {
|
|
|
14
14
|
skippedFilled: 0,
|
|
15
15
|
unknownKeys: 0,
|
|
16
16
|
orphans: res.orphans.length,
|
|
17
|
-
publishedOrphans: res.orphans.filter((orphan) => orphan.published).length,
|
|
18
17
|
pruned: res.pruned,
|
|
18
|
+
keysRestored: res.keysRestored ?? 0,
|
|
19
19
|
staleReset: res.staleReset ?? 0,
|
|
20
20
|
};
|
|
21
21
|
for (const counts of Object.values(res.locales)) {
|
|
@@ -47,36 +47,36 @@ export function renderPush(res, view = {}) {
|
|
|
47
47
|
if (stale > 0) {
|
|
48
48
|
lines.push(pc.yellow(`${stale} approved translation(s) ${res.dryRun ? 'would go' : 'went'} back to review because their source text changed.`));
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
lines.push(` …and ${list.length - MAX_ORPHANS} more`);
|
|
59
|
-
};
|
|
50
|
+
const restored = res.keysRestored ?? 0;
|
|
51
|
+
if (restored > 0) {
|
|
52
|
+
lines.push(pc.green(`${res.dryRun ? 'Would restore' : 'Restored'} ${restored} key(s) from the trash.`));
|
|
53
|
+
}
|
|
54
|
+
// A prune archives every orphan (it goes to the trash, purged 30 days later); `pruned` is what a dry
|
|
55
|
+
// run would archive or a real push did. A real prune can archive fewer than it listed when some
|
|
56
|
+
// orphans were archived from the web app meanwhile.
|
|
57
|
+
const listed = res.orphans;
|
|
60
58
|
if (listed.length > 0) {
|
|
59
|
+
const meanwhile = listed.length - res.pruned;
|
|
61
60
|
const header = res.pruned === 0
|
|
62
61
|
? `${listed.length} key(s) in Translify are not in your source files:`
|
|
63
62
|
: res.dryRun
|
|
64
|
-
? `${listed.length} key(s) not in your source files would be
|
|
65
|
-
: `
|
|
63
|
+
? `${listed.length} key(s) not in your source files would be moved to the trash:`
|
|
64
|
+
: `Moved ${res.pruned} key(s) that were not in your source files to the trash (restorable for 30 days${meanwhile > 0 ? `; ${meanwhile} already archived meanwhile` : ''}):`;
|
|
66
65
|
lines.push(pc.yellow(` ${header}`));
|
|
67
|
-
|
|
66
|
+
for (const orphan of listed.slice(0, MAX_ORPHANS))
|
|
67
|
+
lines.push(` ${orphan.namespace}:${orphan.key}`);
|
|
68
|
+
if (listed.length > MAX_ORPHANS)
|
|
69
|
+
lines.push(` …and ${listed.length - MAX_ORPHANS} more`);
|
|
68
70
|
if (res.pruned === 0 && view.prune !== true)
|
|
69
|
-
lines.push(pc.dim(' Use --prune to
|
|
71
|
+
lines.push(pc.dim(' Use --prune to move them to the trash.'));
|
|
70
72
|
}
|
|
71
|
-
if (
|
|
72
|
-
lines.push(pc.yellow(` ${
|
|
73
|
-
names(published);
|
|
73
|
+
if (res.pruned > 0 && res.dryRun) {
|
|
74
|
+
lines.push(pc.yellow(` Would move ${res.pruned} key(s) to the trash (restorable for 30 days).`));
|
|
74
75
|
}
|
|
75
|
-
if (res.pruned > 0 && res.dryRun)
|
|
76
|
-
lines.push(pc.yellow(` Would delete ${res.pruned} key(s).`));
|
|
77
76
|
// A real prune's orphan header already says it; repeat it only when there was no list.
|
|
78
|
-
if (res.pruned > 0 && !res.dryRun && listed.length === 0)
|
|
79
|
-
lines.push(pc.
|
|
77
|
+
if (res.pruned > 0 && !res.dryRun && listed.length === 0) {
|
|
78
|
+
lines.push(pc.yellow(` Moved ${res.pruned} key(s) to the Translify trash (restorable for 30 days).`));
|
|
79
|
+
}
|
|
80
80
|
return lines.join('\n');
|
|
81
81
|
}
|
|
82
82
|
const MAX_VALUE = 80;
|
package/dist/version.js
CHANGED