@eifi1/ui-kit 0.18.0 → 0.19.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 +60 -6
- package/dist/chart.d.ts +4 -1
- package/dist/components/amount-input.d.ts +4 -1
- package/dist/components/button-group.d.ts +4 -1
- package/dist/components/calculator.d.ts +4 -1
- package/dist/components/copy-button.d.ts +4 -1
- package/dist/components/facing-pair.d.ts +4 -1
- package/dist/components/file-button.d.ts +4 -1
- package/dist/components/file-dropzone.d.ts +4 -1
- package/dist/components/floating-panel.d.ts +4 -1
- package/dist/components/floating-panel.js +15 -1
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +4 -1
- package/dist/components/legal.d.ts +84 -0
- package/dist/components/legal.js +55 -0
- package/dist/components/legal.js.map +1 -0
- package/dist/components/money-field.d.ts +4 -1
- package/dist/components/number-field.d.ts +4 -1
- package/dist/components/number-input.d.ts +4 -1
- package/dist/components/numpad-sheet.d.ts +4 -1
- package/dist/components/series-chart.d.ts +4 -1
- package/dist/components/server-wake.d.ts +4 -2
- package/dist/components/server-wake.js +23 -2
- package/dist/components/server-wake.js.map +1 -1
- package/dist/components/settings-fields.d.ts +4 -1
- package/dist/components/share-card.d.ts +4 -1
- package/dist/components/text-link.d.ts +4 -1
- package/dist/components/time-input.d.ts +4 -1
- package/dist/components/translation-review-editor.d.ts +56 -0
- package/dist/components/translation-review-editor.js +159 -0
- package/dist/components/translation-review-editor.js.map +1 -0
- package/dist/components/translation-review-labels.d.ts +92 -0
- package/dist/components/translation-review-labels.js +67 -0
- package/dist/components/translation-review-labels.js.map +1 -0
- package/dist/components/translation-review-parts.d.ts +97 -0
- package/dist/components/translation-review-parts.js +163 -0
- package/dist/components/translation-review-parts.js.map +1 -0
- package/dist/components/translation-review.d.ts +94 -0
- package/dist/components/translation-review.js +344 -0
- package/dist/components/translation-review.js.map +1 -0
- package/dist/components/ui.d.ts +4 -1
- package/dist/hooks/use-file-drop.d.ts +4 -1
- package/dist/i18n/defaults.d.ts +4 -1
- package/dist/i18n/defaults.js +5 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +4 -1
- package/dist/i18n/german.js +51 -0
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +4 -1
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +243 -0
- package/dist/i18n/languages.js +70 -0
- package/dist/i18n/languages.js.map +1 -0
- package/dist/i18n/locales/de-CH.d.ts +4 -1
- package/dist/i18n/locales/en.d.ts +4 -1
- package/dist/i18n/locales/en.js +8 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +4 -1
- package/dist/i18n/locales/es.js +83 -33
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +4 -1
- package/dist/i18n/locales/fr.js +50 -0
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +4 -1
- package/dist/i18n/locales/hu.js +51 -0
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +4 -1
- package/dist/i18n/locales/it.js +90 -40
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +4 -1
- package/dist/i18n/locales/zh.js +51 -0
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +102 -0
- package/dist/i18n/review.js +158 -0
- package/dist/i18n/review.js.map +1 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +64 -0
- package/dist/index.js.map +1 -1
- package/dist/{kit-labels-CPbsjlXr.d.ts → kit-labels-D1J2_jyD.d.ts} +7 -0
- package/dist/lib/floating-stack.d.ts +31 -0
- package/dist/lib/floating-stack.js +42 -0
- package/dist/lib/floating-stack.js.map +1 -0
- package/dist/lib/server-wake.d.ts +24 -8
- package/dist/lib/server-wake.js +6 -5
- package/dist/lib/server-wake.js.map +1 -1
- package/dist/lib/translation-review.d.ts +232 -0
- package/dist/lib/translation-review.js +194 -0
- package/dist/lib/translation-review.js.map +1 -0
- package/dist/rhf/fields.d.ts +4 -1
- package/dist/rhf/form.d.ts +4 -1
- package/dist/rhf.d.ts +4 -1
- package/dist/shell/app-shell.d.ts +4 -1
- package/dist/shell/top-bar-brand.d.ts +4 -1
- package/dist/shell.d.ts +4 -1
- package/dist/wizard/stepper-nav.d.ts +4 -1
- package/dist/wizard.d.ts +4 -1
- package/package.json +1 -1
- package/src/components/floating-panel.tsx +19 -1
- package/src/components/legal.tsx +159 -0
- package/src/components/server-wake.tsx +37 -5
- package/src/components/translation-review-editor.tsx +261 -0
- package/src/components/translation-review-labels.ts +152 -0
- package/src/components/translation-review-parts.tsx +269 -0
- package/src/components/translation-review.tsx +502 -0
- package/src/i18n/defaults.ts +4 -0
- package/src/i18n/german.ts +52 -0
- package/src/i18n/kit-labels.tsx +7 -0
- package/src/i18n/languages.ts +285 -0
- package/src/i18n/locales/en.ts +8 -0
- package/src/i18n/locales/es.ts +84 -33
- package/src/i18n/locales/fr.ts +51 -0
- package/src/i18n/locales/hu.ts +52 -0
- package/src/i18n/locales/it.ts +91 -40
- package/src/i18n/locales/zh.ts +52 -0
- package/src/i18n/review.ts +193 -0
- package/src/index.ts +63 -0
- package/src/lib/floating-stack.ts +66 -0
- package/src/lib/server-wake.ts +34 -13
- package/src/lib/translation-review.ts +465 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { useKitFormat } from "../lib/format.js";
|
|
7
|
+
import { placeholderTokens, reviewWrite } from "../lib/translation-review.js";
|
|
8
|
+
import { AlertBanner } from "./alert-banner.js";
|
|
9
|
+
import { Caption, SectionLabel } from "./text.js";
|
|
10
|
+
import { DEFAULT_TRANSLATION_REVIEW_LABELS } from "./translation-review-labels.js";
|
|
11
|
+
import { Button, Textarea } from "./ui.js";
|
|
12
|
+
function Then({ label, text }) {
|
|
13
|
+
return /* @__PURE__ */ jsxs("div", { className: "mt-1.5 min-w-0", children: [
|
|
14
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium", children: label }),
|
|
15
|
+
/* @__PURE__ */ jsx("p", { className: "whitespace-pre-line break-words text-[var(--text-secondary)]", children: text })
|
|
16
|
+
] });
|
|
17
|
+
}
|
|
18
|
+
function TranslationReviewEditor({
|
|
19
|
+
row,
|
|
20
|
+
referenceLabel,
|
|
21
|
+
localeLabel,
|
|
22
|
+
onSave,
|
|
23
|
+
onClear,
|
|
24
|
+
onClose,
|
|
25
|
+
readOnly = false,
|
|
26
|
+
formatDate: formatDateProp,
|
|
27
|
+
formatError,
|
|
28
|
+
className,
|
|
29
|
+
labels: labelsProp
|
|
30
|
+
}) {
|
|
31
|
+
const labels = useKitLabels("translationReview", DEFAULT_TRANSLATION_REVIEW_LABELS, labelsProp);
|
|
32
|
+
const { formatDate } = useKitFormat();
|
|
33
|
+
const review = row.review;
|
|
34
|
+
const missing = row.text === "";
|
|
35
|
+
const editable = !readOnly && onSave !== void 0;
|
|
36
|
+
const [suggestion, setSuggestion] = useState(review?.suggestion ?? row.text);
|
|
37
|
+
const [note, setNote] = useState(review?.note ?? "");
|
|
38
|
+
const [busy, setBusy] = useState(null);
|
|
39
|
+
const [failure, setFailure] = useState(null);
|
|
40
|
+
const canFlag = note.trim() !== "" || suggestion.trim() !== row.text.trim();
|
|
41
|
+
const changed = row.status === "changed" ? review : null;
|
|
42
|
+
const date = (iso) => formatDateProp ? formatDateProp(iso) : formatDate(iso, "medium");
|
|
43
|
+
const run = async (kind, action) => {
|
|
44
|
+
setBusy(kind);
|
|
45
|
+
setFailure(null);
|
|
46
|
+
try {
|
|
47
|
+
await action();
|
|
48
|
+
onClose?.();
|
|
49
|
+
} catch (caught) {
|
|
50
|
+
setFailure(formatError ? formatError(caught) : labels.failed);
|
|
51
|
+
} finally {
|
|
52
|
+
setBusy(null);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const save = (verdict) => {
|
|
56
|
+
if (!onSave) return;
|
|
57
|
+
const trimmedNote = note.trim() || null;
|
|
58
|
+
const sent = verdict === "APPROVED" ? null : suggestion.trim() || null;
|
|
59
|
+
void run(
|
|
60
|
+
verdict === "APPROVED" ? "approve" : "flag",
|
|
61
|
+
() => onSave(reviewWrite(row, verdict, trimmedNote, sent))
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("min-w-0 space-y-3 text-sm", className), children: [
|
|
65
|
+
/* @__PURE__ */ jsx("code", { className: "block break-all text-xs text-[var(--text-muted)]", children: row.key }),
|
|
66
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-3 md:grid-cols-2", children: [
|
|
67
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
68
|
+
/* @__PURE__ */ jsx(SectionLabel, { as: "span", size: "xs", children: referenceLabel }),
|
|
69
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 whitespace-pre-line break-words text-[var(--text-secondary)]", children: row.reference })
|
|
70
|
+
] }),
|
|
71
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
72
|
+
/* @__PURE__ */ jsx(SectionLabel, { as: "span", size: "xs", children: localeLabel }),
|
|
73
|
+
missing ? /* @__PURE__ */ jsx("p", { className: "mt-1 italic text-[var(--text-muted)]", children: labels.missingText }) : /* @__PURE__ */ jsx("p", { lang: row.locale, className: "mt-1 whitespace-pre-line break-words", children: row.text })
|
|
74
|
+
] })
|
|
75
|
+
] }),
|
|
76
|
+
changed && /* @__PURE__ */ jsx(AlertBanner, { tone: "warning", variant: "inline", size: "sm", block: true, live: false, children: /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
77
|
+
/* @__PURE__ */ jsx("p", { children: labels.changedSince }),
|
|
78
|
+
changed.text !== row.text && /* @__PURE__ */ jsx(Then, { label: labels.reviewedWording, text: changed.text || "\u2014" }),
|
|
79
|
+
changed.referenceText != null && changed.referenceText !== row.reference && /* @__PURE__ */ jsx(Then, { label: labels.reviewedReference, text: changed.referenceText })
|
|
80
|
+
] }) }),
|
|
81
|
+
row.placeholderMismatch && /* @__PURE__ */ jsx(AlertBanner, { tone: "danger", variant: "inline", size: "sm", block: true, live: false, children: /* @__PURE__ */ jsx("span", { className: "min-w-0 break-words", children: labels.placeholderMismatch(
|
|
82
|
+
placeholderTokens(row.reference).join(" ") || "\u2014",
|
|
83
|
+
placeholderTokens(row.text).join(" ") || "\u2014"
|
|
84
|
+
) }) }),
|
|
85
|
+
review && /* @__PURE__ */ jsx(Caption, { children: (review.verdict === "APPROVED" ? labels.lastApproved : labels.lastFlagged)(
|
|
86
|
+
review.reviewerName ?? labels.erasedReviewer,
|
|
87
|
+
date(review.reviewedAt)
|
|
88
|
+
) }),
|
|
89
|
+
!editable && review?.verdict === "NEEDS_CHANGE" && (review.suggestion || review.note) && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
90
|
+
review.suggestion && /* @__PURE__ */ jsx(Then, { label: missing ? labels.translation : labels.suggestion, text: review.suggestion }),
|
|
91
|
+
review.note && /* @__PURE__ */ jsx(Then, { label: labels.note, text: review.note })
|
|
92
|
+
] }),
|
|
93
|
+
editable && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
94
|
+
/* @__PURE__ */ jsx(
|
|
95
|
+
Textarea,
|
|
96
|
+
{
|
|
97
|
+
label: missing ? labels.translation : labels.suggestion,
|
|
98
|
+
lang: row.locale,
|
|
99
|
+
rows: Math.min(8, Math.max(2, Math.ceil(Math.max(row.text.length, row.reference.length) / 80))),
|
|
100
|
+
value: suggestion,
|
|
101
|
+
disabled: busy !== null,
|
|
102
|
+
onChange: (e) => setSuggestion(e.target.value)
|
|
103
|
+
}
|
|
104
|
+
),
|
|
105
|
+
/* @__PURE__ */ jsx(
|
|
106
|
+
Textarea,
|
|
107
|
+
{
|
|
108
|
+
label: labels.note,
|
|
109
|
+
rows: 2,
|
|
110
|
+
value: note,
|
|
111
|
+
placeholder: labels.notePlaceholder,
|
|
112
|
+
disabled: busy !== null,
|
|
113
|
+
onChange: (e) => setNote(e.target.value)
|
|
114
|
+
}
|
|
115
|
+
),
|
|
116
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
117
|
+
!missing && /* @__PURE__ */ jsx(
|
|
118
|
+
Button,
|
|
119
|
+
{
|
|
120
|
+
variant: "primary",
|
|
121
|
+
commit: true,
|
|
122
|
+
pending: busy === "approve",
|
|
123
|
+
disabled: busy !== null,
|
|
124
|
+
onClick: () => save("APPROVED"),
|
|
125
|
+
children: labels.approve
|
|
126
|
+
}
|
|
127
|
+
),
|
|
128
|
+
/* @__PURE__ */ jsx(
|
|
129
|
+
Button,
|
|
130
|
+
{
|
|
131
|
+
variant: missing ? "primary" : "danger",
|
|
132
|
+
commit: true,
|
|
133
|
+
pending: busy === "flag",
|
|
134
|
+
disabled: busy !== null || !canFlag,
|
|
135
|
+
onClick: () => save("NEEDS_CHANGE"),
|
|
136
|
+
children: missing ? labels.suggest : labels.flag
|
|
137
|
+
}
|
|
138
|
+
),
|
|
139
|
+
review && onClear && /* @__PURE__ */ jsx(
|
|
140
|
+
Button,
|
|
141
|
+
{
|
|
142
|
+
variant: "ghost",
|
|
143
|
+
commit: true,
|
|
144
|
+
pending: busy === "clear",
|
|
145
|
+
disabled: busy !== null,
|
|
146
|
+
onClick: () => void run("clear", () => onClear({ locale: row.locale, key: row.key })),
|
|
147
|
+
children: labels.reset
|
|
148
|
+
}
|
|
149
|
+
),
|
|
150
|
+
onClose && /* @__PURE__ */ jsx(Button, { variant: "ghost", disabled: busy !== null, onClick: onClose, children: labels.cancel })
|
|
151
|
+
] })
|
|
152
|
+
] }),
|
|
153
|
+
failure && /* @__PURE__ */ jsx(AlertBanner, { tone: "danger", size: "sm", role: "alert", children: failure })
|
|
154
|
+
] });
|
|
155
|
+
}
|
|
156
|
+
export {
|
|
157
|
+
TranslationReviewEditor
|
|
158
|
+
};
|
|
159
|
+
//# sourceMappingURL=translation-review-editor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/translation-review-editor.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { useKitFormat } from \"../lib/format\";\nimport { placeholderTokens, reviewWrite } from \"../lib/translation-review\";\nimport type {\n TranslationReviewKey,\n TranslationReviewWrite,\n TranslationRow,\n TranslationVerdict,\n} from \"../lib/translation-review\";\nimport { AlertBanner } from \"./alert-banner\";\nimport { Caption, SectionLabel } from \"./text\";\nimport { DEFAULT_TRANSLATION_REVIEW_LABELS } from \"./translation-review-labels\";\nimport type { TranslationReviewLabels } from \"./translation-review-labels\";\nimport { Button, Textarea } from \"./ui\";\n\n/** A callback may return a promise: resolve and the editor closes, reject and it stays\n * open with the error. */\ntype MaybePromise = void | Promise<unknown>;\n\nexport interface TranslationReviewEditorProps {\n row: TranslationRow;\n /** The reference language's own name (\"English\", \"Deutsch\") — the app's choice of\n * reference, so the app's word for it. */\n referenceLabel: ReactNode;\n /** The reviewed language's own name (\"Français\"). */\n localeLabel: ReactNode;\n /** Store a verdict. Left out (or `readOnly`): the editor shows the row and its last\n * verdict, with no fields and no actions. */\n onSave?: (write: TranslationReviewWrite) => MaybePromise;\n /** Clear the verdict — back to unreviewed. Left out: no reset button. */\n onClear?: (key: TranslationReviewKey) => MaybePromise;\n /** The Cancel button — and called after a save or a reset resolves, so the panel\n * that opened the editor closes it. */\n onClose?: () => void;\n /** Look, do not touch: a locale the viewer may read but not review. */\n readOnly?: boolean;\n /** How the verdict's date reads. Default: the kit's `formatDate`, medium, in the\n * provider's locale. */\n formatDate?: (iso: string) => string;\n /** Turns a rejection into words. Default {@link TranslationReviewLabels.failed}. */\n formatError?: (error: unknown) => ReactNode;\n className?: string;\n labels?: Partial<TranslationReviewLabels>;\n}\n\n/** The wording a verdict was given on, under the line that says it changed. */\nfunction Then({ label, text }: { label: string; text: string }) {\n return (\n <div className=\"mt-1.5 min-w-0\">\n <span className=\"text-xs font-medium\">{label}</span>\n <p className=\"whitespace-pre-line break-words text-[var(--text-secondary)]\">{text}</p>\n </div>\n );\n}\n\n/**\n * One string, opened: the reference beside the translation, what changed since the last\n * verdict, what a machine can see is wrong (the placeholders), and the two things a\n * reviewer can say about it — keksdose's and kastlan's `translation-review-editor.tsx`.\n *\n * \"Better wording\" opens on the CURRENT text rather than empty, because the usual\n * correction is a word or an ending, and retyping a sentence to change one word is how\n * new mistakes get in. It is sent only with \"Needs a change\" — approving says the text is\n * fine as it stands. A \"Needs a change\" with neither a note nor a different wording tells\n * whoever fixes it nothing, so it takes one of the two.\n *\n * A MISSING string (kastlan: the locale lacks the key) cannot be approved — there is\n * nothing to approve. Its field starts empty and is the translation to add; the verdict\n * is NEEDS_CHANGE on the empty text, which the export hands a developer as the string to\n * add.\n *\n * Every action waits for its callback: the buttons hold while the promise is out, a\n * resolution closes the editor through `onClose`, and a rejection keeps it open with what\n * was typed and the error under the buttons.\n */\nexport function TranslationReviewEditor({\n row,\n referenceLabel,\n localeLabel,\n onSave,\n onClear,\n onClose,\n readOnly = false,\n formatDate: formatDateProp,\n formatError,\n className,\n labels: labelsProp,\n}: TranslationReviewEditorProps) {\n const labels = useKitLabels(\"translationReview\", DEFAULT_TRANSLATION_REVIEW_LABELS, labelsProp);\n const { formatDate } = useKitFormat();\n const review = row.review;\n const missing = row.text === \"\";\n const editable = !readOnly && onSave !== undefined;\n const [suggestion, setSuggestion] = useState(review?.suggestion ?? row.text);\n const [note, setNote] = useState(review?.note ?? \"\");\n const [busy, setBusy] = useState<\"approve\" | \"flag\" | \"clear\" | null>(null);\n const [failure, setFailure] = useState<ReactNode>(null);\n\n const canFlag = note.trim() !== \"\" || suggestion.trim() !== row.text.trim();\n const changed = row.status === \"changed\" ? review : null;\n const date = (iso: string) => (formatDateProp ? formatDateProp(iso) : formatDate(iso, \"medium\"));\n\n const run = async (kind: \"approve\" | \"flag\" | \"clear\", action: () => MaybePromise) => {\n setBusy(kind);\n setFailure(null);\n try {\n await action();\n onClose?.();\n } catch (caught) {\n setFailure(formatError ? formatError(caught) : labels.failed);\n } finally {\n setBusy(null);\n }\n };\n\n const save = (verdict: TranslationVerdict) => {\n if (!onSave) return;\n const trimmedNote = note.trim() || null;\n // Approving says the text is right as it stands: there is no wording to suggest.\n const sent = verdict === \"APPROVED\" ? null : suggestion.trim() || null;\n void run(verdict === \"APPROVED\" ? \"approve\" : \"flag\", () =>\n onSave(reviewWrite(row, verdict, trimmedNote, sent)),\n );\n };\n\n return (\n <div className={cn(\"min-w-0 space-y-3 text-sm\", className)}>\n <code className=\"block break-all text-xs text-[var(--text-muted)]\">{row.key}</code>\n\n <div className=\"grid gap-3 md:grid-cols-2\">\n <div className=\"min-w-0\">\n <SectionLabel as=\"span\" size=\"xs\">\n {referenceLabel}\n </SectionLabel>\n <p className=\"mt-1 whitespace-pre-line break-words text-[var(--text-secondary)]\">{row.reference}</p>\n </div>\n <div className=\"min-w-0\">\n <SectionLabel as=\"span\" size=\"xs\">\n {localeLabel}\n </SectionLabel>\n {missing ? (\n <p className=\"mt-1 italic text-[var(--text-muted)]\">{labels.missingText}</p>\n ) : (\n <p lang={row.locale} className=\"mt-1 whitespace-pre-line break-words\">\n {row.text}\n </p>\n )}\n </div>\n </div>\n\n {changed && (\n <AlertBanner tone=\"warning\" variant=\"inline\" size=\"sm\" block live={false}>\n <div className=\"min-w-0\">\n <p>{labels.changedSince}</p>\n {changed.text !== row.text && <Then label={labels.reviewedWording} text={changed.text || \"—\"} />}\n {changed.referenceText != null && changed.referenceText !== row.reference && (\n <Then label={labels.reviewedReference} text={changed.referenceText} />\n )}\n </div>\n </AlertBanner>\n )}\n\n {row.placeholderMismatch && (\n <AlertBanner tone=\"danger\" variant=\"inline\" size=\"sm\" block live={false}>\n <span className=\"min-w-0 break-words\">\n {labels.placeholderMismatch(\n placeholderTokens(row.reference).join(\" \") || \"—\",\n placeholderTokens(row.text).join(\" \") || \"—\",\n )}\n </span>\n </AlertBanner>\n )}\n\n {review && (\n <Caption>\n {(review.verdict === \"APPROVED\" ? labels.lastApproved : labels.lastFlagged)(\n review.reviewerName ?? labels.erasedReviewer,\n date(review.reviewedAt),\n )}\n </Caption>\n )}\n\n {/* Read-only: what the reviewer said, as text — the fields are for saying it. */}\n {!editable && review?.verdict === \"NEEDS_CHANGE\" && (review.suggestion || review.note) && (\n <div className=\"space-y-2\">\n {review.suggestion && <Then label={missing ? labels.translation : labels.suggestion} text={review.suggestion} />}\n {review.note && <Then label={labels.note} text={review.note} />}\n </div>\n )}\n\n {editable && (\n <>\n <Textarea\n label={missing ? labels.translation : labels.suggestion}\n lang={row.locale}\n rows={Math.min(8, Math.max(2, Math.ceil(Math.max(row.text.length, row.reference.length) / 80)))}\n value={suggestion}\n disabled={busy !== null}\n onChange={(e) => setSuggestion(e.target.value)}\n />\n <Textarea\n label={labels.note}\n rows={2}\n value={note}\n placeholder={labels.notePlaceholder}\n disabled={busy !== null}\n onChange={(e) => setNote(e.target.value)}\n />\n <div className=\"flex flex-wrap gap-2\">\n {!missing && (\n <Button\n variant=\"primary\"\n commit\n pending={busy === \"approve\"}\n disabled={busy !== null}\n onClick={() => save(\"APPROVED\")}\n >\n {labels.approve}\n </Button>\n )}\n <Button\n variant={missing ? \"primary\" : \"danger\"}\n commit\n pending={busy === \"flag\"}\n disabled={busy !== null || !canFlag}\n onClick={() => save(\"NEEDS_CHANGE\")}\n >\n {missing ? labels.suggest : labels.flag}\n </Button>\n {review && onClear && (\n <Button\n variant=\"ghost\"\n commit\n pending={busy === \"clear\"}\n disabled={busy !== null}\n onClick={() => void run(\"clear\", () => onClear({ locale: row.locale, key: row.key }))}\n >\n {labels.reset}\n </Button>\n )}\n {onClose && (\n <Button variant=\"ghost\" disabled={busy !== null} onClick={onClose}>\n {labels.cancel}\n </Button>\n )}\n </div>\n </>\n )}\n\n {failure && (\n <AlertBanner tone=\"danger\" size=\"sm\" role=\"alert\">\n {failure}\n </AlertBanner>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { ReviewStatus } from '../lib/translation-review.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The `translationReview` namespace: every word of the translation-review parts
|
|
5
|
+
* (`ReviewStatusChip`, `TranslationProgress`, `TranslationLocaleTabs`,
|
|
6
|
+
* `TranslationExportButton`, `TranslationReviewEditor`, `TranslationReviewPanel`).
|
|
7
|
+
*
|
|
8
|
+
* Its own module, like `data-table-labels.ts`: `kit-labels.tsx` and `defaults.ts` import
|
|
9
|
+
* it, and neither has to load the components to get the words. The English is keksdose's
|
|
10
|
+
* and kastlan's own (`translation_review.*`, `translations:*`), British.
|
|
11
|
+
*/
|
|
12
|
+
interface TranslationReviewLabels {
|
|
13
|
+
/** The five statuses — the chip, the filter, the progress legend. */
|
|
14
|
+
statusMissing: string;
|
|
15
|
+
statusUnreviewed: string;
|
|
16
|
+
statusChanged: string;
|
|
17
|
+
statusNeedsChange: string;
|
|
18
|
+
statusApproved: string;
|
|
19
|
+
/** Accessible name of the status filter. */
|
|
20
|
+
statusFilter: string;
|
|
21
|
+
/** The status filter's first option. */
|
|
22
|
+
all: string;
|
|
23
|
+
/** A status filter option with its count: "Unreviewed · 12". */
|
|
24
|
+
filterCount: (label: string, count: number) => string;
|
|
25
|
+
/** The source filter's label — shown only when the rows come from several sources
|
|
26
|
+
* (kastlan's screen texts and documents). */
|
|
27
|
+
source: string;
|
|
28
|
+
allSources: string;
|
|
29
|
+
/** The namespace filter's label and its "every one" option. */
|
|
30
|
+
namespace: string;
|
|
31
|
+
allNamespaces: string;
|
|
32
|
+
search: string;
|
|
33
|
+
/** The placeholder filter, with how many rows it would show. */
|
|
34
|
+
placeholdersOnly: (count: number) => string;
|
|
35
|
+
/** The chip on a row whose placeholders differ from the reference's. */
|
|
36
|
+
placeholderChip: string;
|
|
37
|
+
/** The key column, and the key's caption in the editor. */
|
|
38
|
+
key: string;
|
|
39
|
+
statusColumn: string;
|
|
40
|
+
/** No row matches the filters. */
|
|
41
|
+
empty: string;
|
|
42
|
+
/** In place of the text of a string the locale lacks. */
|
|
43
|
+
missingText: string;
|
|
44
|
+
/** The progress bar's label. */
|
|
45
|
+
progress: (approved: number, total: number) => string;
|
|
46
|
+
/** A locale tab's badge. */
|
|
47
|
+
localeProgress: (approved: number, total: number) => string;
|
|
48
|
+
/** Accessible name of the locale tabs. */
|
|
49
|
+
locales: string;
|
|
50
|
+
approve: string;
|
|
51
|
+
/** Send a string back: "Needs a change". */
|
|
52
|
+
flag: string;
|
|
53
|
+
/** "Needs a change" for a MISSING string: the suggestion is the translation to add. */
|
|
54
|
+
suggest: string;
|
|
55
|
+
/** Clear a verdict: back to unreviewed. */
|
|
56
|
+
reset: string;
|
|
57
|
+
/** The editor's way out without saving. */
|
|
58
|
+
cancel: string;
|
|
59
|
+
approveSelected: string;
|
|
60
|
+
resetSelected: string;
|
|
61
|
+
/** Selects every row the filters show — the phone's way into the bulk actions, where
|
|
62
|
+
* the table has no checkboxes. */
|
|
63
|
+
selectShown: string;
|
|
64
|
+
/** Over the old wording of a row that changed since its verdict. */
|
|
65
|
+
changedSince: string;
|
|
66
|
+
reviewedWording: string;
|
|
67
|
+
reviewedReference: string;
|
|
68
|
+
/** The placeholders differ: the reference's and the text's, each a list. */
|
|
69
|
+
placeholderMismatch: (reference: string, text: string) => string;
|
|
70
|
+
lastApproved: (name: string, date: string) => string;
|
|
71
|
+
lastFlagged: (name: string, date: string) => string;
|
|
72
|
+
/** The reviewer's name once the account is gone — what `name` above becomes. */
|
|
73
|
+
erasedReviewer: string;
|
|
74
|
+
/** The editor's wording field, opened on the current text. */
|
|
75
|
+
suggestion: string;
|
|
76
|
+
/** The same field for a missing string. */
|
|
77
|
+
translation: string;
|
|
78
|
+
note: string;
|
|
79
|
+
notePlaceholder: string;
|
|
80
|
+
/** The viewer may read this locale but not review it. */
|
|
81
|
+
readOnly: string;
|
|
82
|
+
/** The reviewer is limited to some areas — `areas` is their names, joined. */
|
|
83
|
+
scope: (areas: string) => string;
|
|
84
|
+
exportCorrections: (count: number) => string;
|
|
85
|
+
/** A callback rejected and no `formatError` says better. */
|
|
86
|
+
failed: string;
|
|
87
|
+
}
|
|
88
|
+
declare const DEFAULT_TRANSLATION_REVIEW_LABELS: TranslationReviewLabels;
|
|
89
|
+
/** A status's word, from the namespace's five. */
|
|
90
|
+
declare function reviewStatusLabel(labels: TranslationReviewLabels, status: ReviewStatus): string;
|
|
91
|
+
|
|
92
|
+
export { DEFAULT_TRANSLATION_REVIEW_LABELS, type TranslationReviewLabels, reviewStatusLabel };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
const DEFAULT_TRANSLATION_REVIEW_LABELS = {
|
|
3
|
+
statusMissing: "Missing",
|
|
4
|
+
statusUnreviewed: "Unreviewed",
|
|
5
|
+
statusChanged: "Changed since review",
|
|
6
|
+
statusNeedsChange: "Needs a change",
|
|
7
|
+
statusApproved: "Approved",
|
|
8
|
+
statusFilter: "Status",
|
|
9
|
+
all: "All",
|
|
10
|
+
filterCount: (label, count) => `${label} \xB7 ${count}`,
|
|
11
|
+
source: "Texts",
|
|
12
|
+
allSources: "All texts",
|
|
13
|
+
namespace: "Area",
|
|
14
|
+
allNamespaces: "All areas",
|
|
15
|
+
search: "Search keys and text",
|
|
16
|
+
placeholdersOnly: (count) => `Only placeholder problems (${count})`,
|
|
17
|
+
placeholderChip: "Placeholders",
|
|
18
|
+
key: "Key",
|
|
19
|
+
statusColumn: "Status",
|
|
20
|
+
empty: "No strings match these filters.",
|
|
21
|
+
missingText: "Missing in this language",
|
|
22
|
+
progress: (approved, total) => `${approved} of ${total} approved`,
|
|
23
|
+
localeProgress: (approved, total) => `${approved}/${total}`,
|
|
24
|
+
locales: "Languages",
|
|
25
|
+
approve: "Approve",
|
|
26
|
+
flag: "Needs a change",
|
|
27
|
+
suggest: "Suggest translation",
|
|
28
|
+
reset: "Mark unreviewed",
|
|
29
|
+
cancel: "Cancel",
|
|
30
|
+
approveSelected: "Approve selected",
|
|
31
|
+
resetSelected: "Mark selected unreviewed",
|
|
32
|
+
selectShown: "Select all shown",
|
|
33
|
+
changedSince: "This string changed after it was reviewed. Please read it again.",
|
|
34
|
+
reviewedWording: "Wording when reviewed",
|
|
35
|
+
reviewedReference: "Reference when reviewed",
|
|
36
|
+
placeholderMismatch: (reference, text) => `The placeholders differ from the reference \u2014 reference: ${reference}; this text: ${text}. The app fills these in, so they must stay exactly as they are.`,
|
|
37
|
+
lastApproved: (name, date) => `Approved by ${name} on ${date}`,
|
|
38
|
+
lastFlagged: (name, date) => `Marked as needing a change by ${name} on ${date}`,
|
|
39
|
+
erasedReviewer: "a deleted account",
|
|
40
|
+
suggestion: "Better wording",
|
|
41
|
+
translation: "Translation",
|
|
42
|
+
note: "Note (optional)",
|
|
43
|
+
notePlaceholder: "What is wrong, or what to keep in mind",
|
|
44
|
+
readOnly: "You can read this language but not review it.",
|
|
45
|
+
scope: (areas) => `Your review is limited to: ${areas}.`,
|
|
46
|
+
exportCorrections: (count) => `Export corrections (${count})`,
|
|
47
|
+
failed: "That did not work. Please try again."
|
|
48
|
+
};
|
|
49
|
+
function reviewStatusLabel(labels, status) {
|
|
50
|
+
switch (status) {
|
|
51
|
+
case "missing":
|
|
52
|
+
return labels.statusMissing;
|
|
53
|
+
case "unreviewed":
|
|
54
|
+
return labels.statusUnreviewed;
|
|
55
|
+
case "changed":
|
|
56
|
+
return labels.statusChanged;
|
|
57
|
+
case "needs_change":
|
|
58
|
+
return labels.statusNeedsChange;
|
|
59
|
+
case "approved":
|
|
60
|
+
return labels.statusApproved;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
DEFAULT_TRANSLATION_REVIEW_LABELS,
|
|
65
|
+
reviewStatusLabel
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=translation-review-labels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/translation-review-labels.ts"],"sourcesContent":["import type { ReviewStatus } from \"../lib/translation-review\";\n\n/**\n * The `translationReview` namespace: every word of the translation-review parts\n * (`ReviewStatusChip`, `TranslationProgress`, `TranslationLocaleTabs`,\n * `TranslationExportButton`, `TranslationReviewEditor`, `TranslationReviewPanel`).\n *\n * Its own module, like `data-table-labels.ts`: `kit-labels.tsx` and `defaults.ts` import\n * it, and neither has to load the components to get the words. The English is keksdose's\n * and kastlan's own (`translation_review.*`, `translations:*`), British.\n */\nexport interface TranslationReviewLabels {\n /** The five statuses — the chip, the filter, the progress legend. */\n statusMissing: string;\n statusUnreviewed: string;\n statusChanged: string;\n statusNeedsChange: string;\n statusApproved: string;\n /** Accessible name of the status filter. */\n statusFilter: string;\n /** The status filter's first option. */\n all: string;\n /** A status filter option with its count: \"Unreviewed · 12\". */\n filterCount: (label: string, count: number) => string;\n /** The source filter's label — shown only when the rows come from several sources\n * (kastlan's screen texts and documents). */\n source: string;\n allSources: string;\n /** The namespace filter's label and its \"every one\" option. */\n namespace: string;\n allNamespaces: string;\n search: string;\n /** The placeholder filter, with how many rows it would show. */\n placeholdersOnly: (count: number) => string;\n /** The chip on a row whose placeholders differ from the reference's. */\n placeholderChip: string;\n /** The key column, and the key's caption in the editor. */\n key: string;\n statusColumn: string;\n /** No row matches the filters. */\n empty: string;\n /** In place of the text of a string the locale lacks. */\n missingText: string;\n /** The progress bar's label. */\n progress: (approved: number, total: number) => string;\n /** A locale tab's badge. */\n localeProgress: (approved: number, total: number) => string;\n /** Accessible name of the locale tabs. */\n locales: string;\n approve: string;\n /** Send a string back: \"Needs a change\". */\n flag: string;\n /** \"Needs a change\" for a MISSING string: the suggestion is the translation to add. */\n suggest: string;\n /** Clear a verdict: back to unreviewed. */\n reset: string;\n /** The editor's way out without saving. */\n cancel: string;\n approveSelected: string;\n resetSelected: string;\n /** Selects every row the filters show — the phone's way into the bulk actions, where\n * the table has no checkboxes. */\n selectShown: string;\n /** Over the old wording of a row that changed since its verdict. */\n changedSince: string;\n reviewedWording: string;\n reviewedReference: string;\n /** The placeholders differ: the reference's and the text's, each a list. */\n placeholderMismatch: (reference: string, text: string) => string;\n lastApproved: (name: string, date: string) => string;\n lastFlagged: (name: string, date: string) => string;\n /** The reviewer's name once the account is gone — what `name` above becomes. */\n erasedReviewer: string;\n /** The editor's wording field, opened on the current text. */\n suggestion: string;\n /** The same field for a missing string. */\n translation: string;\n note: string;\n notePlaceholder: string;\n /** The viewer may read this locale but not review it. */\n readOnly: string;\n /** The reviewer is limited to some areas — `areas` is their names, joined. */\n scope: (areas: string) => string;\n exportCorrections: (count: number) => string;\n /** A callback rejected and no `formatError` says better. */\n failed: string;\n}\n\nexport const DEFAULT_TRANSLATION_REVIEW_LABELS: TranslationReviewLabels = {\n statusMissing: \"Missing\",\n statusUnreviewed: \"Unreviewed\",\n statusChanged: \"Changed since review\",\n statusNeedsChange: \"Needs a change\",\n statusApproved: \"Approved\",\n statusFilter: \"Status\",\n all: \"All\",\n filterCount: (label, count) => `${label} · ${count}`,\n source: \"Texts\",\n allSources: \"All texts\",\n namespace: \"Area\",\n allNamespaces: \"All areas\",\n search: \"Search keys and text\",\n placeholdersOnly: (count) => `Only placeholder problems (${count})`,\n placeholderChip: \"Placeholders\",\n key: \"Key\",\n statusColumn: \"Status\",\n empty: \"No strings match these filters.\",\n missingText: \"Missing in this language\",\n progress: (approved, total) => `${approved} of ${total} approved`,\n localeProgress: (approved, total) => `${approved}/${total}`,\n locales: \"Languages\",\n approve: \"Approve\",\n flag: \"Needs a change\",\n suggest: \"Suggest translation\",\n reset: \"Mark unreviewed\",\n cancel: \"Cancel\",\n approveSelected: \"Approve selected\",\n resetSelected: \"Mark selected unreviewed\",\n selectShown: \"Select all shown\",\n changedSince: \"This string changed after it was reviewed. Please read it again.\",\n reviewedWording: \"Wording when reviewed\",\n reviewedReference: \"Reference when reviewed\",\n placeholderMismatch: (reference, text) =>\n `The placeholders differ from the reference — reference: ${reference}; this text: ${text}. The app fills these in, so they must stay exactly as they are.`,\n lastApproved: (name, date) => `Approved by ${name} on ${date}`,\n lastFlagged: (name, date) => `Marked as needing a change by ${name} on ${date}`,\n erasedReviewer: \"a deleted account\",\n suggestion: \"Better wording\",\n translation: \"Translation\",\n note: \"Note (optional)\",\n notePlaceholder: \"What is wrong, or what to keep in mind\",\n readOnly: \"You can read this language but not review it.\",\n scope: (areas) => `Your review is limited to: ${areas}.`,\n exportCorrections: (count) => `Export corrections (${count})`,\n failed: \"That did not work. Please try again.\",\n};\n\n/** A status's word, from the namespace's five. */\nexport function reviewStatusLabel(labels: TranslationReviewLabels, status: ReviewStatus): string {\n switch (status) {\n case \"missing\":\n return labels.statusMissing;\n case \"unreviewed\":\n return labels.statusUnreviewed;\n case \"changed\":\n return labels.statusChanged;\n case \"needs_change\":\n return labels.statusNeedsChange;\n case \"approved\":\n return labels.statusApproved;\n }\n}\n"],"mappings":";AAwFO,MAAM,oCAA6D;AAAA,EACxE,eAAe;AAAA,EACf,kBAAkB;AAAA,EAClB,eAAe;AAAA,EACf,mBAAmB;AAAA,EACnB,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,KAAK;AAAA,EACL,aAAa,CAAC,OAAO,UAAU,GAAG,KAAK,SAAM,KAAK;AAAA,EAClD,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,eAAe;AAAA,EACf,QAAQ;AAAA,EACR,kBAAkB,CAAC,UAAU,8BAA8B,KAAK;AAAA,EAChE,iBAAiB;AAAA,EACjB,KAAK;AAAA,EACL,cAAc;AAAA,EACd,OAAO;AAAA,EACP,aAAa;AAAA,EACb,UAAU,CAAC,UAAU,UAAU,GAAG,QAAQ,OAAO,KAAK;AAAA,EACtD,gBAAgB,CAAC,UAAU,UAAU,GAAG,QAAQ,IAAI,KAAK;AAAA,EACzD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf,aAAa;AAAA,EACb,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,qBAAqB,CAAC,WAAW,SAC/B,gEAA2D,SAAS,gBAAgB,IAAI;AAAA,EAC1F,cAAc,CAAC,MAAM,SAAS,eAAe,IAAI,OAAO,IAAI;AAAA,EAC5D,aAAa,CAAC,MAAM,SAAS,iCAAiC,IAAI,OAAO,IAAI;AAAA,EAC7E,gBAAgB;AAAA,EAChB,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,MAAM;AAAA,EACN,iBAAiB;AAAA,EACjB,UAAU;AAAA,EACV,OAAO,CAAC,UAAU,8BAA8B,KAAK;AAAA,EACrD,mBAAmB,CAAC,UAAU,uBAAuB,KAAK;AAAA,EAC1D,QAAQ;AACV;AAGO,SAAS,kBAAkB,QAAiC,QAA8B;AAC/F,UAAQ,QAAQ;AAAA,IACd,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,EAClB;AACF;","names":[]}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { ReviewStatus, TranslationRow, TranslationCorrection, TranslationSummary } from '../lib/translation-review.js';
|
|
4
|
+
import { ChipTone, ChipSize, ChipVariant, ChipShape } from './chip.js';
|
|
5
|
+
import { TranslationReviewLabels } from './translation-review-labels.js';
|
|
6
|
+
import 'lucide-react';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The small parts of the translation review, each usable on its own: the status
|
|
10
|
+
* vocabulary as a chip, one locale's progress, the locale tabs and the export. The page
|
|
11
|
+
* body that puts them together is `TranslationReviewPanel` (translation-review.tsx).
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* The tone of each status: nothing yet is neutral, a gap is orange (kastlan's choice — a
|
|
15
|
+
* hue, not a verdict), a change since review is amber, sent back is red, done is green.
|
|
16
|
+
*/
|
|
17
|
+
declare const REVIEW_STATUS_TONES: Readonly<Record<ReviewStatus, ChipTone>>;
|
|
18
|
+
interface ReviewStatusChipProps {
|
|
19
|
+
status: ReviewStatus;
|
|
20
|
+
/** Over {@link REVIEW_STATUS_TONES}. */
|
|
21
|
+
tone?: ChipTone;
|
|
22
|
+
/** Over the label. */
|
|
23
|
+
children?: ReactNode;
|
|
24
|
+
/** Default `sm` — a chip in a table cell. */
|
|
25
|
+
size?: ChipSize;
|
|
26
|
+
variant?: ChipVariant;
|
|
27
|
+
shape?: ChipShape;
|
|
28
|
+
caps?: boolean;
|
|
29
|
+
className?: string;
|
|
30
|
+
/** Prop > `<UiKitProvider labels={{ translationReview }}>` > English. */
|
|
31
|
+
labels?: Partial<TranslationReviewLabels>;
|
|
32
|
+
[key: `data-${string}`]: string | number | boolean | undefined;
|
|
33
|
+
}
|
|
34
|
+
/** One row's status as a chip, in the kit's words and tones. Carries `data-status`. */
|
|
35
|
+
declare function ReviewStatusChip({ status, tone, children, size, variant, shape, caps, className, labels: labelsProp, ...rest }: ReviewStatusChipProps): react.JSX.Element;
|
|
36
|
+
interface TranslationProgressProps {
|
|
37
|
+
/** One locale's rows — or their {@link summariseRows} when the app has only counts. */
|
|
38
|
+
rows?: readonly TranslationRow[];
|
|
39
|
+
summary?: TranslationSummary;
|
|
40
|
+
/** The list under the bar naming each part with its count. Default true. */
|
|
41
|
+
legend?: boolean;
|
|
42
|
+
className?: string;
|
|
43
|
+
labels?: Partial<TranslationReviewLabels>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* "120 of 3859 approved", and the bar split by status: approved, sent back, changed since
|
|
47
|
+
* review, missing — in the chips' colours. What is left of the track is unreviewed. The
|
|
48
|
+
* legend counts rows rather than percentages: "12 need a change" is the next hour's work,
|
|
49
|
+
* "0.3 %" is not.
|
|
50
|
+
*/
|
|
51
|
+
declare function TranslationProgress({ rows, summary: summaryProp, legend, className, labels: labelsProp, }: TranslationProgressProps): react.JSX.Element;
|
|
52
|
+
interface TranslationLocaleTab {
|
|
53
|
+
/** The locale code. */
|
|
54
|
+
value: string;
|
|
55
|
+
/** Its name, in its own language ("Français"). */
|
|
56
|
+
label: ReactNode;
|
|
57
|
+
/** The locale's rows, for the badge — built with the same `areas` as the panel's, so
|
|
58
|
+
* a scoped reviewer's tab counts what they can see. */
|
|
59
|
+
rows: readonly TranslationRow[];
|
|
60
|
+
}
|
|
61
|
+
interface TranslationLocaleTabsProps {
|
|
62
|
+
/** The locales the viewer may see, in the app's order — the server's answer, never a
|
|
63
|
+
* guess from a role name. */
|
|
64
|
+
locales: readonly TranslationLocaleTab[];
|
|
65
|
+
active: string;
|
|
66
|
+
onChange: (locale: string) => void;
|
|
67
|
+
className?: string;
|
|
68
|
+
labels?: Partial<TranslationReviewLabels>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* One tab per locale with its approved/total, wrapping on a phone. Nothing for a single
|
|
72
|
+
* locale: a strip with one tab is a heading that looks clickable.
|
|
73
|
+
*/
|
|
74
|
+
declare function TranslationLocaleTabs({ locales, active, onChange, className, labels: labelsProp }: TranslationLocaleTabsProps): react.JSX.Element | null;
|
|
75
|
+
interface TranslationExportButtonProps {
|
|
76
|
+
/** Every row the export covers — usually every locale's, not only the open one. */
|
|
77
|
+
rows: readonly TranslationRow[];
|
|
78
|
+
/** Listed in the file. Default: the locales the rows come from. */
|
|
79
|
+
locales?: readonly string[];
|
|
80
|
+
/** Default `translation-corrections-<date>.json`. */
|
|
81
|
+
fileName?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Take the export instead of downloading it — the entries, and the file as a ready
|
|
84
|
+
* JSON Blob. Left out, the kit downloads the Blob as `fileName`.
|
|
85
|
+
*/
|
|
86
|
+
onExport?: (corrections: TranslationCorrection[], file: Blob) => void;
|
|
87
|
+
className?: string;
|
|
88
|
+
labels?: Partial<TranslationReviewLabels>;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* "Export corrections (n)": every string sent back, as the JSON file keksdose and kastlan
|
|
92
|
+
* hand a developer — `{ exported_at, locales, corrections }`. Corrections are not applied
|
|
93
|
+
* on the page: the strings are source code. Disabled while there is nothing to export.
|
|
94
|
+
*/
|
|
95
|
+
declare function TranslationExportButton({ rows, locales, fileName, onExport, className, labels: labelsProp, }: TranslationExportButtonProps): react.JSX.Element;
|
|
96
|
+
|
|
97
|
+
export { REVIEW_STATUS_TONES, ReviewStatusChip, type ReviewStatusChipProps, TranslationExportButton, type TranslationExportButtonProps, type TranslationLocaleTab, TranslationLocaleTabs, type TranslationLocaleTabsProps, TranslationProgress, type TranslationProgressProps };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { Download } from "lucide-react";
|
|
5
|
+
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
6
|
+
import { useKitFormat } from "../lib/format.js";
|
|
7
|
+
import { summariseRows, translationCorrections } from "../lib/translation-review.js";
|
|
8
|
+
import { Chip } from "./chip.js";
|
|
9
|
+
import { ProgressBar } from "./progress-bar.js";
|
|
10
|
+
import { DEFAULT_TRANSLATION_REVIEW_LABELS, reviewStatusLabel } from "./translation-review-labels.js";
|
|
11
|
+
import { Button, Tabs } from "./ui.js";
|
|
12
|
+
const REVIEW_STATUS_TONES = {
|
|
13
|
+
missing: "orange",
|
|
14
|
+
unreviewed: "neutral",
|
|
15
|
+
changed: "warning",
|
|
16
|
+
needs_change: "danger",
|
|
17
|
+
approved: "success"
|
|
18
|
+
};
|
|
19
|
+
function ReviewStatusChip({
|
|
20
|
+
status,
|
|
21
|
+
tone,
|
|
22
|
+
children,
|
|
23
|
+
size = "sm",
|
|
24
|
+
variant,
|
|
25
|
+
shape,
|
|
26
|
+
caps,
|
|
27
|
+
className,
|
|
28
|
+
labels: labelsProp,
|
|
29
|
+
...rest
|
|
30
|
+
}) {
|
|
31
|
+
const labels = useKitLabels("translationReview", DEFAULT_TRANSLATION_REVIEW_LABELS, labelsProp);
|
|
32
|
+
return /* @__PURE__ */ jsx(
|
|
33
|
+
Chip,
|
|
34
|
+
{
|
|
35
|
+
...rest,
|
|
36
|
+
"data-status": status,
|
|
37
|
+
tone: tone ?? REVIEW_STATUS_TONES[status],
|
|
38
|
+
size,
|
|
39
|
+
variant,
|
|
40
|
+
shape,
|
|
41
|
+
caps,
|
|
42
|
+
className,
|
|
43
|
+
children: children ?? reviewStatusLabel(labels, status)
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
function TranslationProgress({
|
|
48
|
+
rows,
|
|
49
|
+
summary: summaryProp,
|
|
50
|
+
legend = true,
|
|
51
|
+
className,
|
|
52
|
+
labels: labelsProp
|
|
53
|
+
}) {
|
|
54
|
+
const labels = useKitLabels("translationReview", DEFAULT_TRANSLATION_REVIEW_LABELS, labelsProp);
|
|
55
|
+
const { formatNumber } = useKitFormat();
|
|
56
|
+
const summary = useMemo(() => summaryProp ?? summariseRows(rows ?? []), [summaryProp, rows]);
|
|
57
|
+
const part = (status, tone, color) => ({
|
|
58
|
+
key: status,
|
|
59
|
+
value: summary[status],
|
|
60
|
+
tone,
|
|
61
|
+
color,
|
|
62
|
+
label: reviewStatusLabel(labels, status),
|
|
63
|
+
// A status with no rows stays in the legend ("Needs a change 0" is news too) but out
|
|
64
|
+
// of the bar, where it would only be a gap.
|
|
65
|
+
legendOnly: summary[status] === 0 ? true : void 0
|
|
66
|
+
});
|
|
67
|
+
const segments = [
|
|
68
|
+
part("approved", "success"),
|
|
69
|
+
part("needs_change", "danger"),
|
|
70
|
+
part("changed", "warning"),
|
|
71
|
+
// The chip's orange, which is a hue and not a ProgressBar tone. Only where there is
|
|
72
|
+
// a gap: an app whose locales are complete never hears of "missing".
|
|
73
|
+
...summary.missing > 0 ? [part("missing", void 0, "var(--hue-orange)")] : []
|
|
74
|
+
];
|
|
75
|
+
return /* @__PURE__ */ jsx(
|
|
76
|
+
ProgressBar,
|
|
77
|
+
{
|
|
78
|
+
className,
|
|
79
|
+
label: labels.progress(summary.approved, summary.total),
|
|
80
|
+
max: Math.max(summary.total, 1),
|
|
81
|
+
segments,
|
|
82
|
+
legend,
|
|
83
|
+
formatValue: (value) => formatNumber(value)
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
function TranslationLocaleTabs({ locales, active, onChange, className, labels: labelsProp }) {
|
|
88
|
+
const labels = useKitLabels("translationReview", DEFAULT_TRANSLATION_REVIEW_LABELS, labelsProp);
|
|
89
|
+
if (locales.length < 2) return null;
|
|
90
|
+
return /* @__PURE__ */ jsx(
|
|
91
|
+
Tabs,
|
|
92
|
+
{
|
|
93
|
+
"aria-label": labels.locales,
|
|
94
|
+
wrap: true,
|
|
95
|
+
className,
|
|
96
|
+
tabs: locales.map((locale) => {
|
|
97
|
+
const summary = summariseRows(locale.rows);
|
|
98
|
+
return {
|
|
99
|
+
id: locale.value,
|
|
100
|
+
label: locale.label,
|
|
101
|
+
name: typeof locale.label === "string" ? locale.label : locale.value,
|
|
102
|
+
badge: (
|
|
103
|
+
// The tab's own colour, dimmed — a fixed muted grey vanished on the selected
|
|
104
|
+
// tab's brand fill (390px browser check).
|
|
105
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs tabular-nums opacity-75", children: labels.localeProgress(summary.approved, summary.total) })
|
|
106
|
+
)
|
|
107
|
+
};
|
|
108
|
+
}),
|
|
109
|
+
active,
|
|
110
|
+
onChange
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
function TranslationExportButton({
|
|
115
|
+
rows,
|
|
116
|
+
locales,
|
|
117
|
+
fileName,
|
|
118
|
+
onExport,
|
|
119
|
+
className,
|
|
120
|
+
labels: labelsProp
|
|
121
|
+
}) {
|
|
122
|
+
const labels = useKitLabels("translationReview", DEFAULT_TRANSLATION_REVIEW_LABELS, labelsProp);
|
|
123
|
+
const corrections = useMemo(() => translationCorrections(rows), [rows]);
|
|
124
|
+
const run = () => {
|
|
125
|
+
const now = /* @__PURE__ */ new Date();
|
|
126
|
+
const file = new Blob(
|
|
127
|
+
[
|
|
128
|
+
JSON.stringify(
|
|
129
|
+
{
|
|
130
|
+
exported_at: now.toISOString(),
|
|
131
|
+
locales: locales ?? [...new Set(rows.map((r) => r.locale))],
|
|
132
|
+
corrections
|
|
133
|
+
},
|
|
134
|
+
null,
|
|
135
|
+
2
|
|
136
|
+
)
|
|
137
|
+
],
|
|
138
|
+
{ type: "application/json" }
|
|
139
|
+
);
|
|
140
|
+
if (onExport) {
|
|
141
|
+
onExport(corrections, file);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const url = URL.createObjectURL(file);
|
|
145
|
+
const a = document.createElement("a");
|
|
146
|
+
a.href = url;
|
|
147
|
+
a.download = fileName ?? `translation-corrections-${now.toISOString().slice(0, 10)}.json`;
|
|
148
|
+
a.click();
|
|
149
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
150
|
+
};
|
|
151
|
+
return /* @__PURE__ */ jsxs(Button, { variant: "secondary", disabled: corrections.length === 0, onClick: run, className, children: [
|
|
152
|
+
/* @__PURE__ */ jsx(Download, { className: "size-4", "aria-hidden": true }),
|
|
153
|
+
labels.exportCorrections(corrections.length)
|
|
154
|
+
] });
|
|
155
|
+
}
|
|
156
|
+
export {
|
|
157
|
+
REVIEW_STATUS_TONES,
|
|
158
|
+
ReviewStatusChip,
|
|
159
|
+
TranslationExportButton,
|
|
160
|
+
TranslationLocaleTabs,
|
|
161
|
+
TranslationProgress
|
|
162
|
+
};
|
|
163
|
+
//# sourceMappingURL=translation-review-parts.js.map
|