@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,152 @@
|
|
|
1
|
+
import type { ReviewStatus } from "../lib/translation-review";
|
|
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
|
+
export 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
|
+
|
|
89
|
+
export const DEFAULT_TRANSLATION_REVIEW_LABELS: TranslationReviewLabels = {
|
|
90
|
+
statusMissing: "Missing",
|
|
91
|
+
statusUnreviewed: "Unreviewed",
|
|
92
|
+
statusChanged: "Changed since review",
|
|
93
|
+
statusNeedsChange: "Needs a change",
|
|
94
|
+
statusApproved: "Approved",
|
|
95
|
+
statusFilter: "Status",
|
|
96
|
+
all: "All",
|
|
97
|
+
filterCount: (label, count) => `${label} · ${count}`,
|
|
98
|
+
source: "Texts",
|
|
99
|
+
allSources: "All texts",
|
|
100
|
+
namespace: "Area",
|
|
101
|
+
allNamespaces: "All areas",
|
|
102
|
+
search: "Search keys and text",
|
|
103
|
+
placeholdersOnly: (count) => `Only placeholder problems (${count})`,
|
|
104
|
+
placeholderChip: "Placeholders",
|
|
105
|
+
key: "Key",
|
|
106
|
+
statusColumn: "Status",
|
|
107
|
+
empty: "No strings match these filters.",
|
|
108
|
+
missingText: "Missing in this language",
|
|
109
|
+
progress: (approved, total) => `${approved} of ${total} approved`,
|
|
110
|
+
localeProgress: (approved, total) => `${approved}/${total}`,
|
|
111
|
+
locales: "Languages",
|
|
112
|
+
approve: "Approve",
|
|
113
|
+
flag: "Needs a change",
|
|
114
|
+
suggest: "Suggest translation",
|
|
115
|
+
reset: "Mark unreviewed",
|
|
116
|
+
cancel: "Cancel",
|
|
117
|
+
approveSelected: "Approve selected",
|
|
118
|
+
resetSelected: "Mark selected unreviewed",
|
|
119
|
+
selectShown: "Select all shown",
|
|
120
|
+
changedSince: "This string changed after it was reviewed. Please read it again.",
|
|
121
|
+
reviewedWording: "Wording when reviewed",
|
|
122
|
+
reviewedReference: "Reference when reviewed",
|
|
123
|
+
placeholderMismatch: (reference, text) =>
|
|
124
|
+
`The placeholders differ from the reference — reference: ${reference}; this text: ${text}. The app fills these in, so they must stay exactly as they are.`,
|
|
125
|
+
lastApproved: (name, date) => `Approved by ${name} on ${date}`,
|
|
126
|
+
lastFlagged: (name, date) => `Marked as needing a change by ${name} on ${date}`,
|
|
127
|
+
erasedReviewer: "a deleted account",
|
|
128
|
+
suggestion: "Better wording",
|
|
129
|
+
translation: "Translation",
|
|
130
|
+
note: "Note (optional)",
|
|
131
|
+
notePlaceholder: "What is wrong, or what to keep in mind",
|
|
132
|
+
readOnly: "You can read this language but not review it.",
|
|
133
|
+
scope: (areas) => `Your review is limited to: ${areas}.`,
|
|
134
|
+
exportCorrections: (count) => `Export corrections (${count})`,
|
|
135
|
+
failed: "That did not work. Please try again.",
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/** A status's word, from the namespace's five. */
|
|
139
|
+
export function reviewStatusLabel(labels: TranslationReviewLabels, status: ReviewStatus): string {
|
|
140
|
+
switch (status) {
|
|
141
|
+
case "missing":
|
|
142
|
+
return labels.statusMissing;
|
|
143
|
+
case "unreviewed":
|
|
144
|
+
return labels.statusUnreviewed;
|
|
145
|
+
case "changed":
|
|
146
|
+
return labels.statusChanged;
|
|
147
|
+
case "needs_change":
|
|
148
|
+
return labels.statusNeedsChange;
|
|
149
|
+
case "approved":
|
|
150
|
+
return labels.statusApproved;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { Download } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import { useKitLabels } from "../i18n/kit-labels";
|
|
6
|
+
import { useKitFormat } from "../lib/format";
|
|
7
|
+
import { summariseRows, translationCorrections } from "../lib/translation-review";
|
|
8
|
+
import type {
|
|
9
|
+
ReviewStatus,
|
|
10
|
+
TranslationCorrection,
|
|
11
|
+
TranslationRow,
|
|
12
|
+
TranslationSummary,
|
|
13
|
+
} from "../lib/translation-review";
|
|
14
|
+
import { Chip } from "./chip";
|
|
15
|
+
import type { ChipShape, ChipSize, ChipTone, ChipVariant } from "./chip";
|
|
16
|
+
import { ProgressBar } from "./progress-bar";
|
|
17
|
+
import type { ProgressBarSegment } from "./progress-bar";
|
|
18
|
+
import { DEFAULT_TRANSLATION_REVIEW_LABELS, reviewStatusLabel } from "./translation-review-labels";
|
|
19
|
+
import type { TranslationReviewLabels } from "./translation-review-labels";
|
|
20
|
+
import { Button, Tabs } from "./ui";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The small parts of the translation review, each usable on its own: the status
|
|
24
|
+
* vocabulary as a chip, one locale's progress, the locale tabs and the export. The page
|
|
25
|
+
* body that puts them together is `TranslationReviewPanel` (translation-review.tsx).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// ── ReviewStatusChip ──────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The tone of each status: nothing yet is neutral, a gap is orange (kastlan's choice — a
|
|
32
|
+
* hue, not a verdict), a change since review is amber, sent back is red, done is green.
|
|
33
|
+
*/
|
|
34
|
+
export const REVIEW_STATUS_TONES: Readonly<Record<ReviewStatus, ChipTone>> = {
|
|
35
|
+
missing: "orange",
|
|
36
|
+
unreviewed: "neutral",
|
|
37
|
+
changed: "warning",
|
|
38
|
+
needs_change: "danger",
|
|
39
|
+
approved: "success",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export interface ReviewStatusChipProps {
|
|
43
|
+
status: ReviewStatus;
|
|
44
|
+
/** Over {@link REVIEW_STATUS_TONES}. */
|
|
45
|
+
tone?: ChipTone;
|
|
46
|
+
/** Over the label. */
|
|
47
|
+
children?: ReactNode;
|
|
48
|
+
/** Default `sm` — a chip in a table cell. */
|
|
49
|
+
size?: ChipSize;
|
|
50
|
+
variant?: ChipVariant;
|
|
51
|
+
shape?: ChipShape;
|
|
52
|
+
caps?: boolean;
|
|
53
|
+
className?: string;
|
|
54
|
+
/** Prop > `<UiKitProvider labels={{ translationReview }}>` > English. */
|
|
55
|
+
labels?: Partial<TranslationReviewLabels>;
|
|
56
|
+
[key: `data-${string}`]: string | number | boolean | undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** One row's status as a chip, in the kit's words and tones. Carries `data-status`. */
|
|
60
|
+
export function ReviewStatusChip({
|
|
61
|
+
status,
|
|
62
|
+
tone,
|
|
63
|
+
children,
|
|
64
|
+
size = "sm",
|
|
65
|
+
variant,
|
|
66
|
+
shape,
|
|
67
|
+
caps,
|
|
68
|
+
className,
|
|
69
|
+
labels: labelsProp,
|
|
70
|
+
...rest
|
|
71
|
+
}: ReviewStatusChipProps) {
|
|
72
|
+
const labels = useKitLabels("translationReview", DEFAULT_TRANSLATION_REVIEW_LABELS, labelsProp);
|
|
73
|
+
return (
|
|
74
|
+
<Chip
|
|
75
|
+
{...rest}
|
|
76
|
+
data-status={status}
|
|
77
|
+
tone={tone ?? REVIEW_STATUS_TONES[status]}
|
|
78
|
+
size={size}
|
|
79
|
+
variant={variant}
|
|
80
|
+
shape={shape}
|
|
81
|
+
caps={caps}
|
|
82
|
+
className={className}
|
|
83
|
+
>
|
|
84
|
+
{children ?? reviewStatusLabel(labels, status)}
|
|
85
|
+
</Chip>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── TranslationProgress ───────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
export interface TranslationProgressProps {
|
|
92
|
+
/** One locale's rows — or their {@link summariseRows} when the app has only counts. */
|
|
93
|
+
rows?: readonly TranslationRow[];
|
|
94
|
+
summary?: TranslationSummary;
|
|
95
|
+
/** The list under the bar naming each part with its count. Default true. */
|
|
96
|
+
legend?: boolean;
|
|
97
|
+
className?: string;
|
|
98
|
+
labels?: Partial<TranslationReviewLabels>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* "120 of 3859 approved", and the bar split by status: approved, sent back, changed since
|
|
103
|
+
* review, missing — in the chips' colours. What is left of the track is unreviewed. The
|
|
104
|
+
* legend counts rows rather than percentages: "12 need a change" is the next hour's work,
|
|
105
|
+
* "0.3 %" is not.
|
|
106
|
+
*/
|
|
107
|
+
export function TranslationProgress({
|
|
108
|
+
rows,
|
|
109
|
+
summary: summaryProp,
|
|
110
|
+
legend = true,
|
|
111
|
+
className,
|
|
112
|
+
labels: labelsProp,
|
|
113
|
+
}: TranslationProgressProps) {
|
|
114
|
+
const labels = useKitLabels("translationReview", DEFAULT_TRANSLATION_REVIEW_LABELS, labelsProp);
|
|
115
|
+
const { formatNumber } = useKitFormat();
|
|
116
|
+
const summary = useMemo(() => summaryProp ?? summariseRows(rows ?? []), [summaryProp, rows]);
|
|
117
|
+
const part = (status: ReviewStatus, tone: ProgressBarSegment["tone"], color?: string): ProgressBarSegment => ({
|
|
118
|
+
key: status,
|
|
119
|
+
value: summary[status],
|
|
120
|
+
tone,
|
|
121
|
+
color,
|
|
122
|
+
label: reviewStatusLabel(labels, status),
|
|
123
|
+
// A status with no rows stays in the legend ("Needs a change 0" is news too) but out
|
|
124
|
+
// of the bar, where it would only be a gap.
|
|
125
|
+
legendOnly: summary[status] === 0 ? true : undefined,
|
|
126
|
+
});
|
|
127
|
+
const segments = [
|
|
128
|
+
part("approved", "success"),
|
|
129
|
+
part("needs_change", "danger"),
|
|
130
|
+
part("changed", "warning"),
|
|
131
|
+
// The chip's orange, which is a hue and not a ProgressBar tone. Only where there is
|
|
132
|
+
// a gap: an app whose locales are complete never hears of "missing".
|
|
133
|
+
...(summary.missing > 0 ? [part("missing", undefined, "var(--hue-orange)")] : []),
|
|
134
|
+
];
|
|
135
|
+
return (
|
|
136
|
+
<ProgressBar
|
|
137
|
+
className={className}
|
|
138
|
+
label={labels.progress(summary.approved, summary.total)}
|
|
139
|
+
max={Math.max(summary.total, 1)}
|
|
140
|
+
segments={segments}
|
|
141
|
+
legend={legend}
|
|
142
|
+
formatValue={(value) => formatNumber(value)}
|
|
143
|
+
/>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── TranslationLocaleTabs ─────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
export interface TranslationLocaleTab {
|
|
150
|
+
/** The locale code. */
|
|
151
|
+
value: string;
|
|
152
|
+
/** Its name, in its own language ("Français"). */
|
|
153
|
+
label: ReactNode;
|
|
154
|
+
/** The locale's rows, for the badge — built with the same `areas` as the panel's, so
|
|
155
|
+
* a scoped reviewer's tab counts what they can see. */
|
|
156
|
+
rows: readonly TranslationRow[];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface TranslationLocaleTabsProps {
|
|
160
|
+
/** The locales the viewer may see, in the app's order — the server's answer, never a
|
|
161
|
+
* guess from a role name. */
|
|
162
|
+
locales: readonly TranslationLocaleTab[];
|
|
163
|
+
active: string;
|
|
164
|
+
onChange: (locale: string) => void;
|
|
165
|
+
className?: string;
|
|
166
|
+
labels?: Partial<TranslationReviewLabels>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* One tab per locale with its approved/total, wrapping on a phone. Nothing for a single
|
|
171
|
+
* locale: a strip with one tab is a heading that looks clickable.
|
|
172
|
+
*/
|
|
173
|
+
export function TranslationLocaleTabs({ locales, active, onChange, className, labels: labelsProp }: TranslationLocaleTabsProps) {
|
|
174
|
+
const labels = useKitLabels("translationReview", DEFAULT_TRANSLATION_REVIEW_LABELS, labelsProp);
|
|
175
|
+
if (locales.length < 2) return null;
|
|
176
|
+
return (
|
|
177
|
+
<Tabs
|
|
178
|
+
aria-label={labels.locales}
|
|
179
|
+
wrap
|
|
180
|
+
className={className}
|
|
181
|
+
tabs={locales.map((locale) => {
|
|
182
|
+
const summary = summariseRows(locale.rows);
|
|
183
|
+
return {
|
|
184
|
+
id: locale.value,
|
|
185
|
+
label: locale.label,
|
|
186
|
+
name: typeof locale.label === "string" ? locale.label : locale.value,
|
|
187
|
+
badge: (
|
|
188
|
+
// The tab's own colour, dimmed — a fixed muted grey vanished on the selected
|
|
189
|
+
// tab's brand fill (390px browser check).
|
|
190
|
+
<span className="text-xs tabular-nums opacity-75">
|
|
191
|
+
{labels.localeProgress(summary.approved, summary.total)}
|
|
192
|
+
</span>
|
|
193
|
+
),
|
|
194
|
+
};
|
|
195
|
+
})}
|
|
196
|
+
active={active}
|
|
197
|
+
onChange={onChange}
|
|
198
|
+
/>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── TranslationExportButton ───────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
export interface TranslationExportButtonProps {
|
|
205
|
+
/** Every row the export covers — usually every locale's, not only the open one. */
|
|
206
|
+
rows: readonly TranslationRow[];
|
|
207
|
+
/** Listed in the file. Default: the locales the rows come from. */
|
|
208
|
+
locales?: readonly string[];
|
|
209
|
+
/** Default `translation-corrections-<date>.json`. */
|
|
210
|
+
fileName?: string;
|
|
211
|
+
/**
|
|
212
|
+
* Take the export instead of downloading it — the entries, and the file as a ready
|
|
213
|
+
* JSON Blob. Left out, the kit downloads the Blob as `fileName`.
|
|
214
|
+
*/
|
|
215
|
+
onExport?: (corrections: TranslationCorrection[], file: Blob) => void;
|
|
216
|
+
className?: string;
|
|
217
|
+
labels?: Partial<TranslationReviewLabels>;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* "Export corrections (n)": every string sent back, as the JSON file keksdose and kastlan
|
|
222
|
+
* hand a developer — `{ exported_at, locales, corrections }`. Corrections are not applied
|
|
223
|
+
* on the page: the strings are source code. Disabled while there is nothing to export.
|
|
224
|
+
*/
|
|
225
|
+
export function TranslationExportButton({
|
|
226
|
+
rows,
|
|
227
|
+
locales,
|
|
228
|
+
fileName,
|
|
229
|
+
onExport,
|
|
230
|
+
className,
|
|
231
|
+
labels: labelsProp,
|
|
232
|
+
}: TranslationExportButtonProps) {
|
|
233
|
+
const labels = useKitLabels("translationReview", DEFAULT_TRANSLATION_REVIEW_LABELS, labelsProp);
|
|
234
|
+
const corrections = useMemo(() => translationCorrections(rows), [rows]);
|
|
235
|
+
const run = () => {
|
|
236
|
+
const now = new Date();
|
|
237
|
+
const file = new Blob(
|
|
238
|
+
[
|
|
239
|
+
JSON.stringify(
|
|
240
|
+
{
|
|
241
|
+
exported_at: now.toISOString(),
|
|
242
|
+
locales: locales ?? [...new Set(rows.map((r) => r.locale))],
|
|
243
|
+
corrections,
|
|
244
|
+
},
|
|
245
|
+
null,
|
|
246
|
+
2,
|
|
247
|
+
),
|
|
248
|
+
],
|
|
249
|
+
{ type: "application/json" },
|
|
250
|
+
);
|
|
251
|
+
if (onExport) {
|
|
252
|
+
onExport(corrections, file);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const url = URL.createObjectURL(file);
|
|
256
|
+
const a = document.createElement("a");
|
|
257
|
+
a.href = url;
|
|
258
|
+
a.download = fileName ?? `translation-corrections-${now.toISOString().slice(0, 10)}.json`;
|
|
259
|
+
a.click();
|
|
260
|
+
// After the click has handed the URL to the download, not before.
|
|
261
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
262
|
+
};
|
|
263
|
+
return (
|
|
264
|
+
<Button variant="secondary" disabled={corrections.length === 0} onClick={run} className={className}>
|
|
265
|
+
<Download className="size-4" aria-hidden />
|
|
266
|
+
{labels.exportCorrections(corrections.length)}
|
|
267
|
+
</Button>
|
|
268
|
+
);
|
|
269
|
+
}
|