@object-ui/i18n 17.3.0 → 17.6.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +1929 -1
  2. package/README.md +41 -12
  3. package/dist/i18n.d.ts +1 -1
  4. package/dist/i18n.js +1 -2
  5. package/dist/index.d.ts +23 -21
  6. package/dist/index.js +31 -21
  7. package/dist/locales/ar.d.ts +638 -369
  8. package/dist/locales/ar.js +744 -424
  9. package/dist/locales/de.d.ts +638 -369
  10. package/dist/locales/de.js +759 -446
  11. package/dist/locales/en.d.ts +673 -404
  12. package/dist/locales/en.js +899 -406
  13. package/dist/locales/es.d.ts +638 -369
  14. package/dist/locales/es.js +739 -427
  15. package/dist/locales/fr.d.ts +638 -369
  16. package/dist/locales/fr.js +738 -423
  17. package/dist/locales/index.d.ts +6476 -3786
  18. package/dist/locales/index.js +20 -20
  19. package/dist/locales/ja.d.ts +638 -369
  20. package/dist/locales/ja.js +732 -417
  21. package/dist/locales/ko.d.ts +638 -369
  22. package/dist/locales/ko.js +736 -424
  23. package/dist/locales/pt.d.ts +638 -369
  24. package/dist/locales/pt.js +737 -425
  25. package/dist/locales/ru.d.ts +638 -369
  26. package/dist/locales/ru.js +750 -424
  27. package/dist/locales/zh.d.ts +672 -403
  28. package/dist/locales/zh.js +720 -403
  29. package/dist/pickLocalized.d.ts +58 -0
  30. package/dist/pickLocalized.js +115 -7
  31. package/dist/provider.d.ts +84 -2
  32. package/dist/provider.js +305 -31
  33. package/dist/useDisplayLocale.d.ts +49 -0
  34. package/dist/useDisplayLocale.js +55 -0
  35. package/dist/useObjectLabel.d.ts +94 -14
  36. package/dist/useObjectLabel.js +134 -21
  37. package/dist/useSafeTranslation.js +51 -3
  38. package/dist/utils/index.d.ts +4 -3
  39. package/dist/utils/index.js +4 -3
  40. package/dist/utils/number-display.d.ts +32 -0
  41. package/dist/utils/number-display.js +32 -0
  42. package/dist/utils/spec-formatters.d.ts +39 -13
  43. package/dist/utils/spec-formatters.js +3 -3
  44. package/package.json +4 -3
@@ -2,6 +2,20 @@
2
2
  * English (en) - Default language pack for Object UI
3
3
  */
4
4
  const en = {
5
+ // objectui#4467 — the impersonation banner (app-shell ImpersonationBanner).
6
+ // Raised by `session.impersonatedBy`, so it survives SPA reboots; it names
7
+ // BOTH parties because the audit trail attributes the work to the impersonated
8
+ // user, and the exit states its own failure rather than appearing to succeed.
9
+ impersonation: {
10
+ banner: {
11
+ message: 'You are impersonating {{user}} — every action is recorded as them.',
12
+ startedBy: 'Started by administrator {{admin}}.',
13
+ stop: 'Stop impersonating',
14
+ stopping: 'Stopping…',
15
+ stopFailed: 'Could not stop impersonating: {{reason}}',
16
+ notRestored: 'The server accepted the request but did not restore your administrator session — you are still impersonating {{user}}. Sign out and sign in again to end it.',
17
+ },
18
+ },
5
19
  // objectui#2600 B5 — capability picker scope group headers (labels come from
6
20
  // the sys_capability registry; only these group titles are UI strings).
7
21
  capability: {
@@ -22,6 +36,34 @@ const en = {
22
36
  studio_access: 'Studio Access',
23
37
  },
24
38
  },
39
+ // objectui#3546 slice six — the read-only facet summary + Studio deep-link a
40
+ // `sys_permission_set` record shows for its six authorization facets
41
+ // (ADR-0056 P1, plugin-detail's PermissionFacetLink).
42
+ //
43
+ // The four count labels are plural families. `_one` is the singular; the BASE
44
+ // key (no suffix) is the form every OTHER CLDR plural category resolves to,
45
+ // which is what keeps `ru` (few/many) and `ar` (two/few/many/zero) in their own
46
+ // language instead of falling through to English — i18next only looks up the
47
+ // one suffix a language's rules ask for, and falls back to the base key when
48
+ // that suffix is absent. `perm-home-namespace-3546.test.tsx` renders every
49
+ // language at counts 1/2/3/5/11/21/100 to hold this.
50
+ perm: {
51
+ facet: {
52
+ none: 'None',
53
+ more: '+{{count}} more',
54
+ objects: '{{count}} objects',
55
+ objects_one: '{{count}} object',
56
+ fields: '{{count}} field rules',
57
+ fields_one: '{{count}} field rule',
58
+ rls: '{{count}} RLS policies',
59
+ rls_one: '{{count}} RLS policy',
60
+ tabs: '{{count}} tab rules',
61
+ tabs_one: '{{count}} tab rule',
62
+ adminScope: 'Delegated admin configured',
63
+ designInStudio: 'Design in Studio →',
64
+ designInStudioHint: 'Design in Studio',
65
+ },
66
+ },
25
67
  lookup: {
26
68
  recentlyUsed: 'Recently used',
27
69
  allResults: 'All results',
@@ -50,13 +92,21 @@ const en = {
50
92
  retry: 'Retry',
51
93
  },
52
94
  common: {
53
- loading: 'Loading...',
95
+ loading: 'Loading…',
54
96
  save: 'Save',
97
+ // The SHARED "throw away the unsaved draft" verb, read by
98
+ // `@object-ui/components`' `ConfigPanelRenderer` footer (objectui#4750).
99
+ // The three older spellings stay where they are because each belongs to one
100
+ // surface's wording: `form.discard` is the confirm button of plugin-form's
101
+ // "Discard changes?" alert dialog, `console.settingsView.discard` and
102
+ // `console.objectView.discard` are two console view footers — and the last
103
+ // of those already disagrees with the other two in zh/ko/fr. This one is
104
+ // the shared component's, and its ten values are the majority spelling.
105
+ discard: 'Discard',
55
106
  cancel: 'Cancel',
56
107
  delete: 'Delete',
57
108
  edit: 'Edit',
58
109
  create: 'Create',
59
- search: 'Search',
60
110
  filter: 'Filter',
61
111
  reset: 'Reset',
62
112
  confirm: 'Confirm',
@@ -82,7 +132,7 @@ const en = {
82
132
  required: 'Required',
83
133
  optional: 'Optional',
84
134
  selectOption: 'Select an option',
85
- select: 'Select...',
135
+ select: 'Select…',
86
136
  openChat: 'Open chat',
87
137
  closeChat: 'Close chat',
88
138
  closePanel: 'Close panel',
@@ -93,6 +143,11 @@ const en = {
93
143
  itemCountOne: '{{count}} item',
94
144
  toggleSidebar: 'Toggle sidebar',
95
145
  package: 'Package',
146
+ done: 'Done',
147
+ editInStudio: 'Edit in studio',
148
+ record: 'Record',
149
+ retry: 'Retry',
150
+ printDialogHint: 'Opens your browser’s print dialog (not a PDF export)',
96
151
  },
97
152
  actions: {
98
153
  decisionOutput: {
@@ -136,6 +191,12 @@ const en = {
136
191
  unsavedChanges: 'You have unsaved changes. Are you sure you want to leave?',
137
192
  discardTitle: 'Discard changes?',
138
193
  discardMessage: 'You have unsaved changes. If you close this form now, your edits will be lost.',
194
+ // The create/edit dialog's `sr-only` accessible description, used when the
195
+ // form declares no `description` of its own (objectui#4024). Not visible
196
+ // copy: it is what assistive tech announces for the dialog. An app could
197
+ // only displace it by authoring a `description`, which makes a VISIBLE
198
+ // subtitle appear on every form — so the fallback has to come from here.
199
+ dialogDescriptionFallback: 'Complete the form fields, then submit or cancel.',
139
200
  keepEditing: 'Keep editing',
140
201
  discard: 'Discard',
141
202
  conflictTitle: 'Save conflict',
@@ -197,7 +258,7 @@ const en = {
197
258
  richText: {
198
259
  format: 'Format: {{format}}',
199
260
  basicEditorHint: 'Rich text editor (basic)',
200
- placeholder: 'Enter text...',
261
+ placeholder: 'Enter text…',
201
262
  },
202
263
  // objectui#3231 — the "this option list cannot be filled" copy shared by
203
264
  // the fixed-option widgets (select / multiselect / radio / checkboxes) AND
@@ -239,6 +300,30 @@ const en = {
239
300
  tags: {
240
301
  placeholder: 'Type and press Enter to add…',
241
302
  },
303
+ // objectui#4028 — `AddressField`'s five sub-labels, previously English
304
+ // string literals with no key at all: on a Chinese console every address
305
+ // field showed five English words in the middle of an otherwise fully
306
+ // translated form, and an app had NO way to reach them. The parts are not
307
+ // fields on the object (`billing_address` is a single `address` column),
308
+ // so there was nothing for a translation bundle to key on and no
309
+ // `subLabels` property to declare — the only workaround left was to stop
310
+ // using `Field.address()` and author five text fields, losing the
311
+ // structured value, geocoding and map view.
312
+ //
313
+ // Values are byte-identical to the literals they replace, so English and
314
+ // provider-less rendering are unchanged (`FIELD_DEFAULTS` in
315
+ // `packages/fields/src/widgets/useFieldTranslation.ts` carries the same
316
+ // five, which is what a host with no `I18nProvider` renders).
317
+ //
318
+ // The five INPUT PLACEHOLDERS that sat next to these labels are gone
319
+ // rather than keyed — see the widget for that measurement.
320
+ address: {
321
+ street: 'Street Address',
322
+ city: 'City',
323
+ state: 'State / Province',
324
+ postalCode: 'ZIP / Postal Code',
325
+ country: 'Country',
326
+ },
242
327
  // objectui#3406 — the accessible sentence of the textarea character
243
328
  // counter, rendered only when the field declares `maxLength`. The visible
244
329
  // text is `{count}/{max}` digits and needs no locale. ONE interpolated key
@@ -286,7 +371,7 @@ const en = {
286
371
  saveAll: 'Save All ({{count}})',
287
372
  addRecord: 'Add record',
288
373
  open: 'Open',
289
- search: 'Search...',
374
+ search: 'Search…',
290
375
  modified: '{{count}} row modified',
291
376
  saveFailed: 'Save failed',
292
377
  selected: '{{count}} selected',
@@ -299,7 +384,7 @@ const en = {
299
384
  delete: 'Delete',
300
385
  export: 'Export',
301
386
  exportAs: 'Export as {{format}}',
302
- loading: 'Loading grid...',
387
+ loading: 'Loading grid…',
303
388
  errorLoading: 'Error loading grid',
304
389
  pullToRefresh: 'Pull to refresh',
305
390
  refreshing: 'Refreshing…',
@@ -316,6 +401,32 @@ const en = {
316
401
  yes: 'Yes',
317
402
  no: 'No',
318
403
  systemFields: 'System',
404
+ // Column-footer aggregate prefixes, keyed by the spec's `ColumnSummary`
405
+ // vocabulary (objectui#4024). The footer already formatted its NUMBER
406
+ // through the locale-aware formatter and then put a hardcoded English
407
+ // `Avg: ` / `Sum: ` in front of it.
408
+ //
409
+ // `pattern` is a key rather than a `': '` baked into the renderer: the
410
+ // separator is translatable content — ja/zh set a fullwidth colon, ar runs
411
+ // right-to-left — so a pack owns the whole shape. Same reasoning as
412
+ // `collaboration.resolvedSuffix` below.
413
+ //
414
+ // `countEmpty`/`percentEmpty` (and the filled pair) deliberately share a
415
+ // word: the trailing `%` is what tells the two families apart on screen,
416
+ // exactly as the renderer's own comment says.
417
+ summary: {
418
+ pattern: '{{label}}: {{value}}',
419
+ count: 'Count',
420
+ countEmpty: 'Empty',
421
+ countFilled: 'Filled',
422
+ countUnique: 'Unique',
423
+ percentEmpty: 'Empty',
424
+ percentFilled: 'Filled',
425
+ sum: 'Sum',
426
+ avg: 'Avg',
427
+ min: 'Min',
428
+ max: 'Max',
429
+ },
319
430
  toolbar: {
320
431
  densityMode: 'Density',
321
432
  densityCompact: 'Compact',
@@ -519,7 +630,8 @@ const en = {
519
630
  sort: 'Sort',
520
631
  sortRecords: 'Sort Records',
521
632
  sortByIdSuffix: '(by ID)',
522
- sortRelationalHint: 'Columns that link to another record are not listed: they can only be sorted by the stored ID, not by the name shown in the cell. To sort by that name, add a formula field holding it.',
633
+ sortRelationalHint: 'Columns that link to another record are not listed: they can only be sorted by the stored ID, not by the name shown in the cell. To sort by that name, denormalize it onto this object as a stored field, written when the source changes, and sort by that. Not a formula field: it is virtual, so no column is stored for it and the server refuses to sort by one.',
634
+ resetSortToDefault: 'Reset to view default',
523
635
  group: 'Group',
524
636
  groupBy: 'Group By',
525
637
  export: 'Export',
@@ -544,6 +656,8 @@ const en = {
544
656
  loadErrorUnauthorizedMessage: 'Your session has expired or you are signed out. Sign in again to view these records.',
545
657
  loadErrorRejectedTitle: 'This view’s query was rejected',
546
658
  loadErrorRejectedMessage: 'The server could not process this view’s filter or query options. Clearing the filters usually fixes it; if the view is saved this way, an administrator needs to correct it.',
659
+ loadErrorApiDisabledTitle: 'This object isn’t available through the API',
660
+ loadErrorApiDisabledMessage: 'This page can’t load its records because the object is not exposed through the API. That is a setting on the object itself, not a permission — an administrator has to enable API access for it before this page can work.',
547
661
  retry: 'Retry',
548
662
  managedBy: {
549
663
  system: {
@@ -618,8 +732,11 @@ const en = {
618
732
  deleteCard: 'Delete card',
619
733
  deleteColumn: 'Delete column',
620
734
  noCards: 'No cards',
621
- cardTitlePlaceholder: 'Enter card title...',
735
+ cardTitlePlaceholder: 'Enter card title…',
622
736
  uncategorized: 'Uncategorized',
737
+ columns: 'columns',
738
+ requiredFieldsTitle: 'Complete required fields',
739
+ requiredFieldsDescription: 'This move makes the fields below required. Fill them in to continue.',
623
740
  },
624
741
  timeline: {
625
742
  bucket: {
@@ -632,6 +749,13 @@ const en = {
632
749
  noDate: 'No date',
633
750
  unassigned: 'Unassigned',
634
751
  },
752
+ scale: {
753
+ week: 'Week {{n}}',
754
+ quarter: 'Q{{quarter}} {{year}}',
755
+ },
756
+ gantt: {
757
+ rowLabel: 'Items',
758
+ },
635
759
  },
636
760
  gantt: {
637
761
  column: {
@@ -658,6 +782,7 @@ const en = {
658
782
  exportPng: 'Export as PNG',
659
783
  exportPdf: 'Export PDF',
660
784
  saveLayout: 'Save layout',
785
+ refresh: 'Refresh',
661
786
  undo: 'Undo',
662
787
  redo: 'Redo',
663
788
  },
@@ -705,6 +830,18 @@ const en = {
705
830
  ff: 'Finish → Finish',
706
831
  sf: 'Start → Finish',
707
832
  },
833
+ linkEnd: {
834
+ start: 'start',
835
+ end: 'end',
836
+ },
837
+ link: {
838
+ rejected: {
839
+ self: 'A task cannot depend on itself.',
840
+ locked: 'This row is locked and cannot take a new dependency.',
841
+ group: 'A summary row cannot take a dependency — link one of its tasks instead.',
842
+ cycle: 'That link would create a circular dependency.',
843
+ },
844
+ },
708
845
  conflict: {
709
846
  title: 'Schedule conflict',
710
847
  body: 'This move conflicts with dependency constraints. Auto-reschedule {{count}} affected task(s)?',
@@ -713,8 +850,8 @@ const en = {
713
850
  },
714
851
  autoScheduleDlg: {
715
852
  title: 'Auto-schedule',
716
- body: 'Shift {count} task(s) later to satisfy dependency links?',
717
- skipped: '{count} locked task(s) also violate links and were skipped.',
853
+ body: 'Shift {{count}} task(s) later to satisfy dependency links?',
854
+ skipped: '{{count}} locked task(s) also violate links and were skipped.',
718
855
  confirm: 'Apply',
719
856
  cancel: 'Cancel',
720
857
  none: 'All dependencies satisfied — nothing to reschedule.',
@@ -730,8 +867,11 @@ const en = {
730
867
  clear: 'Clear filters',
731
868
  empty: 'No options',
732
869
  // SINGLE braces on purpose: the ObjectGantt call site resolves these
733
- // with a literal `.replace('{shown}', …)`, not i18next interpolation
734
- // (same convention as `autoScheduleDlg.body` above).
870
+ // with a literal `.replace('{shown}', …)`, not i18next interpolation.
871
+ // The last key in the gantt namespace on that idiom — `conflict.body`
872
+ // and the two `autoScheduleDlg` counts moved to `{{count}}` + i18next
873
+ // interpolation in objectui#4157, where the single-brace call site met
874
+ // a `{{count}}` pack and rendered a literal `{2}`.
735
875
  resultSummary: 'Showing {shown} / {total} tasks',
736
876
  },
737
877
  readOnly: 'Read-only',
@@ -783,6 +923,8 @@ const en = {
783
923
  writeStrippedTitle: 'Saved — but some fields did not take effect',
784
924
  writeStrippedReadonly: 'Read-only, so it did not take effect: {{fields}}',
785
925
  writeStrippedByState: "Not editable in this record's current state, so it did not take effect: {{fields}}",
926
+ writeStrippedPrimaryKey: "The record's identifier cannot be changed by a save, so it did not take effect: {{fields}}",
927
+ writeStrippedUnknownReason: 'Not applied by the server: {{fields}}',
786
928
  approvalPendingEditable: 'In approval · editable',
787
929
  approvalPendingTooltip: 'This record has a pending approval request; this step still allows editing',
788
930
  approvalProgress: 'Approvals — {{got}} of {{need}}',
@@ -795,6 +937,18 @@ const en = {
795
937
  cancelApprovalTooltipUnlocked: 'Recall the pending approval request',
796
938
  cancelApprovalFailed: 'Failed to recall approval',
797
939
  cancelApprovalUnavailable: 'Recalling approvals is not supported on this data source',
940
+ // objectui#5916 — `record:path` stage state in the ACCESSIBLE NAME.
941
+ // Each stage is a `role="listitem"`, which is name-from-AUTHOR only, so
942
+ // visually-hidden text inside it computes to an EMPTY name; these compose
943
+ // the stage's own (already picklist-localized) label with its state into the
944
+ // `aria-label`. The ✓/✗ glyphs stay `aria-hidden` decoration.
945
+ pathLabel: 'Record path',
946
+ pathStageCompleted: '{{stage}}, completed',
947
+ pathStageCurrent: '{{stage}}, current stage',
948
+ pathStageUpcoming: '{{stage}}, upcoming',
949
+ pathStageLostCurrent: '{{stage}}, closed lost, current stage',
950
+ pathStageLostUpcoming: '{{stage}}, closed lost, not reached',
951
+ pathStageWonUpcoming: '{{stage}}, goal stage, not reached',
798
952
  linkCopied: 'Link copied to clipboard',
799
953
  linkCopyFailed: 'Failed to copy link',
800
954
  cancel: 'Cancel',
@@ -810,6 +964,7 @@ const en = {
810
964
  concurrentUpdateReload: 'Reload latest',
811
965
  concurrentUpdateOverwrite: 'Overwrite anyway',
812
966
  concurrentUpdateCancel: 'Cancel',
967
+ concurrentUpdateRecordLabel: 'this record',
813
968
  openInNewTab: 'Open in new tab',
814
969
  share: 'Share',
815
970
  duplicate: 'Duplicate',
@@ -852,16 +1007,19 @@ const en = {
852
1007
  relatedRecords: '{{count}} records',
853
1008
  relatedRecordOne: '{{count}} record',
854
1009
  noRelatedRecords: 'No related records found',
855
- loading: 'Loading...',
1010
+ loading: 'Loading…',
856
1011
  copyToClipboard: 'Copy to clipboard',
857
1012
  copied: 'Copied!',
858
1013
  deleteConfirmation: 'Are you sure you want to delete this record?',
1014
+ deleted: 'Record deleted',
859
1015
  editRecord: 'Edit record',
860
1016
  viewAll: 'View All',
861
1017
  new: 'New',
1018
+ add: 'Add',
862
1019
  emptyValue: '—',
863
1020
  activity: 'Activity',
864
1021
  history: 'History',
1022
+ historyEmpty: 'No history yet',
865
1023
  editRow: 'Edit',
866
1024
  deleteRow: 'Delete',
867
1025
  deleteRowConfirmation: 'Are you sure you want to delete this record?',
@@ -871,7 +1029,7 @@ const en = {
871
1029
  nextPage: 'Next',
872
1030
  pageOf: 'Page {{current}} of {{total}}',
873
1031
  sortBy: 'Sort by',
874
- filterPlaceholder: 'Filter...',
1032
+ filterPlaceholder: 'Filter…',
875
1033
  highlightFields: 'Key Fields',
876
1034
  // Comments
877
1035
  comments: 'Comments',
@@ -913,6 +1071,10 @@ const en = {
913
1071
  attachmentDownloadDenied: "You don't have access to download this attachment.",
914
1072
  attachmentAuthRequired: 'Please sign in to download this attachment.',
915
1073
  attachmentPermissionDenied: "You don't have permission to do that.",
1074
+ attachmentsAccessDenied: "You don't have access to these attachments.",
1075
+ attachmentsLoadFailed: "We couldn't load the attachments for this record.",
1076
+ attachmentsApiUnavailable: 'The attachments list is not available on this object.',
1077
+ retryLoadAttachments: 'Retry',
916
1078
  // Diff
917
1079
  unifiedDiff: 'Unified diff',
918
1080
  sideBySideDiff: 'Side-by-side diff',
@@ -942,6 +1104,13 @@ const en = {
942
1104
  nextRecordKey: 'Next record (→)',
943
1105
  lastRecord: 'Last record (End)',
944
1106
  noRecords: 'No records',
1107
+ // objectui#3863 — the BASE key is the slot every plural category a pack did not
1108
+ // enumerate resolves to, keeping that pack in its own language instead of falling
1109
+ // through `fallbackLng` to English. `en` itself can never reach it (its categories
1110
+ // are exactly `one`/`other`), so this value is parity ballast — and it must stay
1111
+ // byte-identical to the call site's inline `defaultValue` in
1112
+ // `record-reference-rail.tsx`, which `check:i18n-keys` now judges as class 3.
1113
+ showEmptyRelated: '+ {{count}} empty',
945
1114
  showEmptyRelated_one: '+ {{count}} empty',
946
1115
  showEmptyRelated_other: '+ {{count}} empty',
947
1116
  searchWhileNavigating: 'Search while navigating',
@@ -956,6 +1125,7 @@ const en = {
956
1125
  loadMore: 'Load more',
957
1126
  edited: '(edited)',
958
1127
  via: 'via {{source}}',
1128
+ viewSource: 'View source',
959
1129
  // Replies
960
1130
  replyCount: '{{count}} reply',
961
1131
  replyCountPlural: '{{count}} replies',
@@ -976,7 +1146,8 @@ const en = {
976
1146
  },
977
1147
  chart: {
978
1148
  noData: 'No chart data available',
979
- loading: 'Loading chart...',
1149
+ loading: 'Loading chart…',
1150
+ nullCategory: '(None)',
980
1151
  },
981
1152
  report: {
982
1153
  total: 'Total',
@@ -1001,111 +1172,21 @@ const en = {
1001
1172
  first: 'First',
1002
1173
  },
1003
1174
  editor: {
1004
- breadcrumb: 'Configuration',
1005
- basic: 'Basic',
1006
- title: 'Title',
1007
- titlePlaceholder: 'e.g. Pipeline by Quarter',
1008
- description: 'Description',
1009
- descriptionPlaceholder: 'What does this report show?',
1010
- type: 'Report type',
1011
- typeTabular: 'Tabular — flat list',
1012
- typeSummary: 'Summary — grouped with totals',
1013
- typeMatrix: 'Matrix — pivot rows × columns',
1014
- typeJoined: 'Joined — multiple blocks',
1015
- typeHelp: 'Pick the layout that matches what you want to see.',
1016
- data: 'Data',
1017
- objectName: 'Data source',
1018
- objectNamePlaceholder: 'e.g. opportunity',
1019
- objectNameHelp: 'Which object should this report query?',
1020
- limit: 'Row limit',
1021
- limitPlaceholder: 'e.g. 100',
1022
- columns: 'Columns',
1023
- columnsHint: 'Pick which fields appear as columns. Add an aggregate (Sum, Count…) on Summary or Matrix reports.',
1024
- filters: 'Filters',
1025
- filtersHint: 'Limit the rows that go into this report.',
1026
- filtersComplex: 'This report uses an advanced filter that cannot be edited here. Save will keep it unchanged.',
1027
- groupBy: 'Group by',
1028
- groupByHint: 'Group the data and compute subtotals.',
1029
- rows: 'Rows',
1030
- rowsHint: 'Group rows by these fields.',
1031
- columnsAxis: 'Columns',
1032
- columnsAxisHint: 'Pivot these fields across the top.',
1033
- values: 'Values',
1034
- valuesHint: 'Numbers shown in each cell. Pick one or more fields and add an aggregate (Sum, Count…).',
1035
- grouping: 'Grouping',
1036
- addGrouping: 'Add grouping',
1037
- dateGranularity: 'Date bucket',
1038
- dateGranularityNone: '(use raw value)',
1039
- day: 'Day',
1040
- week: 'Week',
1041
- month: 'Month',
1042
- quarter: 'Quarter',
1043
- year: 'Year',
1044
- sortAsc: 'Ascending',
1045
- sortDesc: 'Descending',
1046
- chart: 'Chart',
1047
- chartHint: 'Optional visualization rendered alongside the table.',
1048
- chartType: 'Chart type',
1049
- chartTitle: 'Chart title',
1050
- chartTitlePlaceholder: 'Defaults to the report title',
1051
- chartXAxis: 'X-axis (category)',
1052
- chartYAxis: 'Y-axis (value)',
1053
- chartShowLegend: 'Show legend',
1054
- chartShowDataLabels: 'Show data labels',
1055
- chartNone: '(no chart)',
1056
- chartBar: 'Bar',
1057
- chartLine: 'Line',
1058
- chartArea: 'Area',
1059
- chartPie: 'Pie',
1060
- chartDonut: 'Donut',
1061
- chartFunnel: 'Funnel',
1062
- validationNeedsObject: 'Pick a data source before adding columns.',
1063
- validationMatrixNeedsRowsCols: 'Matrix reports need at least one Row and one Column.',
1064
- validationSummaryNeedsRows: 'Summary reports need at least one grouping field.',
1065
- blocks: 'Blocks',
1066
- blocksHint: 'Each block is its own table or chart. Block-level filters are ANDed with the report filter.',
1067
- addBlock: 'Add block',
1068
- removeBlock: 'Remove block',
1069
- blockName: 'Block name',
1070
- blockNamePlaceholder: 'unique_block_name',
1071
- blockLabel: 'Display label',
1072
- blockLabelPlaceholder: 'Shown above the block',
1073
- blockDescription: 'Description',
1074
- blockDescriptionPlaceholder: 'Optional context for this block',
1075
- validationJoinedNeedsBlocks: 'Joined reports need at least one block.',
1076
- validationBlockNameRequired: 'Every block needs a non-empty name.',
1077
- validationBlockNameDuplicate: 'Block names must be unique within the report.',
1078
- validationBlockNeedsColumns: 'Every block needs at least one column.',
1079
- noneOption: '(none)',
1080
- addCondition: 'Add condition',
1081
- combineLogic: 'Combine with',
1082
- opContains: 'contains',
1083
- opIsEmpty: 'is empty',
1084
- opIsNotEmpty: 'is not empty',
1085
- formatAuto: 'Auto',
1086
- formatCurrency: 'Currency',
1087
- formatPercent: 'Percent',
1088
- formatInteger: 'Integer',
1089
- formatDate: 'Date',
1090
- formatDatetime: 'Date & time',
1091
- columnLabelPlaceholder: 'Label override',
1092
- aggregateColumn: 'Aggregate',
1093
- formatColumn: 'Format',
1094
- addColumns: 'Add fields',
1095
- searchFields: 'Search fields…',
1096
- noMatchingFields: 'No fields match your search.',
1097
- noFieldsAvailable: 'No fields available.',
1098
- columnsCount: '{n} column(s) selected',
1099
- columnsEmpty: 'No columns selected yet.',
1100
- fieldPickerTitle: 'Select fields',
1101
- fieldPickerDescription: 'Pick one or more fields to add. Use the search box to narrow the list.',
1102
- fieldPickerChangeTitle: 'Change field',
1103
- fieldPickerAddGroupingTitle: 'Add grouping',
1104
- fieldPickerEmpty: '(pick field)',
1105
- fieldPickerSelected: '{n} selected',
1106
- fieldPickerClear: 'Clear selection',
1107
- fieldPickerAdd: 'Add',
1108
- fieldPickerAddN: 'Add {n}',
1175
+ // ONE key by design, not by accident (objectui#4145). This namespace held
1176
+ // 106 keys labelling the hand-rolled report editor form; that form is gone
1177
+ // — `ReportConfigPanel`'s body is `ReportDefaultInspector`, a spec-driven
1178
+ // inspector whose labels come from the report spec's own metadata. The
1179
+ // other 105 keys had no reader in any package, in any pack, in any dynamic
1180
+ // `t()` form, so they were retired here rather than left for the next
1181
+ // author to infer a live UI from.
1182
+ //
1183
+ // `panelTitle` names the PANEL (its heading and its `role="complementary"`
1184
+ // landmark). It is minted by objectui#4137 and is the namespace's only
1185
+ // consumer. Do not grow this namespace back: the spec-driven inspector is
1186
+ // the settled direction, and
1187
+ // `packages/i18n/src/__tests__/report-editor-retired-4145.test.ts` fails on
1188
+ // any retired key that returns.
1189
+ panelTitle: 'Edit report',
1109
1190
  },
1110
1191
  },
1111
1192
  map: {
@@ -1115,66 +1196,6 @@ const en = {
1115
1196
  invalidCoordinates: '{{count}} record with missing or invalid coordinates excluded from the map.',
1116
1197
  invalidCoordinatesPlural: '{{count}} records with missing or invalid coordinates excluded from the map.',
1117
1198
  },
1118
- workflow: {
1119
- draft: 'Draft',
1120
- active: 'Active',
1121
- paused: 'Paused',
1122
- completed: 'Completed',
1123
- cancelled: 'Cancelled',
1124
- save: 'Save',
1125
- publish: 'Publish',
1126
- addNode: 'Add Node',
1127
- workflowFlow: 'Workflow Flow',
1128
- properties: 'Properties',
1129
- connections: 'Connections',
1130
- quickConnect: 'Quick Connect',
1131
- fromPlaceholder: 'From...',
1132
- toPlaceholder: 'To...',
1133
- connect: 'Connect',
1134
- label: 'Label',
1135
- type: 'Type',
1136
- description: 'Description',
1137
- assignee: 'Assignee',
1138
- assigneeType: 'Assignee Type',
1139
- user: 'User',
1140
- role: 'Role',
1141
- group: 'Group',
1142
- expression: 'Expression',
1143
- timeoutMinutes: 'Timeout (minutes)',
1144
- selectNodeToEdit: 'Select a node to edit its properties',
1145
- workflowTitle: 'Workflow Title',
1146
- newWorkflow: 'New Workflow',
1147
- outConnections: '{{count}} out',
1148
- start: 'Start',
1149
- end: 'End',
1150
- task: 'Task',
1151
- userTask: 'User Task',
1152
- serviceTask: 'Service Task',
1153
- scriptTask: 'Script Task',
1154
- approval: 'Approval',
1155
- condition: 'Condition',
1156
- parallelGateway: 'Parallel Gateway',
1157
- joinGateway: 'Join Gateway',
1158
- boundaryEvent: 'Boundary Event',
1159
- delay: 'Delay',
1160
- notification: 'Notification',
1161
- webhook: 'Webhook',
1162
- allowConcurrency: 'Allow (run concurrently)',
1163
- forbidConcurrency: 'Forbid (skip new)',
1164
- replaceConcurrency: 'Replace (cancel existing)',
1165
- queueConcurrency: 'Queue (run after current)',
1166
- conditionEvent: 'Condition',
1167
- manualEvent: 'Manual',
1168
- webhookEvent: 'Webhook',
1169
- timerEvent: 'Timer',
1170
- signalEvent: 'Signal',
1171
- versionHistory: 'Version History',
1172
- importBpmn: 'Import BPMN',
1173
- exportBpmn: 'Export BPMN',
1174
- undo: 'Undo',
1175
- redo: 'Redo',
1176
- resetZoom: 'Reset zoom',
1177
- },
1178
1199
  designer: {
1179
1200
  undo: 'Undo',
1180
1201
  redo: 'Redo',
@@ -1189,11 +1210,111 @@ const en = {
1189
1210
  noDataAvailable: 'No data available',
1190
1211
  noDataSourceFor: 'No data source available for',
1191
1212
  noRows: 'No rows',
1213
+ loading: 'Loading…',
1192
1214
  pickMeasures: 'Pick measures (values) for this dataset widget.',
1193
1215
  datasetUnsupported: 'This data source does not support dataset queries.',
1194
1216
  details: 'Details',
1195
1217
  exportCsv: 'Export CSV',
1196
1218
  openInList: 'Open in list',
1219
+ config: {
1220
+ breadcrumb: {
1221
+ dashboard: 'Dashboard',
1222
+ configuration: 'Configuration',
1223
+ widget: 'Widget',
1224
+ chart: 'Chart',
1225
+ table: 'Table',
1226
+ pivotTable: 'Pivot table',
1227
+ },
1228
+ section: {
1229
+ general: 'General',
1230
+ layout: 'Layout',
1231
+ data: 'Data',
1232
+ dataBinding: 'Data Binding',
1233
+ appearance: 'Appearance',
1234
+ },
1235
+ field: {
1236
+ title: 'Title',
1237
+ description: 'Description',
1238
+ columns: 'Columns',
1239
+ gap: 'Gap',
1240
+ rowHeight: 'Row height (px)',
1241
+ autoRefresh: 'Auto refresh',
1242
+ showDescription: 'Show description',
1243
+ theme: 'Theme',
1244
+ widgetType: 'Widget type',
1245
+ colorVariant: 'Color variant',
1246
+ dataset: 'Dataset',
1247
+ dimensions: 'Dimensions',
1248
+ values: 'Values',
1249
+ width: 'Width (columns)',
1250
+ height: 'Height (rows)',
1251
+ },
1252
+ placeholder: {
1253
+ dashboardTitle: 'Dashboard title',
1254
+ dashboardDescription: 'Short summary shown under the title',
1255
+ widgetTitle: 'Widget title',
1256
+ widgetDescription: 'Widget description',
1257
+ datasetName: 'Dataset name',
1258
+ selectDataset: 'Select dataset…',
1259
+ searchDatasets: 'Search datasets…',
1260
+ addDimension: 'Add dimension…',
1261
+ addMeasure: 'Add measure…',
1262
+ searchMembers: 'Search…',
1263
+ },
1264
+ empty: {
1265
+ datasets: 'No datasets found.',
1266
+ dimensions: 'No dimensions selected.',
1267
+ measures: 'No measures selected.',
1268
+ nothingToAdd: 'Nothing left to add.',
1269
+ },
1270
+ help: {
1271
+ dataset: 'The semantic-layer dataset this widget binds to (ADR-0021).',
1272
+ dimensions: 'Group/split the values by these dataset dimensions.',
1273
+ dimensionsPivot: 'Group/split fields. The last dimension spreads across as columns.',
1274
+ values: 'Measure(s) from the dataset to display (at least one).',
1275
+ },
1276
+ action: {
1277
+ add: 'Add',
1278
+ remove: 'Remove {{name}}',
1279
+ },
1280
+ refresh: {
1281
+ off: 'Off',
1282
+ every30s: 'Every 30s',
1283
+ every1m: 'Every 1 min',
1284
+ every5m: 'Every 5 min',
1285
+ every15m: 'Every 15 min',
1286
+ every30m: 'Every 30 min',
1287
+ every1h: 'Every 1 hour',
1288
+ },
1289
+ themeOption: {
1290
+ light: 'Light',
1291
+ dark: 'Dark',
1292
+ auto: 'Auto',
1293
+ },
1294
+ widgetType: {
1295
+ metric: 'Metric',
1296
+ bar: 'Bar Chart',
1297
+ horizontalBar: 'Horizontal Bar',
1298
+ line: 'Line Chart',
1299
+ pie: 'Pie Chart',
1300
+ donut: 'Donut Chart',
1301
+ area: 'Area Chart',
1302
+ scatter: 'Scatter Plot',
1303
+ funnel: 'Funnel',
1304
+ table: 'Table',
1305
+ pivot: 'Pivot Table',
1306
+ },
1307
+ color: {
1308
+ default: 'Default',
1309
+ blue: 'Blue',
1310
+ teal: 'Teal',
1311
+ orange: 'Orange',
1312
+ purple: 'Purple',
1313
+ success: 'Success',
1314
+ warning: 'Warning',
1315
+ danger: 'Danger',
1316
+ },
1317
+ },
1197
1318
  trend: {
1198
1319
  vsLastQuarter: 'vs last quarter',
1199
1320
  vsLastMonth: 'vs last month',
@@ -1226,24 +1347,6 @@ const en = {
1226
1347
  },
1227
1348
  },
1228
1349
  },
1229
- configPanel: {
1230
- save: 'Save',
1231
- discard: 'Discard',
1232
- close: 'Close',
1233
- layout: 'Layout',
1234
- columns: 'Columns',
1235
- gap: 'Gap',
1236
- rowHeight: 'Row height',
1237
- data: 'Data',
1238
- refreshInterval: 'Refresh interval',
1239
- appearance: 'Appearance',
1240
- title: 'Title',
1241
- showDescription: 'Show description',
1242
- theme: 'Theme',
1243
- configuration: 'Configuration',
1244
- general: 'General',
1245
- advanced: 'Advanced',
1246
- },
1247
1350
  appDesigner: {
1248
1351
  createApp: 'Create Application',
1249
1352
  editApp: 'Edit Application',
@@ -1294,8 +1397,6 @@ const en = {
1294
1397
  showSort: 'Show Sort',
1295
1398
  appearance: 'Appearance',
1296
1399
  rowHeight: 'Row Height',
1297
- stripedRows: 'Striped Rows',
1298
- bordered: 'Bordered',
1299
1400
  livePreview: 'Live Preview',
1300
1401
  stepBasicDesc: 'Name, title, and layout',
1301
1402
  stepObjectsDesc: 'Select business objects',
@@ -1409,7 +1510,6 @@ const en = {
1409
1510
  unique: 'Unique',
1410
1511
  readOnly: 'Read Only',
1411
1512
  hidden: 'Hidden',
1412
- indexed: 'Indexed',
1413
1513
  externalId: 'External ID',
1414
1514
  trackHistory: 'Track History',
1415
1515
  defaultValue: 'Default Value',
@@ -1438,8 +1538,9 @@ const en = {
1438
1538
  },
1439
1539
  },
1440
1540
  console: {
1541
+ saveAdvisoryTitle: 'Saved — the authoring check raised {{count}} advisory finding(s)',
1441
1542
  title: 'ObjectOS',
1442
- initializing: 'Initializing application...',
1543
+ initializing: 'Initializing application…',
1443
1544
  search: 'Search…',
1444
1545
  breadcrumb: {
1445
1546
  dashboards: 'Dashboards',
@@ -1471,6 +1572,58 @@ const en = {
1471
1572
  Other: 'Other',
1472
1573
  },
1473
1574
  },
1575
+ // The single-namespace settings screen (objectui#4024). Placed beside
1576
+ // `settingsHub` deliberately: `SettingsView.tsx` and `SettingsHub.tsx` are
1577
+ // the same feature in the same directory, and the view was the one file
1578
+ // written against a different convention — every string below was a
1579
+ // hardcoded English literal while its sibling resolved through the bundle.
1580
+ //
1581
+ // `useSettingsLabel` already translates the manifest-authored CONTENT
1582
+ // (title, description, field labels), so a plugin author could translate
1583
+ // what is INSIDE a settings namespace but had no key reaching the chrome
1584
+ // around it — a zh-CN admin read translated field labels inside an English
1585
+ // save bar.
1586
+ settingsView: {
1587
+ backToHub: 'All settings',
1588
+ back: 'Back',
1589
+ noNamespace: 'No namespace selected.',
1590
+ loadError: 'Failed to load settings',
1591
+ saved: 'Settings saved',
1592
+ saveFailed: 'Save failed',
1593
+ // Parameterized, never concatenated: `{{key}}` is spliced into the
1594
+ // sentence so a pack can put the key where its own grammar wants it.
1595
+ lockedByEnv: 'Locked by environment: {{key}}',
1596
+ // The server named no key — the same refusal without a subject.
1597
+ lockedByEnvNoKey: 'Locked by environment',
1598
+ actionSucceeded: 'Action succeeded',
1599
+ actionFailed: 'Action failed',
1600
+ discard: 'Discard',
1601
+ saveChanges: 'Save changes',
1602
+ // Save-bar counter. A REAL i18next plural family, not an English-only
1603
+ // `change(s)` and not the two-sibling-key `xxxCountOne` shape used
1604
+ // elsewhere in this file.
1605
+ //
1606
+ // The BASE key is load-bearing (objectui#3863): i18next asks
1607
+ // `Intl.PluralRules` for the one suffix a language needs and, finding no
1608
+ // such slot, walks `fallbackLng` to `en`. `ru` has four categories and
1609
+ // `ar` six; no pack here enumerates `_few`/`_many`/`_two`/`_zero`, so
1610
+ // without this base key `ru` would render ENGLISH at counts 2-20.
1611
+ // `all-locales-key-parity.test.ts` owns that rule.
1612
+ unsavedCount: '{{count}} unsaved changes',
1613
+ unsavedCount_one: '{{count}} unsaved change',
1614
+ unsavedCount_other: '{{count}} unsaved changes',
1615
+ // The fail-closed crypto refusal (objectstack#8396). objectui#4579 added
1616
+ // these as English literals on purpose — routing one string through i18n
1617
+ // would have left a single translated string among a dozen hardcoded
1618
+ // ones — and deferred them to this card, which converts the screen whole.
1619
+ cryptoRefusalTitle: 'This deployment cannot encrypt secrets',
1620
+ // `{{subject}}` is rendered as a `< code >` element by the view, so the
1621
+ // sentence is split around it rather than interpolated.
1622
+ cryptoRefusalSubjectSuffix: 'is declared encrypted, so nothing was written.',
1623
+ cryptoRefusalNoSubject: 'The declared-encrypted value was refused, so nothing was written.',
1624
+ cryptoRefusalToast: 'Cannot encrypt secrets: {{subject}}',
1625
+ cryptoRefusalToastNoSubject: 'Cannot encrypt secrets',
1626
+ },
1474
1627
  loadingSteps: {
1475
1628
  connecting: 'Connecting to data source',
1476
1629
  loadingConfig: 'Loading configuration',
@@ -1503,6 +1656,7 @@ const en = {
1503
1656
  general: 'General',
1504
1657
  navigation: 'Navigation',
1505
1658
  dataViews: 'Data Views',
1659
+ aiChat: 'AI assistant',
1506
1660
  preferences: 'Preferences',
1507
1661
  },
1508
1662
  openCommandPalette: 'Open command palette',
@@ -1513,11 +1667,13 @@ const en = {
1513
1667
  createRecord: 'Create new record',
1514
1668
  refreshData: 'Refresh data',
1515
1669
  editRecord: 'Edit selected record',
1670
+ newChat: 'New chat',
1671
+ toggleChatsList: 'Toggle conversations list',
1516
1672
  toggleDarkMode: 'Toggle dark mode',
1517
1673
  },
1518
1674
  commandPalette: {
1519
1675
  title: 'Command palette',
1520
- placeholder: 'Type a command or search...',
1676
+ placeholder: 'Type a command or search…',
1521
1677
  noResults: 'No results found.',
1522
1678
  searching: 'Searching…',
1523
1679
  records: 'Records',
@@ -1537,6 +1693,13 @@ const en = {
1537
1693
  createApp: 'Create New App',
1538
1694
  },
1539
1695
  ai: {
1696
+ pendingDrafts: {
1697
+ count: '{{count}} change(s) are not published yet — users cannot see them.',
1698
+ publish: 'Publish',
1699
+ published: 'All pending changes are live.',
1700
+ failed: 'Publish failed.',
1701
+ publishedWithFindings: 'Published, but the runtime probes reported problems: {{detail}}',
1702
+ },
1540
1703
  usage: {
1541
1704
  title: 'AI usage',
1542
1705
  meterBuild: 'Build',
@@ -1559,7 +1722,7 @@ const en = {
1559
1722
  shareTitle: 'Share this conversation',
1560
1723
  shareDisabledTitle: 'Start chatting to enable sharing',
1561
1724
  newChat: 'New',
1562
- searchChats: 'Search chats...',
1725
+ searchChats: 'Search chats…',
1563
1726
  noChatsYet: 'No chats yet',
1564
1727
  noChatsDescription: 'Start a new conversation to see it here.',
1565
1728
  noMatchingChats: 'No matching chats.',
@@ -1568,18 +1731,18 @@ const en = {
1568
1731
  deleteConversation: 'Delete conversation',
1569
1732
  saveRename: 'Save rename',
1570
1733
  cancelRename: 'Cancel rename',
1571
- loadingHistory: 'Loading conversation history...',
1734
+ loadingHistory: 'Loading conversation history…',
1572
1735
  conversationReady: 'Conversation ready',
1573
1736
  preparingConversation: 'Preparing a new conversation',
1574
1737
  offlineDemoMode: 'Offline demo mode — agent list unavailable',
1575
1738
  sendFailedRateLimited: "You're sending messages too quickly. Your message is kept below — wait a moment and try again.",
1576
1739
  sendFailedGeneric: "Couldn't send your message. It's kept below — please try again.",
1577
- askAgent: 'Ask {{agent}}...',
1740
+ askAgent: 'Ask {{agent}}…',
1578
1741
  assistant: 'Assistant',
1579
1742
  liveCanvas: 'Live preview — {{app}} (draft)',
1580
1743
  liveCanvasUnlisted: 'Live app — {{app}} (unlisted until published)',
1581
- loadingAgents: 'Loading agents...',
1582
- askAnything: 'Ask anything...',
1744
+ loadingAgents: 'Loading agents…',
1745
+ askAnything: 'Ask anything…',
1583
1746
  emptyTitle: 'Start a conversation',
1584
1747
  emptyDescription: 'Ask anything — the assistant has access to your current app context.',
1585
1748
  switchAssistant: 'Switch assistant',
@@ -1642,6 +1805,11 @@ const en = {
1642
1805
  changesConfirmed: 'Confirmed',
1643
1806
  changesConfirm: 'Confirm',
1644
1807
  changesConfirmHint: 'Reply to confirm or adjust this change.',
1808
+ changesApplying: 'Applying…',
1809
+ changesApplied: 'Applied',
1810
+ changesDrafted: 'Saved as draft',
1811
+ changesFailed: 'Not applied',
1812
+ discussing: 'Discussing: {{target}}',
1645
1813
  // Wording is load-bearing: this is SENT to the agent and must satisfy the
1646
1814
  // cloud confirm gate's English clause `apply (this|the) change`
1647
1815
  // (service-ai-studio confirm-gate.ts APPROVAL_RE). "apply what you just
@@ -1694,6 +1862,57 @@ const en = {
1694
1862
  addAutomation: 'Add an automation — an approval, a status flow, or a notification.',
1695
1863
  },
1696
1864
  },
1865
+ // objectui#3546 slice four — the AI console surfaces: the /ai chat page's app switcher,
1866
+ // connection banner and plan/publish cards, the ChatDock chrome, and the
1867
+ // conversation-list date buckets. `group.*` is the template-key family
1868
+ // `console.ai.group.${key}` (ConversationsSidebar), enumerated from the
1869
+ // ConversationGroupKey union — all five members, no wildcard.
1870
+ collapseToDock: 'Collapse to side panel',
1871
+ switchApp: 'Switch app',
1872
+ switchAppLabel: 'Build conversations by app',
1873
+ newApp: 'New app',
1874
+ connectionWaiting: 'Waiting for server…',
1875
+ connectionStalled: 'Still working…',
1876
+ connectionOffline: 'Connection lost — reconnecting…',
1877
+ designingPlan: 'Designing your app…',
1878
+ designingPlanHint: {
1879
+ data: 'Mapping out the data you’ll track…',
1880
+ objects: 'Shaping objects and their fields…',
1881
+ relations: 'Connecting related records…',
1882
+ lookups: 'Setting up relationships and lookups…',
1883
+ views: 'Planning the screens and views…',
1884
+ forms: 'Laying out forms and lists…',
1885
+ defaults: 'Adding sensible defaults and validations…',
1886
+ dashboard: 'Sketching a dashboard to track it…',
1887
+ review: 'Double-checking the structure hangs together…',
1888
+ finalize: 'Pulling the plan together…',
1889
+ },
1890
+ planBuilding: 'Building…',
1891
+ planBuilt: 'Built',
1892
+ planDeferred: 'Not yet built',
1893
+ planReady: 'The plan is ready. Build it now, or tell me what to adjust.',
1894
+ published: 'Published',
1895
+ publishFailed: 'Publish failed',
1896
+ unavailableTitle: 'AI assistant unavailable',
1897
+ unavailableDescription: "This deployment doesn't have an AI assistant enabled. Everything else works as usual.",
1898
+ unavailableError: "Couldn't reach the AI service. It may be temporarily offline — try again, or head back home.",
1899
+ unavailableRetry: 'Try again',
1900
+ unavailableHome: 'Back to home',
1901
+ dock: {
1902
+ title: 'Assistant',
1903
+ description: 'AI assistant chat',
1904
+ resize: 'Resize chat',
1905
+ collapse: 'Collapse chat',
1906
+ maximize: 'Open full page',
1907
+ open: 'Open assistant',
1908
+ },
1909
+ group: {
1910
+ today: 'Today',
1911
+ yesterday: 'Yesterday',
1912
+ previous7Days: 'Previous 7 days',
1913
+ previous30Days: 'Previous 30 days',
1914
+ older: 'Older',
1915
+ },
1697
1916
  },
1698
1917
  errors: {
1699
1918
  somethingWentWrong: 'Something went wrong',
@@ -1702,6 +1921,12 @@ const en = {
1702
1921
  goHome: 'Go Home',
1703
1922
  errorDetails: 'Error Details (dev only)',
1704
1923
  },
1924
+ // objectui#3546 slice four — the console's catch-all route (AppContent `RouteNotFound`).
1925
+ notFound: {
1926
+ title: 'Page not found',
1927
+ description: 'The URL you followed does not match any view in this app.',
1928
+ back: 'Go back',
1929
+ },
1705
1930
  theme: {
1706
1931
  toggle: 'Toggle theme',
1707
1932
  light: 'Light',
@@ -1742,66 +1967,25 @@ const en = {
1742
1967
  filterOrNotSavable: 'This filter uses OR between conditions, which a saved view cannot store yet. It still applies to this list — remove the OR grouping to save it to the view.',
1743
1968
  filterNestedNotSavable: 'This filter uses nested condition groups, which a saved view cannot store. Flatten it to a single list of conditions to save it to the view.',
1744
1969
  systemViewReadonly: 'System view defined in code — read-only.',
1970
+ // ObjectView.tsx references ONE edit-denied key from four call sites
1971
+ // (rename / pin / set-as-default / configure), so the copy has to cover
1972
+ // any change to a built-in view rather than name one operation.
1973
+ cannotEditMetaView: 'This view is built in and defined in code — it cannot be changed.',
1974
+ cannotDeleteMetaView: 'This view is built in and defined in code — it cannot be deleted.',
1745
1975
  expandToPage: 'Open as full page',
1746
1976
  allRecords: 'All Records',
1747
- exitDesignMode: 'Exit Design Mode',
1748
- enterDesignMode: 'Enter Design Mode',
1749
- metadataInspector: 'Metadata Inspector',
1750
- editView: 'Edit View',
1751
- addView: 'Add View',
1752
- designTools: 'Design tools',
1753
1977
  new: 'New',
1754
1978
  import: 'Import',
1755
1979
  importTitle: 'Import from CSV',
1756
1980
  importedToast: 'Imported {{count}} row(s).',
1757
1981
  importedWithSkipped: 'Imported {{ok}} row(s); skipped {{skipped}}.',
1758
1982
  configureView: 'Configure View',
1759
- closePanel: 'Close panel',
1760
- showAdvancedSettings: 'Show advanced settings',
1761
- showFewerSettings: 'Show fewer settings',
1762
- page: 'Page',
1763
- pageConfigHint: 'Toolbar, navigation & view shell settings',
1764
- listConfigHint: 'Data rendering, columns & appearance for this list type',
1765
- general: 'General',
1766
- generalHint: 'View title, description, and type',
1767
1983
  toolbar: 'Toolbar',
1768
- toolbarHint: 'Search, filter, sort, group, and density toggles',
1769
1984
  toolbarEnabledCount: '{{count}} of {{total}} enabled',
1770
- navigationSection: 'Navigation',
1771
- navigationHint: 'Row click behavior and detail view settings',
1772
- records: 'Records',
1773
- recordsHint: 'Selection mode, add record, and inline editing',
1774
- exportPrint: 'Export & Print',
1775
- exportPrintHint: 'Export formats, printing, and record count display',
1776
- showAllFields: 'Show All',
1777
- hideAllFields: 'Hide All',
1778
- searchFields: 'Search fields...',
1985
+ searchFields: 'Search fields…',
1779
1986
  title: 'Title',
1780
- description: 'Description',
1781
- data: 'Data',
1782
- source: 'Source',
1783
- columns: 'Columns',
1784
- filterBy: 'Filter by',
1785
- sortBy: 'Sort by',
1786
- appearance: 'Appearance',
1787
- showDescription: 'Show description',
1788
1987
  viewType: 'View type',
1789
- userFilters: 'User filters',
1790
- enableSearch: 'Enable search',
1791
- enableFilter: 'Enable filter',
1792
- enableSort: 'Enable sort',
1793
- enableHideFields: 'Enable hide fields',
1794
- enableGroup: 'Enable group',
1795
- enableColor: 'Enable color',
1796
- enableDensity: 'Enable density',
1797
- userActions: 'User actions',
1798
- addRecordViaForm: 'Add records through a form',
1799
- advanced: 'Advanced',
1800
- allowExport: 'Allow export',
1801
1988
  recordCount: '{{count}} records',
1802
- noDescription: 'No description',
1803
- none: 'None',
1804
- columnsConfigured: '{{count}} columns',
1805
1989
  save: 'Save',
1806
1990
  discard: 'Discard',
1807
1991
  createView: 'Create View',
@@ -1838,7 +2022,6 @@ const en = {
1838
2022
  viewNameHelp: 'Machine key (snake_case). Auto-filled from the title — edit if you like.',
1839
2023
  viewNameRequired: 'Enter a key (lowercase letters, numbers, underscore). Not auto-filled for non-Latin titles.',
1840
2024
  viewNameInvalid: 'Use lowercase letters, numbers and underscores; start with a letter or underscore.',
1841
- typeOptions: 'Type Options',
1842
2025
  groupByField: 'Group by field',
1843
2026
  groupByFieldHelp: 'The kanban groups records into columns by this field\'s values.',
1844
2027
  startDateField: 'Start date field',
@@ -1853,9 +2036,8 @@ const en = {
1853
2036
  longitudeFieldHelp: 'Field providing the longitude coordinate (geo fields only).',
1854
2037
  imageField: 'Image field',
1855
2038
  imageFieldHelp: 'The gallery cover comes from this image or attachment field.',
1856
- dateField: 'Date field',
1857
- selectField: 'Select field...',
1858
- selectOption: 'Select...',
2039
+ selectField: 'Select field…',
2040
+ selectOption: 'Select…',
1859
2041
  noEligibleFieldForType: 'No eligible field of this type. Add one to the object first.',
1860
2042
  viewTypeUnavailable: 'This object has no field that can serve as "{{field}}".',
1861
2043
  viewTypeUnavailableShort: 'Not supported by this object.',
@@ -1870,82 +2052,11 @@ const en = {
1870
2052
  xAxisFieldHelp: 'The categorical or time dimension.',
1871
2053
  yAxisField: 'Y-axis field',
1872
2054
  yAxisFieldHelp: 'The numeric field to aggregate.',
1873
- gridOptionsHint: 'Grid view uses the columns configured above.',
1874
2055
  groupBy: 'Group by',
1875
- prefixField: 'Prefix field',
1876
- fields: 'Fields',
1877
- fieldsVisible: '{{count}} visible',
1878
- sortsCount: '{{count}} sorts',
1879
- filtersCount: '{{count}} filters',
1880
2056
  endDateField: 'End date field',
1881
- color: 'Color',
1882
- fieldTextColor: 'Field text color',
1883
- rowHeight: 'Row height',
1884
- wrapHeaders: 'Wrap headers',
1885
- showFieldDescriptions: 'Show field descriptions',
1886
- collapseAllByDefault: 'Collapse all by default',
1887
- striped: 'Striped rows',
1888
- bordered: 'Bordered cells',
1889
- inlineEdit: 'Edit records inline',
1890
- addDeleteRecordsInline: 'Add/delete records inline',
1891
- clickIntoRecordDetails: 'Click into record details',
1892
- navigationMode: 'Navigation mode',
1893
- navigationWidth: 'Navigation width',
1894
- navigationWidthHint: 'Available for drawer, modal, and split navigation modes',
1895
- openNewTab: 'Open in new tab',
1896
- openNewTabHint: 'Available for page and new window navigation modes',
1897
- selectionMode: 'Selection mode',
1898
- selectionNone: 'None',
1899
- selectionSingle: 'Single',
1900
- selectionMultiple: 'Multiple',
1901
- pageSize: 'Page size',
1902
- pageSizeOptions: 'Page size options',
1903
- exportFormats: 'Export formats',
1904
- exportMaxRecords: 'Max records',
1905
- exportIncludeHeaders: 'Include headers',
1906
- exportFileNamePrefix: 'File name prefix',
1907
- searchableFields: 'Searchable fields',
1908
- filterableFields: 'Filterable fields',
1909
- resizableColumns: 'Resizable columns',
1910
- densityCompact: 'Compact',
1911
- densityComfortable: 'Comfortable',
1912
- densitySpacious: 'Spacious',
1913
- densityMode: 'Density mode',
1914
- hiddenFields: 'Hidden fields',
1915
- rowActions: 'Row actions',
1916
- bulkActions: 'Bulk actions',
1917
- sharing: 'Sharing',
1918
- sharingEnabled: 'Enable sharing',
1919
- sharingVisibility: 'Visibility',
1920
- addRecordEnabled: 'Enable add record',
1921
- addRecordPosition: 'Position',
1922
- addRecordMode: 'Mode',
1923
- addRecordFormView: 'Form view',
1924
- conditionalFormatting: 'Conditional formatting',
1925
- addRule: 'Add rule',
1926
- quickFilters: 'Quick filters',
1927
- addQuickFilter: 'Add quick filter',
1928
- ufElements: 'Elements',
1929
- ufDropdown: 'Dropdown',
1930
2057
  ufTabs: 'Tabs',
1931
- ufToggle: 'Toggle',
1932
- ufAddField: '+ Add filter field...',
1933
- ufNoFields: 'No fields available',
1934
- ufAddTab: '+ Add tab',
1935
- ufTabLabel: 'Label',
2058
+ ufAddField: '+ Add filter field…',
1936
2059
  ufShowAllRecords: 'Show "All records" tab',
1937
- ufAllowAddTab: 'Allow adding tabs',
1938
- showRecordCount: 'Show record count',
1939
- allowPrinting: 'Allow printing',
1940
- virtualScroll: 'Virtual scroll',
1941
- emptyStateTitle: 'Empty state title',
1942
- emptyStateMessage: 'Empty state message',
1943
- emptyStateIcon: 'Empty state icon',
1944
- ariaLabel: 'ARIA label',
1945
- ariaDescribedBy: 'ARIA described by',
1946
- ariaLive: 'ARIA live',
1947
- accessibility: 'Accessibility',
1948
- viewTabs: 'View Tabs',
1949
2060
  },
1950
2061
  localeSwitcher: {
1951
2062
  label: 'Language',
@@ -1961,11 +2072,25 @@ const en = {
1961
2072
  passwordPlaceholder: 'Enter your password',
1962
2073
  forgotPasswordText: 'Forgot password?',
1963
2074
  submitButton: 'Sign In',
1964
- submittingButton: 'Signing in...',
2075
+ submittingButton: 'Signing in…',
1965
2076
  noAccountText: "Don't have an account?",
1966
2077
  signUpText: 'Sign up',
1967
2078
  signingIn: 'Signing you in…',
1968
2079
  ssoHandoff: 'Continue to {{target}}',
2080
+ // Phone/OTP sign-in labels. `LoginForm` interpolates `{seconds}` with a
2081
+ // literal `.replace()` of its own (packages/auth/src/LoginForm.tsx:429),
2082
+ // so those SINGLE braces must survive translation — i18next never sees
2083
+ // them.
2084
+ emailOrPhoneLabel: 'Email or phone number',
2085
+ emailOrPhonePlaceholder: 'name@example.com or +1 555 000 0000',
2086
+ phoneLabel: 'Phone number',
2087
+ phonePlaceholder: '+1 555 000 0000',
2088
+ otpCodeLabel: 'Verification code',
2089
+ otpCodePlaceholder: '6-digit code',
2090
+ sendOtpButton: 'Get code',
2091
+ resendOtpCountdownText: 'Resend in {seconds}s',
2092
+ usePhoneOtpText: 'Sign in with verification code',
2093
+ usePasswordSignInText: 'Sign in with password instead',
1969
2094
  devAdminHint: {
1970
2095
  title: 'Development instance',
1971
2096
  body: 'Sign in with the seeded dev admin:',
@@ -1991,7 +2116,7 @@ const en = {
1991
2116
  passwordMismatchError: 'Passwords do not match',
1992
2117
  passwordTooShortError: 'Password must be at least 8 characters',
1993
2118
  submitButton: 'Create Account',
1994
- submittingButton: 'Creating account...',
2119
+ submittingButton: 'Creating account…',
1995
2120
  hasAccountText: 'Already have an account?',
1996
2121
  signInText: 'Sign in',
1997
2122
  errors: {
@@ -2012,12 +2137,28 @@ const en = {
2012
2137
  emailLabel: 'Email',
2013
2138
  emailPlaceholder: 'name@example.com',
2014
2139
  submitButton: 'Send Reset Link',
2015
- submittingButton: 'Sending...',
2140
+ submittingButton: 'Sending…',
2016
2141
  successTitle: 'Check your email',
2017
- successDescription: "We've sent a password reset link to {{email}}. Please check your inbox.",
2142
+ successDescription: "We've sent a password reset link to {email}. Please check your inbox.",
2018
2143
  backToSignInText: 'Back to sign in',
2019
2144
  rememberPasswordText: 'Remember your password?',
2020
2145
  signInText: 'Sign in',
2146
+ // The SMS branch of the same form: request an OTP, then set the new
2147
+ // password inline instead of following an emailed link. `{seconds}` is
2148
+ // ForgotPasswordForm's own hole (packages/auth/src/ForgotPasswordForm.tsx:367).
2149
+ phoneLabel: 'Phone number',
2150
+ phonePlaceholder: '+1 555 000 0000',
2151
+ otpCodeLabel: 'Verification code',
2152
+ otpCodePlaceholder: '6-digit code',
2153
+ sendOtpButton: 'Get code',
2154
+ resendOtpCountdownText: 'Resend in {seconds}s',
2155
+ newPasswordLabel: 'New password',
2156
+ newPasswordPlaceholder: 'Enter a new password',
2157
+ resetButton: 'Reset Password',
2158
+ usePhoneResetText: 'Reset via SMS code',
2159
+ useEmailResetText: 'Reset via email instead',
2160
+ phoneSuccessTitle: 'Password reset',
2161
+ phoneSuccessDescription: 'Your password has been reset. You can now sign in with your new password.',
2021
2162
  },
2022
2163
  resetPassword: {
2023
2164
  title: 'Set a new password',
@@ -2051,6 +2192,7 @@ const en = {
2051
2192
  resentSuccess: 'Verification email sent!',
2052
2193
  resentDescription: 'Please check your inbox and click the verification link.',
2053
2194
  resendFailed: 'Cannot resend verification email',
2195
+ resendUnavailable: 'Email delivery may not be configured for this environment. Contact support if this persists.',
2054
2196
  signInLink: 'Go to sign in',
2055
2197
  backToSignIn: 'Back to sign in',
2056
2198
  backToLogin: 'Back to login',
@@ -2092,6 +2234,10 @@ const en = {
2092
2234
  denyFailed: 'Failed to deny request',
2093
2235
  invalidTitle: 'Invalid device link',
2094
2236
  invalidDescription: 'No device code was provided in the URL.',
2237
+ // Shown when the deployment has the device-authorization plugin off, so
2238
+ // the endpoints 404 (framework#2874 / objectui#2513).
2239
+ disabledTitle: 'Device authorization not enabled',
2240
+ disabledDescription: 'This deployment does not have device authorization enabled, so this device cannot be approved here.',
2095
2241
  loading: 'Loading…',
2096
2242
  cancel: 'Cancel',
2097
2243
  },
@@ -2152,6 +2298,38 @@ const en = {
2152
2298
  },
2153
2299
  },
2154
2300
  },
2301
+ // The OAuth authorization-code consent screen (`/oauth/consent`). A third
2302
+ // party asks for scopes and the signed-in user grants or denies them, so
2303
+ // every string here is a security decision the user must read in their own
2304
+ // language — objectui#3546 slice three.
2305
+ oauth: {
2306
+ consent: {
2307
+ // `{{appName}}` is the client's registered name, or `unknownApp` when the
2308
+ // client metadata carries none. `{{suffix}}` is the parenthesised account
2309
+ // hint the page passes (empty when no user is loaded), so it must stay
2310
+ // attached to the sentence's last word.
2311
+ title: '{{appName}} wants to access your account',
2312
+ request: '{{appName}} is requesting permission{{suffix}}.',
2313
+ unknownApp: 'an application',
2314
+ willAllow: 'This app will be able to:',
2315
+ // Only the four scopes the page maps by name; anything else renders the
2316
+ // raw scope string, which is why there is no generic entry here.
2317
+ scope: {
2318
+ openid: 'Confirm your identity',
2319
+ profile: 'Read your basic profile (name, picture)',
2320
+ email: 'Read your email address',
2321
+ offlineAccess: 'Stay signed in (refresh access)',
2322
+ },
2323
+ deny: 'Deny',
2324
+ authorize: 'Authorize',
2325
+ submitting: 'Authorizing…',
2326
+ granted: 'Access granted',
2327
+ denied: 'Access denied',
2328
+ noRedirect: 'No redirect URL returned by the server.',
2329
+ failed: 'Consent failed',
2330
+ footer: 'You can revoke access at any time from your account settings.',
2331
+ },
2332
+ },
2155
2333
  profile: {
2156
2334
  title: 'Profile',
2157
2335
  subtitle: 'Manage your account settings',
@@ -2213,6 +2391,8 @@ const en = {
2213
2391
  invite: 'Invite member',
2214
2392
  members: 'Members',
2215
2393
  settings: 'Workspace settings',
2394
+ multiOrgDisabled: 'Creating new organizations is disabled on this instance.',
2395
+ createFailed: 'Failed to create workspace',
2216
2396
  },
2217
2397
  help: {
2218
2398
  onThisPage: 'On this page',
@@ -2233,7 +2413,7 @@ const en = {
2233
2413
  area: 'Area',
2234
2414
  scope: 'Scope',
2235
2415
  packageManagement: 'Package management',
2236
- searchNavigation: 'Search navigation...',
2416
+ searchNavigation: 'Search navigation…',
2237
2417
  recent: 'Recent',
2238
2418
  favorites: 'Favorites',
2239
2419
  starred: 'Starred',
@@ -2249,8 +2429,8 @@ const en = {
2249
2429
  switchObject: 'Switch Object',
2250
2430
  connection: {
2251
2431
  connected: 'Connected',
2252
- connecting: 'Connecting...',
2253
- reconnecting: 'Reconnecting...',
2432
+ connecting: 'Connecting…',
2433
+ reconnecting: 'Reconnecting…',
2254
2434
  disconnected: 'Disconnected',
2255
2435
  error: 'Connection Error',
2256
2436
  },
@@ -2282,10 +2462,12 @@ const en = {
2282
2462
  build: {
2283
2463
  title: 'Build an app',
2284
2464
  subtitle: 'Start from scratch — design objects, forms, automations and interfaces.',
2465
+ noCapability: 'Building apps isn\'t available in this workspace — it requires the “Manage Metadata” permission, which your account doesn\'t have.',
2285
2466
  },
2286
2467
  template: {
2287
2468
  title: 'Start with a template',
2288
2469
  subtitle: 'Install a template app from the marketplace and customize it.',
2470
+ marketplaceDisabled: 'This runtime has no app marketplace configured, so there are no templates to install here.',
2289
2471
  },
2290
2472
  open: 'Open',
2291
2473
  stats: {
@@ -2293,7 +2475,7 @@ const en = {
2293
2475
  starred: 'Starred',
2294
2476
  recent: 'Recent items',
2295
2477
  },
2296
- loading: 'Loading workspace...',
2478
+ loading: 'Loading workspace…',
2297
2479
  recent: 'Recent',
2298
2480
  starred: 'Starred',
2299
2481
  welcome: 'Build your business system with AI',
@@ -2316,6 +2498,15 @@ const en = {
2316
2498
  publishing: 'Publishing…',
2317
2499
  published: 'Published! Your changes are live.',
2318
2500
  publishFailed: 'Publish failed',
2501
+ // objectui#3546 slice six — the rest of `usePublishAllDrafts`'s toasts
2502
+ // (the ADR-0038 L3 probe / seed health report and the ADR-0066 ⑨
2503
+ // capability-reference lint). Same banner, same publish button, so they
2504
+ // live beside the five keys above rather than in a namespace of their own.
2505
+ nothing: 'Nothing to publish.',
2506
+ probeWarn: 'Published, but verification found problems.',
2507
+ seedWarn: 'Published, but some sample data failed to load.',
2508
+ publishedVerified: 'Published & verified — {{count}} sample row(s) live.',
2509
+ capabilityWarn: 'Authoring check: {{count}} capability reference(s) resolve nowhere.',
2319
2510
  },
2320
2511
  createFirstApp: 'Create app manually',
2321
2512
  systemSettings: 'System Settings',
@@ -2373,6 +2564,9 @@ const en = {
2373
2564
  roles: 'Roles',
2374
2565
  configuration: 'Configuration',
2375
2566
  createApp: 'Create App',
2567
+ administration: 'Administration',
2568
+ datasources: 'Datasources',
2569
+ documentation: 'Documentation',
2376
2570
  },
2377
2571
  activityFeed: {
2378
2572
  title: 'Recent Activity',
@@ -2401,7 +2595,7 @@ const en = {
2401
2595
  search: {
2402
2596
  title: 'Search',
2403
2597
  back: 'Back',
2404
- placeholder: 'Search objects, dashboards, pages, reports...',
2598
+ placeholder: 'Search objects, dashboards, pages, reports…',
2405
2599
  inputAriaLabel: 'Search objects, dashboards, pages, reports',
2406
2600
  resultsCount: '{{count}} result for "{{query}}"',
2407
2601
  resultsCountPlural: '{{count}} results for "{{query}}"',
@@ -2420,6 +2614,7 @@ const en = {
2420
2614
  empty: {
2421
2615
  objectNotFound: 'Object Not Found',
2422
2616
  objectNotFoundDescription: 'Object "{{name}}" definition missing. Check your configuration or navigate back to select a valid object.',
2617
+ interfacePageSourceMissing: 'This interface page references "{{name}}", which is not available.',
2423
2618
  recordNotFound: 'Record not found',
2424
2619
  recordNotFoundDescription: 'The record you are looking for does not exist or may have been deleted.',
2425
2620
  pageNotFound: 'Page Not Found',
@@ -2430,6 +2625,11 @@ const en = {
2430
2625
  reportNotFoundDescription: 'The report "{{name}}" could not be found. It may have been removed or renamed.',
2431
2626
  noAppsConfigured: 'No Apps Configured',
2432
2627
  noAppsConfiguredDescription: 'No applications have been registered. Create your first app or visit System Settings to configure your environment.',
2628
+ appNotAvailable: 'App not available',
2629
+ appNotAvailableDescription: 'This app is not available yet — it may still be publishing. Try again in a moment.',
2630
+ appAccessDenied: "You don't have access to this app",
2631
+ appAccessDeniedDescription: 'This app exists, but your account is not authorized to open it. Ask an administrator to grant you access.',
2632
+ appAccessDeniedHome: 'Back to home',
2433
2633
  createFirstApp: 'Create Your First App',
2434
2634
  systemSettings: 'System Settings',
2435
2635
  back: 'Back',
@@ -2471,22 +2671,45 @@ const en = {
2471
2671
  detailChangedKeys: 'Also changed:',
2472
2672
  confirmNote: 'Publishing releases all {{count}} pending drafts of this package atomically.',
2473
2673
  publishConfirm: 'Publish all',
2674
+ // [objectui#5418] Pre-publish security-posture findings, shown next to
2675
+ // the confirm button so a refusal the door would issue is read BEFORE
2676
+ // the click rather than as a toast after the batch rolled back.
2677
+ securityBlockTitle: 'Publishing will be refused — {{count}} item(s) need a decision first',
2678
+ securityBlockWhere: 'Fix it on the object under Settings → Record sharing, then publish again.',
2679
+ },
2680
+ // ADR-0045 — the materialized-but-unlisted app banner
2681
+ // (UnpublishedAppBar.tsx), sibling of draftBar above.
2682
+ unpublishedBar: {
2683
+ message: 'Unpublished app — fully functional, but only builders can see it. Publish to make it visible to your users.',
2684
+ publish: 'Publish',
2685
+ publishing: 'Publishing…',
2686
+ published: 'Published! The app is now visible to your users.',
2687
+ publishFailed: 'Publish failed',
2688
+ },
2689
+ // ADR-0067 — the append-only build/revert timeline (CommitTimeline.tsx);
2690
+ // `button` is the banner's entry point into it.
2691
+ history: {
2692
+ button: 'History',
2693
+ title: 'Build history',
2694
+ description: 'Every change to this app, newest first. Revert any step to undo it — no publish confirmation needed.',
2695
+ loadFailed: 'Could not load history:',
2696
+ // objectui#3529 — the retryable class gets its own sentence: a 503 means
2697
+ // the read never reached the commit store, which is a different operator
2698
+ // disposition from a 404/500 and must not read as a bare status code.
2699
+ loadFailedUnavailable: 'Commit store temporarily unreachable — this read did not happen, so no history is shown. Retry in a moment.',
2700
+ loading: 'Loading history…',
2701
+ empty: 'No history yet for this app.',
2702
+ revertLabel: 'Reverted a change',
2703
+ applyLabel: 'Build change',
2704
+ revert: 'revert',
2705
+ items: 'item(s)',
2706
+ revertAction: 'Revert',
2707
+ reverted: 'Reverted — the change has been undone.',
2708
+ revertFailed: 'Revert failed',
2709
+ // The WRITE half of the same fact. Not "try again": the revert may have
2710
+ // landed before the 503, and re-issuing appends a second revert commit.
2711
+ revertUnavailable: 'Commit store temporarily unreachable — the revert may not have been applied. Reopen this history to check before retrying.',
2474
2712
  },
2475
- },
2476
- renderer: {
2477
- noPageSchema: 'No page schema provided',
2478
- noFormSchema: 'No form schema provided',
2479
- noDashboardSchema: 'No dashboard schema provided',
2480
- pageRendering: 'Page rendering: {{name}}',
2481
- dashboardRendering: 'Dashboard rendering: {{name}}',
2482
- formRenderingMode: 'Form rendering in {{mode}} mode',
2483
- formRenderingFor: 'for record {{id}}',
2484
- createRecord: 'Create Record',
2485
- editRecord: 'Edit Record',
2486
- page: 'Page',
2487
- dashboard: 'Dashboard',
2488
- save: 'Save',
2489
- cancel: 'Cancel',
2490
2713
  },
2491
2714
  actionDialog: {
2492
2715
  title: 'Action Parameters',
@@ -2539,7 +2762,7 @@ const en = {
2539
2762
  deleteFailed: 'Failed to delete view',
2540
2763
  },
2541
2764
  dashboardActions: {
2542
- pdfPreparing: 'Preparing PDF export…',
2765
+ printDialogOpening: 'Opening your browser’s print dialog (not a PDF export)',
2543
2766
  exportFailed: 'Export failed: {{message}}',
2544
2767
  forecastSoon: 'Forecast view coming soon',
2545
2768
  },
@@ -2574,6 +2797,152 @@ const en = {
2574
2797
  emptyDescription: 'Create your first workspace to get started.',
2575
2798
  noMatches: 'No workspaces match your search.',
2576
2799
  },
2800
+ // Organization MANAGEMENT (packages/app-shell/src/console/organizations/**
2801
+ // plus the workspace switcher). Distinct from `organizations` above, which
2802
+ // is the org PICKER — same domain, different surface, and the singular /
2803
+ // plural spelling is the only thing telling them apart at a call site.
2804
+ organization: {
2805
+ roles: {
2806
+ owner: 'Owner',
2807
+ admin: 'Admin',
2808
+ delegatedAdmin: 'Delegated Admin',
2809
+ member: 'Member',
2810
+ },
2811
+ errors: {
2812
+ notAllowedToInvite: 'You are not allowed to invite users to this organization',
2813
+ notAllowedToInviteWithRole: 'You are not allowed to invite a user with this role',
2814
+ alreadyInvited: 'This user has already been invited to this organization',
2815
+ organizationExists: 'Organization already exists',
2816
+ slugTaken: 'That URL slug is already taken',
2817
+ notAllowedToCreate: 'You are not allowed to create a new organization',
2818
+ notTheRecipient: 'You are not the recipient of the invitation',
2819
+ invitationNotFound: 'This invitation no longer exists or has expired',
2820
+ unknown: 'Something went wrong. Please try again.',
2821
+ },
2822
+ backToList: 'Back to organizations',
2823
+ notFound: 'Organization not found',
2824
+ notFoundDescription: 'This organization does not exist or you do not have access.',
2825
+ tabs: {
2826
+ members: 'Members',
2827
+ invitations: 'Invitations',
2828
+ settings: 'Settings',
2829
+ },
2830
+ members: {
2831
+ title: 'Members',
2832
+ inviteMember: 'Invite member',
2833
+ inviteRestrictedNote: 'Only organization admins can invite members.',
2834
+ removeMember: 'Remove member',
2835
+ removeConfirmTitle: 'Remove member?',
2836
+ removeConfirmDescription: 'This will remove {{name}} from the organization. They will lose access immediately.',
2837
+ removeConfirmAction: 'Remove',
2838
+ memberRemoved: 'Member removed',
2839
+ removeFailed: 'Failed to remove member',
2840
+ roleUpdated: 'Role updated',
2841
+ roleUpdateFailed: 'Failed to update role',
2842
+ memberActions: 'Member actions',
2843
+ loadFailed: 'Failed to load members',
2844
+ },
2845
+ invitations: {
2846
+ title: 'Invitations',
2847
+ empty: 'No invitations found.',
2848
+ expiresAt: 'Expires',
2849
+ linkCopied: 'Invitation link copied',
2850
+ copyFailed: 'Failed to copy link',
2851
+ canceled: 'Invitation canceled',
2852
+ cancelFailed: 'Failed to cancel invitation',
2853
+ cancelTitle: 'Cancel invitation?',
2854
+ cancelDescription: 'The invitation for {{email}} will be revoked.',
2855
+ cancelAction: 'Cancel invitation',
2856
+ inviteTitle: 'Invite a member',
2857
+ inviteDescription: 'They will receive an invitation to join this organization.',
2858
+ emailLabel: 'Email',
2859
+ roleLabel: 'Role',
2860
+ // Placement = business unit + positions applied to the invitee on
2861
+ // accept. The lists are already filtered to what the inviter may
2862
+ // delegate.
2863
+ placementLabel: 'Placement (optional)',
2864
+ placementDescription: 'Applied when the invitation is accepted. Only units and positions you may delegate are listed.',
2865
+ businessUnitLabel: 'Business unit',
2866
+ businessUnitPlaceholder: 'No placement',
2867
+ positionsLabel: 'Positions',
2868
+ sendInvite: 'Send invite',
2869
+ sentTitle: 'Invitation created',
2870
+ sentDescription: 'Share the link below with the invitee. They will need to sign in to accept.',
2871
+ linkLabel: 'Accept link',
2872
+ invitedAs: '{{email}} invited as {{role}}',
2873
+ copyLinkLabel: 'Copy invitation link',
2874
+ loadFailed: 'Failed to load invitations',
2875
+ inviteFailed: 'Failed to invite member',
2876
+ status: {
2877
+ all: 'All',
2878
+ pending: 'Pending',
2879
+ accepted: 'Accepted',
2880
+ rejected: 'Rejected',
2881
+ canceled: 'Canceled',
2882
+ },
2883
+ },
2884
+ settings: {
2885
+ generalTitle: 'General',
2886
+ generalDescription: 'Update your organization information.',
2887
+ readOnlyNote: 'Only owners can change settings.',
2888
+ nameLabel: 'Organization name',
2889
+ // The org's URL segment. `deleteConfirmSlugLabel` asks the user to
2890
+ // retype it, so the two must keep naming the same thing.
2891
+ slugLabel: 'Slug',
2892
+ logoLabel: 'Logo',
2893
+ logoUpload: 'Upload',
2894
+ logoReplace: 'Replace',
2895
+ logoClear: 'Remove',
2896
+ logoUploaded: 'Logo uploaded — save to apply',
2897
+ logoUploadFailed: 'Failed to upload logo',
2898
+ save: 'Save changes',
2899
+ saved: 'Settings saved',
2900
+ saveFailed: 'Failed to save settings',
2901
+ leaveTitle: 'Leave organization',
2902
+ leaveDescription: 'You will lose access to this organization.',
2903
+ leaveAction: 'Leave',
2904
+ leaveConfirmTitle: 'Leave organization?',
2905
+ leaveConfirmDescription: 'Are you sure you want to leave {{name}}? You will lose access immediately.',
2906
+ leaveConfirmAction: 'Leave',
2907
+ leftOrg: 'You have left the organization',
2908
+ leaveFailed: 'Failed to leave organization',
2909
+ dangerZone: 'Danger zone',
2910
+ deleteTitle: 'Delete organization',
2911
+ deleteDescription: 'Permanently delete this organization and all its data.',
2912
+ deleteAction: 'Delete',
2913
+ deleteConfirmTitle: 'Delete organization?',
2914
+ deleteConfirmDescription: 'This action is irreversible. All data will be permanently deleted. Type the organization slug to confirm.',
2915
+ deleteConfirmSlugLabel: 'Type "{{slug}}" to confirm',
2916
+ deleteConfirmAction: 'Delete organization',
2917
+ deleted: 'Organization deleted',
2918
+ deleteFailed: 'Failed to delete organization',
2919
+ },
2920
+ accept: {
2921
+ title: 'You have been invited',
2922
+ description: 'You have been invited to join {{orgName}} as {{role}}.',
2923
+ errorTitle: 'Invitation unavailable',
2924
+ goToOrgs: 'Go to organizations',
2925
+ loading: 'Loading invitation…',
2926
+ organization: 'Organization',
2927
+ role: 'Role',
2928
+ expiresAt: 'Expires',
2929
+ accept: 'Accept invitation',
2930
+ accepted: 'Invitation accepted',
2931
+ acceptFailed: 'Failed to accept invitation',
2932
+ decline: 'Decline',
2933
+ declined: 'Invitation declined',
2934
+ declineFailed: 'Failed to decline invitation',
2935
+ },
2936
+ current: {
2937
+ label: 'Current organization',
2938
+ },
2939
+ switcher: {
2940
+ label: 'Switch organization',
2941
+ groupLabel: 'Working organization',
2942
+ groupHint: 'New records are created here. Views show data from all your organizations.',
2943
+ manageMembers: 'Manage members',
2944
+ },
2945
+ },
2577
2946
  notifications: {
2578
2947
  regionLabel: 'Notifications',
2579
2948
  empty: 'No notifications',
@@ -2590,6 +2959,12 @@ const en = {
2590
2959
  viewApprovals: 'View approvals',
2591
2960
  noPendingApprovals: 'No pending approvals',
2592
2961
  openApprovalsInbox: 'Open Approvals Inbox',
2962
+ // Bell-badge breakdown (#7233): the badge sums unread notification topics
2963
+ // and pending approvals, then clamps at "9+". These three spell the sum
2964
+ // out inside the popover so the number is explainable.
2965
+ badgeTotal: '{{total}} total',
2966
+ badgeNotifications: '{{unread}} notifications',
2967
+ badgeApprovals: '{{approvals}} pending approvals',
2593
2968
  },
2594
2969
  publicForm: {
2595
2970
  submit: 'Submit',
@@ -2611,52 +2986,6 @@ const en = {
2611
2986
  consentRequired: 'Please accept the privacy policy to continue.',
2612
2987
  rateLimited: 'Please take a moment to review your answers before submitting.',
2613
2988
  redirectBlocked: 'Submission accepted, but the redirect URL was blocked for security.',
2614
- demo: {
2615
- contactTitle: 'Contact us',
2616
- contactDescription: 'Tell us about your project and a sales representative will get back to you within one business day.',
2617
- supportTitle: 'Submit a support request',
2618
- supportDescription: 'Tell us what is going wrong — our team responds within one business day.',
2619
- thankYouSalesTitle: 'Thanks — we received your message!',
2620
- thankYouSalesMessage: 'A sales representative will be in touch within one business day.',
2621
- thankYouSupportTitle: 'Got it — your request is in the queue.',
2622
- thankYouSupportMessage: 'A support engineer will follow up shortly. Save this page if you have more screenshots to add.',
2623
- field: {
2624
- firstName: 'First name',
2625
- lastName: 'Last name',
2626
- email: 'Work email',
2627
- phone: 'Phone',
2628
- jobTitle: 'Job title',
2629
- company: 'Company',
2630
- website: 'Website',
2631
- industry: 'Industry',
2632
- companySize: 'Company size',
2633
- howCanWeHelp: 'How can we help?',
2634
- subject: 'Subject',
2635
- description: 'Description',
2636
- issueType: 'Issue type',
2637
- priority: 'Priority',
2638
- },
2639
- industry: {
2640
- technology: 'Technology',
2641
- software: 'Software / SaaS',
2642
- finance: 'Finance',
2643
- healthcare: 'Healthcare',
2644
- retail: 'Retail',
2645
- other: 'Other',
2646
- },
2647
- issueType: {
2648
- question: 'Question',
2649
- problem: 'Problem',
2650
- bug: 'Bug',
2651
- feature_request: 'Feature request',
2652
- },
2653
- priority: {
2654
- low: 'Low',
2655
- medium: 'Medium',
2656
- high: 'High',
2657
- critical: 'Critical',
2658
- },
2659
- },
2660
2989
  },
2661
2990
  connectAgent: {
2662
2991
  disabled: {
@@ -2770,6 +3099,7 @@ const en = {
2770
3099
  retry: 'Try again',
2771
3100
  errors: {
2772
3101
  expired: 'The request expired before it was approved. Start again.',
3102
+ accessDenied: 'The connection request was denied. Start again.',
2773
3103
  bindFailed: 'Binding failed.',
2774
3104
  deviceCodeFailed: 'Device code request failed.',
2775
3105
  },
@@ -2826,10 +3156,16 @@ const en = {
2826
3156
  installedBadge: 'Installed v{{version}}',
2827
3157
  load: {
2828
3158
  failed: 'Failed to load marketplace',
2829
- failedHint: 'By default this runtime points at the public ObjectStack cloud. Check the runtime is online, or override <code>OS_CLOUD_URL</code> to point at a self-hosted control plane.',
3159
+ failedHintConfigured: 'This runtime reaches the marketplace through the control plane at {{url}}. Check that it is online and reachable from here.',
3160
+ failedHintSameOrigin: 'This runtime serves the marketplace catalog itself. Check that the runtime is online.',
2830
3161
  packageFailed: 'Failed to load package',
2831
3162
  notFound: 'Not found.',
2832
3163
  },
3164
+ disabled: {
3165
+ title: 'App Marketplace is turned off',
3166
+ description: 'This runtime has no marketplace configured, so there is nothing to browse or install from here.',
3167
+ hint: 'That is the expected state when the runtime is started with <code>OS_CLOUD_URL=off</code> (or <code>none</code>, <code>local</code>, <code>disabled</code>). To turn the marketplace on, point <code>OS_CLOUD_URL</code> at a control plane and restart the runtime.',
3168
+ },
2833
3169
  detail: {
2834
3170
  homepage: 'Homepage',
2835
3171
  installedV: 'Installed · v{{version}}',
@@ -2865,6 +3201,7 @@ const en = {
2865
3201
  dismiss: 'Dismiss',
2866
3202
  openOnCloud: 'Open on cloud',
2867
3203
  backHome: 'Back to home',
3204
+ updateTo: 'Update',
2868
3205
  },
2869
3206
  install: {
2870
3207
  dialogTitle: 'Install {{name}}',
@@ -2881,6 +3218,37 @@ const en = {
2881
3218
  localManifestConflict: '{{message}}\nTip: a local app already owns this manifest_id. Remove it from objectstack.config.ts first.',
2882
3219
  localUnauthorized: 'Sign in to this runtime first, then try again.',
2883
3220
  localMarketplaceUnavailable: 'This runtime has no OS_CLOUD_URL configured, so the marketplace catalog is unreachable.',
3221
+ updateTo: 'Update → v{{version}}',
3222
+ installedVersion: 'Installed v{{version}}',
3223
+ },
3224
+ // objectui#3546 — the MarketplacePage "Your organization" strip.
3225
+ org: {
3226
+ heading: 'Your organization',
3227
+ install: 'Install',
3228
+ installed: 'Installed {{name}}',
3229
+ installedBadge: 'Installed',
3230
+ installing: 'Installing…',
3231
+ },
3232
+ // ADR-0025 PD4 §3.5/§3.11 — the pre-install consent panel
3233
+ // (PluginDisclosure.tsx). `runtime` is a CLOSED enum: spec
3234
+ // PluginRuntimeSchema = z.enum(['node', 'sandbox', 'worker']).
3235
+ disclosure: {
3236
+ containsCode: 'This package contains code',
3237
+ reviewed: 'Reviewed & approved',
3238
+ unreviewed: 'Not yet reviewed',
3239
+ signed: 'Signed',
3240
+ grantsIntro: 'On install, this package will be granted:',
3241
+ services: 'Platform services',
3242
+ hooks: 'Lifecycle hooks',
3243
+ network: 'Network access',
3244
+ fs: 'Filesystem access',
3245
+ noPermissions: 'Requests no special permissions.',
3246
+ acknowledge: 'I understand this package runs code and grants the permissions above.',
3247
+ runtime: {
3248
+ node: 'In-process · full trust',
3249
+ sandbox: 'Sandboxed',
3250
+ worker: 'Out-of-process',
3251
+ },
2884
3252
  },
2885
3253
  // ADR-0090 D5/D9 — a package's isDefault permission set is an
2886
3254
  // install-time suggestion to bind it to the everyone/guest position;
@@ -3049,12 +3417,19 @@ const en = {
3049
3417
  submittedAgo: 'Submitted {{when}}',
3050
3418
  completedAt: 'Completed {{when}}',
3051
3419
  waitingOn: 'Waiting on',
3420
+ approverNameSeparator: ', ',
3421
+ approverUnstaffed: '{{seat}} (no current holder)',
3052
3422
  history: 'Activity',
3053
3423
  noActions: 'No actions yet.',
3054
3424
  actSubmit: 'Submitted',
3055
3425
  actApprove: 'Approved',
3056
3426
  actReject: 'Rejected',
3057
3427
  actRecall: 'Recalled',
3428
+ overrideActionLabel: 'Override {{action}}',
3429
+ overrideNotice: 'You hold no approver slot on this step. Continuing uses your admin override: it finalises the step immediately, bypassing any approver who has not acted. The decision is recorded as an admin override.',
3430
+ overrideNoticeWho: 'You hold no approver slot on this step. Continuing uses your admin override: it finalises the step immediately and bypasses the approvers who have not acted — {{who}}. The decision is recorded as an admin override.',
3431
+ viaOverrideChip: 'Admin override',
3432
+ viaOverrideHint: 'The actor held no approver slot on this step — admitted by the admin-override path.',
3058
3433
  rawData: 'Raw data (JSON)',
3059
3434
  copy: 'Copy',
3060
3435
  copied: 'Copied',
@@ -3123,10 +3498,15 @@ const en = {
3123
3498
  noResults: 'No results',
3124
3499
  searchField: 'Search {{label}}…',
3125
3500
  enterId: 'Enter {{label}} id',
3501
+ addValue: 'Type a value, press Enter',
3502
+ removeValue: 'Remove {{value}}',
3503
+ rangeStart: 'From',
3504
+ rangeEnd: 'To',
3126
3505
  operators: {
3127
3506
  equals: 'Equals',
3128
3507
  notEquals: 'Does not equal',
3129
3508
  contains: 'Contains',
3509
+ containsCaseInsensitive: 'Contains (ignore case)',
3130
3510
  notContains: 'Does not contain',
3131
3511
  isEmpty: 'Is empty',
3132
3512
  isNotEmpty: 'Is not empty',
@@ -3139,6 +3519,12 @@ const en = {
3139
3519
  between: 'Between',
3140
3520
  in: 'In',
3141
3521
  notIn: 'Not in',
3522
+ startsWith: 'Starts with',
3523
+ endsWith: 'Ends with',
3524
+ isNull: 'Is null',
3525
+ isNotNull: 'Is not null',
3526
+ exists: 'Is set',
3527
+ notExists: 'Is not set',
3142
3528
  },
3143
3529
  },
3144
3530
  sortBuilder: {
@@ -3202,12 +3588,12 @@ const en = {
3202
3588
  // Not `common.cancel`: an accessible name has to say what is cancelled.
3203
3589
  // Only the reply TARGET is dropped — the composer keeps whatever was typed.
3204
3590
  cancelReply: 'Cancel reply',
3205
- replyingTo: 'Replying to {{name}}...',
3591
+ replyingTo: 'Replying to {{name}}…',
3206
3592
  // The no-author-found half of the reply banner, as a whole sentence:
3207
3593
  // languages that inflect around the addressee cannot build it by
3208
3594
  // substituting a noun into the `{{name}}` form.
3209
- replyingToComment: 'Replying to comment...',
3210
- commentPlaceholder: 'Add a comment... (use @ to mention)',
3595
+ replyingToComment: 'Replying to comment…',
3596
+ commentPlaceholder: 'Add a comment… (use @ to mention)',
3211
3597
  send: 'Send',
3212
3598
  // Presence avatar stack (objectui#3440). `presentUserCount*` is the avatar
3213
3599
  // group's `aria-label` — with only images and initials inside, that label
@@ -3227,5 +3613,112 @@ const en = {
3227
3613
  statusIdle: 'idle',
3228
3614
  statusAway: 'away',
3229
3615
  },
3616
+ // Multi-step form (plugin-form's WizardForm). `{{fields}}` is a
3617
+ // comma-joined, already-truncated label list built at the call site.
3618
+ wizard: {
3619
+ missingRequired: 'Please complete the required fields: {{fields}}',
3620
+ },
3621
+ // The screen-flow runner dialog (`app-shell/views/FlowRunner`) — the modal a
3622
+ // `type: 'flow'` action opens when its run pauses at a `screen` node.
3623
+ //
3624
+ // What is deliberately NOT here: the sentence a refused resume shows. That is
3625
+ // the ADR-0112 envelope's own `error.message`, prose the automation engine
3626
+ // composed for a human ("Node 'create_quote' failed: … at most 2 decimal
3627
+ // places"), so it has no fixed catalogue to key against and is passed through
3628
+ // untranslated — the same division `appManagement` states above. The runner
3629
+ // borrows `common.{loading,cancel,close,submit}` for its chrome and
3630
+ // `wizard.missingRequired` for the pre-submit check, which is the same
3631
+ // sentence `plugin-form`'s WizardForm raises.
3632
+ flowRunner: {
3633
+ // Fallback dialog title, used only when the `screen` node declares none.
3634
+ title: 'Input',
3635
+ submitting: 'Submitting…',
3636
+ // The submit label of an `object-form` step: it saves a record AND advances
3637
+ // the run, so it is not the plain `common.submit`.
3638
+ saveAndContinue: 'Save & Continue',
3639
+ // A multi-screen wizard advanced to the next step (not the end of the run).
3640
+ nextStep: 'Saved — next step',
3641
+ // Terminal success, used only when the flow declares no `successMessage`
3642
+ // of its own.
3643
+ completed: 'Flow "{{flow}}" completed',
3644
+ },
3645
+ // Console › System › Applications (objectui#4307). The page's OWN chrome —
3646
+ // headings, controls, badges and the frames of its toasts.
3647
+ //
3648
+ // What is deliberately NOT here: the server's refusal text. Each failure
3649
+ // toast is a template with a `{{reason}}` hole, and what fills that hole is
3650
+ // whatever `PUT/DELETE /api/v1/meta/app/:name` answered (`forbidden:
3651
+ // manage_metadata required`, a 5xx body, an offline message), passed through
3652
+ // untranslated. The server does not promise a fixed catalogue of sentences,
3653
+ // so there is nothing here for those to key against — see PR #4300 and the
3654
+ // page's own header.
3655
+ appManagement: {
3656
+ title: 'Applications',
3657
+ subtitle: 'Manage all configured applications',
3658
+ newApp: 'New App',
3659
+ // `searchLabel` is the input's sr-only `<label>`; `searchPlaceholder` is
3660
+ // what a sighted user reads. Both, because a placeholder is not a label.
3661
+ searchLabel: 'Search apps',
3662
+ searchPlaceholder: 'Search apps…',
3663
+ selectedCount: '{{n}} selected',
3664
+ // The two bulk buttons. Bare verbs — the selection badge beside them is
3665
+ // what says how many rows they act on.
3666
+ bulkEnable: 'Enable',
3667
+ bulkDisable: 'Disable',
3668
+ selectAll: 'Select all ({{n}})',
3669
+ empty: 'No apps found.',
3670
+ defaultBadge: 'Default',
3671
+ active: 'Active',
3672
+ inactive: 'Inactive',
3673
+ // Row controls. Each is a pair: the `title` tooltip names the ACTION, the
3674
+ // `aria-label` names the action AND its target, because a screen reader
3675
+ // reaching an icon button has no row context to borrow from.
3676
+ selectApp: 'Select {{name}}',
3677
+ openApp: 'Open app',
3678
+ openAppNamed: 'Open {{name}}',
3679
+ editApp: 'Edit app',
3680
+ editAppNamed: 'Edit {{name}}',
3681
+ editWithAi: 'Edit with AI',
3682
+ editWithAiNamed: 'Edit {{name}} with AI',
3683
+ enableApp: 'Enable app',
3684
+ disableApp: 'Disable app',
3685
+ enableAppNamed: 'Enable {{name}}',
3686
+ disableAppNamed: 'Disable {{name}}',
3687
+ setDefault: 'Set as default',
3688
+ setDefaultNamed: 'Set {{name}} as default',
3689
+ deleteApp: 'Delete app',
3690
+ deleteAppNamed: 'Delete {{name}}',
3691
+ // Delete is a two-click control: the first click arms it and the label
3692
+ // becomes the confirmation prompt.
3693
+ confirmDelete: 'Click again to confirm delete',
3694
+ confirmDeleteNamed: 'Confirm delete {{name}}',
3695
+ toast: {
3696
+ noClient: 'Cannot reach the metadata service',
3697
+ // What `reason()` says when the failure carried no message at all. The
3698
+ // ONE localized part of an otherwise verbatim server string.
3699
+ unknownError: 'unknown error',
3700
+ appEnabled: '{{name}} enabled',
3701
+ appDisabled: '{{name}} disabled',
3702
+ toggleFailed: 'Failed to toggle app status: {{reason}}',
3703
+ setDefaultDone: '{{name}} set as default',
3704
+ setDefaultFailed: 'Failed to set default app: {{reason}}',
3705
+ appDeleted: '{{name}} deleted',
3706
+ deleteFailed: 'Failed to delete app: {{reason}}',
3707
+ // `{{n}}` is the count that actually LANDED, not the selection size — a
3708
+ // bulk toggle is N independent writes with no transaction behind them.
3709
+ bulkEnabled: '{{n}} apps enabled',
3710
+ bulkDisabled: '{{n}} apps disabled',
3711
+ bulkFailed: 'Failed for {{n}}: {{details}}',
3712
+ // One entry of `{{details}}`. The bracket pair and the space before it
3713
+ // are part of the translation, so the CJK packs can set their own — the
3714
+ // same reason `detail.userStatusTitle` is keyed rather than composed.
3715
+ bulkFailureEntry: '{{name}} ({{reason}})',
3716
+ // Separator between those entries. Per-locale because list punctuation
3717
+ // is a locale property, not a code constant — `validation.
3718
+ // formInvalidJoiner` is the same key for the same past defect.
3719
+ bulkFailureJoiner: '; ',
3720
+ bulkOperationFailed: 'Bulk operation failed: {{reason}}',
3721
+ },
3722
+ },
3230
3723
  };
3231
3724
  export default en;