hazo_files 3.2.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGE_LOG.md CHANGED
@@ -5,6 +5,41 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## 3.4.0 — 2026-09-19
9
+
10
+ ### Changed
11
+
12
+ - Migrated onto the canonical `hazo_core/i18n` bridge (`createPackageT` /
13
+ `resolvePackageLocale`), replacing this package's local ~120-line copy at
14
+ `src/lib/create_package_t.ts` (now deleted). All 13 call sites across
15
+ `src/ui/components/**` and `src/background_upload/react/use-file-upload-toasts.ts`
16
+ now import directly from `hazo_core/i18n`. Behavior is unchanged — same
17
+ flat `src/locales/en.json` fallback catalog, same `{{var}}` mustache
18
+ interpolation, same optional/no-op `hazo_i18n` registration side effect.
19
+ `hazo_core` peer bumped to `^2.4.0` (first version shipping the `/i18n`
20
+ subpath) and `hazo_i18n` peer bumped to `^0.5.0`. Added an "i18n bridge"
21
+ autotest scenario group to the test-app covering catalog load, key
22
+ coverage, and interpolation.
23
+
24
+ ## 3.3.0 — 2026-08-13
25
+
26
+ ### Fixed
27
+
28
+ `HazoFileUploadProvider` handed the live tree a destroyed `UploadManager` after
29
+ a remount. The provider created the manager in a `useRef` and destroyed it in
30
+ its `useEffect` cleanup, but never rebuilt it. Under React StrictMode (dev
31
+ double-mount) or any real remount, the cleanup ran `manager.destroy()`, then the
32
+ re-run setup reused the same destroyed instance — so every `submit_batch` threw
33
+ `UploadManager is destroyed`. The effect now rebuilds the manager when it finds
34
+ a destroyed one and forces a re-render so the context value points at the fresh
35
+ instance.
36
+
37
+ ### Added
38
+
39
+ - `UploadManager.is_destroyed` getter, so consumers (and the provider) can check
40
+ lifecycle state without touching internals.
41
+
42
+
8
43
  ## 3.2.0 — 2026-08-04
9
44
 
10
45
  ### Fixed
@@ -152,6 +152,7 @@ declare class UploadManager {
152
152
  private destroyed;
153
153
  private readonly config;
154
154
  constructor(config?: UploadManagerConfig);
155
+ get is_destroyed(): boolean;
155
156
  on<K extends keyof UploadManagerEvents>(event: K, listener: (data: UploadManagerEvents[K]) => void): () => void;
156
157
  submit_batch(options: CreateBatchOptions): string;
157
158
  get_jobs_for_group(group_id: string): JobSnapshot[];
@@ -152,6 +152,7 @@ declare class UploadManager {
152
152
  private destroyed;
153
153
  private readonly config;
154
154
  constructor(config?: UploadManagerConfig);
155
+ get is_destroyed(): boolean;
155
156
  on<K extends keyof UploadManagerEvents>(event: K, listener: (data: UploadManagerEvents[K]) => void): () => void;
156
157
  submit_batch(options: CreateBatchOptions): string;
157
158
  get_jobs_for_group(group_id: string): JobSnapshot[];
@@ -201,6 +201,9 @@ var UploadManager = class {
201
201
  this.emitter.on("job:completed", ({ job }) => this.on_job_settled(job));
202
202
  this.emitter.on("job:error", ({ job }) => this.on_job_settled(job));
203
203
  }
204
+ get is_destroyed() {
205
+ return this.destroyed;
206
+ }
204
207
  on(event, listener) {
205
208
  return this.emitter.on(event, listener);
206
209
  }
@@ -172,6 +172,9 @@ var UploadManager = class {
172
172
  this.emitter.on("job:completed", ({ job }) => this.on_job_settled(job));
173
173
  this.emitter.on("job:error", ({ job }) => this.on_job_settled(job));
174
174
  }
175
+ get is_destroyed() {
176
+ return this.destroyed;
177
+ }
175
178
  on(event, listener) {
176
179
  return this.emitter.on(event, listener);
177
180
  }
@@ -118,6 +118,7 @@ declare class UploadManager {
118
118
  private destroyed;
119
119
  private readonly config;
120
120
  constructor(config?: UploadManagerConfig);
121
+ get is_destroyed(): boolean;
121
122
  on<K extends keyof UploadManagerEvents>(event: K, listener: (data: UploadManagerEvents[K]) => void): () => void;
122
123
  submit_batch(options: CreateBatchOptions): string;
123
124
  get_jobs_for_group(group_id: string): JobSnapshot[];
@@ -118,6 +118,7 @@ declare class UploadManager {
118
118
  private destroyed;
119
119
  private readonly config;
120
120
  constructor(config?: UploadManagerConfig);
121
+ get is_destroyed(): boolean;
121
122
  on<K extends keyof UploadManagerEvents>(event: K, listener: (data: UploadManagerEvents[K]) => void): () => void;
122
123
  submit_batch(options: CreateBatchOptions): string;
123
124
  get_jobs_for_group(group_id: string): JobSnapshot[];
@@ -217,6 +217,9 @@ var UploadManager = class {
217
217
  this.emitter.on("job:completed", ({ job }) => this.on_job_settled(job));
218
218
  this.emitter.on("job:error", ({ job }) => this.on_job_settled(job));
219
219
  }
220
+ get is_destroyed() {
221
+ return this.destroyed;
222
+ }
220
223
  on(event, listener) {
221
224
  return this.emitter.on(event, listener);
222
225
  }
@@ -317,6 +320,108 @@ var FileUploadContext = (0, import_react.createContext)(null);
317
320
 
318
321
  // src/background_upload/react/use-file-upload-toasts.ts
319
322
  var import_react2 = require("react");
323
+
324
+ // src/locales/en.json
325
+ var en_default = {
326
+ "upload_dialog.title": "Upload Files",
327
+ "upload_dialog.uploading_to": "Uploading to:",
328
+ "upload_dialog.drop_text": "Drag and drop files here, or",
329
+ "upload_dialog.browse": "browse",
330
+ "upload_dialog.files_selected": "{{count}} file(s) selected",
331
+ "upload_dialog.total": "Total:",
332
+ "upload_dialog.cancel": "Cancel",
333
+ "upload_dialog.upload": "Upload",
334
+ "upload_dialog.select_files_error": "Please select files to upload",
335
+ "create_folder_dialog.title": "Create New Folder",
336
+ "create_folder_dialog.creating_in": "Creating folder in:",
337
+ "create_folder_dialog.name_label": "Folder Name",
338
+ "create_folder_dialog.name_placeholder": "Enter folder name",
339
+ "create_folder_dialog.cancel": "Cancel",
340
+ "create_folder_dialog.submit": "Create Folder",
341
+ "create_folder_dialog.name_required_error": "Please enter a folder name",
342
+ "create_folder_dialog.invalid_chars_error": "Folder name contains invalid characters",
343
+ "rename_dialog.title_folder": "Rename Folder",
344
+ "rename_dialog.title_file": "Rename File",
345
+ "rename_dialog.name_label": "New Name",
346
+ "rename_dialog.name_placeholder": "Enter new name",
347
+ "rename_dialog.cancel": "Cancel",
348
+ "rename_dialog.submit": "Rename",
349
+ "rename_dialog.name_required_error": "Please enter a name",
350
+ "rename_dialog.invalid_chars_error": "Name contains invalid characters",
351
+ "delete_confirm_dialog.title_folder": "Delete Folder",
352
+ "delete_confirm_dialog.title_file": "Delete File",
353
+ "delete_confirm_dialog.confirm_message": "Are you sure you want to delete",
354
+ "delete_confirm_dialog.folder_warning": "Warning: This folder and all its contents will be permanently deleted.",
355
+ "delete_confirm_dialog.cancel": "Cancel",
356
+ "delete_confirm_dialog.submit": "Delete",
357
+ "metadata_dialog.title": "File Metadata",
358
+ "metadata_dialog.close": "Close",
359
+ "file_actions.new_folder": "New Folder",
360
+ "file_actions.upload": "Upload",
361
+ "file_actions.download": "Download",
362
+ "file_actions.rename": "Rename",
363
+ "file_actions.delete": "Delete",
364
+ "file_actions.refresh": "Refresh",
365
+ "file_list.empty": "This folder is empty",
366
+ "file_list.header_name": "Name",
367
+ "file_list.header_size": "Size",
368
+ "file_list.header_modified": "Modified",
369
+ "folder_tree.root": "Root",
370
+ "file_preview.select_prompt": "Select a file to preview",
371
+ "file_preview.folder_label": "Folder",
372
+ "file_preview.created_label": "Created:",
373
+ "file_preview.modified_label": "Modified:",
374
+ "file_preview.error_loading": "Error loading preview",
375
+ "file_preview.video_unsupported": "Your browser does not support video playback.",
376
+ "file_preview.audio_unsupported": "Your browser does not support audio playback.",
377
+ "file_preview.no_preview": "No preview available",
378
+ "file_preview.size_label": "Size:",
379
+ "file_preview.type_label": "Type:",
380
+ "file_info_panel.no_item_selected": "No item selected",
381
+ "file_info_panel.no_custom_metadata": "No custom metadata",
382
+ "file_info_panel.name_label": "Name:",
383
+ "file_info_panel.path_label": "Path:",
384
+ "file_info_panel.type_label": "Type:",
385
+ "file_info_panel.size_label": "Size:",
386
+ "file_info_panel.created_label": "Created:",
387
+ "file_info_panel.modified_label": "Modified:",
388
+ "file_info_panel.storage_label": "Storage:",
389
+ "file_info_panel.custom_metadata_title": "Custom Metadata",
390
+ "file_info_panel.type_folder": "Folder",
391
+ "file_info_panel.type_unknown": "Unknown",
392
+ "file_browser.load_directory_error": "Failed to load directory",
393
+ "file_browser.move_item_error": "Failed to move item",
394
+ "file_browser.upload_file_error": "Failed to upload {{name}}",
395
+ "file_browser.download_file_error": "Failed to download file",
396
+ "file_browser.create_folder_error": "Failed to create folder",
397
+ "file_browser.delete_error": "Failed to delete",
398
+ "file_browser.rename_error": "Failed to rename",
399
+ "file_browser.context_menu_view_metadata": "View Metadata",
400
+ "file_browser.context_menu_rename": "Rename",
401
+ "file_browser.context_menu_download": "Download",
402
+ "file_browser.context_menu_delete": "Delete",
403
+ "use_file_upload_toasts.job_completed": "{{count}} {{file_word}} processed for {{label}}",
404
+ "use_file_upload_toasts.upload_failed": "Upload failed for {{label}}: {{error}}",
405
+ "use_file_upload_toasts.action_required": "Action required for {{label}}",
406
+ "use_file_upload_toasts.action_required_description": "Review and confirm data conflicts",
407
+ "use_file_upload_toasts.batch_complete": "Batch complete: {{completed}}/{{total}} succeeded",
408
+ "pattern_preview.title": "Preview",
409
+ "pattern_preview.no_pattern": "(no pattern defined)",
410
+ "pattern_preview.file_label": "File:",
411
+ "pattern_preview.folder_label": "Folder:",
412
+ "pattern_preview.full_path_label": "Full path:",
413
+ "pattern_preview.example_note": "Preview uses example values from your variables",
414
+ "separator_picker.add_label": "Add:",
415
+ "separator_picker.title_hyphen": "Hyphen",
416
+ "separator_picker.title_underscore": "Underscore",
417
+ "separator_picker.title_slash": "Slash (for folder paths)",
418
+ "separator_picker.title_space": "Space",
419
+ "separator_picker.title_period": "Period"
420
+ };
421
+
422
+ // src/background_upload/react/use-file-upload-toasts.ts
423
+ var import_i18n = require("hazo_core/i18n");
424
+ var t_pkg = (0, import_i18n.createPackageT)("hazo_files", en_default);
320
425
  var toast_promise = null;
321
426
  function get_toast() {
322
427
  if (!toast_promise) {
@@ -332,22 +437,26 @@ function useFileUploadToasts(manager) {
332
437
  if (!t) return;
333
438
  const label = job.group_label || job.group_id;
334
439
  const n = job.files.length;
335
- t.success(`${n} file${n > 1 ? "s" : ""} processed for ${label}`);
440
+ t.success(t_pkg("use_file_upload_toasts.job_completed", {
441
+ count: String(n),
442
+ file_word: n > 1 ? "files" : "file",
443
+ label
444
+ }));
336
445
  });
337
446
  }));
338
447
  unsubs.push(manager.on("job:error", ({ job, error }) => {
339
448
  void get_toast().then((t) => {
340
449
  if (!t) return;
341
450
  const label = job.group_label || job.group_id;
342
- t.error(`Upload failed for ${label}: ${error}`);
451
+ t.error(t_pkg("use_file_upload_toasts.upload_failed", { label, error: String(error) }));
343
452
  });
344
453
  }));
345
454
  unsubs.push(manager.on("job:confirmation_needed", ({ job }) => {
346
455
  void get_toast().then((t) => {
347
456
  if (!t) return;
348
457
  const label = job.group_label || job.group_id;
349
- t.info(`Action required for ${label}`, {
350
- description: "Review and confirm data conflicts",
458
+ t.info(t_pkg("use_file_upload_toasts.action_required", { label }), {
459
+ description: t_pkg("use_file_upload_toasts.action_required_description"),
351
460
  duration: 1e4
352
461
  });
353
462
  });
@@ -356,7 +465,10 @@ function useFileUploadToasts(manager) {
356
465
  void get_toast().then((t) => {
357
466
  if (!t) return;
358
467
  if (batch.failed > 0) {
359
- t.warning(`Batch complete: ${batch.completed}/${batch.total} succeeded`);
468
+ t.warning(t_pkg("use_file_upload_toasts.batch_complete", {
469
+ completed: String(batch.completed),
470
+ total: String(batch.total)
471
+ }));
360
472
  }
361
473
  });
362
474
  }));
@@ -381,7 +493,12 @@ function HazoFileUploadProvider({
381
493
  if (!manager_ref.current) {
382
494
  manager_ref.current = new UploadManager(config);
383
495
  }
496
+ const [, force_render] = (0, import_react3.useReducer)((n) => n + 1, 0);
384
497
  (0, import_react3.useEffect)(() => {
498
+ if (manager_ref.current.is_destroyed) {
499
+ manager_ref.current = new UploadManager(config);
500
+ force_render();
501
+ }
385
502
  const m = manager_ref.current;
386
503
  return () => {
387
504
  m.destroy();
@@ -1,5 +1,5 @@
1
1
  // src/background_upload/react/provider.tsx
2
- import { useRef, useEffect as useEffect2 } from "react";
2
+ import { useRef, useReducer, useEffect as useEffect2 } from "react";
3
3
 
4
4
  // src/background_upload/core/upload-manager.ts
5
5
  import { generateRequestId as generateRequestId2 } from "hazo_core";
@@ -177,6 +177,9 @@ var UploadManager = class {
177
177
  this.emitter.on("job:completed", ({ job }) => this.on_job_settled(job));
178
178
  this.emitter.on("job:error", ({ job }) => this.on_job_settled(job));
179
179
  }
180
+ get is_destroyed() {
181
+ return this.destroyed;
182
+ }
180
183
  on(event, listener) {
181
184
  return this.emitter.on(event, listener);
182
185
  }
@@ -277,6 +280,108 @@ var FileUploadContext = createContext(null);
277
280
 
278
281
  // src/background_upload/react/use-file-upload-toasts.ts
279
282
  import { useEffect } from "react";
283
+
284
+ // src/locales/en.json
285
+ var en_default = {
286
+ "upload_dialog.title": "Upload Files",
287
+ "upload_dialog.uploading_to": "Uploading to:",
288
+ "upload_dialog.drop_text": "Drag and drop files here, or",
289
+ "upload_dialog.browse": "browse",
290
+ "upload_dialog.files_selected": "{{count}} file(s) selected",
291
+ "upload_dialog.total": "Total:",
292
+ "upload_dialog.cancel": "Cancel",
293
+ "upload_dialog.upload": "Upload",
294
+ "upload_dialog.select_files_error": "Please select files to upload",
295
+ "create_folder_dialog.title": "Create New Folder",
296
+ "create_folder_dialog.creating_in": "Creating folder in:",
297
+ "create_folder_dialog.name_label": "Folder Name",
298
+ "create_folder_dialog.name_placeholder": "Enter folder name",
299
+ "create_folder_dialog.cancel": "Cancel",
300
+ "create_folder_dialog.submit": "Create Folder",
301
+ "create_folder_dialog.name_required_error": "Please enter a folder name",
302
+ "create_folder_dialog.invalid_chars_error": "Folder name contains invalid characters",
303
+ "rename_dialog.title_folder": "Rename Folder",
304
+ "rename_dialog.title_file": "Rename File",
305
+ "rename_dialog.name_label": "New Name",
306
+ "rename_dialog.name_placeholder": "Enter new name",
307
+ "rename_dialog.cancel": "Cancel",
308
+ "rename_dialog.submit": "Rename",
309
+ "rename_dialog.name_required_error": "Please enter a name",
310
+ "rename_dialog.invalid_chars_error": "Name contains invalid characters",
311
+ "delete_confirm_dialog.title_folder": "Delete Folder",
312
+ "delete_confirm_dialog.title_file": "Delete File",
313
+ "delete_confirm_dialog.confirm_message": "Are you sure you want to delete",
314
+ "delete_confirm_dialog.folder_warning": "Warning: This folder and all its contents will be permanently deleted.",
315
+ "delete_confirm_dialog.cancel": "Cancel",
316
+ "delete_confirm_dialog.submit": "Delete",
317
+ "metadata_dialog.title": "File Metadata",
318
+ "metadata_dialog.close": "Close",
319
+ "file_actions.new_folder": "New Folder",
320
+ "file_actions.upload": "Upload",
321
+ "file_actions.download": "Download",
322
+ "file_actions.rename": "Rename",
323
+ "file_actions.delete": "Delete",
324
+ "file_actions.refresh": "Refresh",
325
+ "file_list.empty": "This folder is empty",
326
+ "file_list.header_name": "Name",
327
+ "file_list.header_size": "Size",
328
+ "file_list.header_modified": "Modified",
329
+ "folder_tree.root": "Root",
330
+ "file_preview.select_prompt": "Select a file to preview",
331
+ "file_preview.folder_label": "Folder",
332
+ "file_preview.created_label": "Created:",
333
+ "file_preview.modified_label": "Modified:",
334
+ "file_preview.error_loading": "Error loading preview",
335
+ "file_preview.video_unsupported": "Your browser does not support video playback.",
336
+ "file_preview.audio_unsupported": "Your browser does not support audio playback.",
337
+ "file_preview.no_preview": "No preview available",
338
+ "file_preview.size_label": "Size:",
339
+ "file_preview.type_label": "Type:",
340
+ "file_info_panel.no_item_selected": "No item selected",
341
+ "file_info_panel.no_custom_metadata": "No custom metadata",
342
+ "file_info_panel.name_label": "Name:",
343
+ "file_info_panel.path_label": "Path:",
344
+ "file_info_panel.type_label": "Type:",
345
+ "file_info_panel.size_label": "Size:",
346
+ "file_info_panel.created_label": "Created:",
347
+ "file_info_panel.modified_label": "Modified:",
348
+ "file_info_panel.storage_label": "Storage:",
349
+ "file_info_panel.custom_metadata_title": "Custom Metadata",
350
+ "file_info_panel.type_folder": "Folder",
351
+ "file_info_panel.type_unknown": "Unknown",
352
+ "file_browser.load_directory_error": "Failed to load directory",
353
+ "file_browser.move_item_error": "Failed to move item",
354
+ "file_browser.upload_file_error": "Failed to upload {{name}}",
355
+ "file_browser.download_file_error": "Failed to download file",
356
+ "file_browser.create_folder_error": "Failed to create folder",
357
+ "file_browser.delete_error": "Failed to delete",
358
+ "file_browser.rename_error": "Failed to rename",
359
+ "file_browser.context_menu_view_metadata": "View Metadata",
360
+ "file_browser.context_menu_rename": "Rename",
361
+ "file_browser.context_menu_download": "Download",
362
+ "file_browser.context_menu_delete": "Delete",
363
+ "use_file_upload_toasts.job_completed": "{{count}} {{file_word}} processed for {{label}}",
364
+ "use_file_upload_toasts.upload_failed": "Upload failed for {{label}}: {{error}}",
365
+ "use_file_upload_toasts.action_required": "Action required for {{label}}",
366
+ "use_file_upload_toasts.action_required_description": "Review and confirm data conflicts",
367
+ "use_file_upload_toasts.batch_complete": "Batch complete: {{completed}}/{{total}} succeeded",
368
+ "pattern_preview.title": "Preview",
369
+ "pattern_preview.no_pattern": "(no pattern defined)",
370
+ "pattern_preview.file_label": "File:",
371
+ "pattern_preview.folder_label": "Folder:",
372
+ "pattern_preview.full_path_label": "Full path:",
373
+ "pattern_preview.example_note": "Preview uses example values from your variables",
374
+ "separator_picker.add_label": "Add:",
375
+ "separator_picker.title_hyphen": "Hyphen",
376
+ "separator_picker.title_underscore": "Underscore",
377
+ "separator_picker.title_slash": "Slash (for folder paths)",
378
+ "separator_picker.title_space": "Space",
379
+ "separator_picker.title_period": "Period"
380
+ };
381
+
382
+ // src/background_upload/react/use-file-upload-toasts.ts
383
+ import { createPackageT } from "hazo_core/i18n";
384
+ var t_pkg = createPackageT("hazo_files", en_default);
280
385
  var toast_promise = null;
281
386
  function get_toast() {
282
387
  if (!toast_promise) {
@@ -292,22 +397,26 @@ function useFileUploadToasts(manager) {
292
397
  if (!t) return;
293
398
  const label = job.group_label || job.group_id;
294
399
  const n = job.files.length;
295
- t.success(`${n} file${n > 1 ? "s" : ""} processed for ${label}`);
400
+ t.success(t_pkg("use_file_upload_toasts.job_completed", {
401
+ count: String(n),
402
+ file_word: n > 1 ? "files" : "file",
403
+ label
404
+ }));
296
405
  });
297
406
  }));
298
407
  unsubs.push(manager.on("job:error", ({ job, error }) => {
299
408
  void get_toast().then((t) => {
300
409
  if (!t) return;
301
410
  const label = job.group_label || job.group_id;
302
- t.error(`Upload failed for ${label}: ${error}`);
411
+ t.error(t_pkg("use_file_upload_toasts.upload_failed", { label, error: String(error) }));
303
412
  });
304
413
  }));
305
414
  unsubs.push(manager.on("job:confirmation_needed", ({ job }) => {
306
415
  void get_toast().then((t) => {
307
416
  if (!t) return;
308
417
  const label = job.group_label || job.group_id;
309
- t.info(`Action required for ${label}`, {
310
- description: "Review and confirm data conflicts",
418
+ t.info(t_pkg("use_file_upload_toasts.action_required", { label }), {
419
+ description: t_pkg("use_file_upload_toasts.action_required_description"),
311
420
  duration: 1e4
312
421
  });
313
422
  });
@@ -316,7 +425,10 @@ function useFileUploadToasts(manager) {
316
425
  void get_toast().then((t) => {
317
426
  if (!t) return;
318
427
  if (batch.failed > 0) {
319
- t.warning(`Batch complete: ${batch.completed}/${batch.total} succeeded`);
428
+ t.warning(t_pkg("use_file_upload_toasts.batch_complete", {
429
+ completed: String(batch.completed),
430
+ total: String(batch.total)
431
+ }));
320
432
  }
321
433
  });
322
434
  }));
@@ -341,7 +453,12 @@ function HazoFileUploadProvider({
341
453
  if (!manager_ref.current) {
342
454
  manager_ref.current = new UploadManager(config);
343
455
  }
456
+ const [, force_render] = useReducer((n) => n + 1, 0);
344
457
  useEffect2(() => {
458
+ if (manager_ref.current.is_destroyed) {
459
+ manager_ref.current = new UploadManager(config);
460
+ force_render();
461
+ }
345
462
  const m = manager_ref.current;
346
463
  return () => {
347
464
  m.destroy();