@chesterbrains/translify-cli 0.7.0 → 0.8.1

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 }}
@@ -305,9 +309,97 @@ When the project has **Require review** turned on in Translify:
305
309
  - `publish` refuses when a translation that would go live has a placeholder error (a missing or extra placeholder, invalid ICU, a printf type mismatch). Nothing is published, the refused translations are listed like `lint` lists them, and the command exits 6. Warnings never block. On an environment with **Publish approved text only**, only approved translations count: one carried over from its last approved text never blocks. `--allow-errors` publishes anyway and reports how many went out with errors; the publish is recorded in the activity log with the override.
306
310
  - `pull --only-approved` writes the approved text from the working copy and leaves out translations that were never approved, for building a release bundle without publishing. `status --only-approved` compares against the same. It works with `--from working` only: environments already apply the gate.
307
311
 
312
+ ## Webhooks
313
+
314
+ A project's admins can add webhooks under **Integrations**. Translify calls them with `publication.created` every time
315
+ an environment's live batch changes: a publish, a rollback or an unpublish (`batch.kind` says which). A publish whose
316
+ result is identical to what is live emits nothing. `ping` is the test event, sent by "Send test" and by the
317
+ verification dispatch that saves a GitHub webhook.
318
+
319
+ Deliveries are made by a worker, not by the request that published: five attempts, waiting 1 minute, 5 minutes,
320
+ 30 minutes and 2 hours between them, then the delivery is `DEAD` and can be resent from the app. The log keeps 30
321
+ days. The delivery id is the same on every attempt (`X-Translify-Delivery` and the payload's `id`), so a receiver that
322
+ answers slowly and gets the retry can dedupe on it. Answer 2xx as soon as the request is accepted and do the work
323
+ afterwards; the sender waits 10 seconds.
324
+
325
+ ### Payload
326
+
327
+ ```json
328
+ {
329
+ "id": "<delivery id>",
330
+ "event": "publication.created",
331
+ "createdAt": "2026-10-25T14:02:11.000Z",
332
+ "project": { "id": "…", "slug": "shop", "name": "Shop" },
333
+ "environment": { "id": "…", "slug": "production", "name": "Production", "requireApproved": true },
334
+ "batch": {
335
+ "id": "…", "seq": 41, "kind": "PUBLISH",
336
+ "publishedCount": 1280, "addedCount": 3, "changedCount": 14, "removedCount": 0,
337
+ "withheldCount": 0, "errorCount": 0
338
+ },
339
+ "actor": { "type": "user", "id": "…", "name": "Anna Rossi" }
340
+ }
341
+ ```
342
+
343
+ `addedCount`, `changedCount` and `removedCount` are the batch's diff against the previous live batch: a first publish
344
+ has everything in `addedCount`; an `UNPUBLISH` has `publishedCount: 0` and everything that went offline in
345
+ `removedCount`. "Did anything change" is the sum of the three. `withheldCount` is what a `requireApproved` gate kept
346
+ back, `errorCount` the cells published with placeholder errors (`--allow-errors`). `actor.type` is `user`, `apiKey`
347
+ (a CLI publish; `name` is the key's name) or `system`. A `ping` carries `id`, `event`, `createdAt`, `project` and
348
+ `webhook { id, target, destination }` only.
349
+
350
+ ### Verify a delivery
351
+
352
+ Generic webhooks `POST` the payload as `application/json` to an `https://` URL on a public host, with:
353
+
354
+ | Header | Value |
355
+ | --- | --- |
356
+ | `X-Translify-Event` | `publication.created` or `ping` |
357
+ | `X-Translify-Delivery` | the delivery id, identical on every attempt |
358
+ | `X-Translify-Timestamp` | unix seconds when the attempt was sent |
359
+ | `X-Translify-Signature` | `v1=<hex hmac-sha256(secret, "<timestamp>.<body>")>` |
360
+ | `User-Agent` | `translify-webhooks/1` |
361
+
362
+ The signing secret (`whsec_…`) is shown once when the webhook is created. Verify over the raw body, compare in constant
363
+ time and refuse a timestamp more than 300 s (5 minutes) away from now:
364
+
365
+ ```js
366
+ import { createHmac, timingSafeEqual } from 'node:crypto';
367
+
368
+ export function verify(rawBody, headers, secret) {
369
+ const timestamp = Number(headers['x-translify-timestamp']);
370
+ if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
371
+ const expected = `v1=${createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')}`;
372
+ const given = Buffer.from(String(headers['x-translify-signature'] ?? ''));
373
+ const wanted = Buffer.from(expected);
374
+ return given.length === wanted.length && timingSafeEqual(given, wanted);
375
+ }
376
+ ```
377
+
378
+ ### GitHub
379
+
380
+ A webhook with the **GitHub** target sends a
381
+ [`repository_dispatch`](https://docs.github.com/en/rest/repos/repos#create-a-repository-dispatch-event) instead:
382
+ `POST https://api.github.com/repos/<owner>/<repo>/dispatches` with `event_type` `translify.publication` (or
383
+ `translify.ping`) and the payload above as `client_payload`. No signature: GitHub authenticates the token.
384
+
385
+ The token is a fine-grained personal access token with **Contents: Read and write** on that one repository (that is
386
+ the permission `repository_dispatch` needs). Translify verifies it with a `translify.ping` dispatch before saving and
387
+ refuses a token or repository GitHub rejects; the expiry GitHub reports is shown in the list from 14 days out.
388
+
389
+ [`recipes/github-actions/translify-pull-on-publish.yml`](https://github.com/chesterbrains/translify-cli/blob/main/recipes/github-actions/translify-pull-on-publish.yml)
390
+ is the workflow that closes the loop: on `translify.publication` it runs `translify pull --from env:<slug>` for the
391
+ environment in the payload, commits, opens a pull request whose body carries the publish summary (batch, counts,
392
+ actor), runs your build and merges. Two things to set after copying it: the repository setting **Allow GitHub Actions
393
+ to create and approve pull requests** (Settings → Actions → General; without it the job token cannot open the PR),
394
+ and the environment the repository follows: a literal in the job's `if:`, and the webhook limited to that environment
395
+ in Translify, since every environment pulls into the same files. The workflow builds and merges inside the
396
+ same run because a pull request opened with `GITHUB_TOKEN` never triggers other workflows. The
397
+ [translify-examples](https://github.com/chesterbrains/translify-examples/blob/main/.github/workflows/translify-pull-on-publish.yml)
398
+ repository runs this workflow against the demo project.
399
+
308
400
  ## CI (GitHub Actions)
309
401
 
310
- Store the key in the repository secrets as `TRANSLIFY_SECRET_KEY`. Two ready-made workflows live in
402
+ Store the key in the repository secrets as `TRANSLIFY_SECRET_KEY`. Three ready-made workflows live in
311
403
  [`recipes/github-actions/`](https://github.com/chesterbrains/translify-cli/tree/main/recipes/github-actions); copy the
312
404
  one you need to `.github/workflows/` as it is.
313
405
 
@@ -319,8 +411,11 @@ one you need to `.github/workflows/` as it is.
319
411
  - [`translify-release.yml`](https://github.com/chesterbrains/translify-cli/blob/main/recipes/github-actions/translify-release.yml)
320
412
  is for apps that bundle translations at build time: `lint`, then `pull`, your build, then `publish` to the environment
321
413
  chosen in the Actions tab (or on a tag). Key scopes: `PULL` and `PUBLISH`.
414
+ - [`translify-pull-on-publish.yml`](https://github.com/chesterbrains/translify-cli/blob/main/recipes/github-actions/translify-pull-on-publish.yml)
415
+ runs when Translify's GitHub webhook announces a publish: `pull --from env:<slug>` for the published environment,
416
+ commit, a pull request with the publish summary, your build, merge. Key scope: `PULL`. Setup in [Webhooks](#webhooks).
322
417
 
323
- Both use `npx -y translify`, so they follow every 0.x release; pin a version (`npx -y @chesterbrains/translify-cli@0.6`) if a breaking
418
+ All three use `npx -y translify`, so they follow every 0.x release; pin a version (`npx -y @chesterbrains/translify-cli@0.6`) if a breaking
324
419
  release must not turn the workflow red on its own.
325
420
 
326
421
  Fail a pull request when local files drift from Translify:
@@ -364,7 +459,7 @@ The server's error `code` decides the exit code first; the HTTP status is only u
364
459
  | `VALIDATION_FAILED` | 1 | Nothing was written. The output lists `file:line key: reason`; fix those entries and push again. |
365
460
  | `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
461
  | `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. |
462
+ | `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
463
  | `JSON_STYLE_REQUIRES_JSON` | 1 | `jsonStyle` only applies to rules with `"format": "json"`. Remove it from the other rules. |
369
464
  | `BAD_REQUEST` | 1 | The request was malformed. Re-run with `--debug` for the server's message. |
370
465
  | `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 +472,7 @@ The server's error `code` decides the exit code first; the HTTP status is only u
377
472
  | `QUOTA_EXCEEDED` | 3 | Your plan limit was reached (usually translation keys). Remove keys, or push a smaller set. |
378
473
  | `PUSH_TOO_LARGE` | 4 | Split the push by namespace: `translify push --namespace <ns>`. |
379
474
  | `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. |
475
+ | `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
476
  | `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
477
  | `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
478
  | `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.1';
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.1",
4
4
  "description": "Push, pull and publish Translify translations from your repository",
5
5
  "license": "MIT",
6
6
  "repository": {