@firecms/collection_editor 3.1.0-canary.1df3b2c → 3.1.0-canary.75005e4

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 (67) hide show
  1. package/dist/index.es.js +6984 -3759
  2. package/dist/index.es.js.map +1 -1
  3. package/dist/index.umd.js +6982 -3757
  4. package/dist/index.umd.js.map +1 -1
  5. package/dist/locales/de.d.ts +118 -0
  6. package/dist/locales/en.d.ts +118 -0
  7. package/dist/locales/es.d.ts +118 -0
  8. package/dist/locales/fr.d.ts +118 -0
  9. package/dist/locales/hi.d.ts +118 -0
  10. package/dist/locales/it.d.ts +118 -0
  11. package/dist/locales/pt.d.ts +118 -0
  12. package/dist/ui/collection_editor/AICollectionGeneratorPopover.d.ts +5 -1
  13. package/dist/ui/collection_editor/CollectionEditorDialog.d.ts +4 -0
  14. package/dist/ui/collection_editor/CollectionEditorWelcomeView.d.ts +2 -1
  15. package/package.json +12 -12
  16. package/src/ConfigControllerProvider.tsx +1 -0
  17. package/src/locales/de.ts +123 -0
  18. package/src/locales/en.ts +143 -0
  19. package/src/locales/es.ts +123 -0
  20. package/src/locales/fr.ts +123 -0
  21. package/src/locales/hi.ts +123 -0
  22. package/src/locales/it.ts +123 -0
  23. package/src/locales/pt.ts +123 -0
  24. package/src/ui/EditorCollectionAction.tsx +3 -3
  25. package/src/ui/EditorEntityAction.tsx +3 -2
  26. package/src/ui/NewCollectionButton.tsx +3 -1
  27. package/src/ui/NewCollectionCard.tsx +7 -4
  28. package/src/ui/PropertyAddColumnComponent.tsx +3 -2
  29. package/src/ui/collection_editor/AICollectionGeneratorPopover.tsx +29 -11
  30. package/src/ui/collection_editor/CollectionDetailsForm.tsx +28 -25
  31. package/src/ui/collection_editor/CollectionEditorDialog.tsx +55 -42
  32. package/src/ui/collection_editor/CollectionEditorWelcomeView.tsx +23 -19
  33. package/src/ui/collection_editor/CollectionPropertiesEditorForm.tsx +12 -10
  34. package/src/ui/collection_editor/DisplaySettingsForm.tsx +19 -17
  35. package/src/ui/collection_editor/EntityActionsEditTab.tsx +11 -12
  36. package/src/ui/collection_editor/EntityActionsSelectDialog.tsx +5 -6
  37. package/src/ui/collection_editor/EntityCustomViewsSelectDialog.tsx +5 -5
  38. package/src/ui/collection_editor/EnumForm.tsx +6 -2
  39. package/src/ui/collection_editor/ExtendSettingsForm.tsx +8 -7
  40. package/src/ui/collection_editor/GeneralSettingsForm.tsx +42 -42
  41. package/src/ui/collection_editor/GetCodeDialog.tsx +13 -12
  42. package/src/ui/collection_editor/KanbanConfigSection.tsx +11 -9
  43. package/src/ui/collection_editor/LayoutModeSwitch.tsx +7 -4
  44. package/src/ui/collection_editor/PropertyEditView.tsx +75 -66
  45. package/src/ui/collection_editor/SubcollectionsEditTab.tsx +16 -19
  46. package/src/ui/collection_editor/ViewModeSwitch.tsx +8 -6
  47. package/src/ui/collection_editor/import/CollectionEditorImportDataPreview.tsx +6 -3
  48. package/src/ui/collection_editor/import/CollectionEditorImportMapping.tsx +5 -2
  49. package/src/ui/collection_editor/properties/BooleanPropertyField.tsx +3 -1
  50. package/src/ui/collection_editor/properties/CommonPropertyFields.tsx +6 -4
  51. package/src/ui/collection_editor/properties/DateTimePropertyField.tsx +20 -18
  52. package/src/ui/collection_editor/properties/EnumPropertyField.tsx +5 -4
  53. package/src/ui/collection_editor/properties/MapPropertyField.tsx +8 -7
  54. package/src/ui/collection_editor/properties/MarkdownPropertyField.tsx +22 -23
  55. package/src/ui/collection_editor/properties/NumberPropertyField.tsx +3 -1
  56. package/src/ui/collection_editor/properties/ReferencePropertyField.tsx +5 -4
  57. package/src/ui/collection_editor/properties/StoragePropertyField.tsx +46 -51
  58. package/src/ui/collection_editor/properties/StringPropertyField.tsx +3 -1
  59. package/src/ui/collection_editor/properties/UrlPropertyField.tsx +12 -10
  60. package/src/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +7 -4
  61. package/src/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +8 -3
  62. package/src/ui/collection_editor/properties/validation/ArrayPropertyValidation.tsx +5 -2
  63. package/src/ui/collection_editor/properties/validation/GeneralPropertyValidation.tsx +7 -5
  64. package/src/ui/collection_editor/properties/validation/NumberPropertyValidation.tsx +10 -7
  65. package/src/ui/collection_editor/properties/validation/StringPropertyValidation.tsx +11 -9
  66. package/src/ui/collection_editor/properties/validation/ValidationPanel.tsx +4 -1
  67. package/src/useCollectionEditorPlugin.tsx +22 -6
@@ -1,5 +1,5 @@
1
1
  import React, { useState } from "react";
2
- import { EntityCollection, useNavigationController, useSnackbarController, AIIcon } from "@firecms/core";
2
+ import { EntityCollection, useNavigationController, useSnackbarController, AIIcon, useTranslation } from "@firecms/core";
3
3
  import {
4
4
  Button,
5
5
  CircularProgress,
@@ -48,6 +48,11 @@ export interface AICollectionGeneratorPopoverProps {
48
48
  * Whether to show the label text
49
49
  */
50
50
  showLabel?: boolean;
51
+
52
+ /**
53
+ * Optional analytics callback
54
+ */
55
+ onAnalyticsEvent?: (event: string, params?: object) => void;
51
56
  }
52
57
 
53
58
  export function AICollectionGeneratorPopover({
@@ -56,13 +61,15 @@ export function AICollectionGeneratorPopover({
56
61
  generateCollection,
57
62
  trigger,
58
63
  size = "small",
59
- showLabel = true
64
+ showLabel = true,
65
+ onAnalyticsEvent
60
66
  }: AICollectionGeneratorPopoverProps) {
61
67
  const [menuOpen, setMenuOpen] = useState(false);
62
68
  const [prompt, setPrompt] = useState("");
63
69
  const [loading, setLoading] = useState(false);
64
70
  const [error, setError] = useState<string | null>(null);
65
71
 
72
+ const { t } = useTranslation();
66
73
  const navigation = useNavigationController();
67
74
  const snackbarController = useSnackbarController();
68
75
 
@@ -74,6 +81,9 @@ export function AICollectionGeneratorPopover({
74
81
  setLoading(true);
75
82
  setError(null);
76
83
 
84
+ const mode = existingCollection ? "modify" : "create";
85
+ onAnalyticsEvent?.("ai_collection_generate_start", { mode });
86
+
77
87
  try {
78
88
  const collectionsContext = existingCollections.map(c => ({
79
89
  path: c.path,
@@ -100,6 +110,10 @@ export function AICollectionGeneratorPopover({
100
110
  onGenerated(result.collection, result.operations);
101
111
  setMenuOpen(false);
102
112
  setPrompt("");
113
+ onAnalyticsEvent?.("ai_collection_generate_success", {
114
+ mode,
115
+ operationsCount: result.operations?.length
116
+ });
103
117
  snackbarController.open({
104
118
  type: "success",
105
119
  message: existingCollection
@@ -112,6 +126,10 @@ export function AICollectionGeneratorPopover({
112
126
  ? e.message
113
127
  : "Failed to generate collection. Please try again.";
114
128
  setError(errorMessage);
129
+ onAnalyticsEvent?.("ai_collection_generate_error", {
130
+ mode,
131
+ error: errorMessage
132
+ });
115
133
  snackbarController.open({
116
134
  type: "error",
117
135
  message: errorMessage
@@ -138,13 +156,13 @@ export function AICollectionGeneratorPopover({
138
156
  : <AIIcon size="small" />
139
157
  }
140
158
  >
141
- AI Assist
159
+ {t("ai_assist")}
142
160
  </Button>
143
161
  ) : (
144
162
  <IconButton
145
163
  size={size}
146
164
  disabled={loading}
147
- aria-label="AI Assist"
165
+ aria-label={t("ai_assist")}
148
166
  >
149
167
  {loading
150
168
  ? <CircularProgress size="smallest" />
@@ -168,14 +186,14 @@ export function AICollectionGeneratorPopover({
168
186
  <div className="flex items-center gap-2">
169
187
  <AIIcon size="small" />
170
188
  <Typography variant="subtitle2">
171
- {existingCollection ? "Modify Collection with AI" : "Generate Collection with AI"}
189
+ {existingCollection ? t("modify_collection_with_ai") : t("generate_collection_with_ai")}
172
190
  </Typography>
173
191
  </div>
174
192
 
175
193
  <Typography variant="caption" color="secondary">
176
194
  {existingCollection
177
- ? "Describe the changes you want to make to this collection."
178
- : "Describe the collection you want to create."
195
+ ? t("describe_changes_to_make")
196
+ : t("describe_collection_to_create")
179
197
  }
180
198
  </Typography>
181
199
 
@@ -188,8 +206,8 @@ export function AICollectionGeneratorPopover({
188
206
  onChange={(e) => setPrompt(e.target.value)}
189
207
  onKeyDown={handleKeyDown}
190
208
  placeholder={existingCollection
191
- ? "e.g., Add a thumbnail image field with storage, make price required..."
192
- : "e.g., Create a products collection with name, price, description, and category..."
209
+ ? t("ai_placeholder_modify")
210
+ : t("ai_placeholder_create")
193
211
  }
194
212
  disabled={loading}
195
213
  />
@@ -207,7 +225,7 @@ export function AICollectionGeneratorPopover({
207
225
  onClick={() => setMenuOpen(false)}
208
226
  disabled={loading}
209
227
  >
210
- Cancel
228
+ {t("cancel")}
211
229
  </Button>
212
230
  <Button
213
231
  variant="filled"
@@ -216,7 +234,7 @@ export function AICollectionGeneratorPopover({
216
234
  disabled={!prompt.trim() || loading}
217
235
  startIcon={loading ? <CircularProgress size="smallest" /> : undefined}
218
236
  >
219
- {loading ? "Generating..." : "Generate"}
237
+ {loading ? t("generating") : t("generate_with_ai")}
220
238
  </Button>
221
239
  </div>
222
240
  </div>
@@ -1,5 +1,5 @@
1
1
  import React, { useMemo, useState } from "react";
2
- import { EntityCollection, FieldCaption, getFieldConfig, IconForView, Property, PropertyConfigBadge, resolveCollection, SearchIconsView, singular, toSnakeCase, unslugify, useAuthController, useCustomizationController } from "@firecms/core";
2
+ import { EntityCollection, FieldCaption, getFieldConfig, IconForView, Property, PropertyConfigBadge, resolveCollection, SearchIconsView, singular, toSnakeCase, unslugify, useAuthController, useCustomizationController, useTranslation } from "@firecms/core";
3
3
  import {
4
4
  BooleanSwitchWithLabel,
5
5
  Chip,
@@ -8,6 +8,7 @@ import {
8
8
  Container,
9
9
  DebouncedTextField,
10
10
  Dialog,
11
+ HistoryIcon,
11
12
  IconButton,
12
13
  Select,
13
14
  SelectItem,
@@ -60,6 +61,8 @@ export function CollectionDetailsForm({
60
61
  const [iconDialogOpen, setIconDialogOpen] = useState(false);
61
62
  const [orderPropertyDialogOpen, setOrderPropertyDialogOpen] = useState(false);
62
63
 
64
+ const { t } = useTranslation();
65
+
63
66
  const authController = useAuthController();
64
67
  const customizationController = useCustomizationController();
65
68
 
@@ -136,12 +139,12 @@ export function CollectionDetailsForm({
136
139
  <div
137
140
  className="flex flex-row gap-2 py-2 pt-3 items-center">
138
141
  <Typography variant={!isNewCollection ? "h5" : "h4"} className={"flex-grow"}>
139
- {isNewCollection ? "New collection" : `${values?.name} collection`}
142
+ {isNewCollection ? t("new_collection") : t("collection_with_name", { name: values?.name || "" })}
140
143
  </Typography>
141
144
  <DefaultDatabaseField databaseId={values.databaseId}
142
145
  onDatabaseIdUpdate={updateDatabaseId} />
143
146
 
144
- <Tooltip title={"Change icon"}
147
+ <Tooltip title={t("change_icon")}
145
148
  asChild={true}>
146
149
  <IconButton
147
150
  shape={"square"}
@@ -153,7 +156,7 @@ export function CollectionDetailsForm({
153
156
 
154
157
  {parentCollection && <Chip colorScheme={"tealDarker"}>
155
158
  <Typography variant={"caption"}>
156
- This is a subcollection of <b>{parentCollection.name}</b>
159
+ {t("is_subcollection_of")} <b>{parentCollection.name}</b>
157
160
  </Typography>
158
161
  </Chip>}
159
162
 
@@ -164,26 +167,26 @@ export function CollectionDetailsForm({
164
167
  <TextField
165
168
  value={values.name ?? ""}
166
169
  onChange={(e: any) => updateName(e.target.value)}
167
- label={"Name"}
170
+ label={t("name")}
168
171
  autoFocus={true}
169
172
  required
170
173
  error={showErrors && Boolean(errors.name)} />
171
174
  <FieldCaption error={touched.name && Boolean(errors.name)}>
172
- {touched.name && Boolean(errors.name) ? errors.name : "Name of this collection, usually a plural name (e.g. Products)"}
175
+ {touched.name && Boolean(errors.name) ? errors.name : t("collection_name_description")}
173
176
  </FieldCaption>
174
177
  </div>
175
178
 
176
179
  <div className={cls("col-span-12 ")}>
177
180
  <Field name={"path"}
178
181
  as={DebouncedTextField}
179
- label={"Path"}
182
+ label={t("path")}
180
183
  required
181
184
  error={showErrors && Boolean(errors.path)} />
182
185
 
183
186
  <FieldCaption error={touched.path && Boolean(errors.path)}>
184
187
  {touched.path && Boolean(errors.path)
185
188
  ? errors.path
186
- : isSubcollection ? "Relative path to the parent (no need to include the parent path)" : "Path that this collection is stored in, in the database"}
189
+ : isSubcollection ? t("relative_path_to_parent") : t("path_in_database")}
187
190
  </FieldCaption>
188
191
 
189
192
  </div>
@@ -246,7 +249,7 @@ export function CollectionDetailsForm({
246
249
  <Select
247
250
  key={`order-select-${numberProperties.length}`}
248
251
  name="orderProperty"
249
- label="Order Property"
252
+ label={t("order_property")}
250
253
  size={"large"}
251
254
  fullWidth={true}
252
255
  position={"item-aligned"}
@@ -258,10 +261,10 @@ export function CollectionDetailsForm({
258
261
  }}
259
262
  renderValue={(value) => {
260
263
  if (orderPropertyMissing) {
261
- return <span className="text-red-500">{value} (not found)</span>;
264
+ return <span className="text-red-500">{value} ({t("not_found_suffix")})</span>;
262
265
  }
263
266
  const prop = numberProperties.find(p => p.key === value);
264
- if (!prop) return "Select a property";
267
+ if (!prop) return t("select_a_property");
265
268
  const fieldConfig = getFieldConfig(prop.property, customizationController.propertyConfigs);
266
269
  return (
267
270
  <div className="flex items-center gap-2">
@@ -291,7 +294,7 @@ export function CollectionDetailsForm({
291
294
  <div>
292
295
  <div>{prop.label}</div>
293
296
  <Typography variant="caption" color="secondary">
294
- {fieldConfig?.name || "Number"}
297
+ {fieldConfig?.name || t("number")}
295
298
  </Typography>
296
299
  </div>
297
300
  </div>
@@ -301,10 +304,10 @@ export function CollectionDetailsForm({
301
304
  </Select>
302
305
  <FieldCaption error={orderPropertyMissing}>
303
306
  {orderPropertyMissing
304
- ? `Property "${values.orderProperty}" does not exist or is not a number property. Please select a valid property or clear the selection.`
307
+ ? t("order_property_not_found", { property: values.orderProperty ?? "" })
305
308
  : numberProperties.length === 0
306
- ? "No number properties found. Add a number property to enable ordering."
307
- : "Select a number property to persist the order of items"
309
+ ? t("no_number_properties")
310
+ : t("order_property_description")
308
311
  }
309
312
  </FieldCaption>
310
313
  </>
@@ -322,7 +325,7 @@ export function CollectionDetailsForm({
322
325
  : "__order";
323
326
  const dialogPropertyName = orderPropertyMissing && values.orderProperty
324
327
  ? unslugify(values.orderProperty)
325
- : "Order";
328
+ : t("order_label");
326
329
 
327
330
  if (!showCreateButton) return null;
328
331
 
@@ -333,7 +336,7 @@ export function CollectionDetailsForm({
333
336
  className="ml-3.5 text-sm text-primary hover:text-primary-dark mt-2"
334
337
  onClick={() => setOrderPropertyDialogOpen(true)}
335
338
  >
336
- + Create "{dialogPropertyKey}" property
339
+ {t("create_property", { property: dialogPropertyKey })}
337
340
  </button>
338
341
  <PropertyFormDialog
339
342
  open={orderPropertyDialogOpen}
@@ -375,16 +378,14 @@ export function CollectionDetailsForm({
375
378
  position={"start"}
376
379
  size={"large"}
377
380
  allowIndeterminate={true}
378
- label={values.history === null || values.history === undefined ? "Document history revisions enabled if enabled globally" : (
379
- values.history ? "Document history revisions ENABLED" : "Document history revisions NOT enabled"
380
- )}
381
+ label={<span className="flex items-center gap-2"><HistoryIcon size={"smallest"} />{values.history === null || values.history === undefined ? t("doc_history_global") : (
382
+ values.history ? t("doc_history_enabled") : t("doc_history_not_enabled")
383
+ )}</span>}
381
384
  onValueChange={(v) => setFieldValue("history", v)}
382
385
  value={values.history === undefined ? null : values.history}
383
386
  />
384
387
  <FieldCaption>
385
- When enabled, each document in this collection will have a history of changes.
386
- This is useful for auditing purposes. The data is stored in a subcollection of the document
387
- in your database, called <b>__history</b>.
388
+ {t("doc_history_description")}
388
389
  </FieldCaption>
389
390
  </div>
390
391
 
@@ -423,7 +424,9 @@ function DefaultDatabaseField({
423
424
  onDatabaseIdUpdate
424
425
  }: { databaseId?: string, onDatabaseIdUpdate: (databaseId: string) => void }) {
425
426
 
426
- return <Tooltip title={"Database ID"}
427
+ const { t } = useTranslation();
428
+
429
+ return <Tooltip title={t("database_id")}
427
430
  side={"top"}
428
431
  align={"start"}>
429
432
  <TextField size={"small"}
@@ -431,6 +434,6 @@ function DefaultDatabaseField({
431
434
  inputClassName={"text-end"}
432
435
  value={databaseId ?? ""}
433
436
  onChange={(e: any) => onDatabaseIdUpdate(e.target.value)}
434
- placeholder={"(default)"}></TextField>
437
+ placeholder={t("default_text")}></TextField>
435
438
  </Tooltip>
436
439
  }
@@ -22,7 +22,8 @@ import {
22
22
  useCustomizationController,
23
23
  useNavigationController,
24
24
  User,
25
- useSnackbarController
25
+ useSnackbarController,
26
+ useTranslation,
26
27
  } from "@firecms/core";
27
28
  import {
28
29
  ArrowBackIcon,
@@ -106,9 +107,15 @@ export interface CollectionEditorDialogProps {
106
107
  * The plugin is API-agnostic - the consumer provides the implementation.
107
108
  */
108
109
  generateCollection?: CollectionGenerationCallback;
110
+ /**
111
+ * Optional analytics callback
112
+ */
113
+ onAnalyticsEvent?: (event: string, params?: object) => void;
109
114
  }
110
115
 
111
116
  export function CollectionEditorDialog(props: CollectionEditorDialogProps) {
117
+ const { t } = useTranslation();
118
+
112
119
 
113
120
  const open = props.open;
114
121
 
@@ -139,7 +146,7 @@ export function CollectionEditorDialog(props: CollectionEditorDialogProps) {
139
146
  maxWidth={"7xl"}
140
147
  onOpenChange={(open) => !open ? handleCancel() : undefined}
141
148
  >
142
- <DialogTitle hidden>Collection editor</DialogTitle>
149
+ <DialogTitle hidden>{t("collection_editor")}</DialogTitle>
143
150
  <AIModifiedPathsProvider>
144
151
  {open && <CollectionEditor {...props}
145
152
  handleCancel={handleCancel}
@@ -149,7 +156,7 @@ export function CollectionEditorDialog(props: CollectionEditorDialogProps) {
149
156
  open={unsavedChangesDialogOpen}
150
157
  handleOk={() => props.handleClose(undefined)}
151
158
  handleCancel={() => setUnsavedChangesDialogOpen(false)}
152
- body={"There are unsaved changes in this collection"} />
159
+ body={t("unsaved_changes_in_collection")} />
153
160
  </AIModifiedPathsProvider>
154
161
  </Dialog>
155
162
  );
@@ -290,7 +297,8 @@ function CollectionEditorInternal<M extends Record<string, any>>({
290
297
  existingEntities,
291
298
  initialView: initialViewProp,
292
299
  expandKanban,
293
- generateCollection
300
+ generateCollection,
301
+ onAnalyticsEvent
294
302
  }: CollectionEditorDialogProps & {
295
303
  handleCancel: () => void,
296
304
  setFormDirty: (dirty: boolean) => void,
@@ -301,10 +309,11 @@ function CollectionEditorInternal<M extends Record<string, any>>({
301
309
  collection: PersistedCollection<M> | undefined,
302
310
  setCollection: (collection: PersistedCollection<M>) => void,
303
311
  propertyConfigs: Record<string, PropertyConfig<any>>,
304
- groups: string[],
312
+ groups: (string | null)[],
305
313
  }
306
314
  ) {
307
315
 
316
+ const { t } = useTranslation();
308
317
  const importConfig = useImportConfig();
309
318
  const navigation = useNavigationController();
310
319
  const snackbarController = useSnackbarController();
@@ -337,7 +346,7 @@ function CollectionEditorInternal<M extends Record<string, any>>({
337
346
  console.error(e);
338
347
  snackbarController.open({
339
348
  type: "error",
340
- message: "Error persisting collection: " + (e.message ?? "Details in the console")
349
+ message: t("error_persisting_collection", { error: e.message ?? t("details_in_console") })
341
350
  });
342
351
  return false;
343
352
  });
@@ -418,7 +427,7 @@ function CollectionEditorInternal<M extends Record<string, any>>({
418
427
  console.error(e);
419
428
  snackbarController.open({
420
429
  type: "error",
421
- message: "Error inferring collection: " + (e.message ?? "Details in the console")
430
+ message: t("error_inferring_collection", { error: e.message ?? t("details_in_console") })
422
431
  });
423
432
  return newCollection;
424
433
  }
@@ -481,7 +490,7 @@ function CollectionEditorInternal<M extends Record<string, any>>({
481
490
  } catch (e: any) {
482
491
  snackbarController.open({
483
492
  type: "error",
484
- message: "Error persisting collection: " + (e.message ?? "Details in the console")
493
+ message: t("error_persisting_collection", { error: e.message ?? t("details_in_console") })
485
494
  });
486
495
  console.error(e);
487
496
  formexController.resetForm({ values: newCollectionState });
@@ -505,11 +514,11 @@ function CollectionEditorInternal<M extends Record<string, any>>({
505
514
  errors = { ...errors, ...propertyErrorsRef.current };
506
515
  }
507
516
  if (currentView === "general") {
508
- const pathError = validatePath(col.path, isNewCollection, existingPaths, col.id);
517
+ const pathError = validatePath(t, col.path, isNewCollection, existingPaths, col.id);
509
518
  if (pathError) {
510
519
  errors.path = pathError;
511
520
  }
512
- const idError = validateId(col.id, isNewCollection, existingPaths, existingIds);
521
+ const idError = validateId(t, col.id, isNewCollection, existingPaths, existingIds);
513
522
  if (idError) {
514
523
  errors.id = idError;
515
524
  }
@@ -534,7 +543,7 @@ function CollectionEditorInternal<M extends Record<string, any>>({
534
543
 
535
544
  const path = values.path;
536
545
  const updatedFullPath = fullPath?.includes("/") ? fullPath?.split("/").slice(0, -1).join("/") + "/" + path : path; // TODO: this path is wrong
537
- const pathError = validatePath(path, isNewCollection, existingPaths, values.id);
546
+ const pathError = validatePath(t, path, isNewCollection, existingPaths, values.id);
538
547
 
539
548
  const parentPaths = !pathError && parentCollectionIds ? navigation.convertIdsToPaths(parentCollectionIds) : undefined;
540
549
  const resolvedPath = !pathError ? navigation.resolveIdsFrom(updatedFullPath) : undefined;
@@ -596,7 +605,7 @@ function CollectionEditorInternal<M extends Record<string, any>>({
596
605
  setDeleteRequested(false);
597
606
  handleCancel();
598
607
  snackbarController.open({
599
- message: "Collection deleted",
608
+ message: t("collection_deleted"),
600
609
  type: "success"
601
610
  });
602
611
  });
@@ -630,21 +639,22 @@ function CollectionEditorInternal<M extends Record<string, any>>({
630
639
  existingCollection={values}
631
640
  onGenerated={handleAIGenerated}
632
641
  generateCollection={generateCollection}
642
+ onAnalyticsEvent={onAnalyticsEvent}
633
643
  />
634
644
  )}
635
645
  <Tabs value={currentView}
636
646
  onValueChange={(v) => setCurrentView(v as EditorView)}>
637
647
  <Tab value={"general"}>
638
- General
648
+ {t("tab_general")}
639
649
  </Tab>
640
650
  <Tab value={"display"}>
641
- Display
651
+ {t("tab_display")}
642
652
  </Tab>
643
653
  <Tab value={"properties"}>
644
- Properties
654
+ {t("tab_properties")}
645
655
  </Tab>
646
656
  <Tab value={"extend"}>
647
- Extend
657
+ {t("tab_extend")}
648
658
  </Tab>
649
659
  </Tabs>
650
660
  </div>}
@@ -669,7 +679,8 @@ function CollectionEditorInternal<M extends Record<string, any>>({
669
679
  onContinue={onWelcomeScreenContinue}
670
680
  existingCollectionPaths={existingPaths}
671
681
  parentCollection={parentCollection}
672
- generateCollection={generateCollection} />}
682
+ generateCollection={generateCollection}
683
+ onAnalyticsEvent={onAnalyticsEvent} />}
673
684
 
674
685
  {currentView === "import_data_mapping" && importConfig &&
675
686
  <CollectionEditorImportMapping importConfig={importConfig}
@@ -688,7 +699,7 @@ function CollectionEditorInternal<M extends Record<string, any>>({
688
699
  onImportSuccess={async (importedCollection) => {
689
700
  snackbarController.open({
690
701
  type: "info",
691
- message: "Data imported successfully"
702
+ message: t("data_imported_successfully_msg")
692
703
  });
693
704
  await saveCollection(values);
694
705
  handleClose(importedCollection);
@@ -756,7 +767,7 @@ function CollectionEditorInternal<M extends Record<string, any>>({
756
767
  importConfig.setInUse(false);
757
768
  return setCurrentView("welcome");
758
769
  }}>
759
- Back
770
+ {t("back")}
760
771
  </Button>}
761
772
 
762
773
  {isNewCollection && includeTemplates && currentView === "import_data_preview" &&
@@ -765,14 +776,14 @@ function CollectionEditorInternal<M extends Record<string, any>>({
765
776
  onClick={() => {
766
777
  setCurrentView("import_data_mapping");
767
778
  }}>
768
- Back
779
+ {t("back")}
769
780
  </Button>}
770
781
 
771
782
  {isNewCollection && includeTemplates && currentView === "general" &&
772
783
  <Button variant={"text"}
773
784
  type="button"
774
785
  onClick={() => setCurrentView("welcome")}>
775
- Back
786
+ {t("back")}
776
787
  </Button>}
777
788
 
778
789
  {isNewCollection && currentView === "properties" && <Button variant={"text"}
@@ -780,7 +791,7 @@ function CollectionEditorInternal<M extends Record<string, any>>({
780
791
  color={"neutral"}
781
792
  onClick={() => setCurrentView("general")}>
782
793
  <ArrowBackIcon />
783
- Back
794
+ {t("back")}
784
795
  </Button>}
785
796
 
786
797
  <Button variant={"text"}
@@ -788,12 +799,12 @@ function CollectionEditorInternal<M extends Record<string, any>>({
788
799
  onClick={() => {
789
800
  handleCancel();
790
801
  }}>
791
- Cancel
802
+ {t("cancel")}
792
803
  </Button>
793
804
 
794
805
  {currentView === "welcome" &&
795
806
  <Button variant={"text"} onClick={() => onWelcomeScreenContinue()}>
796
- Continue from scratch
807
+ {t("continue_from_scratch")}
797
808
  </Button>}
798
809
 
799
810
  {isNewCollection && currentView === "import_data_mapping" &&
@@ -802,7 +813,7 @@ function CollectionEditorInternal<M extends Record<string, any>>({
802
813
  color="primary"
803
814
  onClick={onImportMappingComplete}
804
815
  >
805
- Next
816
+ {t("next")}
806
817
  </Button>}
807
818
 
808
819
  {isNewCollection && currentView === "import_data_preview" &&
@@ -813,7 +824,7 @@ function CollectionEditorInternal<M extends Record<string, any>>({
813
824
  setNextMode();
814
825
  }}
815
826
  >
816
- Next
827
+ {t("next")}
817
828
  </Button>}
818
829
 
819
830
  {isNewCollection && (currentView === "general" || currentView === "properties") &&
@@ -827,8 +838,8 @@ function CollectionEditorInternal<M extends Record<string, any>>({
827
838
  ? <CheckIcon />
828
839
  : undefined}
829
840
  >
830
- {currentView === "general" && "Next"}
831
- {currentView === "properties" && "Create collection"}
841
+ {currentView === "general" && t("next")}
842
+ {currentView === "properties" && t("create_collection")}
832
843
  </LoadingButton>}
833
844
 
834
845
  {!isNewCollection && <LoadingButton
@@ -837,7 +848,7 @@ function CollectionEditorInternal<M extends Record<string, any>>({
837
848
  type="submit"
838
849
  loading={isSubmitting}
839
850
  >
840
- Update collection
851
+ {t("update_collection")}
841
852
  </LoadingButton>}
842
853
 
843
854
  </DialogActions>
@@ -850,10 +861,8 @@ function CollectionEditorInternal<M extends Record<string, any>>({
850
861
  open={deleteRequested}
851
862
  onAccept={deleteCollection}
852
863
  onCancel={() => setDeleteRequested(false)}
853
- title={<>Delete the stored config?</>}
854
- body={<> This will <b>not
855
- delete any data</b>, only
856
- the stored config, and reset to the code state.</>} />
864
+ title={<>{t("delete_stored_config")}</>}
865
+ body={<>{t("delete_stored_config_body")}</>} />
857
866
 
858
867
  </DialogContent>
859
868
 
@@ -867,7 +876,9 @@ function applyPropertyConfigs<M extends Record<string, any> = any>(collection: P
867
876
  const propertiesResult: PropertiesOrBuilders<any> = {};
868
877
  if (properties) {
869
878
  Object.keys(properties).forEach((key) => {
870
- propertiesResult[key] = applyPropertiesConfig(properties[key] as PropertyOrBuilder, propertyConfigs);
879
+ const prop = properties[key];
880
+ if (prop == null) return;
881
+ propertiesResult[key] = applyPropertiesConfig(prop as PropertyOrBuilder, propertyConfigs);
871
882
  });
872
883
  }
873
884
 
@@ -879,7 +890,7 @@ function applyPropertyConfigs<M extends Record<string, any> = any>(collection: P
879
890
 
880
891
  function applyPropertiesConfig(property: PropertyOrBuilder, propertyConfigs: Record<string, PropertyConfig<any>>) {
881
892
  let internalProperty = property;
882
- if (propertyConfigs && typeof internalProperty === "object" && internalProperty.propertyConfig) {
893
+ if (propertyConfigs && internalProperty && typeof internalProperty === "object" && internalProperty.propertyConfig) {
883
894
  const propertyConfig = propertyConfigs[internalProperty.propertyConfig];
884
895
  if (propertyConfig && isPropertyBuilder(propertyConfig.property)) {
885
896
  internalProperty = propertyConfig.property;
@@ -906,30 +917,32 @@ function applyPropertiesConfig(property: PropertyOrBuilder, propertyConfigs: Rec
906
917
 
907
918
  }
908
919
 
909
- const validatePath = (value: string, isNewCollection: boolean, existingPaths: string[], idValue?: string) => {
920
+ type TranslateFn = (key: string, params?: Record<string, string>) => string;
921
+
922
+ const validatePath = (t: TranslateFn, value: string, isNewCollection: boolean, existingPaths: string[], idValue?: string) => {
910
923
  let error;
911
924
  if (!value) {
912
- error = "You must specify a path in the database for this collection";
925
+ error = t("must_specify_path");
913
926
  }
914
927
  // if (isNewCollection && existingIds?.includes(value.trim().toLowerCase()))
915
928
  // error = "There is already a collection which uses this path as an id";
916
929
  if (isNewCollection && existingPaths?.includes(value.trim().toLowerCase()) && !idValue)
917
- error = "There is already a collection with the specified path. If you want to have multiple collections referring to the same database path, make sure the have different ids";
930
+ error = t("collection_path_already_exists");
918
931
 
919
932
  const subpaths = removeInitialAndTrailingSlashes(value).split("/");
920
933
  if (subpaths.length % 2 === 0) {
921
- error = `Collection paths must have an odd number of segments: ${value}`;
934
+ error = t("collection_path_odd_segments", { path: value });
922
935
  }
923
936
  return error;
924
937
  };
925
938
 
926
- const validateId = (value: string, isNewCollection: boolean, existingPaths: string[], existingIds: string[]) => {
939
+ const validateId = (t: TranslateFn, value: string, isNewCollection: boolean, existingPaths: string[], existingIds: string[]) => {
927
940
  if (!value) return undefined;
928
941
  let error;
929
942
  if (isNewCollection && existingPaths?.includes(value.trim().toLowerCase()))
930
- error = "There is already a collection that uses this value as a path";
943
+ error = t("collection_uses_path_as_id");
931
944
  if (isNewCollection && existingIds?.includes(value.trim().toLowerCase()))
932
- error = "There is already a collection which uses this id";
945
+ error = t("collection_uses_this_id");
933
946
  // if (error) {
934
947
  // setAdvancedPanelExpanded(true);
935
948
  // }