@chesterbrains/translify-cli 0.3.0 → 0.4.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
@@ -133,7 +133,7 @@ Options:
133
133
 
134
134
  - `push`: `--dry-run`, `--overwrite-targets`, `--prune`, `-y/--yes`, `--namespace <ns>`, `--status <draft|needs-review|approved>`
135
135
  - `pull` and `status`: `--from <working|env:slug>`, `--locale <locale>` (as named on disk), `--namespace <ns>`, `--only-approved`
136
- - `publish`: `--fail-on-withheld`
136
+ - `publish`: `--fail-on-withheld`, `--allow-errors`
137
137
  - `lint`: `--namespace <ns>`, `--locale <locale>` (as named on disk), `--severity <error|warning>` (only list cells whose worst issue is this severity; counts and the exit code are unaffected), `--max-warnings <n>`
138
138
  - `--json` on `push`, `pull`, `status`, `publish` and `lint`: machine-readable output on stdout
139
139
  - `--debug` (global): print error details and stack traces
@@ -156,6 +156,7 @@ When the project has **Require review** turned on in Translify:
156
156
  - `push` lands target-locale translations it writes as **needs review**. `--status draft` or `--status needs-review` picks the status explicitly, and `--status approved` approves them as they land. The source file is always approved. Each locale row shows the status its cells landed in, and the output counts approved translations that went back to review because this push changed their source text.
157
157
  - Without review, `push` approves everything it writes, and `--status draft` / `--status needs-review` are refused (`REVIEW_DISABLED`).
158
158
  - `publish` to an environment with **Publish approved text only** ships the last approved text of each translation. The output adds how many translations were **carried over** (the approved text went out instead of a newer, unapproved edit) and how many were **withheld** (never approved, so left out). Publishing still succeeds; `--fail-on-withheld` exits 5 when anything was withheld, for pipelines that must ship everything reviewed.
159
+ - `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.
159
160
  - `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.
160
161
 
161
162
  ## CI (GitHub Actions)
@@ -234,7 +235,7 @@ Pull and publish on deploy:
234
235
  | 3 | Plan quota exceeded |
235
236
  | 4 | Network failure, server error, push too large, CLI too old, transaction conflict, or rate limited after 3 retries |
236
237
  | 5 | `publish --fail-on-withheld`: the publish went through, but the review gate withheld some translations |
237
- | 6 | `lint` found placeholder errors, or more warnings than `--max-warnings` |
238
+ | 6 | Placeholder errors: `lint` found them (or more warnings than `--max-warnings`), or `publish` refused because translations with errors would go live |
238
239
 
239
240
  The server's error `code` decides the exit code first; the HTTP status is only used for codes the CLI does not know.
240
241
 
@@ -1,15 +1,37 @@
1
- import { EXIT } from '../errors.js';
1
+ import { CliError, EXIT } from '../errors.js';
2
+ import { renderIssueCells } from '../render.js';
3
+ const isInvalidCells = (details) => typeof details === 'object' && details !== null && details.code === 'INVALID_CELLS';
4
+ /** The server's one-line refusal, the cells grouped like `lint`, and the way out. */
5
+ const refusal = (body, config) => {
6
+ const cells = Array.isArray(body.cells) ? body.cells : [];
7
+ const lines = [
8
+ `Publish refused: ${body.total} translation(s) with placeholder errors would go live. Nothing was published.`,
9
+ ...renderIssueCells(cells, config),
10
+ ];
11
+ if (body.total > cells.length)
12
+ lines.push(`Showing first ${cells.length} of ${body.total}.`);
13
+ lines.push('Fix them (translify lint lists every issue) or re-run with --allow-errors.');
14
+ return new CliError(EXIT.issues, lines.join('\n'), body);
15
+ };
2
16
  export async function runPublish(environment, deps) {
3
- const res = await deps.api.post('/cli/v1/publish', { environment });
17
+ // Only send the flag when it is set: a server older than the gate rejects unknown fields.
18
+ const request = deps.allowErrors === true ? { environment, allowErrors: true } : { environment };
19
+ const res = await deps.api.post('/cli/v1/publish', request).catch((error) => {
20
+ if (error instanceof CliError && isInvalidCells(error.details))
21
+ throw refusal(error.details, deps.config);
22
+ throw error;
23
+ });
4
24
  const carriedOverCount = res.carriedOverCount ?? 0;
5
25
  const withheldCount = res.withheldCount ?? 0;
26
+ const errorCount = res.errorCount ?? 0;
6
27
  // An ungated environment reports 0 / 0; keep its output as it was before review state.
7
28
  const gate = carriedOverCount > 0 || withheldCount > 0 ? ` ${carriedOverCount} carried over, ${withheldCount} withheld.` : '';
29
+ const errors = errorCount > 0 ? ` ${errorCount} with placeholder errors.` : '';
8
30
  deps.out(deps.json === true
9
31
  ? JSON.stringify({
10
32
  response: res,
11
- summary: { environment, publishedCount: res.publishedCount, carriedOverCount, withheldCount },
33
+ summary: { environment, publishedCount: res.publishedCount, carriedOverCount, withheldCount, errorCount },
12
34
  })
13
- : `Published ${res.publishedCount} translation(s) to ${environment}.${gate}`);
35
+ : `Published ${res.publishedCount} translation(s) to ${environment}.${gate}${errors}`);
14
36
  return deps.failOnWithheld === true && withheldCount > 0 ? EXIT.withheld : EXIT.ok;
15
37
  }
package/dist/http.js CHANGED
@@ -32,6 +32,7 @@ const CODE_EXIT = {
32
32
  RATE_LIMITED: EXIT.network,
33
33
  REVIEW_DISABLED: EXIT.failed,
34
34
  APPROVED_ONLY_REQUIRES_WORKING: EXIT.failed,
35
+ INVALID_CELLS: EXIT.issues,
35
36
  };
36
37
  const exitFor = (status, code) => {
37
38
  const byCode = code === undefined ? undefined : CODE_EXIT[code];
@@ -88,6 +89,8 @@ const describeBody = (status, body) => {
88
89
  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.';
89
90
  case 'APPROVED_ONLY_REQUIRES_WORKING':
90
91
  return ONLY_APPROVED_NEEDS_WORKING;
92
+ case 'INVALID_CELLS':
93
+ return `Publish refused: ${String(body.total)} translation(s) with placeholder errors would go live. Nothing was published.`;
91
94
  case 'TRANSACTION_CONFLICT':
92
95
  return 'The server hit a transaction conflict. Try again.';
93
96
  case 'RATE_LIMITED':
package/dist/index.js CHANGED
@@ -106,13 +106,17 @@ program
106
106
  .command('publish <environment>')
107
107
  .description('Publish an environment')
108
108
  .option('--fail-on-withheld', 'exit 5 when the review gate withheld any translation (the publish still happens)')
109
+ .option('--allow-errors', 'publish even when translations with placeholder errors would go live (otherwise refused, exit 6)')
109
110
  .option('--json', 'machine-readable output')
110
111
  .action(async (environment, opts) => {
112
+ const { api, config } = await withApi();
111
113
  process.exitCode = await runPublish(environment, {
112
- api: (await withApi()).api,
114
+ api,
115
+ config,
113
116
  out,
114
117
  json: opts.json,
115
118
  failOnWithheld: opts.failOnWithheld,
119
+ allowErrors: opts.allowErrors,
116
120
  });
117
121
  });
118
122
  program
package/dist/render.js CHANGED
@@ -109,14 +109,11 @@ const cut = (value) => (value.length > MAX_VALUE ? `${value.slice(0, MAX_VALUE -
109
109
  * ESLint-style: namespace, then key, then one line per issue with the locale as named on disk.
110
110
  * Rows arrive sorted by the server; only the grouping headers are added here.
111
111
  */
112
- export function renderLint(res, config, view = {}) {
113
- const { errors, warnings } = res.summary;
114
- if (errors + warnings === 0)
115
- return pc.green(`✔ No issues in ${res.summary.locales.length} locale(s).`);
112
+ export function renderIssueCells(cells, config) {
116
113
  const lines = [];
117
114
  let namespace;
118
115
  let key;
119
- for (const cell of res.cells) {
116
+ for (const cell of cells) {
120
117
  if (cell.namespace !== namespace) {
121
118
  namespace = cell.namespace;
122
119
  key = undefined;
@@ -126,14 +123,22 @@ export function renderLint(res, config, view = {}) {
126
123
  key = cell.key;
127
124
  lines.push(` ${key}`);
128
125
  }
129
- const locale = toFsLocale(cell.locale, config).padEnd(8);
126
+ const locale = (config === undefined ? cell.locale : toFsLocale(cell.locale, config)).padEnd(8);
130
127
  for (const issue of cell.issues) {
131
128
  const label = issue.severity === 'ERROR' ? pc.red('error'.padEnd(8)) : pc.yellow('warning'.padEnd(8));
132
129
  lines.push(` ${locale} ${label} ${issueText(issue)}`);
133
- if (issue.code === 'ICU_INVALID')
130
+ if (issue.code === 'ICU_INVALID' && cell.value !== undefined) {
134
131
  lines.push(`${VALUE_INDENT}${pc.dim(JSON.stringify(cut(cell.value)))}`);
132
+ }
135
133
  }
136
134
  }
135
+ return lines;
136
+ }
137
+ export function renderLint(res, config, view = {}) {
138
+ const { errors, warnings } = res.summary;
139
+ if (errors + warnings === 0)
140
+ return pc.green(`✔ No issues in ${res.summary.locales.length} locale(s).`);
141
+ const lines = renderIssueCells(res.cells, config);
137
142
  if (res.truncated) {
138
143
  const matched = view.severity === 'error' ? errors : view.severity === 'warning' ? warnings : errors + warnings;
139
144
  lines.push(pc.yellow(`Showing first ${res.cells.length} of ${matched} cells. Narrow with --namespace / --locale, or use --json.`));
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.3.0';
2
+ export const VERSION = '0.4.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.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Push, pull and publish Translify translations from your repository",
5
5
  "license": "MIT",
6
6
  "repository": {