@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 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` deletes keys that no longer exist in your source files, together with their translations. See [Prune safety](#prune-safety).
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 deletes keys. What it deletes, and what stops it:
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
- - **Never published keys.** A key published to any environment is listed as kept and survives. Unpublish it in
272
- Translify first if it really should go.
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 deleting every key in that namespace. Emptying a namespace
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 delete and asks
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 published in between), it refuses with `ORPHANS_CHANGED` and writes nothing. A server too old to return
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, deletions included, is one transaction: if anything fails, nothing is written.
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 delete, change nothing
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 delete the whole namespace). Push the real source file, or drop `--prune`; emptying a namespace is done in the Translify web app. |
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 published). Nothing was written; run `translify push --prune` again to review the new list. |
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). |
@@ -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
- // Published orphans are kept by the server, so they are not part of the damage.
46
- const deletable = preview.orphans.filter((orphan) => !orphan.published).length;
47
- if (opts.prune === true && deletable > 0)
48
- damage.push(`delete ${deletable} key(s) and all their translations`);
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 delete without a bound. ' +
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 deleted or overwritten: no question to ask (F19).
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 deletable orphans the real push sends the digest of the list the user confirmed: if the server's
115
- // deletable set differs it answers 409 ORPHANS_CHANGED and writes nothing. With none, there is nothing to prune.
116
- const deletable = preview.orphans.filter((orphan) => !orphan.published).length;
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 published meanwhile). Nothing was written. Run translify push --prune again to review the new list.';
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', 'delete keys missing from your source files; published keys are kept; deletes exactly the keys you confirmed, and refuses if anything changed meanwhile (re-run to review)')
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
- // `pruned` is what a dry run would delete or a real push did delete: unpublished orphans only.
51
- const unpublished = res.orphans.filter((orphan) => !orphan.published);
52
- const published = res.orphans.filter((orphan) => orphan.published);
53
- const listed = unpublished;
54
- const names = (list) => {
55
- for (const orphan of list.slice(0, MAX_ORPHANS))
56
- lines.push(` ${orphan.namespace}:${orphan.key}`);
57
- if (list.length > MAX_ORPHANS)
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 deleted:`
65
- : `Deleted ${res.pruned} key(s) that were not in your source files${listed.length > res.pruned ? ` (${listed.length - res.pruned} kept: published during the push)` : ''}:`;
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
- names(listed);
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 delete them.'));
71
+ lines.push(pc.dim(' Use --prune to move them to the trash.'));
70
72
  }
71
- if (published.length > 0) {
72
- lines.push(pc.yellow(` ${published.length} key(s) kept — published; unpublish it in Translify to delete it:`));
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.red(` Deleted ${res.pruned} key(s).`));
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
@@ -1,3 +1,3 @@
1
1
  // Must equal package.json "version"; src/version.test.ts fails when they drift.
2
- export const VERSION = '0.7.0';
2
+ export const VERSION = '0.8.0';
3
3
  export const USER_AGENT = `translify-cli/${VERSION}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chesterbrains/translify-cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Push, pull and publish Translify translations from your repository",
5
5
  "license": "MIT",
6
6
  "repository": {