@protobase/ui 0.23.9 → 0.23.11

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.
Files changed (85) hide show
  1. package/dist/app/create/create-page.js +3 -2
  2. package/dist/app/create/create-record-form.js +4 -3
  3. package/dist/app/create/create-route.js +5 -8
  4. package/dist/app/create/create-values.js +2 -1
  5. package/dist/app/create/form.js +3 -2
  6. package/dist/app/create/messages.js +32 -0
  7. package/dist/app/delete/blocked-delete.js +6 -5
  8. package/dist/app/delete/dialogs.js +21 -20
  9. package/dist/app/delete/messages.js +66 -0
  10. package/dist/app/delete/use-delete-flow.js +7 -6
  11. package/dist/app/displays/display-controls.js +5 -4
  12. package/dist/app/displays/list-displays.js +2 -1
  13. package/dist/app/displays/messages.js +20 -0
  14. package/dist/app/embedded-list/cut-off-note.js +2 -1
  15. package/dist/app/embedded-list/embedded-list.js +11 -6
  16. package/dist/app/embedded-list/embedded-row-targets.js +4 -3
  17. package/dist/app/embedded-list/messages.js +24 -0
  18. package/dist/app/error-banner.js +2 -1
  19. package/dist/app/field-errors.js +19 -19
  20. package/dist/app/field-messages.js +48 -0
  21. package/dist/app/hand-order/hand-sorted-list.js +6 -11
  22. package/dist/app/hand-order/messages.js +22 -0
  23. package/dist/app/hand-order/sort-button.js +2 -1
  24. package/dist/app/hand-order/use-hand-order.js +2 -1
  25. package/dist/app/incoming-card.js +2 -1
  26. package/dist/app/list-edit/messages.js +14 -0
  27. package/dist/app/list-edit/use-list-edits.js +2 -1
  28. package/dist/app/list-empty-state.js +5 -5
  29. package/dist/app/list-empty.js +3 -2
  30. package/dist/app/list-messages.js +54 -0
  31. package/dist/app/list-page.js +24 -38
  32. package/dist/app/list-search.js +8 -7
  33. package/dist/app/messages.js +16 -0
  34. package/dist/app/pages/record-actions.js +4 -3
  35. package/dist/app/record-lines.js +3 -2
  36. package/dist/app/record-messages.js +78 -0
  37. package/dist/app/record-page.js +31 -32
  38. package/dist/app/related-section.js +5 -4
  39. package/dist/app/use-live-save.js +4 -3
  40. package/dist/data-grid/cell-text.js +2 -1
  41. package/dist/data-grid/cells.js +15 -14
  42. package/dist/data-grid/grid-footer.js +3 -2
  43. package/dist/data-grid/grid-header.js +4 -3
  44. package/dist/data-grid/grid-row.js +2 -1
  45. package/dist/data-grid/header-menu.js +5 -4
  46. package/dist/data-grid/messages.js +50 -0
  47. package/dist/data-grid/row-menu.js +2 -1
  48. package/dist/data-grid/row-reorder.js +8 -8
  49. package/dist/filter-panel/active-filter-chips.js +3 -2
  50. package/dist/filter-panel/date-presets.js +5 -4
  51. package/dist/filter-panel/facet-combobox.js +3 -2
  52. package/dist/filter-panel/facet-group.js +3 -2
  53. package/dist/filter-panel/filter-config.js +15 -12
  54. package/dist/filter-panel/filter-panel.js +4 -3
  55. package/dist/filter-panel/filter-state.js +2 -2
  56. package/dist/filter-panel/messages.js +52 -0
  57. package/dist/filter-panel/more-filters-menu.js +3 -2
  58. package/dist/filter-panel/range-filter.js +2 -1
  59. package/dist/filter-panel/secondary-filters.js +2 -1
  60. package/dist/forms/messages.js +24 -0
  61. package/dist/forms/prove-human.js +2 -1
  62. package/dist/forms/public-form-page.js +14 -8
  63. package/dist/live/display-value.js +3 -2
  64. package/dist/live/file-values.js +2 -1
  65. package/dist/live/messages.js +18 -0
  66. package/dist/package.js +1 -1
  67. package/dist/record-view/action-menu.js +2 -1
  68. package/dist/record-view/action-rail.js +2 -1
  69. package/dist/record-view/field.js +12 -7
  70. package/dist/record-view/messages.js +38 -0
  71. package/dist/record-view/record-header.js +8 -10
  72. package/dist/shell/assets/app-Bc3JCvDX.js +69 -0
  73. package/dist/shell/assets/export-worker-Bz57Pf5g.js +2 -0
  74. package/dist/shell/assets/{index-AuXmSIOC.js → index-DMPV9_11.js} +1 -1
  75. package/dist/shell/assets/{maplibre-ZHMezcAD.js → maplibre-CqykHSB6.js} +1 -1
  76. package/dist/shell/assets/{markdown-content-DDgQDs-m.js → markdown-content-BmKfQOZ6.js} +1 -1
  77. package/dist/shell/assets/{record-map-CkWfz5Uz.js → record-map-D2ccAykf.js} +1 -1
  78. package/dist/shell/assets/{records-map-VqY_uAtH.js → records-map-DKQG53I0.js} +1 -1
  79. package/dist/shell/assets/{shared-protobase-ui-ClGFtlOR.js → shared-protobase-ui-BCsFQ2PD.js} +1 -1
  80. package/dist/shell/index.html +3 -3
  81. package/dist/shell/protobase.shell.json +1 -1
  82. package/dist/shell/protobase.sw.js +2 -2
  83. package/package.json +4 -4
  84. package/dist/shell/assets/app-DCsXeyw4.js +0 -69
  85. package/dist/shell/assets/export-worker-O1HF9R4j.js +0 -2
@@ -2,6 +2,7 @@ import { InlineChildTable } from "../inline-child-table/inline-child-table.js";
2
2
  import { Section } from "../record-view/section.js";
3
3
  import { usePermissions } from "./meta-gate.js";
4
4
  import { can } from "./permissions.js";
5
+ import { text } from "./record-messages.js";
5
6
  import { jsx } from "react/jsx-runtime";
6
7
  //#region src/app/record-lines.tsx
7
8
  /** A resource that holds numbered lines of this one: it points at the parent and has `position` and `quantity`. */
@@ -17,8 +18,8 @@ const toLine = (record) => ({
17
18
  const RecordLines = ({ lines, draft, vatRate, saving, locked = false }) => {
18
19
  const canReorder = can(usePermissions(lines.name), "update");
19
20
  return /* @__PURE__ */ jsx(Section, {
20
- title: "Lines",
21
- help: "What the document bills for. Drag the handle, or use the menu, to change the order; Save applies it.",
21
+ title: text("lines"),
22
+ help: text("linesHelp"),
22
23
  children: /* @__PURE__ */ jsx("div", {
23
24
  className: "-mx-4 overflow-x-auto px-4 [-webkit-overflow-scrolling:touch] md:mx-0 md:px-0",
24
25
  children: /* @__PURE__ */ jsx("div", {
@@ -0,0 +1,78 @@
1
+ import { defineMessages } from "../locale/messages.js";
2
+ //#region src/app/record-messages.ts
3
+ const text = defineMessages({
4
+ en: {
5
+ details: "Details",
6
+ history: "History",
7
+ comments: "Comments",
8
+ summary: "Summary",
9
+ loading: "Loading",
10
+ fetching: "Fetching the record.",
11
+ noRecord: "This record does not exist, or you cannot see it.",
12
+ couldNotLoad: "Could not load the record",
13
+ someoneElse: "Someone else",
14
+ justNow: "just now",
15
+ saved: "Saved",
16
+ save: "Save",
17
+ cancel: "Cancel",
18
+ unsavedChanges: "Unsaved changes",
19
+ notSaved: "Not saved",
20
+ linesChanged: "The lines were changed by someone else",
21
+ linesReloaded: "The lines were reloaded. Your new order is kept: Save again to apply it.",
22
+ unsavedKept: "You have unsaved changes to this record.",
23
+ continueEditing: "Continue editing",
24
+ discard: "Discard",
25
+ unsavedRestored: "Your unsaved changes were restored.",
26
+ keep: "Keep",
27
+ savingSubject: (subject) => `Saving ${subject}...`,
28
+ subjectSaved: (subject) => `${subject} saved`,
29
+ changedBySomeoneElse: "Changed by someone else",
30
+ couldNotSave: "Could not save",
31
+ recordActions: "Record actions",
32
+ edit: "Edit",
33
+ delete: "Delete",
34
+ nothingYet: "Nothing here yet.",
35
+ couldNotLoadRelated: "Could not load",
36
+ relatedRecords: "Related records",
37
+ lines: "Lines",
38
+ linesHelp: "What the document bills for. Drag the handle, or use the menu, to change the order; Save applies it."
39
+ },
40
+ nl: {
41
+ details: "Details",
42
+ history: "Geschiedenis",
43
+ comments: "Opmerkingen",
44
+ summary: "Overzicht",
45
+ loading: "Laden",
46
+ fetching: "Het record wordt opgehaald.",
47
+ noRecord: "Dit record bestaat niet, of je mag het niet zien.",
48
+ couldNotLoad: "Kon het record niet laden",
49
+ someoneElse: "Iemand anders",
50
+ justNow: "zojuist",
51
+ saved: "Bewaard",
52
+ save: "Bewaren",
53
+ cancel: "Annuleren",
54
+ unsavedChanges: "Niet bewaarde wijzigingen",
55
+ notSaved: "Niet bewaard",
56
+ linesChanged: "De regels zijn door iemand anders gewijzigd",
57
+ linesReloaded: "De regels zijn opnieuw geladen. Je nieuwe volgorde is behouden: bewaar opnieuw om hem toe te passen.",
58
+ unsavedKept: "Je hebt niet bewaarde wijzigingen in dit record.",
59
+ continueEditing: "Verder bewerken",
60
+ discard: "Weggooien",
61
+ unsavedRestored: "Je niet bewaarde wijzigingen zijn teruggezet.",
62
+ keep: "Houden",
63
+ savingSubject: (subject) => `${subject} bewaren...`,
64
+ subjectSaved: (subject) => `${subject} bewaard`,
65
+ changedBySomeoneElse: "Gewijzigd door iemand anders",
66
+ couldNotSave: "Kon niet bewaren",
67
+ recordActions: "Recordacties",
68
+ edit: "Bewerken",
69
+ delete: "Verwijderen",
70
+ nothingYet: "Hier staat nog niets.",
71
+ couldNotLoadRelated: "Kon niet laden",
72
+ relatedRecords: "Gerelateerde records",
73
+ lines: "Regels",
74
+ linesHelp: "Waar het document voor factureert. Sleep de greep, of gebruik het menu, om de volgorde te wijzigen; Bewaren past hem toe."
75
+ }
76
+ });
77
+ //#endregion
78
+ export { text };
@@ -29,6 +29,7 @@ import { KeyFacts } from "../record-view/key-facts.js";
29
29
  import { fromDraft, isRequiredOnEdit } from "../live/field-values.js";
30
30
  import { fieldHelp, helpShown, recordShown, whenShown } from "./record-mode/record-mode.js";
31
31
  import { isChosen, keyValue } from "./create/create-values.js";
32
+ import { text } from "./messages.js";
32
33
  import { ErrorBanner } from "./error-banner.js";
33
34
  import { useProjectUi } from "./pages/project-ui.js";
34
35
  import { shownWidget } from "./field-types/shown-widget.js";
@@ -41,6 +42,7 @@ import { can, fieldAccess } from "./permissions.js";
41
42
  import { ValueBadge } from "./value-badge.js";
42
43
  import { FieldValue } from "./pages/field-value.js";
43
44
  import { namedIcon } from "./named-icon.js";
45
+ import { text as text$1 } from "./record-messages.js";
44
46
  import { useDeleteFlow } from "./delete/use-delete-flow.js";
45
47
  import { useRestore } from "./delete/use-restore.js";
46
48
  import { auditField } from "../live/audit-fields.js";
@@ -79,19 +81,20 @@ const layoutFor = (model, view, read) => {
79
81
  if (view && view.layout.length > 0) return view.layout;
80
82
  const shown = Object.values(model.fields).filter((field) => !hidden(model, field.name));
81
83
  const editable = shown.filter((field) => !field.readOnly);
84
+ const fields = (editable.length > 0 && !read ? editable : shown).map((field) => field.name);
82
85
  return [
83
86
  {
84
87
  kind: "section",
85
- title: "Details",
86
- fields: (editable.length > 0 && !read ? editable : shown).map((field) => field.name)
88
+ title: text$1("details"),
89
+ fields
87
90
  },
88
91
  ...model.history ? [{
89
92
  kind: "history",
90
- title: "History"
93
+ title: text$1("history")
91
94
  }] : [],
92
95
  ...model.comments ? [{
93
96
  kind: "comments",
94
- title: "Comments"
97
+ title: text$1("comments")
95
98
  }] : []
96
99
  ];
97
100
  };
@@ -99,25 +102,21 @@ const RecordPage = ({ resource, recordKey }) => {
99
102
  const { resources } = useAdminMeta();
100
103
  const model = resources[resource];
101
104
  const record = useOpenedRecord(resource, recordKey);
102
- if (!model) return /* @__PURE__ */ jsxs(Notice, {
103
- title: "Not found",
104
- children: [
105
- "There is no resource called ",
106
- resource,
107
- "."
108
- ]
105
+ if (!model) return /* @__PURE__ */ jsx(Notice, {
106
+ title: text("notFound"),
107
+ children: text("noResource", resource)
109
108
  });
110
109
  if (record.isPending) return /* @__PURE__ */ jsx(Notice, {
111
- title: "Loading",
112
- children: "Fetching the record."
110
+ title: text$1("loading"),
111
+ children: text$1("fetching")
113
112
  });
114
113
  if (record.error) return /* @__PURE__ */ jsx("div", {
115
114
  className: "p-8",
116
115
  children: isApiError(record.error) && record.error.status === 404 ? /* @__PURE__ */ jsx(Notice, {
117
- title: "Not found",
118
- children: "This record does not exist, or you cannot see it."
116
+ title: text("notFound"),
117
+ children: text$1("noRecord")
119
118
  }) : /* @__PURE__ */ jsx(ErrorBanner, {
120
- title: "Could not load the record",
119
+ title: text$1("couldNotLoad"),
121
120
  error: record.error
122
121
  })
123
122
  });
@@ -241,11 +240,11 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
241
240
  wide: Boolean(hint(name)?.multiline || hint(name)?.markdown),
242
241
  conflict: conflict === void 0 ? void 0 : {
243
242
  user: {
244
- name: "Someone else",
243
+ name: text$1("someoneElse"),
245
244
  initials: "?",
246
245
  hue: 40
247
246
  },
248
- ago: "just now",
247
+ ago: text$1("justNow"),
249
248
  theirValue: conflict || "—",
250
249
  onKeepMine: () => editor.keepMine(name),
251
250
  onUseTheirs: () => editor.useTheirs(name)
@@ -295,7 +294,7 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
295
294
  };
296
295
  const firstSidebar = layout.findIndex((item) => item.kind === "sidebar");
297
296
  const summary = /* @__PURE__ */ jsx(MetadataCard, {
298
- title: "Summary",
297
+ title: text$1("summary"),
299
298
  rows: sidebarFields.flatMap((name) => {
300
299
  const field = model.fields[name];
301
300
  if (!field || !visible(name)) return [];
@@ -347,17 +346,17 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
347
346
  loading: editor.saving,
348
347
  disabled: !editor.dirty && editor.phase !== "saved",
349
348
  onClick: () => void save(),
350
- children: [editor.phase === "saved" && /* @__PURE__ */ jsx(Check, { className: "size-4" }), editor.phase === "saved" ? "Saved" : "Save"]
349
+ children: [editor.phase === "saved" && /* @__PURE__ */ jsx(Check, { className: "size-4" }), editor.phase === "saved" ? text$1("saved") : text$1("save")]
351
350
  }),
352
351
  leavesForm && /* @__PURE__ */ jsx(AppLink, {
353
352
  to: readAddress,
354
353
  className: cn(buttonClass(), "max-md:min-h-11 md:w-full"),
355
354
  onClick: (event) => inApp(event) && editor.discard(),
356
- children: "Cancel"
355
+ children: text$1("cancel")
357
356
  }),
358
357
  editor.dirty && /* @__PURE__ */ jsx("span", {
359
358
  className: "text-xs text-muted-foreground max-md:order-first",
360
- children: "Unsaved changes"
359
+ children: text$1("unsavedChanges")
361
360
  })
362
361
  ] });
363
362
  const mainItem = (item, index) => {
@@ -447,46 +446,46 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
447
446
  editor.message && /* @__PURE__ */ jsx("div", {
448
447
  className: "mt-4",
449
448
  children: /* @__PURE__ */ jsx(ErrorBanner, {
450
- title: "Not saved",
449
+ title: text$1("notSaved"),
451
450
  error: new Error(editor.message)
452
451
  })
453
452
  }),
454
453
  editor.linesConflict && /* @__PURE__ */ jsx("div", {
455
454
  className: "mt-4",
456
455
  children: /* @__PURE__ */ jsx(ErrorBanner, {
457
- title: "The lines were changed by someone else",
458
- error: /* @__PURE__ */ new Error("The lines were reloaded. Your new order is kept: Save again to apply it.")
456
+ title: text$1("linesChanged"),
457
+ error: new Error(text$1("linesReloaded"))
459
458
  })
460
459
  }),
461
460
  shown !== "form" && offersEdit && editor.dirty && /* @__PURE__ */ jsxs("div", {
462
461
  className: "mt-4 flex items-center justify-between gap-3 rounded-lg border bg-surface px-3 py-2",
463
462
  role: "status",
464
- children: [/* @__PURE__ */ jsx("span", { children: "You have unsaved changes to this record." }), /* @__PURE__ */ jsxs("span", {
463
+ children: [/* @__PURE__ */ jsx("span", { children: text$1("unsavedKept") }), /* @__PURE__ */ jsxs("span", {
465
464
  className: "flex gap-2",
466
465
  children: [/* @__PURE__ */ jsx(AppLink, {
467
466
  to: editAddress,
468
467
  className: buttonClass({ size: "sm" }),
469
- children: "Continue editing"
468
+ children: text$1("continueEditing")
470
469
  }), /* @__PURE__ */ jsx(Button, {
471
470
  size: "sm",
472
471
  onClick: editor.discard,
473
- children: "Discard"
472
+ children: text$1("discard")
474
473
  })]
475
474
  })]
476
475
  }),
477
476
  shown === "form" && editor.restored && /* @__PURE__ */ jsxs("div", {
478
477
  className: "mt-4 flex items-center justify-between gap-3 rounded-lg border bg-surface px-3 py-2",
479
478
  role: "status",
480
- children: [/* @__PURE__ */ jsx("span", { children: "Your unsaved changes were restored." }), /* @__PURE__ */ jsxs("span", {
479
+ children: [/* @__PURE__ */ jsx("span", { children: text$1("unsavedRestored") }), /* @__PURE__ */ jsxs("span", {
481
480
  className: "flex gap-2",
482
481
  children: [/* @__PURE__ */ jsx(Button, {
483
482
  size: "sm",
484
483
  onClick: editor.dismissRestored,
485
- children: "Keep"
484
+ children: text$1("keep")
486
485
  }), /* @__PURE__ */ jsx(Button, {
487
486
  size: "sm",
488
487
  onClick: editor.discard,
489
- children: "Discard"
488
+ children: text$1("discard")
490
489
  })]
491
490
  })]
492
491
  }),
@@ -541,7 +540,7 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
541
540
  return index === firstSidebar ? /* @__PURE__ */ jsx(Fragment, { children: summary }, "summary") : null;
542
541
  }),
543
542
  /* @__PURE__ */ jsx(MetadataCard, {
544
- title: "Details",
543
+ title: text$1("details"),
545
544
  rows: [auditField(model, "created"), updatedAt].flatMap((name) => {
546
545
  const field = name ? model.fields[name] : void 0;
547
546
  if (!name || !field || !visible(name)) return [];
@@ -11,6 +11,7 @@ import { relatedHandOrder } from "./hand-order/hand-order.js";
11
11
  import { relatedOpens } from "./related-opens.js";
12
12
  import { EmbeddedDisplaySwitch, EmbeddedList } from "./embedded-list/embedded-list.js";
13
13
  import { useEmbeddedDisplay } from "./embedded-list/use-embedded-display.js";
14
+ import { text } from "./record-messages.js";
14
15
  import { HandSortedList } from "./hand-order/hand-sorted-list.js";
15
16
  import { SortButton } from "./hand-order/sort-button.js";
16
17
  import { useState } from "react";
@@ -71,9 +72,9 @@ const Loading = () => /* @__PURE__ */ jsx("div", {
71
72
  className: "flex flex-col gap-2",
72
73
  children: Array.from({ length: 3 }, (_, index) => /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }, index))
73
74
  });
74
- const Empty = ({ text }) => /* @__PURE__ */ jsx("p", {
75
+ const Empty = ({ empty }) => /* @__PURE__ */ jsx("p", {
75
76
  className: "text-muted-foreground",
76
- children: text ?? "Nothing here yet."
77
+ children: empty ?? text("nothingYet")
77
78
  });
78
79
  /**
79
80
  * A record page section listing the first records of another resource that point at this record, directly or through
@@ -130,9 +131,9 @@ const RelatedSection = ({ item, value, owner }) => {
130
131
  help: item.help,
131
132
  actions,
132
133
  children: match.isPending ? /* @__PURE__ */ jsx(Loading, {}) : match.error ? /* @__PURE__ */ jsx(ErrorBanner, {
133
- title: "Could not load",
134
+ title: text("couldNotLoadRelated"),
134
135
  error: match.error
135
- }) : match.filter === null || match.filter === void 0 ? /* @__PURE__ */ jsx(Empty, { text: item.empty }) : /* @__PURE__ */ jsx(EmbeddedList, {
136
+ }) : match.filter === null || match.filter === void 0 ? /* @__PURE__ */ jsx(Empty, { empty: item.empty }) : /* @__PURE__ */ jsx(EmbeddedList, {
136
137
  model,
137
138
  view,
138
139
  filter: match.filter,
@@ -1,5 +1,6 @@
1
1
  import { useToast } from "../toasts/toast-provider.js";
2
2
  import { saveFailureDescription } from "./field-errors.js";
3
+ import { text } from "./record-messages.js";
3
4
  import { useEffect, useRef, useState } from "react";
4
5
  import { isApiError } from "@protobase/client";
5
6
  //#region src/app/use-live-save.ts
@@ -18,7 +19,7 @@ const useLiveSave = (mode, subject) => {
18
19
  const run = async (action) => {
19
20
  const id = mode === "toast" ? toast.show({
20
21
  state: "loading",
21
- title: `Saving ${subject}...`
22
+ title: text("savingSubject", subject)
22
23
  }) : void 0;
23
24
  setMessage(void 0);
24
25
  if (mode === "button") setPhase("saving");
@@ -26,7 +27,7 @@ const useLiveSave = (mode, subject) => {
26
27
  await action();
27
28
  if (id !== void 0) toast.update(id, {
28
29
  state: "success",
29
- title: `${subject} saved`
30
+ title: text("subjectSaved", subject)
30
31
  });
31
32
  if (mode === "button") {
32
33
  setPhase("saved");
@@ -35,7 +36,7 @@ const useLiveSave = (mode, subject) => {
35
36
  return true;
36
37
  } catch (error) {
37
38
  if (!isApiError(error)) throw error;
38
- const title = error.status === 412 ? "Changed by someone else" : "Could not save";
39
+ const title = error.status === 412 ? text("changedBySomeoneElse") : text("couldNotSave");
39
40
  const description = saveFailureDescription(error);
40
41
  if (id !== void 0) toast.update(id, {
41
42
  state: "error",
@@ -3,6 +3,7 @@ import { formatInt, formatPercent, formatSigned } from "../format/number.js";
3
3
  import { formatDate, formatDateTime, formatRelativeTime } from "../format/date.js";
4
4
  import { userById } from "../mocks/users.js";
5
5
  import { fileShown } from "../live/file-values.js";
6
+ import { text } from "./messages.js";
6
7
  import { enumLabel } from "@protobase/schema";
7
8
  //#region src/data-grid/cell-text.ts
8
9
  /** A relation cell's text: the related record's name when the row carries it, else its key. */
@@ -21,7 +22,7 @@ const cellText = (spec, value) => {
21
22
  case "percent": return Number(value) === 0 ? "—" : formatPercent(Number(value));
22
23
  case "status": return enumLabel(String(value), spec.labels);
23
24
  case "relation": return relationLabel(value);
24
- case "boolean": return value ? void 0 : "No";
25
+ case "boolean": return value ? void 0 : text("no");
25
26
  case "file": return fileShown(value)?.name ?? "—";
26
27
  case "user": return userById(String(value)).name;
27
28
  }
@@ -5,6 +5,7 @@ import { viewerZone, zoneLabel } from "../format/zone.js";
5
5
  import { formatDateTime } from "../format/date.js";
6
6
  import { userById } from "../mocks/users.js";
7
7
  import { fileShown, shownImage } from "../live/file-values.js";
8
+ import { text } from "./messages.js";
8
9
  import { cellText } from "./cell-text.js";
9
10
  import { FileImage } from "../live/file-image.js";
10
11
  import { ContactLink } from "../live/contact-link.js";
@@ -19,50 +20,50 @@ const muted = (text) => /* @__PURE__ */ jsx("span", {
19
20
  });
20
21
  const renderCell = (spec, value) => {
21
22
  if (value === null || value === void 0) return muted("—");
22
- const text = cellText(spec, value) ?? "";
23
+ const text$1 = cellText(spec, value) ?? "";
23
24
  switch (spec.kind) {
24
25
  case "text": return spec.link ? /* @__PURE__ */ jsx(ContactLink, {
25
26
  format: spec.link,
26
- value: text,
27
+ value: text$1,
27
28
  className: "block min-w-0 truncate"
28
- }) : /* @__PURE__ */ jsx(TruncatedText, { text });
29
+ }) : /* @__PURE__ */ jsx(TruncatedText, { text: text$1 });
29
30
  case "id": return /* @__PURE__ */ jsx(TruncatedText, {
30
- text,
31
+ text: text$1,
31
32
  className: "font-mono text-xs text-muted-foreground"
32
33
  });
33
34
  case "money": return /* @__PURE__ */ jsx("span", {
34
35
  className: "tabular-nums",
35
- children: text
36
+ children: text$1
36
37
  });
37
38
  case "date": return /* @__PURE__ */ jsx("span", {
38
39
  className: "tabular-nums text-muted-foreground",
39
- children: text
40
+ children: text$1
40
41
  });
41
42
  case "datetime": return /* @__PURE__ */ jsx("span", {
42
43
  className: "tabular-nums text-muted-foreground",
43
- title: spec.relative ? formatDateTime(Number(value)) : `Shown in ${zoneLabel(Number(value), viewerZone())}`,
44
- children: text
44
+ title: spec.relative ? formatDateTime(Number(value)) : text("shownIn", zoneLabel(Number(value), viewerZone())),
45
+ children: text$1
45
46
  });
46
47
  case "number": return /* @__PURE__ */ jsx("span", {
47
48
  className: "tabular-nums",
48
- children: text
49
+ children: text$1
49
50
  });
50
51
  case "signed": return /* @__PURE__ */ jsx("span", {
51
52
  className: cn("tabular-nums", Number(value) < 0 ? "text-danger-text" : "text-success-text"),
52
- children: text
53
+ children: text$1
53
54
  });
54
55
  case "percent": return Number(value) === 0 ? muted("—") : /* @__PURE__ */ jsx("span", {
55
56
  className: "tabular-nums",
56
- children: text
57
+ children: text$1
57
58
  });
58
59
  case "status": return /* @__PURE__ */ jsx(Badge, {
59
60
  tone: spec.tones?.[String(value)] ?? "neutral",
60
61
  color: valueColor(spec.colors, value),
61
62
  shape: spec.shape,
62
- children: text
63
+ children: text$1
63
64
  });
64
65
  case "relation": return /* @__PURE__ */ jsx(TruncatedText, {
65
- text,
66
+ text: text$1,
66
67
  className: "font-medium text-primary-text hover:underline"
67
68
  });
68
69
  case "boolean": return value ? /* @__PURE__ */ jsx("span", {
@@ -71,7 +72,7 @@ const renderCell = (spec, value) => {
71
72
  className: "size-3",
72
73
  strokeWidth: 3
73
74
  })
74
- }) : muted("No");
75
+ }) : muted(text("no"));
75
76
  case "file": {
76
77
  const file = fileShown(value);
77
78
  if (!file) return muted("—");
@@ -1,5 +1,6 @@
1
1
  import { cn } from "../lib/cn.js";
2
2
  import { formatApproxRows, formatInt } from "../format/number.js";
3
+ import { text } from "./messages.js";
3
4
  import { jsx, jsxs } from "react/jsx-runtime";
4
5
  //#region src/data-grid/grid-footer.tsx
5
6
  /** How many rows match and, with rows selected, how many and what can be done with them. */
@@ -9,9 +10,9 @@ const GridFooter = ({ total, selectedCount, bulkActions = [], className }) => /*
9
10
  className: "flex items-center tabular-nums",
10
11
  children: [
11
12
  formatApproxRows(total),
12
- selectedCount > 0 && /* @__PURE__ */ jsxs("span", {
13
+ selectedCount > 0 && /* @__PURE__ */ jsx("span", {
13
14
  className: "ml-3 font-medium text-primary-text",
14
- children: [formatInt(selectedCount), " selected"]
15
+ children: text("selected", formatInt(selectedCount))
15
16
  }),
16
17
  selectedCount > 0 && bulkActions.map((action) => /* @__PURE__ */ jsx("button", {
17
18
  type: "button",
@@ -1,5 +1,6 @@
1
1
  import { cn } from "../lib/cn.js";
2
2
  import { Checkbox } from "../primitives/checkbox.js";
3
+ import { text } from "./messages.js";
3
4
  import { gridTemplate } from "./grid-template.js";
4
5
  import { isRightAligned } from "./column-spec.js";
5
6
  import { HeaderMenu, unsortableText } from "./header-menu.js";
@@ -16,7 +17,7 @@ const HeaderLabel = ({ spec }) => /* @__PURE__ */ jsxs(Fragment, { children: [/*
16
17
  children: spec.header
17
18
  }), spec.highlight && /* @__PURE__ */ jsx("span", {
18
19
  className: "rounded bg-primary px-1 text-[10px] font-semibold uppercase leading-4 text-primary-foreground",
19
- children: "New"
20
+ children: text("new")
20
21
  })] });
21
22
  const GridHeader = ({ cells, specs, gutter, selectedCount, selectable, trailing, minScale, pins = {}, lastPinned, scrolled, onTogglePin, onClearSelection }) => /* @__PURE__ */ jsxs("div", {
22
23
  className: "grid h-8 shrink-0 items-center border-b bg-surface text-xs font-medium text-muted-foreground",
@@ -29,7 +30,7 @@ const GridHeader = ({ cells, specs, gutter, selectedCount, selectable, trailing,
29
30
  children: [selectable && /* @__PURE__ */ jsx(Checkbox, {
30
31
  checked: false,
31
32
  indeterminate: selectedCount > 0,
32
- label: selectedCount > 0 ? "Clear selection" : "No rows selected",
33
+ label: selectedCount > 0 ? text("clearSelection") : text("noRowsSelected"),
33
34
  disabled: selectedCount === 0,
34
35
  onChange: onClearSelection,
35
36
  className: cn(selectedCount === 0 && "opacity-60")
@@ -50,7 +51,7 @@ const GridHeader = ({ cells, specs, gutter, selectedCount, selectable, trailing,
50
51
  className: cn("group/head flex h-full min-w-0 flex-1 items-center gap-1 pl-3 pr-1 text-left", isRightAligned(spec.kind) && "justify-end", "hover:text-foreground", sorted && "text-foreground"),
51
52
  children: [/* @__PURE__ */ jsx(HeaderLabel, { spec }), /* @__PURE__ */ jsx(SortIcon, { sorted })]
52
53
  }) : /* @__PURE__ */ jsx("div", {
53
- title: unsortableText,
54
+ title: unsortableText(),
54
55
  className: cn("flex h-full min-w-0 flex-1 items-center gap-1 pl-3 pr-1 text-left", isRightAligned(spec.kind) && "justify-end"),
55
56
  children: /* @__PURE__ */ jsx(HeaderLabel, { spec })
56
57
  }), /* @__PURE__ */ jsx("div", {
@@ -2,6 +2,7 @@ import { cn } from "../lib/cn.js";
2
2
  import { Skeleton } from "../primitives/skeleton.js";
3
3
  import { formatInt } from "../format/number.js";
4
4
  import { Checkbox } from "../primitives/checkbox.js";
5
+ import { text } from "./messages.js";
5
6
  import { gridTemplate } from "./grid-template.js";
6
7
  import { isRightAligned } from "./column-spec.js";
7
8
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -27,7 +28,7 @@ const Gutter = ({ width, index, selectable, selected, onToggle, edge }) => /* @_
27
28
  style: { width },
28
29
  children: [selectable && /* @__PURE__ */ jsx(Checkbox, {
29
30
  checked: Boolean(selected),
30
- label: `Select row ${index + 1}`
31
+ label: text("selectRow", index + 1)
31
32
  }), width > 40 && /* @__PURE__ */ jsx("span", {
32
33
  className: "min-w-6 text-right text-[11px] tabular-nums text-faint-foreground",
33
34
  children: formatInt(index + 1)
@@ -1,9 +1,10 @@
1
+ import { text } from "./messages.js";
1
2
  import { useEffect, useRef, useState } from "react";
2
3
  import { MoreHorizontal } from "lucide-react";
3
4
  import { jsx, jsxs } from "react/jsx-runtime";
4
5
  //#region src/data-grid/header-menu.tsx
5
6
  /** Why a column header does nothing when pressed: a list sorts only on the columns its view makes sortable. */
6
- const unsortableText = "This column can't be sorted";
7
+ const unsortableText = () => text("unsortable");
7
8
  /** The "..." in a column header: pinning the column to the left edge, and saying so when the list can't sort by it. */
8
9
  const HeaderMenu = ({ header, pinned, sortable, onTogglePin }) => {
9
10
  const [open, setOpen] = useState(false);
@@ -25,7 +26,7 @@ const HeaderMenu = ({ header, pinned, sortable, onTogglePin }) => {
25
26
  className: "relative ml-auto shrink-0",
26
27
  children: [/* @__PURE__ */ jsx("button", {
27
28
  type: "button",
28
- "aria-label": `${header} column menu`,
29
+ "aria-label": text("columnMenu", header),
29
30
  "aria-haspopup": "menu",
30
31
  "aria-expanded": open,
31
32
  onClick: () => setOpen(!open),
@@ -40,7 +41,7 @@ const HeaderMenu = ({ header, pinned, sortable, onTogglePin }) => {
40
41
  role: "menuitem",
41
42
  "aria-disabled": "true",
42
43
  className: "block min-h-9 whitespace-nowrap px-3 py-1.5 text-muted-foreground",
43
- children: "This column can't be sorted"
44
+ children: unsortableText()
44
45
  })
45
46
  }), /* @__PURE__ */ jsx("li", {
46
47
  role: "none",
@@ -52,7 +53,7 @@ const HeaderMenu = ({ header, pinned, sortable, onTogglePin }) => {
52
53
  setOpen(false);
53
54
  onTogglePin();
54
55
  },
55
- children: pinned ? "Unpin" : "Pin to left"
56
+ children: pinned ? text("unpin") : text("pinToLeft")
56
57
  })
57
58
  })]
58
59
  })]
@@ -0,0 +1,50 @@
1
+ import { defineMessages } from "../locale/messages.js";
2
+ //#region src/data-grid/messages.ts
3
+ const text = defineMessages({
4
+ en: {
5
+ unsortable: "This column can't be sorted",
6
+ columnMenu: (header) => `${header} column menu`,
7
+ unpin: "Unpin",
8
+ pinToLeft: "Pin to left",
9
+ selected: (count) => `${count} selected`,
10
+ no: "No",
11
+ shownIn: (zone) => `Shown in ${zone}`,
12
+ new: "New",
13
+ clearSelection: "Clear selection",
14
+ noRowsSelected: "No rows selected",
15
+ selectRow: (row) => `Select row ${row}`,
16
+ rowActions: "Row actions",
17
+ row: "row",
18
+ move: (label) => `Move ${label}`,
19
+ reorderInstructions: "To move a row, press Space on its handle, move it with the up and down arrow keys, and press Space again to drop it there. Escape puts it back.",
20
+ pickedUp: (name, row, rows) => `Picked up ${name}, row ${row} of ${rows}.`,
21
+ isOver: (name, row, rows) => `${name} is over row ${row} of ${rows}.`,
22
+ droppedAt: (name, row, rows) => `${name} was dropped at row ${row} of ${rows}.`,
23
+ putBack: (name) => `${name} was put back.`,
24
+ cancelled: (name, row) => `Moving ${name} was cancelled. It is back at row ${row}.`
25
+ },
26
+ nl: {
27
+ unsortable: "Op deze kolom kan niet worden gesorteerd",
28
+ columnMenu: (header) => `Kolommenu ${header}`,
29
+ unpin: "Losmaken",
30
+ pinToLeft: "Links vastzetten",
31
+ selected: (count) => `${count} geselecteerd`,
32
+ no: "Nee",
33
+ shownIn: (zone) => `Getoond in ${zone}`,
34
+ new: "Nieuw",
35
+ clearSelection: "Selectie wissen",
36
+ noRowsSelected: "Geen rijen geselecteerd",
37
+ selectRow: (row) => `Rij ${row} selecteren`,
38
+ rowActions: "Rijacties",
39
+ row: "rij",
40
+ move: (label) => `${label} verplaatsen`,
41
+ reorderInstructions: "Om een rij te verplaatsen, druk op Spatie op de greep, verplaats hem met de pijltjestoetsen omhoog en omlaag, en druk nogmaals op Spatie om hem daar neer te zetten. Escape zet hem terug.",
42
+ pickedUp: (name, row, rows) => `${name} opgepakt, rij ${row} van ${rows}.`,
43
+ isOver: (name, row, rows) => `${name} is boven rij ${row} van ${rows}.`,
44
+ droppedAt: (name, row, rows) => `${name} is neergezet op rij ${row} van ${rows}.`,
45
+ putBack: (name) => `${name} is teruggezet.`,
46
+ cancelled: (name, row) => `Verplaatsen van ${name} is geannuleerd. Hij staat weer op rij ${row}.`
47
+ }
48
+ });
49
+ //#endregion
50
+ export { text };
@@ -1,5 +1,6 @@
1
1
  import { cn } from "../lib/cn.js";
2
2
  import { RouteLink } from "../primitives/route-link.js";
3
+ import { text } from "./messages.js";
3
4
  import { usePopoverMenu } from "../primitives/popover-menu.js";
4
5
  import { MoreHorizontal } from "lucide-react";
5
6
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -20,7 +21,7 @@ const RowMenu = ({ row, actions }) => {
20
21
  children: [/* @__PURE__ */ jsx("button", {
21
22
  ref: button,
22
23
  type: "button",
23
- "aria-label": "Row actions",
24
+ "aria-label": text("rowActions"),
24
25
  "aria-haspopup": "menu",
25
26
  "aria-expanded": open,
26
27
  onClick: () => setOpen(!open),