@justanarthur/payload-plugin-translator 3.1.0 → 3.2.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/dist/rsc.js CHANGED
@@ -1,10 +1,11 @@
1
- import"./shared/chunk-9d433kmv.js";
1
+ import"./shared/chunk-d0n3f8ad.js";
2
2
  import {
3
3
  readLocales,
4
4
  loadCollectionReview,
5
5
  loadGlobalsReview,
6
- loadEntityLocaleReview
7
- } from "./shared/chunk-15n5q7wd.js";
6
+ loadEntityLocaleReview,
7
+ loadJobsProgress
8
+ } from "./shared/chunk-4ftfmxgs.js";
8
9
  import {
9
10
  REVIEW_VIEW_PATH
10
11
  } from "./shared/chunk-31qgv1zk.js";
@@ -12,7 +13,7 @@ import {
12
13
  // src/review/view/TranslationsView.tsx
13
14
  import { DefaultTemplate } from "@payloadcms/next/templates";
14
15
  import { Gutter } from "@payloadcms/ui";
15
- import { ReviewStyles } from "@justanarthur/payload-plugin-translator/client";
16
+ import { BulkActions } from "@justanarthur/payload-plugin-translator/client";
16
17
 
17
18
  // src/review/view/Detail.tsx
18
19
  import { ReviewActions } from "@justanarthur/payload-plugin-translator/client";
@@ -30,38 +31,50 @@ var editHref = (adminRoute, { collectionSlug, globalSlug, id }, locale) => `${fo
30
31
 
31
32
  // src/review/view/Detail.tsx
32
33
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
33
- var STATE_LABEL = {
34
- identical: "same as source",
35
- missing: "missing",
36
- ok: "translated",
37
- placeholders: "placeholders differ"
34
+ var STATE = {
35
+ identical: { label: "same as source", tone: "warn" },
36
+ missing: { label: "missing", tone: "bad" },
37
+ ok: { label: "translated", tone: "ok" },
38
+ placeholders: { label: "placeholders differ", tone: "warn" }
38
39
  };
39
40
  var preview = (text) => text.length > 600 ? `${text.slice(0, 597)}…` : text;
40
41
  var readablePath = (path) => path.replace(/\[(\d+)\]/g, (_, index) => ` ${Number(index) + 1}`).replace(/\(([^)]+)\)/g, " · $1").replace(/#(\d+)$/, (_, index) => ` (part ${Number(index) + 1})`).split(".").join(" › ");
41
- var Detail = ({ adminRoute, review, onlyIssues, tab }) => {
42
+ var Detail = ({ adminRoute, review, onlyIssues, tab, page }) => {
42
43
  const rows = onlyIssues ? review.fields.filter((field) => field.state !== "ok") : review.fields;
43
44
  const { summary } = review;
45
+ const tone = summary.slugMissing || summary.coverage < 50 ? "bad" : summary.coverage < 100 ? "warn" : "ok";
46
+ const counters = [
47
+ { label: "translated", value: `${summary.coverage}%`, note: `${summary.ok} of ${summary.total} fields`, bar: true },
48
+ { label: "missing", value: summary.missing, note: "empty in this locale" },
49
+ { label: "same as source", value: summary.identical, note: `identical to ${review.defaultLocale}` },
50
+ { label: "placeholders", value: summary.placeholders, note: "differ from source" }
51
+ ];
44
52
  return /* @__PURE__ */ jsxs(Fragment, {
45
53
  children: [
46
54
  /* @__PURE__ */ jsxs("div", {
47
- className: "translator-review__header",
55
+ className: "tr__head",
48
56
  children: [
49
57
  /* @__PURE__ */ jsxs("div", {
50
58
  children: [
51
59
  /* @__PURE__ */ jsxs("a", {
52
- href: reviewHref(adminRoute, { tab }),
60
+ className: "tr__back",
61
+ href: reviewHref(adminRoute, { tab, page }),
53
62
  children: [
54
63
  "← All ",
55
- tab === "globals" ? "globals" : tab
64
+ tab
56
65
  ]
57
66
  }),
58
- /* @__PURE__ */ jsxs("h2", {
67
+ /* @__PURE__ */ jsx("h1", {
68
+ children: review.label
69
+ }),
70
+ /* @__PURE__ */ jsxs("p", {
71
+ className: "tr__lead",
59
72
  children: [
60
- review.label,
61
- " · ",
62
- review.defaultLocale,
73
+ review.defaultLocale.toUpperCase(),
63
74
  " → ",
64
- review.locale
75
+ review.locale.toUpperCase(),
76
+ " · ",
77
+ review.entity
65
78
  ]
66
79
  })
67
80
  ]
@@ -73,86 +86,106 @@ var Detail = ({ adminRoute, review, onlyIssues, tab }) => {
73
86
  })
74
87
  ]
75
88
  }),
89
+ /* @__PURE__ */ jsx("div", {
90
+ className: "tr__stats",
91
+ children: counters.map((counter) => /* @__PURE__ */ jsxs("div", {
92
+ className: "tr__stat",
93
+ children: [
94
+ /* @__PURE__ */ jsx("span", {
95
+ className: "tr__stat-label",
96
+ children: counter.label
97
+ }),
98
+ /* @__PURE__ */ jsx("span", {
99
+ className: "tr__stat-value",
100
+ children: counter.value
101
+ }),
102
+ counter.bar && /* @__PURE__ */ jsx("div", {
103
+ className: "tr__bar",
104
+ style: { ["--tr-tone"]: `var(--tr-${tone})` },
105
+ children: /* @__PURE__ */ jsx("span", {
106
+ style: { width: `${summary.coverage}%` }
107
+ })
108
+ }),
109
+ /* @__PURE__ */ jsx("span", {
110
+ className: "tr__stat-note",
111
+ children: counter.note
112
+ })
113
+ ]
114
+ }, counter.label))
115
+ }),
76
116
  /* @__PURE__ */ jsxs("div", {
77
- className: "translator-review__meta",
117
+ className: "tr__meta",
78
118
  children: [
79
- /* @__PURE__ */ jsxs("span", {
80
- children: [
81
- summary.ok,
82
- "/",
83
- summary.total,
84
- " translated (",
85
- summary.coverage,
86
- "%)"
87
- ]
88
- }),
89
- summary.missing > 0 && /* @__PURE__ */ jsxs("span", {
90
- children: [
91
- summary.missing,
92
- " missing"
93
- ]
94
- }),
95
- summary.identical > 0 && /* @__PURE__ */ jsxs("span", {
96
- children: [
97
- summary.identical,
98
- " same as ",
99
- review.defaultLocale
100
- ]
101
- }),
102
- summary.placeholders > 0 && /* @__PURE__ */ jsxs("span", {
103
- children: [
104
- summary.placeholders,
105
- " placeholder mismatches"
106
- ]
107
- }),
108
119
  summary.slugMissing && /* @__PURE__ */ jsxs("span", {
109
- className: "translator-review__error",
120
+ className: "tr__chip tr__chip--bad",
110
121
  children: [
111
- "no slug, the page 404s in ",
122
+ "no slug: the page 404s in ",
112
123
  review.locale
113
124
  ]
114
125
  }),
115
126
  review.stale && /* @__PURE__ */ jsx("span", {
116
- children: "source changed since the last translation"
127
+ className: "tr__chip",
128
+ children: "↻ source changed since the last translation"
117
129
  }),
118
130
  review.translatedAt && /* @__PURE__ */ jsxs("span", {
131
+ className: "tr__chip",
119
132
  children: [
120
133
  "translated ",
121
134
  new Date(review.translatedAt).toLocaleString()
122
135
  ]
123
136
  }),
124
137
  review.reviewedAt && /* @__PURE__ */ jsxs("span", {
138
+ className: "tr__chip",
125
139
  children: [
126
- "reviewed ",
140
+ "✓ reviewed ",
127
141
  new Date(review.reviewedAt).toLocaleString(),
128
142
  " by ",
129
143
  review.reviewedBy
130
144
  ]
131
145
  }),
132
146
  review.lastJob && /* @__PURE__ */ jsxs("span", {
133
- className: review.lastJob.error ? "translator-review__error" : undefined,
147
+ className: `tr__chip${review.lastJob.error ? " tr__chip--bad" : ""}`,
148
+ title: review.lastJob.error,
134
149
  children: [
135
- "last auto-translate job #",
150
+ "job #",
136
151
  review.lastJob.id,
137
152
  ": ",
138
- review.lastJob.error ? `failed — ${preview(review.lastJob.error)}` : review.lastJob.completedAt ? "completed" : review.lastJob.processing ? "running" : "queued"
153
+ review.lastJob.error ? `failed — ${preview(review.lastJob.error).slice(0, 80)}` : review.lastJob.completedAt ? "completed" : review.lastJob.processing ? "running" : "queued"
139
154
  ]
140
155
  }),
141
156
  /* @__PURE__ */ jsxs("a", {
157
+ className: "tr__chip",
142
158
  href: editHref(adminRoute, review, review.locale),
143
159
  children: [
144
160
  "Edit in ",
145
- review.locale
161
+ review.locale.toUpperCase(),
162
+ " ↗"
163
+ ]
164
+ })
165
+ ]
166
+ }),
167
+ /* @__PURE__ */ jsxs("div", {
168
+ className: "tr__toolbar",
169
+ children: [
170
+ /* @__PURE__ */ jsxs("span", {
171
+ className: "tr__filters",
172
+ children: [
173
+ rows.length,
174
+ " of ",
175
+ review.fields.length,
176
+ " fields"
146
177
  ]
147
178
  }),
148
179
  /* @__PURE__ */ jsx("a", {
149
- href: reviewHref(adminRoute, { tab, entity: review.entity, locale: review.locale, issues: onlyIssues ? undefined : 1 }),
150
- children: onlyIssues ? "Show all fields" : "Show only problems"
180
+ "aria-pressed": onlyIssues,
181
+ className: "tr__toggle",
182
+ href: reviewHref(adminRoute, { tab, page, entity: review.entity, locale: review.locale, issues: onlyIssues ? undefined : 1 }),
183
+ children: "Only problems"
151
184
  })
152
185
  ]
153
186
  }),
154
187
  /* @__PURE__ */ jsx("div", {
155
- className: "translator-review__scroll",
188
+ className: "tr__card",
156
189
  children: /* @__PURE__ */ jsxs("table", {
157
190
  children: [
158
191
  /* @__PURE__ */ jsx("thead", {
@@ -165,10 +198,10 @@ var Detail = ({ adminRoute, review, onlyIssues, tab }) => {
165
198
  children: "State"
166
199
  }),
167
200
  /* @__PURE__ */ jsx("th", {
168
- children: review.defaultLocale
201
+ children: review.defaultLocale.toUpperCase()
169
202
  }),
170
203
  /* @__PURE__ */ jsx("th", {
171
- children: review.locale
204
+ children: review.locale.toUpperCase()
172
205
  })
173
206
  ]
174
207
  })
@@ -178,28 +211,30 @@ var Detail = ({ adminRoute, review, onlyIssues, tab }) => {
178
211
  rows.map((field) => /* @__PURE__ */ jsxs("tr", {
179
212
  children: [
180
213
  /* @__PURE__ */ jsx("td", {
181
- className: "translator-review__path",
214
+ className: "tr__path",
182
215
  title: field.path,
183
216
  children: readablePath(field.path)
184
217
  }),
185
218
  /* @__PURE__ */ jsx("td", {
186
219
  children: /* @__PURE__ */ jsx("span", {
187
- className: `translator-review__state translator-review__state--${field.state}`,
188
- children: STATE_LABEL[field.state]
220
+ className: "tr__pill",
221
+ "data-tone": STATE[field.state].tone,
222
+ children: STATE[field.state].label
189
223
  })
190
224
  }),
191
225
  /* @__PURE__ */ jsx("td", {
192
- className: "translator-review__text",
226
+ className: "tr__text",
193
227
  children: preview(field.sourceText)
194
228
  }),
195
229
  /* @__PURE__ */ jsx("td", {
196
- className: `translator-review__text${field.targetText.trim() ? "" : " translator-review__text--empty"}`,
230
+ className: `tr__text${field.targetText.trim() ? "" : " tr__text--empty"}`,
197
231
  children: field.targetText.trim() ? preview(field.targetText) : "empty"
198
232
  })
199
233
  ]
200
234
  }, field.path)),
201
235
  rows.length === 0 && /* @__PURE__ */ jsx("tr", {
202
236
  children: /* @__PURE__ */ jsx("td", {
237
+ className: "tr__empty",
203
238
  colSpan: 4,
204
239
  children: "Every field is translated."
205
240
  })
@@ -215,24 +250,55 @@ var Detail = ({ adminRoute, review, onlyIssues, tab }) => {
215
250
 
216
251
  // src/review/view/Overview.tsx
217
252
  import { jsx as jsx2, jsxs as jsxs2, Fragment as Fragment2 } from "react/jsx-runtime";
218
- var cellTone = ({ summary, stale }) => [
219
- "translator-review__cell",
220
- summary.slugMissing || summary.coverage === 0 ? "translator-review__cell--missing" : summary.coverage === 100 ? "translator-review__cell--ok" : "translator-review__cell--partial",
221
- stale ? "translator-review__cell--stale" : ""
222
- ].filter(Boolean).join(" ");
223
- var hasIssue = ({ summary, stale, reviewed }) => summary.coverage < 100 || summary.slugMissing || stale || !reviewed;
224
- var Overview = ({ adminRoute, defaultLocale, entities, locales, onlyIssues, tab }) => {
225
- const rows = onlyIssues ? entities.filter((entity) => Object.values(entity.locales).some((each) => each.summary.coverage < 100 || each.summary.slugMissing || each.stale)) : entities;
226
- const totals = Object.fromEntries(locales.map((locale) => {
253
+ var toneOf = ({ summary }) => summary.slugMissing || summary.coverage < 50 ? "bad" : summary.coverage < 100 ? "warn" : "ok";
254
+ var coverageTone = (coverage) => coverage < 50 ? "bad" : coverage < 100 ? "warn" : "ok";
255
+ var isIncomplete = ({ summary, stale }) => summary.coverage < 100 || summary.slugMissing || stale;
256
+ var Overview = ({ adminRoute, defaultLocale, entities, locales, onlyIssues, tab, page }) => {
257
+ const rows = onlyIssues ? entities.filter((entity) => Object.values(entity.locales).some(isIncomplete)) : entities;
258
+ const stats = locales.map((locale) => {
227
259
  const reviews = entities.map((entity) => entity.locales[locale]);
228
260
  const fields = reviews.reduce((sum, each) => sum + each.summary.total, 0);
229
261
  const ok = reviews.reduce((sum, each) => sum + each.summary.ok, 0);
230
- return [locale, fields ? Math.floor(ok / fields * 100) : 100];
231
- }));
262
+ return {
263
+ locale,
264
+ coverage: fields ? Math.floor(ok / fields * 100) : 100,
265
+ incomplete: reviews.filter(isIncomplete).length
266
+ };
267
+ });
232
268
  return /* @__PURE__ */ jsxs2(Fragment2, {
233
269
  children: [
234
270
  /* @__PURE__ */ jsx2("div", {
235
- className: "translator-review__scroll",
271
+ className: "tr__stats",
272
+ children: stats.map(({ locale, coverage, incomplete }) => /* @__PURE__ */ jsxs2("div", {
273
+ className: "tr__stat",
274
+ children: [
275
+ /* @__PURE__ */ jsx2("span", {
276
+ className: "tr__stat-label",
277
+ children: locale
278
+ }),
279
+ /* @__PURE__ */ jsxs2("span", {
280
+ className: "tr__stat-value",
281
+ children: [
282
+ coverage,
283
+ "%"
284
+ ]
285
+ }),
286
+ /* @__PURE__ */ jsx2("div", {
287
+ className: "tr__bar",
288
+ style: { ["--tr-tone"]: `var(--tr-${coverageTone(coverage)})` },
289
+ children: /* @__PURE__ */ jsx2("span", {
290
+ style: { width: `${coverage}%` }
291
+ })
292
+ }),
293
+ /* @__PURE__ */ jsx2("span", {
294
+ className: "tr__stat-note",
295
+ children: incomplete ? `${incomplete} incomplete` : "all complete"
296
+ })
297
+ ]
298
+ }, locale))
299
+ }),
300
+ /* @__PURE__ */ jsx2("div", {
301
+ className: "tr__card",
236
302
  children: /* @__PURE__ */ jsxs2("table", {
237
303
  children: [
238
304
  /* @__PURE__ */ jsx2("thead", {
@@ -241,17 +307,9 @@ var Overview = ({ adminRoute, defaultLocale, entities, locales, onlyIssues, tab
241
307
  /* @__PURE__ */ jsx2("th", {
242
308
  children: "Document"
243
309
  }),
244
- locales.map((locale) => /* @__PURE__ */ jsxs2("th", {
245
- children: [
246
- locale,
247
- " ",
248
- /* @__PURE__ */ jsxs2("small", {
249
- children: [
250
- totals[locale],
251
- "%"
252
- ]
253
- })
254
- ]
310
+ locales.map((locale) => /* @__PURE__ */ jsx2("th", {
311
+ className: "tr__locale-col",
312
+ children: locale.toUpperCase()
255
313
  }, locale))
256
314
  ]
257
315
  })
@@ -261,37 +319,56 @@ var Overview = ({ adminRoute, defaultLocale, entities, locales, onlyIssues, tab
261
319
  rows.map((entity) => /* @__PURE__ */ jsxs2("tr", {
262
320
  children: [
263
321
  /* @__PURE__ */ jsxs2("td", {
264
- className: "translator-review__doc",
322
+ className: "tr__doc",
265
323
  children: [
266
324
  /* @__PURE__ */ jsx2("a", {
267
325
  href: editHref(adminRoute, entity, defaultLocale),
268
326
  children: entity.label
269
327
  }),
270
- /* @__PURE__ */ jsx2("small", {
328
+ /* @__PURE__ */ jsx2("span", {
329
+ className: "tr__key",
271
330
  children: entity.key
272
331
  })
273
332
  ]
274
333
  }),
275
334
  locales.map((locale) => {
276
335
  const review = entity.locales[locale];
336
+ const { summary } = review;
277
337
  const title = [
278
- `${review.summary.ok}/${review.summary.total} fields translated`,
279
- review.summary.missing && `${review.summary.missing} missing`,
280
- review.summary.identical && `${review.summary.identical} same as ${defaultLocale}`,
281
- review.summary.placeholders && `${review.summary.placeholders} broken placeholders`,
282
- review.summary.slugMissing && "no slug: page 404s",
283
- review.stale && `source changed since translation`,
338
+ `${summary.ok}/${summary.total} fields translated`,
339
+ summary.missing && `${summary.missing} missing`,
340
+ summary.identical && `${summary.identical} same as ${defaultLocale}`,
341
+ summary.placeholders && `${summary.placeholders} broken placeholders`,
342
+ summary.slugMissing && "no slug: the page 404s",
343
+ review.stale && "source changed since translation",
284
344
  review.reviewed && "reviewed"
285
345
  ].filter(Boolean).join(" · ");
286
346
  return /* @__PURE__ */ jsx2("td", {
347
+ className: "tr__locale-col",
287
348
  children: /* @__PURE__ */ jsxs2("a", {
288
- className: cellTone(review),
289
- href: reviewHref(adminRoute, { tab, entity: entity.key, locale, issues: hasIssue(review) ? 1 : undefined }),
349
+ className: "tr__cell",
350
+ "data-tone": toneOf(review),
351
+ href: reviewHref(adminRoute, { tab, page, entity: entity.key, locale, issues: isIncomplete(review) ? 1 : undefined }),
290
352
  title,
291
353
  children: [
292
- review.summary.slugMissing ? "404" : `${review.summary.coverage}%`,
293
- review.reviewed ? " ✓" : "",
294
- review.stale ? " ↻" : ""
354
+ /* @__PURE__ */ jsxs2("span", {
355
+ children: [
356
+ summary.slugMissing ? "404" : `${summary.coverage}%`,
357
+ (review.stale || review.reviewed) && /* @__PURE__ */ jsxs2("span", {
358
+ className: "tr__marks",
359
+ children: [
360
+ " ",
361
+ [review.stale && "↻", review.reviewed && "✓"].filter(Boolean).join(" ")
362
+ ]
363
+ })
364
+ ]
365
+ }),
366
+ /* @__PURE__ */ jsx2("span", {
367
+ className: "tr__bar",
368
+ children: /* @__PURE__ */ jsx2("span", {
369
+ style: { width: `${summary.slugMissing ? 100 : summary.coverage}%` }
370
+ })
371
+ })
295
372
  ]
296
373
  })
297
374
  }, locale);
@@ -300,8 +377,9 @@ var Overview = ({ adminRoute, defaultLocale, entities, locales, onlyIssues, tab
300
377
  }, entity.key)),
301
378
  rows.length === 0 && /* @__PURE__ */ jsx2("tr", {
302
379
  children: /* @__PURE__ */ jsx2("td", {
380
+ className: "tr__empty",
303
381
  colSpan: locales.length + 1,
304
- children: "Nothing to review here."
382
+ children: "Everything here is fully translated."
305
383
  })
306
384
  })
307
385
  ]
@@ -310,19 +388,40 @@ var Overview = ({ adminRoute, defaultLocale, entities, locales, onlyIssues, tab
310
388
  })
311
389
  }),
312
390
  /* @__PURE__ */ jsxs2("div", {
313
- className: "translator-review__legend",
391
+ className: "tr__legend",
314
392
  children: [
315
- /* @__PURE__ */ jsx2("span", {
316
- children: "% share of fields translated"
393
+ /* @__PURE__ */ jsxs2("span", {
394
+ children: [
395
+ /* @__PURE__ */ jsx2("i", {
396
+ className: "tr__dot",
397
+ style: { ["--tr-tone"]: "var(--tr-ok)" }
398
+ }),
399
+ "fully translated"
400
+ ]
317
401
  }),
318
- /* @__PURE__ */ jsx2("span", {
319
- children: "404 no slug in that locale"
402
+ /* @__PURE__ */ jsxs2("span", {
403
+ children: [
404
+ /* @__PURE__ */ jsx2("i", {
405
+ className: "tr__dot",
406
+ style: { ["--tr-tone"]: "var(--tr-warn)" }
407
+ }),
408
+ "partly translated"
409
+ ]
410
+ }),
411
+ /* @__PURE__ */ jsxs2("span", {
412
+ children: [
413
+ /* @__PURE__ */ jsx2("i", {
414
+ className: "tr__dot",
415
+ style: { ["--tr-tone"]: "var(--tr-bad)" }
416
+ }),
417
+ "under half, or 404 without a slug"
418
+ ]
320
419
  }),
321
420
  /* @__PURE__ */ jsx2("span", {
322
421
  children: "↻ source changed since the last translation"
323
422
  }),
324
423
  /* @__PURE__ */ jsx2("span", {
325
- children: "✓ reviewed against the current source"
424
+ children: "✓ reviewed"
326
425
  })
327
426
  ]
328
427
  })
@@ -330,8 +429,93 @@ var Overview = ({ adminRoute, defaultLocale, entities, locales, onlyIssues, tab
330
429
  });
331
430
  };
332
431
 
432
+ // src/review/view/styles.ts
433
+ var reviewStyles = `
434
+ .tr { --tr-ok: var(--theme-success-500); --tr-ok-bg: var(--theme-success-100); --tr-warn: var(--theme-warning-500); --tr-warn-bg: var(--theme-warning-100); --tr-bad: var(--theme-error-500); --tr-bad-bg: var(--theme-error-100); --tr-line: var(--theme-elevation-100); --tr-muted: var(--theme-elevation-500); --tr-card: var(--theme-elevation-50); padding-block: calc(var(--base) * 2) calc(var(--base) * 4); }
435
+ .tr a { text-decoration: none; color: inherit; }
436
+ .tr__head { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: var(--base); margin-bottom: calc(var(--base) * 1.5); }
437
+ .tr__head h1 { margin: 0 0 4px; }
438
+ .tr__lead { margin: 0; color: var(--tr-muted); max-width: 60ch; }
439
+ .tr__back { display: inline-flex; gap: 6px; color: var(--tr-muted); font-size: 13px; margin-bottom: 8px; }
440
+ .tr__back:hover { color: var(--theme-text); }
441
+
442
+ .tr__stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; margin-bottom: calc(var(--base) * 1.5); }
443
+ .tr__stat { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border: 1px solid var(--tr-line); border-radius: var(--style-radius-m); background: var(--tr-card); }
444
+ .tr__stat-label { font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--tr-muted); }
445
+ .tr__stat-value { font-size: 22px; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.1; }
446
+ .tr__stat-note { font-size: 12px; color: var(--tr-muted); }
447
+
448
+ .tr__bar { height: 4px; border-radius: 999px; background: var(--theme-elevation-150); overflow: hidden; }
449
+ .tr__bar > span { display: block; height: 100%; border-radius: inherit; background: var(--tr-tone, var(--tr-ok)); }
450
+
451
+ .tr__toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 12px; }
452
+ .tr__tabs { display: inline-flex; padding: 3px; gap: 2px; border-radius: 999px; background: var(--theme-elevation-100); }
453
+ .tr__tab { padding: 5px 14px; border-radius: 999px; font-size: 13px; color: var(--theme-elevation-700); text-transform: capitalize; }
454
+ .tr__tab:hover { color: var(--theme-text); }
455
+ .tr__tab[aria-current='true'] { background: var(--theme-elevation-0); color: var(--theme-text); box-shadow: 0 1px 2px rgba(0,0,0,.2); }
456
+ .tr__filters { display: inline-flex; align-items: center; gap: 12px; font-size: 13px; color: var(--tr-muted); }
457
+ .tr__toggle { display: inline-flex; align-items: center; gap: 8px; padding: 5px 12px; border: 1px solid var(--tr-line); border-radius: 999px; color: var(--theme-elevation-800); }
458
+ .tr__toggle::before { content: ''; width: 26px; height: 14px; border-radius: 999px; background: var(--theme-elevation-200) radial-gradient(circle at 7px 50%, var(--theme-elevation-0) 4px, transparent 4.5px); }
459
+ .tr__toggle[aria-pressed='true']::before { background: var(--tr-ok) radial-gradient(circle at 19px 50%, var(--theme-elevation-0) 4px, transparent 4.5px); }
460
+
461
+ .tr__bulk { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: calc(var(--base) * 1.25); padding: 10px 12px; border: 1px dashed var(--tr-line); border-radius: var(--style-radius-m); }
462
+ .tr__bulk-main, .tr__bulk-progress { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
463
+ .tr__bulk .btn { margin: 0; }
464
+ .tr__select { height: 28px; padding: 0 8px; border: 1px solid var(--tr-line); border-radius: var(--style-radius-s, 4px); background: var(--theme-input-bg, var(--theme-elevation-0)); color: var(--theme-text); font: inherit; font-size: 13px; }
465
+ .tr__card { border: 1px solid var(--tr-line); border-radius: var(--style-radius-m); overflow: auto; background: var(--theme-elevation-0); }
466
+ .tr table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 13px; }
467
+ .tr th { position: sticky; top: 0; z-index: 1; background: var(--tr-card); text-align: left; font-weight: 500; color: var(--tr-muted); padding: 10px 12px; border-bottom: 1px solid var(--tr-line); white-space: nowrap; }
468
+ .tr th small { display: block; font-size: 11px; font-weight: 400; }
469
+ .tr td { padding: 6px 12px; border-bottom: 1px solid var(--tr-line); vertical-align: middle; }
470
+ .tr tbody tr:last-child td { border-bottom: 0; }
471
+ .tr tbody tr:hover td { background: var(--theme-elevation-50); }
472
+ .tr__locale-col { text-align: center !important; }
473
+ .tr__doc { min-width: 240px; }
474
+ .tr__doc a { font-weight: 500; }
475
+ .tr__doc a:hover { text-decoration: underline; }
476
+ .tr__key { display: block; margin-top: 2px; font-family: var(--font-mono, ui-monospace, monospace); font-size: 11px; color: var(--tr-muted); }
477
+
478
+ .tr__cell { display: flex; flex-direction: column; gap: 4px; min-width: 64px; padding: 6px 8px; border-radius: 6px; text-align: center; font-variant-numeric: tabular-nums; font-weight: 500; transition: background .12s; }
479
+ .tr__cell:hover { background: var(--theme-elevation-100); }
480
+ .tr__cell[data-tone='ok'] { --tr-tone: var(--tr-ok); color: var(--tr-ok); }
481
+ .tr__cell[data-tone='warn'] { --tr-tone: var(--tr-warn); color: var(--tr-warn); }
482
+ .tr__cell[data-tone='bad'] { --tr-tone: var(--tr-bad); color: var(--tr-bad); }
483
+ .tr__marks { font-size: 11px; color: var(--tr-muted); }
484
+
485
+ .tr__legend { display: flex; flex-wrap: wrap; gap: 8px 18px; margin-top: 12px; font-size: 12px; color: var(--tr-muted); }
486
+ .tr__legend span { display: inline-flex; align-items: center; gap: 6px; }
487
+ .tr__dot { width: 8px; height: 8px; border-radius: 999px; background: var(--tr-tone); }
488
+
489
+ .tr__pager { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 12px; font-size: 13px; color: var(--tr-muted); }
490
+ .tr__pager nav { display: inline-flex; gap: 6px; }
491
+ .tr__pager a, .tr__pager span[aria-disabled] { padding: 5px 12px; border: 1px solid var(--tr-line); border-radius: 999px; color: var(--theme-elevation-800); }
492
+ .tr__pager span[aria-disabled] { opacity: .4; }
493
+ .tr__pager a:hover { background: var(--theme-elevation-100); }
494
+
495
+ .tr__pill { display: inline-flex; align-items: center; gap: 6px; padding: 2px 9px; border-radius: 999px; font-size: 12px; font-weight: 500; white-space: nowrap; background: var(--theme-elevation-100); }
496
+ .tr__pill::before { content: ''; width: 6px; height: 6px; border-radius: 999px; background: currentColor; }
497
+ .tr__pill[data-tone='ok'] { color: var(--tr-ok); background: var(--tr-ok-bg); }
498
+ .tr__pill[data-tone='warn'] { color: var(--tr-warn); background: var(--tr-warn-bg); }
499
+ .tr__pill[data-tone='bad'] { color: var(--tr-bad); background: var(--tr-bad-bg); }
500
+
501
+ .tr__meta { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
502
+ .tr__chip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border: 1px solid var(--tr-line); border-radius: 999px; font-size: 12px; color: var(--theme-elevation-800); }
503
+ .tr__chip--bad { color: var(--tr-bad); border-color: var(--tr-bad); }
504
+ a.tr__chip:hover { background: var(--theme-elevation-100); }
505
+
506
+ .tr__path { width: 22%; font-family: var(--font-mono, ui-monospace, monospace); font-size: 12px; color: var(--theme-elevation-700); word-break: break-word; }
507
+ .tr__text { width: 34%; white-space: pre-wrap; word-break: break-word; line-height: 1.45; }
508
+ .tr__text--empty { color: var(--tr-muted); font-style: italic; }
509
+ .tr__empty { padding: 32px; text-align: center; color: var(--tr-muted); }
510
+ .tr__error { color: var(--tr-bad); }
511
+
512
+ .translator-review__actions { display: flex; flex-wrap: wrap; gap: 8px; }
513
+ .translator-review__actions .btn { margin: 0; }
514
+ `;
515
+
333
516
  // src/review/view/TranslationsView.tsx
334
517
  import { jsx as jsx3, jsxs as jsxs3, Fragment as Fragment3 } from "react/jsx-runtime";
518
+ var PAGE_SIZE = 50;
335
519
  var param = (value) => Array.isArray(value) ? value[0] : value;
336
520
  var TranslationsView = async ({ initPageResult, params, searchParams }) => {
337
521
  const { req, permissions, visibleEntities, locale } = initPageResult;
@@ -350,6 +534,7 @@ var TranslationsView = async ({ initPageResult, params, searchParams }) => {
350
534
  const content = async () => {
351
535
  if (!req.user)
352
536
  return /* @__PURE__ */ jsx3("p", {
537
+ className: "tr__empty",
353
538
  children: "Log in to review translations."
354
539
  });
355
540
  if (!tab)
@@ -362,12 +547,13 @@ var TranslationsView = async ({ initPageResult, params, searchParams }) => {
362
547
  return /* @__PURE__ */ jsx3(Detail, {
363
548
  adminRoute,
364
549
  onlyIssues,
550
+ page,
365
551
  review,
366
552
  tab
367
553
  });
368
554
  } catch (error) {
369
555
  return /* @__PURE__ */ jsxs3("p", {
370
- className: "translator-review__error",
556
+ className: "tr__error",
371
557
  children: [
372
558
  "Could not load ",
373
559
  entity,
@@ -388,7 +574,10 @@ var TranslationsView = async ({ initPageResult, params, searchParams }) => {
388
574
  tab
389
575
  });
390
576
  }
391
- const review = await loadCollectionReview({ req, collectionSlug: tab, page });
577
+ const review = await loadCollectionReview({ req, collectionSlug: tab, page, limit: PAGE_SIZE });
578
+ const first = review.totalDocs ? (review.page - 1) * PAGE_SIZE + 1 : 0;
579
+ const last = Math.min(review.page * PAGE_SIZE, review.totalDocs);
580
+ const pageHref = (target) => reviewHref(adminRoute, { tab, page: target, issues: onlyIssues ? 1 : undefined });
392
581
  return /* @__PURE__ */ jsxs3(Fragment3, {
393
582
  children: [
394
583
  /* @__PURE__ */ jsx3(Overview, {
@@ -397,29 +586,47 @@ var TranslationsView = async ({ initPageResult, params, searchParams }) => {
397
586
  entities: review.entities,
398
587
  locales: review.locales,
399
588
  onlyIssues,
589
+ page,
400
590
  tab
401
591
  }),
402
- review.totalPages > 1 && /* @__PURE__ */ jsxs3("div", {
403
- className: "translator-review__pager",
592
+ /* @__PURE__ */ jsxs3("div", {
593
+ className: "tr__pager",
404
594
  children: [
405
- page > 1 && /* @__PURE__ */ jsx3("a", {
406
- href: reviewHref(adminRoute, { tab, page: page - 1, issues: onlyIssues ? 1 : undefined }),
407
- children: "← Newer"
408
- }),
409
595
  /* @__PURE__ */ jsxs3("span", {
410
596
  children: [
411
- "Page ",
412
- page,
597
+ first,
598
+ "–",
599
+ last,
413
600
  " of ",
414
- review.totalPages,
415
- " · ",
416
601
  review.totalDocs,
417
- " documents"
602
+ " documents",
603
+ onlyIssues ? " (complete ones hidden)" : ""
418
604
  ]
419
605
  }),
420
- page < review.totalPages && /* @__PURE__ */ jsx3("a", {
421
- href: reviewHref(adminRoute, { tab, page: page + 1, issues: onlyIssues ? 1 : undefined }),
422
- children: "Older →"
606
+ review.totalPages > 1 && /* @__PURE__ */ jsxs3("nav", {
607
+ children: [
608
+ review.page > 1 ? /* @__PURE__ */ jsx3("a", {
609
+ href: pageHref(review.page - 1),
610
+ children: "← Newer"
611
+ }) : /* @__PURE__ */ jsx3("span", {
612
+ "aria-disabled": true,
613
+ children: "← Newer"
614
+ }),
615
+ /* @__PURE__ */ jsxs3("span", {
616
+ children: [
617
+ review.page,
618
+ " / ",
619
+ review.totalPages
620
+ ]
621
+ }),
622
+ review.page < review.totalPages ? /* @__PURE__ */ jsx3("a", {
623
+ href: pageHref(review.page + 1),
624
+ children: "Older →"
625
+ }) : /* @__PURE__ */ jsx3("span", {
626
+ "aria-disabled": true,
627
+ children: "Older →"
628
+ })
629
+ ]
423
630
  })
424
631
  ]
425
632
  })
@@ -427,6 +634,7 @@ var TranslationsView = async ({ initPageResult, params, searchParams }) => {
427
634
  });
428
635
  };
429
636
  const adminLocale = req.locale;
637
+ const progress = entity || !req.user ? null : await loadJobsProgress(req);
430
638
  const body = await content();
431
639
  req.locale = adminLocale;
432
640
  return /* @__PURE__ */ jsxs3(DefaultTemplate, {
@@ -439,32 +647,56 @@ var TranslationsView = async ({ initPageResult, params, searchParams }) => {
439
647
  user: req.user || undefined,
440
648
  visibleEntities,
441
649
  children: [
442
- /* @__PURE__ */ jsx3(ReviewStyles, {}),
650
+ /* @__PURE__ */ jsx3("style", {
651
+ dangerouslySetInnerHTML: { __html: reviewStyles }
652
+ }),
443
653
  /* @__PURE__ */ jsxs3(Gutter, {
444
- className: "translator-review",
654
+ className: "tr",
445
655
  children: [
446
656
  !entity && /* @__PURE__ */ jsxs3(Fragment3, {
447
657
  children: [
658
+ /* @__PURE__ */ jsx3("div", {
659
+ className: "tr__head",
660
+ children: /* @__PURE__ */ jsxs3("div", {
661
+ children: [
662
+ /* @__PURE__ */ jsx3("h1", {
663
+ children: "Translations"
664
+ }),
665
+ /* @__PURE__ */ jsxs3("p", {
666
+ className: "tr__lead",
667
+ children: [
668
+ "How much of each document is translated from ",
669
+ defaultLocale.toUpperCase(),
670
+ ". Open a cell to compare it field by field."
671
+ ]
672
+ })
673
+ ]
674
+ })
675
+ }),
448
676
  /* @__PURE__ */ jsxs3("div", {
449
- className: "translator-review__header",
677
+ className: "tr__toolbar",
450
678
  children: [
451
- /* @__PURE__ */ jsx3("h1", {
452
- children: "Translations"
679
+ /* @__PURE__ */ jsx3("nav", {
680
+ className: "tr__tabs",
681
+ children: tabs.map((each) => /* @__PURE__ */ jsx3("a", {
682
+ "aria-current": each === tab,
683
+ className: "tr__tab",
684
+ href: reviewHref(adminRoute, { tab: each, issues: onlyIssues ? 1 : undefined }),
685
+ children: each
686
+ }, each))
453
687
  }),
454
688
  /* @__PURE__ */ jsx3("a", {
689
+ "aria-pressed": onlyIssues,
690
+ className: "tr__toggle",
455
691
  href: reviewHref(adminRoute, { tab, issues: onlyIssues ? undefined : 1 }),
456
- children: onlyIssues ? "Show everything" : "Show only incomplete"
692
+ children: "Only incomplete"
457
693
  })
458
694
  ]
459
695
  }),
460
- /* @__PURE__ */ jsx3("nav", {
461
- className: "translator-review__tabs",
462
- children: tabs.map((each) => /* @__PURE__ */ jsx3("a", {
463
- "aria-current": each === tab,
464
- className: "translator-review__tab",
465
- href: reviewHref(adminRoute, { tab: each, issues: onlyIssues ? 1 : undefined }),
466
- children: each
467
- }, each))
696
+ req.user && tab && /* @__PURE__ */ jsx3(BulkActions, {
697
+ locales: targetLocales,
698
+ progress,
699
+ tab
468
700
  })
469
701
  ]
470
702
  }),