@lotics/ui 6.0.0 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +12 -3
- package/package.json +2 -1
- package/src/change_review.tsx +13 -10
- package/src/column_filter.tsx +3 -2
- package/src/combobox.tsx +4 -2
- package/src/confidence.tsx +2 -2
- package/src/date_filter.tsx +4 -21
- package/src/date_range_filter_field.tsx +3 -2
- package/src/drawer.tsx +6 -4
- package/src/filter_chip.tsx +3 -1
- package/src/locale.tsx +190 -0
- package/src/option_list.tsx +6 -11
- package/src/pagination.tsx +8 -12
- package/src/remainder_meter.tsx +6 -9
- package/src/sort_header.tsx +4 -6
package/AGENTS.md
CHANGED
|
@@ -12,9 +12,18 @@ typeahead, async search, virtualization, and a11y the primitive already ships.
|
|
|
12
12
|
`PortalHost` at the app root.
|
|
13
13
|
- RN-Web only: `View`/`ScrollView` from `react-native`, the `Text` primitive (no raw
|
|
14
14
|
`div`/`span`, no raw `fontSize`/`fontWeight`); styles are RN objects.
|
|
15
|
-
- i18n: most components are string-free
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
- i18n: most components are string-free. For the rest, set the language ONCE at the root with
|
|
16
|
+
**`LoticsLocaleProvider`** (`@lotics/ui/locale`, the sibling of `LoticsThemeProvider`) — wrap
|
|
17
|
+
`<LoticsLocaleProvider locale={vi}>` and every wired component picks up the pack, no per-instance
|
|
18
|
+
props. Shipped packs: `en` (default) and `vi` (one canonical Vietnamese translation, so apps don't
|
|
19
|
+
redefine — or drift on — strings). Resolution is **prop → provider locale → English default**, so a
|
|
20
|
+
per-instance `labels`/`clearLabel`/`selectAllLabel` still overrides for one-offs, and an un-wrapped
|
|
21
|
+
app stays English. Wired so far: `Pagination`, `SortHeader`/`Table.sortLabels`, `OptionList`
|
|
22
|
+
(+ `Select`/`Combobox` built on it), `FilterChip`, `Drawer`, `Confidence`, `RemainderMeter`,
|
|
23
|
+
`ChangeReview`/`ChangeReviewActions`, and `DateRangeFilterField`/`DateFilter` (presets + footer +
|
|
24
|
+
time-segment a11y, one `dateRange` slice). Adding a string to a wired component's `*Labels` forces
|
|
25
|
+
both packs in `locale.tsx` to fill it (compile error) — that's how the kit avoids a silent English
|
|
26
|
+
leak. Not yet on the provider (pass `labels` for now): calendar/gantt, the file/preview/comment labels.
|
|
18
27
|
|
|
19
28
|
---
|
|
20
29
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./tokens": "./src/tokens.ts",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"./status_grid": "./src/status_grid.tsx",
|
|
57
57
|
"./heatmap": "./src/heatmap.tsx",
|
|
58
58
|
"./trend_footer": "./src/trend_footer.tsx",
|
|
59
|
+
"./locale": "./src/locale.tsx",
|
|
59
60
|
"./spacing": "./src/spacing.ts",
|
|
60
61
|
"./theme": "./src/theme.tsx",
|
|
61
62
|
"./progress_bar": "./src/progress_bar.tsx",
|
package/src/change_review.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { reviewCardStyle, reviewResolvedStyle } from "./control_surface";
|
|
|
5
5
|
import { Text } from "./text";
|
|
6
6
|
import { Icon } from "./icon";
|
|
7
7
|
import { Button } from "./button";
|
|
8
|
+
import { useLoticsLocale } from "./locale";
|
|
8
9
|
|
|
9
10
|
export type ChangeReviewItemStatus = "pending" | "accepted" | "rejected";
|
|
10
11
|
|
|
@@ -116,17 +117,18 @@ export function ChangeDiff({ before, after }: { before?: string; after: string }
|
|
|
116
117
|
* `ReviewCard` reviews a SINGLE proposal, this is the BATCH engine.
|
|
117
118
|
*/
|
|
118
119
|
export function ChangeReview<T>(props: ChangeReviewProps<T>) {
|
|
120
|
+
const loc = useLoticsLocale().changeReview;
|
|
119
121
|
const status = props.status ?? "open";
|
|
120
122
|
const perItem = props.onAcceptItem != null || props.onRejectItem != null;
|
|
121
123
|
const total = props.items.length;
|
|
122
124
|
const statusFor = (item: T, i: number) => props.statusOf?.(item, i) ?? "pending";
|
|
123
125
|
const keptCount = props.items.filter((it, i) => statusFor(it, i) === "accepted").length;
|
|
124
|
-
const title = props.title ??
|
|
126
|
+
const title = props.title ?? loc.title;
|
|
125
127
|
const L = {
|
|
126
|
-
undo:
|
|
127
|
-
applied:
|
|
128
|
-
discarded:
|
|
129
|
-
keptCount:
|
|
128
|
+
undo: loc.undo,
|
|
129
|
+
applied: loc.applied,
|
|
130
|
+
discarded: loc.discarded,
|
|
131
|
+
keptCount: loc.keptCount,
|
|
130
132
|
...props.labels,
|
|
131
133
|
};
|
|
132
134
|
|
|
@@ -173,8 +175,8 @@ export function ChangeReview<T>(props: ChangeReviewProps<T>) {
|
|
|
173
175
|
<View key={props.getKey(item, i)} style={styles.itemCard}>
|
|
174
176
|
{props.renderItem(item, i)}
|
|
175
177
|
<View style={styles.itemActions}>
|
|
176
|
-
<Button title={props.rejectLabel ??
|
|
177
|
-
<Button title={props.acceptLabel ??
|
|
178
|
+
<Button title={props.rejectLabel ?? loc.reject} color="muted" onPress={() => props.onRejectItem?.(i)} />
|
|
179
|
+
<Button title={props.acceptLabel ?? loc.accept} color="secondary" onPress={() => props.onAcceptItem?.(i)} />
|
|
178
180
|
</View>
|
|
179
181
|
</View>
|
|
180
182
|
);
|
|
@@ -233,6 +235,7 @@ export function ChangeReview<T>(props: ChangeReviewProps<T>) {
|
|
|
233
235
|
* inline wrapper provides the chrome); self-hides once `status` isn't open.
|
|
234
236
|
*/
|
|
235
237
|
export function ChangeReviewActions<T>(props: ChangeReviewActionsProps<T>) {
|
|
238
|
+
const loc = useLoticsLocale().changeReview;
|
|
236
239
|
if ((props.status ?? "open") !== "open") return null;
|
|
237
240
|
const perItem = props.onAcceptItem != null;
|
|
238
241
|
const statusFor = (item: T, i: number) => props.statusOf?.(item, i) ?? "pending";
|
|
@@ -243,12 +246,12 @@ export function ChangeReviewActions<T>(props: ChangeReviewActionsProps<T>) {
|
|
|
243
246
|
});
|
|
244
247
|
return (
|
|
245
248
|
<View style={styles.actionsRow}>
|
|
246
|
-
{perItem ? <Button title={props.acceptAllLabel ??
|
|
249
|
+
{perItem ? <Button title={props.acceptAllLabel ?? loc.acceptAll} color="muted" onPress={acceptAll} /> : null}
|
|
247
250
|
<View style={{ flex: 1 }} />
|
|
248
|
-
{props.onDiscard ? <Button title={props.discardLabel ??
|
|
251
|
+
{props.onDiscard ? <Button title={props.discardLabel ?? loc.discard} color="muted" onPress={props.onDiscard} /> : null}
|
|
249
252
|
{props.onApply ? (
|
|
250
253
|
<Button
|
|
251
|
-
title={props.applyLabel ?? (perItem ?
|
|
254
|
+
title={props.applyLabel ?? (perItem ? loc.applyKept : loc.apply)}
|
|
252
255
|
color="primary"
|
|
253
256
|
disabled={perItem && keptCount === 0}
|
|
254
257
|
onPress={props.onApply}
|
package/src/column_filter.tsx
CHANGED
|
@@ -92,7 +92,8 @@ export interface ColumnFilterProps {
|
|
|
92
92
|
column: FilterableColumn;
|
|
93
93
|
value: ColumnFilterValue | undefined;
|
|
94
94
|
onChange: (value: ColumnFilterValue | undefined) => void;
|
|
95
|
-
/** Accessible name for the clear (X) control.
|
|
95
|
+
/** Accessible name for the clear (X) control. Omit to use the locale's
|
|
96
|
+
* `filterChip.clear` (English "Clear" by default). */
|
|
96
97
|
clearLabel?: string;
|
|
97
98
|
}
|
|
98
99
|
|
|
@@ -105,7 +106,7 @@ export interface ColumnFilterProps {
|
|
|
105
106
|
* `columnFilterToConditions`. Pure UI; no data layer.
|
|
106
107
|
*/
|
|
107
108
|
export function ColumnFilter(props: ColumnFilterProps) {
|
|
108
|
-
const { column, value, onChange, clearLabel
|
|
109
|
+
const { column, value, onChange, clearLabel } = props;
|
|
109
110
|
const active = isColumnFilterActive(value);
|
|
110
111
|
|
|
111
112
|
return (
|
package/src/combobox.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { useCallback, useRef, useState, type ReactNode } from "react";
|
|
10
10
|
import { colors } from "./colors";
|
|
11
11
|
import { FOCUS_RING } from "./control_surface";
|
|
12
|
+
import { useLoticsLocale } from "./locale";
|
|
12
13
|
import { Text } from "./text";
|
|
13
14
|
import { Icon, type IconName } from "./icon";
|
|
14
15
|
import { TextInputField } from "./text_input_field";
|
|
@@ -110,8 +111,6 @@ export function Combobox<T extends string = string, D = unknown>(props: Combobox
|
|
|
110
111
|
searchDebounceMs = 200,
|
|
111
112
|
icon,
|
|
112
113
|
placeholder,
|
|
113
|
-
recentsLabel = "Recent",
|
|
114
|
-
emptyText = "No results",
|
|
115
114
|
accessibilityLabel = "Results",
|
|
116
115
|
disabled = false,
|
|
117
116
|
clearable = false,
|
|
@@ -121,6 +120,9 @@ export function Combobox<T extends string = string, D = unknown>(props: Combobox
|
|
|
121
120
|
testID,
|
|
122
121
|
style,
|
|
123
122
|
} = props;
|
|
123
|
+
const loc = useLoticsLocale().optionList;
|
|
124
|
+
const emptyText = props.emptyText ?? loc.noResults;
|
|
125
|
+
const recentsLabel = props.recentsLabel ?? loc.recent;
|
|
124
126
|
|
|
125
127
|
const single = value ?? null;
|
|
126
128
|
const reflectedText = single ? (single.label ?? single.value) : "";
|
package/src/confidence.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { StyleSheet, View } from "react-native";
|
|
2
2
|
import { colors, solid, type ColorName } from "./colors";
|
|
3
3
|
import { Text } from "./text";
|
|
4
|
+
import { useLoticsLocale } from "./locale";
|
|
4
5
|
|
|
5
6
|
export type ConfidenceLevel = "high" | "medium" | "low";
|
|
6
7
|
|
|
@@ -20,7 +21,6 @@ export interface ConfidenceProps {
|
|
|
20
21
|
labels?: Partial<ConfidenceLabels>;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
const DEFAULT_LABELS: ConfidenceLabels = { high: "High confidence", medium: "Medium confidence", low: "Low confidence" };
|
|
24
24
|
const FILLED: Record<ConfidenceLevel, number> = { high: 3, medium: 2, low: 1 };
|
|
25
25
|
// high emerald, medium amber, low zinc (unsure — not an error). One family per
|
|
26
26
|
// level; the fill count AND the colour both carry it.
|
|
@@ -40,7 +40,7 @@ export function levelFromScore(score: number): ConfidenceLevel {
|
|
|
40
40
|
*/
|
|
41
41
|
export function Confidence(props: ConfidenceProps) {
|
|
42
42
|
const level = props.level ?? (props.score != null ? levelFromScore(props.score) : "medium");
|
|
43
|
-
const l = { ...
|
|
43
|
+
const l = { ...useLoticsLocale().confidence, ...props.labels };
|
|
44
44
|
const filled = FILLED[level];
|
|
45
45
|
const fill = solid(COLOR[level]);
|
|
46
46
|
return (
|
package/src/date_filter.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import { useScreenSize } from "./use_screen_size";
|
|
|
11
11
|
import { SegmentLabels } from "./date_segments";
|
|
12
12
|
import { PresetId, PRESET_IDS, getPresetValue } from "./date_filter_presets";
|
|
13
13
|
import { formatDate } from "./format_date";
|
|
14
|
+
import { useLoticsLocale } from "./locale";
|
|
14
15
|
|
|
15
16
|
type SelectionMode = "single" | "range";
|
|
16
17
|
|
|
@@ -42,25 +43,6 @@ export interface DateFilterLabels extends SegmentLabels {
|
|
|
42
43
|
selectDate: string;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
const DEFAULT_LABELS: DateFilterLabels = {
|
|
46
|
-
year: "Year",
|
|
47
|
-
month: "Month",
|
|
48
|
-
day: "Day",
|
|
49
|
-
hour: "Hour",
|
|
50
|
-
minute: "Minute",
|
|
51
|
-
dayPeriod: "AM/PM",
|
|
52
|
-
today: "Today",
|
|
53
|
-
yesterday: "Yesterday",
|
|
54
|
-
tomorrow: "Tomorrow",
|
|
55
|
-
thisWeek: "This week",
|
|
56
|
-
thisMonth: "This month",
|
|
57
|
-
lastMonth: "Last month",
|
|
58
|
-
custom: "Custom",
|
|
59
|
-
from: "From",
|
|
60
|
-
to: "To",
|
|
61
|
-
selectDateRange: "Select date range",
|
|
62
|
-
selectDate: "Select date",
|
|
63
|
-
};
|
|
64
46
|
|
|
65
47
|
export interface DateFilterProps {
|
|
66
48
|
value: DateFilterValue;
|
|
@@ -106,9 +88,10 @@ function formatDateDisplay(date: Date | null, locale: string | undefined): strin
|
|
|
106
88
|
|
|
107
89
|
export function DateFilter(props: DateFilterProps) {
|
|
108
90
|
const { value, onValueChange, includeTime = false, locale } = props;
|
|
91
|
+
const loc = useLoticsLocale().dateRange;
|
|
109
92
|
const labels = useMemo<DateFilterLabels>(
|
|
110
|
-
() => ({ ...
|
|
111
|
-
[props.labels],
|
|
93
|
+
() => ({ ...loc, ...props.labels }),
|
|
94
|
+
[loc, props.labels],
|
|
112
95
|
);
|
|
113
96
|
const screenSize = useScreenSize();
|
|
114
97
|
const calendarRef = useRef<CalendarRef>(null);
|
|
@@ -9,6 +9,7 @@ import { PressableHighlight } from "./pressable_highlight";
|
|
|
9
9
|
import { Popover, PopoverTrigger, PopoverContent, PopoverFooter } from "./popover";
|
|
10
10
|
import { DateFilter, DateFilterValue, DateFilterLabels } from "./date_filter";
|
|
11
11
|
import { formatDate } from "./format_date";
|
|
12
|
+
import { useLoticsLocale } from "./locale";
|
|
12
13
|
|
|
13
14
|
// =============================================================================
|
|
14
15
|
// DateRangeFilterField — the common filter composition over DateFilter:
|
|
@@ -27,7 +28,6 @@ export interface DateRangeFilterFieldLabels extends DateFilterLabels {
|
|
|
27
28
|
placeholder: string;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const DEFAULT_FIELD_LABELS = { clear: "Clear", done: "Done", placeholder: "All time" };
|
|
31
31
|
|
|
32
32
|
export interface DateRangeFilterFieldProps {
|
|
33
33
|
value: DateFilterValue;
|
|
@@ -110,7 +110,8 @@ function formatTrigger(value: DateFilterValue, includeTime: boolean, locale: str
|
|
|
110
110
|
|
|
111
111
|
export function DateRangeFilterField(props: DateRangeFilterFieldProps) {
|
|
112
112
|
const { value, onValueChange, includeTime, locale, testID } = props;
|
|
113
|
-
const
|
|
113
|
+
const loc = useLoticsLocale().dateRange;
|
|
114
|
+
const labels = useMemo(() => ({ ...loc, ...props.labels }), [loc, props.labels]);
|
|
114
115
|
const [open, setOpen] = useState(false);
|
|
115
116
|
|
|
116
117
|
const hasValue = Boolean(value.start.date || value.end.date);
|
package/src/drawer.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { colors } from "@lotics/ui/colors";
|
|
|
6
6
|
import { IconButton } from "@lotics/ui/icon_button";
|
|
7
7
|
import { Text } from "@lotics/ui/text";
|
|
8
8
|
import { useOverlayScope } from "@lotics/ui/overlay_scope";
|
|
9
|
+
import { useLoticsLocale } from "@lotics/ui/locale";
|
|
9
10
|
|
|
10
11
|
export interface DrawerProps {
|
|
11
12
|
open: boolean;
|
|
@@ -44,6 +45,7 @@ export interface DrawerProps {
|
|
|
44
45
|
*/
|
|
45
46
|
export function Drawer(props: DrawerProps) {
|
|
46
47
|
const { open, onOpenChange, title, width = 420, onPrev, onNext, position, children, testID } = props;
|
|
48
|
+
const loc = useLoticsLocale().drawer;
|
|
47
49
|
const screenSize = useScreenSize();
|
|
48
50
|
useOverlayScope(open);
|
|
49
51
|
const handleClose = () => onOpenChange(false);
|
|
@@ -79,7 +81,7 @@ export function Drawer(props: DrawerProps) {
|
|
|
79
81
|
<Modal visible={open} onRequestClose={handleClose} transparent>
|
|
80
82
|
<View style={styles.base}>
|
|
81
83
|
{/* Scrim is a sibling of the panel, so tapping the panel never closes. */}
|
|
82
|
-
<Pressable style={styles.scrim} onPress={handleClose} accessibilityLabel=
|
|
84
|
+
<Pressable style={styles.scrim} onPress={handleClose} accessibilityLabel={loc.close} tabIndex={-1} />
|
|
83
85
|
<View style={[styles.panel, { width: screenSize.small ? "100%" : width }]}>
|
|
84
86
|
<PortalHost>
|
|
85
87
|
<View style={styles.header}>
|
|
@@ -94,7 +96,7 @@ export function Drawer(props: DrawerProps) {
|
|
|
94
96
|
<View style={styles.nav}>
|
|
95
97
|
<IconButton
|
|
96
98
|
icon="chevron-left"
|
|
97
|
-
accessibilityLabel=
|
|
99
|
+
accessibilityLabel={loc.previous}
|
|
98
100
|
onPress={onPrev ?? (() => {})}
|
|
99
101
|
disabled={!onPrev}
|
|
100
102
|
/>
|
|
@@ -105,13 +107,13 @@ export function Drawer(props: DrawerProps) {
|
|
|
105
107
|
) : null}
|
|
106
108
|
<IconButton
|
|
107
109
|
icon="chevron-right"
|
|
108
|
-
accessibilityLabel=
|
|
110
|
+
accessibilityLabel={loc.next}
|
|
109
111
|
onPress={onNext ?? (() => {})}
|
|
110
112
|
disabled={!onNext}
|
|
111
113
|
/>
|
|
112
114
|
</View>
|
|
113
115
|
) : null}
|
|
114
|
-
<IconButton icon="x" size="lg" accessibilityLabel=
|
|
116
|
+
<IconButton icon="x" size="lg" accessibilityLabel={loc.close} onPress={handleClose} />
|
|
115
117
|
</View>
|
|
116
118
|
<View testID={testID} style={styles.body}>
|
|
117
119
|
{children}
|
package/src/filter_chip.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { TextLink } from "./text_link";
|
|
|
7
7
|
import { Chip } from "./chip";
|
|
8
8
|
import { Popover, PopoverTrigger, PopoverContent, PopoverFooter } from "./popover";
|
|
9
9
|
import type { PopoverSide, PopoverAlign } from "./popover";
|
|
10
|
+
import { useLoticsLocale } from "./locale";
|
|
10
11
|
|
|
11
12
|
export interface FilterChipProps {
|
|
12
13
|
/** The dimension name — shown alone when inactive ("Owner"), prefixed when
|
|
@@ -69,7 +70,8 @@ export function selectSummary(
|
|
|
69
70
|
* toolbar's `ColumnFilter` is this pill plus its query-condition mapping.
|
|
70
71
|
*/
|
|
71
72
|
export function FilterChip(props: FilterChipProps) {
|
|
72
|
-
const { label, summary, onClear,
|
|
73
|
+
const { label, summary, onClear, children, side = "bottom", align = "start", open, onOpenChange, footer } = props;
|
|
74
|
+
const clearLabel = props.clearLabel ?? useLoticsLocale().filterChip.clear;
|
|
73
75
|
const active = summary != null && (typeof summary !== "string" || summary.length > 0);
|
|
74
76
|
// The clear × / Clear footer only when there's a clearable selection AND no
|
|
75
77
|
// custom footer — a valued, non-clearable pill ("Target: 20") keeps its chevron.
|
package/src/locale.tsx
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
2
|
+
import { type PaginationLabels } from "./pagination";
|
|
3
|
+
import { type SortHeaderLabels } from "./sort_header";
|
|
4
|
+
import { type ConfidenceLabels } from "./confidence";
|
|
5
|
+
import { type RemainderMeterLabels } from "./remainder_meter";
|
|
6
|
+
import { type DateRangeFilterFieldLabels } from "./date_range_filter_field";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The kit's localizable strings, one slice per string-bearing component. A
|
|
10
|
+
* `LoticsLocale` is plain DATA (like a date-fns locale) — it carries no i18n
|
|
11
|
+
* LOGIC, so the kit stays translation-agnostic while an app supplies the pack
|
|
12
|
+
* once at its root. Each slice REUSES the component's own `*Labels` type, so
|
|
13
|
+
* adding a localizable string to a component (a new key on its `Labels`) forces
|
|
14
|
+
* BOTH packs below to fill it — a missing translation is a compile error here,
|
|
15
|
+
* never a silent English leak at a call site.
|
|
16
|
+
*
|
|
17
|
+
* Resolution order in every wired component: an explicit per-instance prop wins,
|
|
18
|
+
* else this locale (from `LoticsLocaleProvider`, default `en`), else nothing —
|
|
19
|
+
* the locale is always complete, so there is no third fallback to forget.
|
|
20
|
+
*/
|
|
21
|
+
export interface LoticsLocale {
|
|
22
|
+
/** `Pagination` range + page summaries and the prev/next tooltips. */
|
|
23
|
+
pagination: Required<PaginationLabels>;
|
|
24
|
+
/** `SortHeader` a11y prefix + asc/desc suffixes. */
|
|
25
|
+
sortHeader: Required<SortHeaderLabels>;
|
|
26
|
+
/** `OptionList` (and everything built on it — `Select`, `Combobox`, the
|
|
27
|
+
* in-cell editors): the select-all/deselect-all links, the empty state, and
|
|
28
|
+
* the `Combobox` recents header. */
|
|
29
|
+
optionList: { selectAll: string; deselectAll: string; noResults: string; recent: string };
|
|
30
|
+
/** `FilterChip` (and `ColumnFilter`): the generic clear affordance, used when
|
|
31
|
+
* a call site doesn't pass a dimension-specific `clearLabel`. */
|
|
32
|
+
filterChip: { clear: string };
|
|
33
|
+
/** `Drawer`: the record prev/next + close controls (screen-reader names). */
|
|
34
|
+
drawer: { previous: string; next: string; close: string };
|
|
35
|
+
/** `Confidence`: the full level phrase ("High confidence" …). */
|
|
36
|
+
confidence: ConfidenceLabels;
|
|
37
|
+
/** `RemainderMeter`: the applied / remaining / over / exact captions. */
|
|
38
|
+
remainderMeter: Required<RemainderMeterLabels>;
|
|
39
|
+
/** `ChangeReview` (+ `ChangeReviewActions`): the review-card chrome — the
|
|
40
|
+
* Keep/Drop verdicts, Undo, the Applied/Discarded tags, the kept-counter,
|
|
41
|
+
* the title, and the commit-bar Accept-all / Discard. */
|
|
42
|
+
changeReview: {
|
|
43
|
+
title: string;
|
|
44
|
+
accept: string;
|
|
45
|
+
reject: string;
|
|
46
|
+
undo: string;
|
|
47
|
+
applied: string;
|
|
48
|
+
discarded: string;
|
|
49
|
+
keptCount: (kept: number, total: number) => string;
|
|
50
|
+
acceptAll: string;
|
|
51
|
+
discard: string;
|
|
52
|
+
apply: string;
|
|
53
|
+
applyKept: string;
|
|
54
|
+
};
|
|
55
|
+
/** `DateRangeFilterField` (and the `DateFilter` panel it wraps): presets,
|
|
56
|
+
* from/to, the footer Clear/Done, the trigger placeholder, and the time-field
|
|
57
|
+
* segment a11y names. */
|
|
58
|
+
dateRange: DateRangeFilterFieldLabels;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The platform default — English. Every component's hardcoded default lives
|
|
62
|
+
* HERE now, so the strings are documented in one place and overridable. */
|
|
63
|
+
export const en: LoticsLocale = {
|
|
64
|
+
pagination: {
|
|
65
|
+
rangeWithTotal: (start, end, total) => `${start}–${end} of ${total.toLocaleString()}`,
|
|
66
|
+
range: (start, end) => `${start}–${end}`,
|
|
67
|
+
pageWithTotal: (page, pageCount) => `Page ${page} of ${pageCount}`,
|
|
68
|
+
page: (page) => `Page ${page}`,
|
|
69
|
+
previous: "Previous",
|
|
70
|
+
next: "Next",
|
|
71
|
+
},
|
|
72
|
+
sortHeader: {
|
|
73
|
+
sortBy: (label) => `Sort by ${label}`,
|
|
74
|
+
ascending: ", ascending",
|
|
75
|
+
descending: ", descending",
|
|
76
|
+
},
|
|
77
|
+
optionList: { selectAll: "Select all", deselectAll: "Deselect all", noResults: "No results", recent: "Recent" },
|
|
78
|
+
filterChip: { clear: "Clear" },
|
|
79
|
+
drawer: { previous: "Previous record", next: "Next record", close: "Close" },
|
|
80
|
+
confidence: { high: "High confidence", medium: "Medium confidence", low: "Low confidence" },
|
|
81
|
+
remainderMeter: {
|
|
82
|
+
applied: (allocated, total) => `${allocated} of ${total} applied`,
|
|
83
|
+
remaining: (remainder) => `${remainder} unapplied`,
|
|
84
|
+
over: (amount) => `Over by ${amount}`,
|
|
85
|
+
exact: "Fully applied",
|
|
86
|
+
},
|
|
87
|
+
changeReview: {
|
|
88
|
+
title: "Suggested edits",
|
|
89
|
+
accept: "Keep",
|
|
90
|
+
reject: "Drop",
|
|
91
|
+
undo: "Undo",
|
|
92
|
+
applied: "Applied",
|
|
93
|
+
discarded: "Discarded",
|
|
94
|
+
keptCount: (kept, total) => `${kept} of ${total} kept`,
|
|
95
|
+
acceptAll: "Accept all",
|
|
96
|
+
discard: "Discard",
|
|
97
|
+
apply: "Apply",
|
|
98
|
+
applyKept: "Apply kept",
|
|
99
|
+
},
|
|
100
|
+
dateRange: {
|
|
101
|
+
year: "Year", month: "Month", day: "Day", hour: "Hour", minute: "Minute", dayPeriod: "AM/PM",
|
|
102
|
+
today: "Today", yesterday: "Yesterday", tomorrow: "Tomorrow",
|
|
103
|
+
thisWeek: "This week", thisMonth: "This month", lastMonth: "Last month",
|
|
104
|
+
custom: "Custom", from: "From", to: "To",
|
|
105
|
+
selectDateRange: "Select date range", selectDate: "Select date",
|
|
106
|
+
clear: "Clear", done: "Done", placeholder: "All time",
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Vietnamese. Maintained once here so every app (and the frontend) shares one
|
|
111
|
+
* canonical translation — no per-app drift ("Chọn tất cả" vs "Chọn hết"). */
|
|
112
|
+
export const vi: LoticsLocale = {
|
|
113
|
+
pagination: {
|
|
114
|
+
rangeWithTotal: (start, end, total) => `${start}–${end} trên ${total.toLocaleString("vi-VN")}`,
|
|
115
|
+
range: (start, end) => `${start}–${end}`,
|
|
116
|
+
pageWithTotal: (page, pageCount) => `Trang ${page} / ${pageCount}`,
|
|
117
|
+
page: (page) => `Trang ${page}`,
|
|
118
|
+
previous: "Trang trước",
|
|
119
|
+
next: "Trang sau",
|
|
120
|
+
},
|
|
121
|
+
sortHeader: {
|
|
122
|
+
sortBy: (label) => `Sắp xếp theo ${label}`,
|
|
123
|
+
ascending: " (tăng dần)",
|
|
124
|
+
descending: " (giảm dần)",
|
|
125
|
+
},
|
|
126
|
+
optionList: { selectAll: "Chọn tất cả", deselectAll: "Bỏ chọn tất cả", noResults: "Không có kết quả", recent: "Gần đây" },
|
|
127
|
+
filterChip: { clear: "Xóa" },
|
|
128
|
+
drawer: { previous: "Bản ghi trước", next: "Bản ghi sau", close: "Đóng" },
|
|
129
|
+
confidence: { high: "Độ tin cậy cao", medium: "Độ tin cậy trung bình", low: "Độ tin cậy thấp" },
|
|
130
|
+
remainderMeter: {
|
|
131
|
+
applied: (allocated, total) => `Đã phân bổ ${allocated}/${total}`,
|
|
132
|
+
remaining: (remainder) => `Còn ${remainder}`,
|
|
133
|
+
over: (amount) => `Vượt ${amount}`,
|
|
134
|
+
exact: "Đã phân bổ đủ",
|
|
135
|
+
},
|
|
136
|
+
changeReview: {
|
|
137
|
+
title: "Đề xuất chỉnh sửa",
|
|
138
|
+
accept: "Giữ",
|
|
139
|
+
reject: "Bỏ",
|
|
140
|
+
undo: "Hoàn tác",
|
|
141
|
+
applied: "Đã áp dụng",
|
|
142
|
+
discarded: "Đã bỏ",
|
|
143
|
+
keptCount: (kept, total) => `Giữ ${kept}/${total}`,
|
|
144
|
+
acceptAll: "Giữ tất cả",
|
|
145
|
+
discard: "Hủy",
|
|
146
|
+
apply: "Áp dụng",
|
|
147
|
+
applyKept: "Áp dụng mục đã giữ",
|
|
148
|
+
},
|
|
149
|
+
dateRange: {
|
|
150
|
+
year: "Năm", month: "Tháng", day: "Ngày", hour: "Giờ", minute: "Phút", dayPeriod: "SA/CH",
|
|
151
|
+
today: "Hôm nay", yesterday: "Hôm qua", tomorrow: "Ngày mai",
|
|
152
|
+
thisWeek: "Tuần này", thisMonth: "Tháng này", lastMonth: "Tháng trước",
|
|
153
|
+
custom: "Tùy chọn", from: "Từ", to: "Đến",
|
|
154
|
+
selectDateRange: "Chọn khoảng ngày", selectDate: "Chọn ngày",
|
|
155
|
+
clear: "Xóa", done: "Xong", placeholder: "Tất cả thời gian",
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const LoticsLocaleContext = createContext<LoticsLocale>(en);
|
|
160
|
+
|
|
161
|
+
interface LoticsLocaleProviderProps {
|
|
162
|
+
/** The locale pack supplying every kit string. Import a shipped pack
|
|
163
|
+
* (`import { vi } from "@lotics/ui/locale"`) or pass your own `LoticsLocale`. */
|
|
164
|
+
locale: LoticsLocale;
|
|
165
|
+
children: ReactNode;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* App-root provider that supplies localized strings to @lotics/ui primitives —
|
|
170
|
+
* the sibling of `LoticsThemeProvider`. Wrap your top-level element once and
|
|
171
|
+
* every wired component picks up the pack; no per-instance label props:
|
|
172
|
+
*
|
|
173
|
+
* // src/main.tsx
|
|
174
|
+
* import { vi } from "@lotics/ui/locale";
|
|
175
|
+
* <LoticsLocaleProvider locale={vi}>
|
|
176
|
+
* <App />
|
|
177
|
+
* </LoticsLocaleProvider>
|
|
178
|
+
*
|
|
179
|
+
* Components read the locale via `useLoticsLocale()`. A per-instance label prop
|
|
180
|
+
* still overrides for one-offs; without a provider the English default applies,
|
|
181
|
+
* so an un-wrapped app behaves exactly as before.
|
|
182
|
+
*/
|
|
183
|
+
export function LoticsLocaleProvider(props: LoticsLocaleProviderProps) {
|
|
184
|
+
return <LoticsLocaleContext.Provider value={props.locale}>{props.children}</LoticsLocaleContext.Provider>;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Read the current locale. Wired primitives call this; apps use the provider. */
|
|
188
|
+
export function useLoticsLocale(): LoticsLocale {
|
|
189
|
+
return useContext(LoticsLocaleContext);
|
|
190
|
+
}
|
package/src/option_list.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import { ActivityIndicator } from "./activity_indicator";
|
|
|
10
10
|
import { TextInputField } from "./text_input_field";
|
|
11
11
|
import { useScreenSize } from "./use_screen_size";
|
|
12
12
|
import { useOptionList, type UseOptionListParams } from "./use_option_list";
|
|
13
|
+
import { useLoticsLocale } from "./locale";
|
|
13
14
|
import type { PickerOption } from "./picker";
|
|
14
15
|
|
|
15
16
|
export interface OptionListProps<T extends string = string, MULTI extends boolean = false, D = unknown>
|
|
@@ -38,17 +39,11 @@ export interface OptionListProps<T extends string = string, MULTI extends boolea
|
|
|
38
39
|
export function OptionList<T extends string, MULTI extends boolean = false, D = unknown>(
|
|
39
40
|
props: OptionListProps<T, MULTI, D>,
|
|
40
41
|
) {
|
|
41
|
-
const {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
emptyText = "No results",
|
|
47
|
-
accessibilityLabel,
|
|
48
|
-
selectAllLabel = "Select all",
|
|
49
|
-
deselectAllLabel = "Deselect all",
|
|
50
|
-
search,
|
|
51
|
-
} = props;
|
|
42
|
+
const { testID, renderOptionContent, getOptionDescription, loading = false, accessibilityLabel, search } = props;
|
|
43
|
+
const loc = useLoticsLocale().optionList;
|
|
44
|
+
const emptyText = props.emptyText ?? loc.noResults;
|
|
45
|
+
const selectAllLabel = props.selectAllLabel ?? loc.selectAll;
|
|
46
|
+
const deselectAllLabel = props.deselectAllLabel ?? loc.deselectAll;
|
|
52
47
|
const list = useOptionList(props);
|
|
53
48
|
const { small } = useScreenSize();
|
|
54
49
|
|
package/src/pagination.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import * as React from "react";
|
|
|
2
2
|
import { View, StyleSheet } from "react-native";
|
|
3
3
|
import { IconButton } from "./icon_button";
|
|
4
4
|
import { Text } from "./text";
|
|
5
|
+
import { useLoticsLocale } from "./locale";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Localizable strings for `Pagination`. Every field is optional; an omitted
|
|
@@ -25,12 +26,6 @@ export interface PaginationLabels {
|
|
|
25
26
|
next?: string;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
const defaultRangeWithTotal = (start: number, end: number, total: number): string =>
|
|
29
|
-
`${start}–${end} of ${total.toLocaleString()}`;
|
|
30
|
-
const defaultRange = (start: number, end: number): string => `${start}–${end}`;
|
|
31
|
-
const defaultPageWithTotal = (page: number, pageCount: number): string => `Page ${page} of ${pageCount}`;
|
|
32
|
-
const defaultPage = (page: number): string => `Page ${page}`;
|
|
33
|
-
|
|
34
29
|
export interface PaginationProps {
|
|
35
30
|
/** 0-indexed. */
|
|
36
31
|
page: number;
|
|
@@ -59,20 +54,21 @@ export interface PaginationProps {
|
|
|
59
54
|
*/
|
|
60
55
|
export function Pagination(props: PaginationProps): React.ReactNode {
|
|
61
56
|
const { page, pageSize, rowCount, hasMore, total, loading, onPageChange, labels } = props;
|
|
57
|
+
const loc = useLoticsLocale().pagination;
|
|
62
58
|
const start = page * pageSize + 1;
|
|
63
59
|
const end = page * pageSize + rowCount;
|
|
64
60
|
const showRange = !loading && rowCount > 0;
|
|
65
61
|
|
|
66
62
|
const summary = showRange
|
|
67
63
|
? total !== undefined
|
|
68
|
-
? (labels?.rangeWithTotal ??
|
|
69
|
-
: (labels?.range ??
|
|
64
|
+
? (labels?.rangeWithTotal ?? loc.rangeWithTotal)(start, end, total)
|
|
65
|
+
: (labels?.range ?? loc.range)(start, end)
|
|
70
66
|
: "";
|
|
71
67
|
|
|
72
68
|
const pageLabel =
|
|
73
69
|
total !== undefined
|
|
74
|
-
? (labels?.pageWithTotal ??
|
|
75
|
-
: (labels?.page ??
|
|
70
|
+
? (labels?.pageWithTotal ?? loc.pageWithTotal)(page + 1, Math.max(1, Math.ceil(total / pageSize)))
|
|
71
|
+
: (labels?.page ?? loc.page)(page + 1);
|
|
76
72
|
|
|
77
73
|
return (
|
|
78
74
|
<View style={styles.container}>
|
|
@@ -90,14 +86,14 @@ export function Pagination(props: PaginationProps): React.ReactNode {
|
|
|
90
86
|
color="secondary"
|
|
91
87
|
onPress={() => onPageChange(Math.max(0, page - 1))}
|
|
92
88
|
disabled={page === 0 || !!loading}
|
|
93
|
-
tooltip={labels?.previous ??
|
|
89
|
+
tooltip={labels?.previous ?? loc.previous}
|
|
94
90
|
/>
|
|
95
91
|
<IconButton
|
|
96
92
|
icon="chevron-right"
|
|
97
93
|
color="secondary"
|
|
98
94
|
onPress={() => onPageChange(page + 1)}
|
|
99
95
|
disabled={!hasMore || !!loading}
|
|
100
|
-
tooltip={labels?.next ??
|
|
96
|
+
tooltip={labels?.next ?? loc.next}
|
|
101
97
|
/>
|
|
102
98
|
</View>
|
|
103
99
|
</View>
|
package/src/remainder_meter.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { StyleSheet, View, type DimensionValue } from "react-native";
|
|
2
2
|
import { colors, solid } from "./colors";
|
|
3
3
|
import { Text } from "./text";
|
|
4
|
+
import { useLoticsLocale } from "./locale";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Override the meter's captions for localization. Each key is optional and
|
|
@@ -17,11 +18,6 @@ export interface RemainderMeterLabels {
|
|
|
17
18
|
exact?: string;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
const defaultApplied = (allocated: string, total: string) => `${allocated} of ${total} applied`;
|
|
21
|
-
const defaultRemaining = (remainder: string) => `${remainder} unapplied`;
|
|
22
|
-
const defaultOver = (amount: string) => `Over by ${amount}`;
|
|
23
|
-
const DEFAULT_EXACT = "Fully applied";
|
|
24
|
-
|
|
25
21
|
export interface RemainderMeterProps {
|
|
26
22
|
/** The source amount being distributed — the payment, the available stock. */
|
|
27
23
|
total: number;
|
|
@@ -43,20 +39,21 @@ export interface RemainderMeterProps {
|
|
|
43
39
|
*/
|
|
44
40
|
export function RemainderMeter(props: RemainderMeterProps) {
|
|
45
41
|
const { total, allocated, format = (n) => n.toLocaleString(), labels } = props;
|
|
42
|
+
const loc = useLoticsLocale().remainderMeter;
|
|
46
43
|
const remainder = total - allocated;
|
|
47
44
|
const state = remainder > 0 ? "under" : remainder < 0 ? "over" : "exact";
|
|
48
45
|
const pct: DimensionValue = `${total <= 0 ? 0 : Math.min(100, (allocated / total) * 100)}%`;
|
|
49
46
|
const barColor = state === "over" ? solid("red") : state === "exact" ? solid("emerald") : solid("blue");
|
|
50
47
|
const right =
|
|
51
48
|
state === "exact"
|
|
52
|
-
? (labels?.exact ??
|
|
49
|
+
? (labels?.exact ?? loc.exact)
|
|
53
50
|
: state === "over"
|
|
54
|
-
? (labels?.over ??
|
|
55
|
-
: (labels?.remaining ??
|
|
51
|
+
? (labels?.over ?? loc.over)(format(-remainder))
|
|
52
|
+
: (labels?.remaining ?? loc.remaining)(format(remainder));
|
|
56
53
|
return (
|
|
57
54
|
<View style={{ gap: 8 }}>
|
|
58
55
|
<View style={{ flexDirection: "row", alignItems: "baseline", gap: 8 }}>
|
|
59
|
-
<Text size="sm" color="muted" style={{ flex: 1 }}>{(labels?.applied ??
|
|
56
|
+
<Text size="sm" color="muted" style={{ flex: 1 }}>{(labels?.applied ?? loc.applied)(format(allocated), format(total))}</Text>
|
|
60
57
|
<Text size="sm" weight="medium" tabular color={state === "over" ? "danger" : state === "exact" ? "success" : "default"}>
|
|
61
58
|
{right}
|
|
62
59
|
</Text>
|
package/src/sort_header.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { Text } from "./text";
|
|
|
3
3
|
import { Icon } from "./icon";
|
|
4
4
|
import { colors } from "./colors";
|
|
5
5
|
import { PressableHighlight } from "./pressable_highlight";
|
|
6
|
+
import { useLoticsLocale } from "./locale";
|
|
6
7
|
|
|
7
8
|
export type SortDir = "asc" | "desc";
|
|
8
9
|
export interface SortState {
|
|
@@ -74,14 +75,11 @@ export interface SortHeaderProps {
|
|
|
74
75
|
*/
|
|
75
76
|
export function SortHeader(props: SortHeaderProps) {
|
|
76
77
|
const { label, sortKey, sort, onSort, align = "left", style, labels } = props;
|
|
78
|
+
const loc = useLoticsLocale().sortHeader;
|
|
77
79
|
const active = sort?.key === sortKey;
|
|
78
80
|
const arrow = active ? (sort.dir === "asc" ? "chevron-up" : "chevron-down") : undefined;
|
|
79
|
-
const dirText = active
|
|
80
|
-
|
|
81
|
-
? (labels?.ascending ?? ", ascending")
|
|
82
|
-
: (labels?.descending ?? ", descending")
|
|
83
|
-
: "";
|
|
84
|
-
const sortByLabel = (labels?.sortBy ?? ((l: string) => `Sort by ${l}`))(label);
|
|
81
|
+
const dirText = active ? (sort.dir === "asc" ? (labels?.ascending ?? loc.ascending) : (labels?.descending ?? loc.descending)) : "";
|
|
82
|
+
const sortByLabel = (labels?.sortBy ?? loc.sortBy)(label);
|
|
85
83
|
|
|
86
84
|
return (
|
|
87
85
|
<PressableHighlight
|