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