@protobase/ui 0.23.10 → 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-CPnp4jVD.js → index-DMPV9_11.js} +1 -1
  75. package/dist/shell/assets/{maplibre-CJlfYYmB.js → maplibre-CqykHSB6.js} +1 -1
  76. package/dist/shell/assets/{markdown-content-BDgnIKSJ.js → markdown-content-BmKfQOZ6.js} +1 -1
  77. package/dist/shell/assets/{record-map-BCWeSTdZ.js → record-map-D2ccAykf.js} +1 -1
  78. package/dist/shell/assets/{records-map-BhNGzm42.js → records-map-DKQG53I0.js} +1 -1
  79. package/dist/shell/assets/{shared-protobase-ui-C6_QptrK.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-BPh2QhzJ.js +0 -69
  85. package/dist/shell/assets/export-worker-O1HF9R4j.js +0 -2
@@ -1,42 +1,42 @@
1
1
  import { formatInt } from "../format/number.js";
2
+ import { text } from "./field-messages.js";
2
3
  import { isApiError } from "@protobase/client";
3
4
  //#region src/app/field-errors.ts
4
5
  const numberIn = (params, key) => typeof params?.[key] === "number" ? params[key] : void 0;
5
- const characters = (count) => `${formatInt(count)} character${count === 1 ? "" : "s"}`;
6
6
  const textMessage = (label, params) => {
7
7
  const min = numberIn(params, "min");
8
8
  const max = numberIn(params, "max");
9
- if (min !== void 0) return `${label} needs at least ${characters(min)}`;
10
- if (max !== void 0) return `${label} can have at most ${characters(max)}`;
9
+ if (min !== void 0) return text("atLeastCharacters", label, formatInt(min), min);
10
+ if (max !== void 0) return text("atMostCharacters", label, formatInt(max), max);
11
11
  };
12
12
  const wholeNumber = (params) => {
13
13
  const min = numberIn(params, "min");
14
14
  const max = numberIn(params, "max");
15
- if (min !== void 0) return `Enter a whole number of at least ${formatInt(min)}`;
16
- if (max !== void 0) return `Enter a whole number of at most ${formatInt(max)}`;
17
- return "Enter a whole number";
15
+ if (min !== void 0) return text("wholeNumberAtLeast", formatInt(min));
16
+ if (max !== void 0) return text("wholeNumberAtMost", formatInt(max));
17
+ return text("wholeNumber");
18
18
  };
19
19
  const decimalMessage = (params) => {
20
20
  const precision = numberIn(params, "precision");
21
21
  const scale = numberIn(params, "scale");
22
- if (precision === void 0 || scale === void 0) return "Enter a number";
22
+ if (precision === void 0 || scale === void 0) return text("number");
23
23
  const below = formatInt(10 ** (precision - scale));
24
- return scale === 0 ? `Enter a whole number below ${below}` : `Enter a number with at most ${scale} decimal${scale === 1 ? "" : "s"}, below ${below}`;
24
+ return scale === 0 ? text("wholeNumberBelow", below) : text("decimalsBelow", scale, below);
25
25
  };
26
26
  /** What to say about a value of each field type the server turned down; without an entry, the server's message shows. */
27
27
  const invalidValue = {
28
28
  text: textMessage,
29
29
  integer: (_, params) => wholeNumber(params),
30
- bigint: () => "Enter a whole number",
30
+ bigint: () => text("wholeNumber"),
31
31
  decimal: (_, params) => decimalMessage(params),
32
- date: () => "Enter a date",
33
- timestamp: () => "Enter a date and time",
34
- country: () => "Enter a two-letter country code, such as NL",
35
- currency: () => "Enter a three-letter currency code, such as EUR",
36
- uuid: () => "Enter a valid ID",
37
- enum: () => "Choose one of the options",
38
- relation: () => "Choose a record",
39
- json: () => "Enter valid JSON"
32
+ date: () => text("date"),
33
+ timestamp: () => text("timestamp"),
34
+ country: () => text("country"),
35
+ currency: () => text("currency"),
36
+ uuid: () => text("uuid"),
37
+ enum: () => text("enum"),
38
+ relation: () => text("relation"),
39
+ json: () => text("json")
40
40
  };
41
41
  /**
42
42
  * The message a form shows under a field the server turned down, in the words of the field's label: "Country is
@@ -44,7 +44,7 @@ const invalidValue = {
44
44
  * server words them.
45
45
  */
46
46
  const fieldMessage = (field, label, error) => {
47
- if (error.code === "required") return `${label} is required`;
47
+ if (error.code === "required") return text("required", label);
48
48
  if (error.code !== "invalid-value" || !field) return error.message;
49
49
  return invalidValue[field.type]?.(label, error.params) ?? error.message;
50
50
  };
@@ -64,7 +64,7 @@ const saveFailureDescription = (error) => {
64
64
  const fields = new Set(error.errors.flatMap((entry) => entry.field && entry.field !== "_record" ? [entry.field] : []));
65
65
  const record = error.errors.flatMap((entry) => entry.field === "_record" ? [entry.message] : []);
66
66
  if (fields.size === 0 && record.length === 0) return error.message;
67
- return [...fields.size === 0 ? [] : [`Check ${fields.size === 1 ? "the marked field" : `${fields.size} fields`}.`], ...record].join(" ");
67
+ return [...fields.size === 0 ? [] : [fields.size === 1 ? text("checkField") : text("checkFields", fields.size)], ...record].join(" ");
68
68
  };
69
69
  //#endregion
70
70
  export { fieldErrors, fieldMessage, saveFailureDescription };
@@ -0,0 +1,48 @@
1
+ import { defineMessages } from "../locale/messages.js";
2
+ //#region src/app/field-messages.ts
3
+ const text = defineMessages({
4
+ en: {
5
+ atLeastCharacters: (label, count, n) => `${label} needs at least ${count} character${n === 1 ? "" : "s"}`,
6
+ atMostCharacters: (label, count, n) => `${label} can have at most ${count} character${n === 1 ? "" : "s"}`,
7
+ wholeNumberAtLeast: (min) => `Enter a whole number of at least ${min}`,
8
+ wholeNumberAtMost: (max) => `Enter a whole number of at most ${max}`,
9
+ wholeNumber: "Enter a whole number",
10
+ number: "Enter a number",
11
+ wholeNumberBelow: (below) => `Enter a whole number below ${below}`,
12
+ decimalsBelow: (scale, below) => `Enter a number with at most ${scale} decimal${scale === 1 ? "" : "s"}, below ${below}`,
13
+ date: "Enter a date",
14
+ timestamp: "Enter a date and time",
15
+ country: "Enter a two-letter country code, such as NL",
16
+ currency: "Enter a three-letter currency code, such as EUR",
17
+ uuid: "Enter a valid ID",
18
+ enum: "Choose one of the options",
19
+ relation: "Choose a record",
20
+ json: "Enter valid JSON",
21
+ required: (label) => `${label} is required`,
22
+ checkField: "Check the marked field.",
23
+ checkFields: (count) => `Check ${count} fields.`
24
+ },
25
+ nl: {
26
+ atLeastCharacters: (label, count, n) => `${label} moet minstens ${count} ${n === 1 ? "teken" : "tekens"} hebben`,
27
+ atMostCharacters: (label, count, n) => `${label} mag hoogstens ${count} ${n === 1 ? "teken" : "tekens"} hebben`,
28
+ wholeNumberAtLeast: (min) => `Vul een geheel getal in van minstens ${min}`,
29
+ wholeNumberAtMost: (max) => `Vul een geheel getal in van hoogstens ${max}`,
30
+ wholeNumber: "Vul een geheel getal in",
31
+ number: "Vul een getal in",
32
+ wholeNumberBelow: (below) => `Vul een geheel getal in onder ${below}`,
33
+ decimalsBelow: (scale, below) => `Vul een getal in met hoogstens ${scale} ${scale === 1 ? "decimaal" : "decimalen"}, onder ${below}`,
34
+ date: "Vul een datum in",
35
+ timestamp: "Vul een datum en tijd in",
36
+ country: "Vul een landcode van twee letters in, zoals NL",
37
+ currency: "Vul een valutacode van drie letters in, zoals EUR",
38
+ uuid: "Vul een geldig ID in",
39
+ enum: "Kies een van de opties",
40
+ relation: "Kies een record",
41
+ json: "Vul geldige JSON in",
42
+ required: (label) => `${label} is verplicht`,
43
+ checkField: "Controleer het gemarkeerde veld.",
44
+ checkFields: (count) => `Controleer ${count} velden.`
45
+ }
46
+ });
47
+ //#endregion
48
+ export { text };
@@ -9,9 +9,10 @@ import { ErrorBanner } from "../error-banner.js";
9
9
  import { useProjectUi } from "../pages/project-ui.js";
10
10
  import { DataGrid } from "../../data-grid/data-grid.js";
11
11
  import { embeddedColumns } from "../embedded-list/embedded-columns.js";
12
+ import { text } from "./messages.js";
12
13
  import { useHandOrder } from "./use-hand-order.js";
13
14
  import { useMemo } from "react";
14
- import { jsx, jsxs } from "react/jsx-runtime";
15
+ import { jsx } from "react/jsx-runtime";
15
16
  //#region src/app/hand-order/hand-sorted-list.tsx
16
17
  /**
17
18
  * The records in their order by hand, in the list's table with a drag handle in each row's checkbox place: dragging a
@@ -59,22 +60,16 @@ const HandSortedList = ({ model, view, field, filter, columns }) => {
59
60
  children: Array.from({ length: 3 }, (_, index) => /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }, index))
60
61
  });
61
62
  if (order.error) return /* @__PURE__ */ jsx(ErrorBanner, {
62
- title: "Could not load",
63
+ title: text("couldNotLoad"),
63
64
  error: order.error
64
65
  });
65
- if (order.more) return /* @__PURE__ */ jsxs("p", {
66
+ if (order.more) return /* @__PURE__ */ jsx("p", {
66
67
  className: "text-muted-foreground",
67
- children: [
68
- "There are more than ",
69
- 500 .toLocaleString(formatTag()),
70
- " ",
71
- plural,
72
- ", too many to sort by hand."
73
- ]
68
+ children: text("tooMany", 500 .toLocaleString(formatTag()), plural)
74
69
  });
75
70
  if (!source || source.total === 0) return /* @__PURE__ */ jsx("p", {
76
71
  className: "text-muted-foreground",
77
- children: "Nothing here yet."
72
+ children: text("nothingYet")
78
73
  });
79
74
  const openRecord = (row) => go({
80
75
  kind: "record",
@@ -0,0 +1,22 @@
1
+ import { defineMessages } from "../../locale/messages.js";
2
+ //#region src/app/hand-order/messages.ts
3
+ const text = defineMessages({
4
+ en: {
5
+ done: "Done",
6
+ sort: "Sort",
7
+ couldNotLoad: "Could not load",
8
+ tooMany: (limit, plural) => `There are more than ${limit} ${plural}, too many to sort by hand.`,
9
+ nothingYet: "Nothing here yet.",
10
+ couldNotChange: "Could not change the order"
11
+ },
12
+ nl: {
13
+ done: "Klaar",
14
+ sort: "Sorteren",
15
+ couldNotLoad: "Kon niet laden",
16
+ tooMany: (limit, plural) => `Er zijn meer dan ${limit} ${plural}, te veel om met de hand te sorteren.`,
17
+ nothingYet: "Hier staat nog niets.",
18
+ couldNotChange: "Kon de volgorde niet wijzigen"
19
+ }
20
+ });
21
+ //#endregion
22
+ export { text };
@@ -1,10 +1,11 @@
1
1
  import { Button } from "../../primitives/button.js";
2
+ import { text } from "./messages.js";
2
3
  import { ArrowUpDown, Check } from "lucide-react";
3
4
  import { jsx, jsxs } from "react/jsx-runtime";
4
5
  //#region src/app/hand-order/sort-button.tsx
5
6
  /** The button that turns sorting by hand on, as **Sort**, and off again, as **Done**; with `iconOnly` its icon alone. */
6
7
  const SortButton = ({ sorting, onChange, size, iconOnly = false }) => {
7
- const label = sorting ? "Done" : "Sort";
8
+ const label = sorting ? text("done") : text("sort");
8
9
  return /* @__PURE__ */ jsxs(Button, {
9
10
  size,
10
11
  variant: sorting ? "primary" : "secondary",
@@ -6,6 +6,7 @@ import { refusalReason } from "../displays/board-columns.js";
6
6
  import { useAdminMeta } from "../meta-gate.js";
7
7
  import { useRefreshResource } from "../pages/use-layout-data.js";
8
8
  import { listLabels, withLabels } from "../../live/relation-labels.js";
9
+ import { text } from "./messages.js";
9
10
  import { useRef, useState } from "react";
10
11
  import { encodeKey } from "@protobase/schema/internal";
11
12
  import { useQuery } from "@tanstack/react-query";
@@ -73,7 +74,7 @@ const useHandOrder = ({ model, field, filter, fields }) => {
73
74
  setMoved(void 0);
74
75
  toast.show({
75
76
  state: "error",
76
- title: "Could not change the order",
77
+ title: text("couldNotChange"),
77
78
  description: refusalReason(error)
78
79
  });
79
80
  };
@@ -3,6 +3,7 @@ import { formatInt } from "../format/number.js";
3
3
  import { SidebarCard } from "../record-view/record-sidebar.js";
4
4
  import { useAdminMeta } from "./meta-gate.js";
5
5
  import { AppLink } from "./use-app-address.js";
6
+ import { text } from "./record-messages.js";
6
7
  import { incomingAddress, incomingRelations } from "./incoming-relations.js";
7
8
  import { useIncomingCounts } from "./use-incoming-counts.js";
8
9
  import { ChevronRight } from "lucide-react";
@@ -14,7 +15,7 @@ const IncomingCard = ({ model, recordKey, layout, lines }) => {
14
15
  const counts = useIncomingCounts(incomingRelations(model, resources, views, layout, lines), resources, recordKey);
15
16
  if (counts.length === 0) return null;
16
17
  return /* @__PURE__ */ jsx(SidebarCard, {
17
- title: "Related records",
18
+ title: text("relatedRecords"),
18
19
  children: /* @__PURE__ */ jsx("ul", {
19
20
  className: "-mx-2",
20
21
  children: counts.map(({ relation, count }) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(AppLink, {
@@ -0,0 +1,14 @@
1
+ import { defineMessages } from "../../locale/messages.js";
2
+ //#region src/app/list-edit/messages.ts
3
+ const text = defineMessages({
4
+ en: {
5
+ changedBySomeoneElse: "Changed by someone else",
6
+ couldNotSave: "Could not save"
7
+ },
8
+ nl: {
9
+ changedBySomeoneElse: "Gewijzigd door iemand anders",
10
+ couldNotSave: "Kon niet bewaren"
11
+ }
12
+ });
13
+ //#endregion
14
+ export { text };
@@ -2,6 +2,7 @@ import { keys } from "../../data/query-keys.js";
2
2
  import { useClient } from "../../data/api-provider.js";
3
3
  import { useToast } from "../../toasts/toast-provider.js";
4
4
  import { recordKey } from "../../live/model-helpers.js";
5
+ import { text } from "./messages.js";
5
6
  import { useRef, useState } from "react";
6
7
  import { isApiError } from "@protobase/client";
7
8
  import { encodeKey } from "@protobase/schema/internal";
@@ -54,7 +55,7 @@ const useListEdits = (model) => {
54
55
  const reason = error.errors.find((entry) => entry.field === name)?.message ?? error.message;
55
56
  toast.show({
56
57
  state: "error",
57
- title: changed ? "Changed by someone else" : "Could not save",
58
+ title: changed ? text("changedBySomeoneElse") : text("couldNotSave"),
58
59
  description: reason
59
60
  });
60
61
  }
@@ -1,27 +1,27 @@
1
+ import { text } from "./list-messages.js";
1
2
  //#region src/app/list-empty-state.ts
2
3
  /** Why a list has no rows, and the buttons that help: clear the search, clear the filters, or create the first record. */
3
4
  const listEmptyState = ({ plural, search, filtered, scoped, canCreate }) => {
4
- const noun = plural.toLowerCase();
5
5
  if (search) return {
6
- message: `No ${noun} match “${search}”.`,
6
+ message: text("noMatchSearch", plural, search),
7
7
  clearSearch: true,
8
8
  clearFilters: filtered,
9
9
  create: false
10
10
  };
11
11
  if (filtered) return {
12
- message: `No ${noun} match these filters.`,
12
+ message: text("noMatchFilters", plural),
13
13
  clearSearch: false,
14
14
  clearFilters: true,
15
15
  create: false
16
16
  };
17
17
  if (scoped) return {
18
- message: `No ${noun} match.`,
18
+ message: text("noMatch", plural),
19
19
  clearSearch: false,
20
20
  clearFilters: false,
21
21
  create: false
22
22
  };
23
23
  return {
24
- message: `No ${noun} yet.`,
24
+ message: text("noneYet", plural),
25
25
  clearSearch: false,
26
26
  clearFilters: false,
27
27
  create: canCreate
@@ -1,4 +1,5 @@
1
1
  import { Button } from "../primitives/button.js";
2
+ import { text } from "./list-messages.js";
2
3
  import { jsx, jsxs } from "react/jsx-runtime";
3
4
  //#region src/app/list-empty.tsx
4
5
  /** A list without rows: why (see `listEmptyState`), with buttons to clear its search or filters, or its New button. */
@@ -9,11 +10,11 @@ const ListEmpty = ({ state, onClearSearch, onClearFilters, create }) => /* @__PU
9
10
  children: [
10
11
  state.clearSearch && /* @__PURE__ */ jsx(Button, {
11
12
  onClick: onClearSearch,
12
- children: "Clear search"
13
+ children: text("clearSearch")
13
14
  }),
14
15
  state.clearFilters && /* @__PURE__ */ jsx(Button, {
15
16
  onClick: onClearFilters,
16
- children: "Clear filters"
17
+ children: text("clearFilters")
17
18
  }),
18
19
  state.create && create
19
20
  ]
@@ -0,0 +1,54 @@
1
+ import { defineMessages } from "../locale/messages.js";
2
+ //#region src/app/list-messages.ts
3
+ const text = defineMessages({
4
+ en: {
5
+ noList: (name) => `There is no list called ${name}.`,
6
+ searchPlural: (plural) => `Search ${plural.toLowerCase()}`,
7
+ search: "Search",
8
+ sort: "Sort",
9
+ filters: "Filters",
10
+ filtersCount: (count) => `Filters (${count})`,
11
+ newRecord: (singular) => `New ${singular.toLowerCase()}`,
12
+ export: "Export",
13
+ openRecord: (singular) => `Open ${singular.toLowerCase()}`,
14
+ delete: "Delete",
15
+ exportCount: (count) => `Export ${count}`,
16
+ deleteCount: (count) => `Delete ${count}`,
17
+ dragToSort: "Drag a row by its handle to move it. Every move is saved as you drop it.",
18
+ unreadableFilter: "The filter in the address could not be read",
19
+ couldNotLoadRows: "Could not load rows",
20
+ created: (plural) => `${plural} created`,
21
+ noMatchSearch: (plural, search) => `No ${plural.toLowerCase()} match “${search}”.`,
22
+ noMatchFilters: (plural) => `No ${plural.toLowerCase()} match these filters.`,
23
+ noMatch: (plural) => `No ${plural.toLowerCase()} match.`,
24
+ noneYet: (plural) => `No ${plural.toLowerCase()} yet.`,
25
+ clearSearch: "Clear search",
26
+ clearFilters: "Clear filters"
27
+ },
28
+ nl: {
29
+ noList: (name) => `Er is geen lijst met de naam ${name}.`,
30
+ searchPlural: (plural) => `Zoeken in ${plural.toLowerCase()}`,
31
+ search: "Zoeken",
32
+ sort: "Sorteren",
33
+ filters: "Filters",
34
+ filtersCount: (count) => `Filters (${count})`,
35
+ newRecord: (singular) => `${singular} toevoegen`,
36
+ export: "Exporteren",
37
+ openRecord: (singular) => `${singular} openen`,
38
+ delete: "Verwijderen",
39
+ exportCount: (count) => `${count} exporteren`,
40
+ deleteCount: (count) => `${count} verwijderen`,
41
+ dragToSort: "Sleep een rij aan de greep om hem te verplaatsen. Elke verplaatsing wordt bewaard zodra je loslaat.",
42
+ unreadableFilter: "Het filter in het adres kon niet worden gelezen",
43
+ couldNotLoadRows: "Kon de rijen niet laden",
44
+ created: (plural) => `${plural} aangemaakt`,
45
+ noMatchSearch: (plural, search) => `Geen ${plural.toLowerCase()} gevonden voor “${search}”.`,
46
+ noMatchFilters: (plural) => `Geen ${plural.toLowerCase()} gevonden met deze filters.`,
47
+ noMatch: (plural) => `Geen ${plural.toLowerCase()} gevonden.`,
48
+ noneYet: (plural) => `Nog geen ${plural.toLowerCase()}.`,
49
+ clearSearch: "Zoekopdracht wissen",
50
+ clearFilters: "Filters wissen"
51
+ }
52
+ });
53
+ //#endregion
54
+ export { text };
@@ -12,6 +12,7 @@ import { parseSort, sortToOrderBy } from "./list-order.js";
12
12
  import { listedResource } from "./current-address.js";
13
13
  import { Notice, useAdminMeta, usePermissions } from "./meta-gate.js";
14
14
  import { AppLink, useAppAddress } from "./use-app-address.js";
15
+ import { text } from "./messages.js";
15
16
  import { ErrorBanner } from "./error-banner.js";
16
17
  import { useProjectUi } from "./pages/project-ui.js";
17
18
  import { Sheet } from "../primitives/sheet.js";
@@ -46,6 +47,7 @@ import { createOffered } from "./create/create-offered.js";
46
47
  import { ImportButton } from "./import/import-button.js";
47
48
  import { exportOffered, exportsSelection } from "./export/export-options.js";
48
49
  import { useExportFlow } from "./export/use-export-flow.js";
50
+ import { text as text$1 } from "./list-messages.js";
49
51
  import { ListEmpty } from "./list-empty.js";
50
52
  import { listEmptyState } from "./list-empty-state.js";
51
53
  import { ListCards } from "./displays/list-cards.js";
@@ -81,22 +83,14 @@ const ListPage = ({ address }) => {
81
83
  if (!model) return prompt ? /* @__PURE__ */ jsx(SignInPrompt, {
82
84
  as: "page",
83
85
  prompt: prompt.signIn
84
- }) : /* @__PURE__ */ jsxs(Notice, {
85
- title: "Not found",
86
- children: [
87
- "There is no resource called ",
88
- address.resource,
89
- "."
90
- ]
86
+ }) : /* @__PURE__ */ jsx(Notice, {
87
+ title: text("notFound"),
88
+ children: text("noResource", address.resource)
91
89
  });
92
90
  const view = viewOfList(views[address.resource], address.list);
93
- if (address.list !== void 0 && !view) return /* @__PURE__ */ jsxs(Notice, {
94
- title: "Not found",
95
- children: [
96
- "There is no list called ",
97
- address.list,
98
- "."
99
- ]
91
+ if (address.list !== void 0 && !view) return /* @__PURE__ */ jsx(Notice, {
92
+ title: text("notFound"),
93
+ children: text$1("noList", address.list)
100
94
  });
101
95
  if (view?.searchesOf) return /* @__PURE__ */ jsx(SavedSearches, {
102
96
  model,
@@ -225,7 +219,7 @@ const LoadedList = ({ model, address, scope, heading }) => {
225
219
  const searchBox = model.search?.length ? /* @__PURE__ */ jsx(ListSearch, {
226
220
  value: search?.kind === "search" ? search.text : "",
227
221
  onCommit: commitSearch,
228
- placeholder: `Search ${plural.toLowerCase()}`,
222
+ placeholder: text$1("searchPlural", plural),
229
223
  className: desktop ? void 0 : "min-h-11"
230
224
  }) : null;
231
225
  const panel = /* @__PURE__ */ jsx(FilterPanel, {
@@ -248,7 +242,7 @@ const LoadedList = ({ model, address, scope, heading }) => {
248
242
  onClear: () => setFilter(emptyFilterState)
249
243
  });
250
244
  const phoneSort = sorted && /* @__PURE__ */ jsx(FilterSection, {
251
- title: "Sort",
245
+ title: text$1("sort"),
252
246
  children: /* @__PURE__ */ jsx(SortMenu, {
253
247
  model,
254
248
  view,
@@ -267,11 +261,7 @@ const LoadedList = ({ model, address, scope, heading }) => {
267
261
  className: "min-h-11",
268
262
  onClick: () => setSheetOpen(true),
269
263
  "aria-haspopup": "dialog",
270
- children: [
271
- /* @__PURE__ */ jsx(SlidersHorizontal, { className: "size-3.5" }),
272
- "Filters",
273
- chips.length > 0 && ` (${chips.length})`
274
- ]
264
+ children: [/* @__PURE__ */ jsx(SlidersHorizontal, { className: "size-3.5" }), chips.length > 0 ? text$1("filtersCount", chips.length) : text$1("filters")]
275
265
  }), searchBox ?? /* @__PURE__ */ jsx("div", {
276
266
  className: "min-w-0 flex-1",
277
267
  children: chipsRow
@@ -279,7 +269,7 @@ const LoadedList = ({ model, address, scope, heading }) => {
279
269
  }),
280
270
  searchBox && chipsRow,
281
271
  /* @__PURE__ */ jsx(Sheet, {
282
- title: "Filters",
272
+ title: text$1("filters"),
283
273
  open: sheetOpen,
284
274
  onClose: () => setSheetOpen(false),
285
275
  children: /* @__PURE__ */ jsx(FilterPanel, {
@@ -299,11 +289,7 @@ const LoadedList = ({ model, address, scope, heading }) => {
299
289
  resource
300
290
  },
301
291
  className: buttonClass({ variant: "primary" }),
302
- children: [
303
- /* @__PURE__ */ jsx(Plus, { className: "size-3.5" }),
304
- "New ",
305
- singular.toLowerCase()
306
- ]
292
+ children: [/* @__PURE__ */ jsx(Plus, { className: "size-3.5" }), text$1("newRecord", singular)]
307
293
  });
308
294
  const saveSearch = view?.saveSearch && !scope && parsed.ok && filterText && /* @__PURE__ */ jsx(SaveSearchButton, {
309
295
  model,
@@ -325,10 +311,10 @@ const LoadedList = ({ model, address, scope, heading }) => {
325
311
  const exportButton = exportOffered(view) && /* @__PURE__ */ jsxs(Button, {
326
312
  onClick: () => exportFlow.open(),
327
313
  ...!desktop && {
328
- "aria-label": "Export",
329
- title: "Export"
314
+ "aria-label": text$1("export"),
315
+ title: text$1("export")
330
316
  },
331
- children: [/* @__PURE__ */ jsx(Download, { className: "size-3.5" }), desktop && "Export"]
317
+ children: [/* @__PURE__ */ jsx(Download, { className: "size-3.5" }), desktop && text$1("export")]
332
318
  });
333
319
  const sortButton = handOrder && /* @__PURE__ */ jsx(SortButton, {
334
320
  sorting: handSorting,
@@ -371,19 +357,19 @@ const LoadedList = ({ model, address, scope, heading }) => {
371
357
  initialScrollIndex: scroll.initialIndex,
372
358
  onScrollIndex: scroll.remember,
373
359
  rowActions: [{
374
- label: `Open ${singular.toLowerCase()}`,
360
+ label: text$1("openRecord", singular),
375
361
  onSelect: openRecord
376
362
  }, ...can(permissions, "delete") ? [{
377
- label: "Delete",
363
+ label: text$1("delete"),
378
364
  destructive: true,
379
365
  available: (row) => rowAllows(row, "delete"),
380
366
  onSelect: (row) => void remove.start(target(row))
381
367
  }] : []],
382
368
  bulkActions: [...exportOffered(view) && exportsSelection(model) ? [{
383
- label: (count) => `Export ${count}`,
369
+ label: (count) => text$1("exportCount", count),
384
370
  onRun: (rows) => exportFlow.open(rows)
385
371
  }] : [], ...can(permissions, "delete") ? [{
386
- label: (count) => `Delete ${count}`,
372
+ label: (count) => text$1("deleteCount", count),
387
373
  destructive: true,
388
374
  onRun: (rows) => remove.startBulk(rows.map(target))
389
375
  }] : []],
@@ -465,7 +451,7 @@ const LoadedList = ({ model, address, scope, heading }) => {
465
451
  className: "flex flex-col gap-3",
466
452
  children: [/* @__PURE__ */ jsx("p", {
467
453
  className: "text-muted-foreground",
468
- children: "Drag a row by its handle to move it. Every move is saved as you drop it."
454
+ children: text$1("dragToSort")
469
455
  }), /* @__PURE__ */ jsx(HandSortedList, {
470
456
  model,
471
457
  view,
@@ -499,11 +485,11 @@ const LoadedList = ({ model, address, scope, heading }) => {
499
485
  ] })
500
486
  }),
501
487
  !parsed.ok && /* @__PURE__ */ jsx(ErrorBanner, {
502
- title: "The filter in the address could not be read",
488
+ title: text$1("unreadableFilter"),
503
489
  error: new Error(parsed.errors.map((error) => error.message).join("; "))
504
490
  }),
505
491
  rows.error && !display && !handSorting && /* @__PURE__ */ jsx(ErrorBanner, {
506
- title: "Could not load rows",
492
+ title: text$1("couldNotLoadRows"),
507
493
  error: rows.error
508
494
  }),
509
495
  view?.chart && !handSorting && (isGroupChart(view.chart) ? /* @__PURE__ */ jsx(ListGroupChart, {
@@ -515,7 +501,7 @@ const LoadedList = ({ model, address, scope, heading }) => {
515
501
  }) : /* @__PURE__ */ jsx(ListChart, {
516
502
  resource,
517
503
  chart: view.chart,
518
- title: `${plural} created`,
504
+ title: text$1("created", plural),
519
505
  noun: plural.toLowerCase(),
520
506
  filter: loadFilter
521
507
  })),
@@ -1,19 +1,20 @@
1
1
  import { cn } from "../lib/cn.js";
2
2
  import { Input } from "../primitives/input.js";
3
+ import { text } from "./list-messages.js";
3
4
  import { useEffect, useState } from "react";
4
5
  import { Search } from "lucide-react";
5
6
  import { jsx } from "react/jsx-runtime";
6
7
  //#region src/app/list-search.tsx
7
8
  /** A search box that commits after a short pause or on Enter; `className` goes on its frame. */
8
9
  const ListSearch = ({ value, onCommit, placeholder, className }) => {
9
- const [text, setText] = useState(value);
10
+ const [text$1, setText] = useState(value);
10
11
  useEffect(() => setText(value), [value]);
11
12
  useEffect(() => {
12
- if (text === value) return;
13
- const timer = setTimeout(() => onCommit(text), 350);
13
+ if (text$1 === value) return;
14
+ const timer = setTimeout(() => onCommit(text$1), 350);
14
15
  return () => clearTimeout(timer);
15
16
  }, [
16
- text,
17
+ text$1,
17
18
  value,
18
19
  onCommit
19
20
  ]);
@@ -21,10 +22,10 @@ const ListSearch = ({ value, onCommit, placeholder, className }) => {
21
22
  wrapperClassName: cn("min-w-0 flex-1 sm:w-72 sm:flex-initial", className),
22
23
  leading: /* @__PURE__ */ jsx(Search, { className: "size-4" }),
23
24
  placeholder,
24
- "aria-label": "Search",
25
- value: text,
25
+ "aria-label": text("search"),
26
+ value: text$1,
26
27
  onChange: (event) => setText(event.target.value),
27
- onKeyDown: (event) => event.key === "Enter" && onCommit(text)
28
+ onKeyDown: (event) => event.key === "Enter" && onCommit(text$1)
28
29
  });
29
30
  };
30
31
  //#endregion
@@ -0,0 +1,16 @@
1
+ import { defineMessages } from "../locale/messages.js";
2
+ //#region src/app/messages.ts
3
+ const text = defineMessages({
4
+ en: {
5
+ notFound: "Not found",
6
+ noResource: (name) => `There is no resource called ${name}.`,
7
+ somethingWentWrong: "Something went wrong"
8
+ },
9
+ nl: {
10
+ notFound: "Niet gevonden",
11
+ noResource: (name) => `Er is geen onderdeel met de naam ${name}.`,
12
+ somethingWentWrong: "Er ging iets mis"
13
+ }
14
+ });
15
+ //#endregion
16
+ export { text };
@@ -5,6 +5,7 @@ import { AppLink, useAppAddress } from "../use-app-address.js";
5
5
  import { ActionMenu } from "../../record-view/action-menu.js";
6
6
  import { ActionPrompt, asksFirst } from "./action-prompt.js";
7
7
  import { useActionRunner } from "./use-run-action.js";
8
+ import { text } from "../record-messages.js";
8
9
  import { actionIcon } from "../action-icon.js";
9
10
  import { useState } from "react";
10
11
  import { Pencil, Trash2 } from "lucide-react";
@@ -43,7 +44,7 @@ const RecordActions = ({ model, stored, recordKey, edit, onDelete }) => {
43
44
  const deletes = runner.actions.some((action) => action.run?.kind === "delete");
44
45
  const actions = [
45
46
  ...edit ? [{
46
- name: "Edit",
47
+ name: text("edit"),
47
48
  icon: Pencil,
48
49
  render: ({ className, children, role }) => /* @__PURE__ */ jsx(AppLink, {
49
50
  to: edit,
@@ -54,7 +55,7 @@ const RecordActions = ({ model, stored, recordKey, edit, onDelete }) => {
54
55
  }] : [],
55
56
  ...named,
56
57
  ...onDelete && !deletes ? [{
57
- name: "Delete",
58
+ name: text("delete"),
58
59
  icon: Trash2,
59
60
  tone: "danger",
60
61
  onRun: onDelete
@@ -68,7 +69,7 @@ const RecordActions = ({ model, stored, recordKey, edit, onDelete }) => {
68
69
  }),
69
70
  slot && createPortal(/* @__PURE__ */ jsx(ActionMenu, {
70
71
  actions,
71
- label: "Record actions",
72
+ label: text("recordActions"),
72
73
  className: "inline-flex size-11 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted md:hidden"
73
74
  }), slot),
74
75
  asking && /* @__PURE__ */ jsx(ActionPrompt, {