@parhelia/localization 0.1.12901 → 0.1.12903

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.
@@ -1,8 +1,14 @@
1
- import { Command, CommandContext, CommandData, FullItem } from "@parhelia/core";
1
+ import { Command, CommandContext, CommandData, EditContextType, FullItem } from "@parhelia/core";
2
+ import { TranslationDialogResult } from "./steps/types";
2
3
  export type LocalizeItemCommandData = CommandData & {
3
4
  items: FullItem[];
4
5
  };
5
6
  export type LocalizeItemCommandContext = CommandContext<LocalizeItemCommandData>;
6
7
  export type LocalizeItemCommand = Command<LocalizeItemCommandData>;
8
+ export declare function openLocalizeItemDialog({ editContext, openDialog, items, }: {
9
+ editContext: EditContextType;
10
+ openDialog?: EditContextType["openDialog"];
11
+ items: FullItem[];
12
+ }): Promise<TranslationDialogResult | null>;
7
13
  export declare const localizeItemCommand: LocalizeItemCommand;
8
14
  //# sourceMappingURL=LocalizeItemCommand.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"LocalizeItemCommand.d.ts","sourceRoot":"","sources":["../src/LocalizeItemCommand.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAUhF,MAAM,MAAM,uBAAuB,GAAG,WAAW,GAAG;IAClD,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,0BAA0B,GACpC,cAAc,CAAC,uBAAuB,CAAC,CAAC;AAC1C,MAAM,MAAM,mBAAmB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC;AAEnE,eAAO,MAAM,mBAAmB,EAAE,mBA0DjC,CAAC"}
1
+ {"version":3,"file":"LocalizeItemCommand.d.ts","sourceRoot":"","sources":["../src/LocalizeItemCommand.tsx"],"names":[],"mappings":"AACA,OAAO,EACL,OAAO,EACP,cAAc,EACd,WAAW,EACX,eAAe,EACf,QAAQ,EACT,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAEL,uBAAuB,EACxB,MAAM,eAAe,CAAC;AAQvB,MAAM,MAAM,uBAAuB,GAAG,WAAW,GAAG;IAClD,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,0BAA0B,GACpC,cAAc,CAAC,uBAAuB,CAAC,CAAC;AAC1C,MAAM,MAAM,mBAAmB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC;AAEnE,wBAAsB,sBAAsB,CAAC,EAC3C,WAAW,EACX,UAAmC,EACnC,KAAK,GACN,EAAE;IACD,WAAW,EAAE,eAAe,CAAC;IAC7B,UAAU,CAAC,EAAE,eAAe,CAAC,YAAY,CAAC,CAAC;IAC3C,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB,2CAiCA;AAED,eAAO,MAAM,mBAAmB,EAAE,mBA6BjC,CAAC"}
@@ -2,9 +2,35 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React from "react";
3
3
  import { Globe as LucideGlobe } from "lucide-react";
4
4
  import { LocalizeItemDialog } from "./LocalizeItemDialog";
5
+ import { dispatchTranslationBatchStarted } from "./translationEvents";
5
6
  // Icon wrapper to avoid React 19 JSX type issues with forwardRef components.
6
7
  // Use React.createElement so we don't call the forwardRef objects as functions.
7
8
  const GlobeIcon = (props) => React.createElement(LucideGlobe, props);
9
+ export async function openLocalizeItemDialog({ editContext, openDialog = editContext.openDialog, items, }) {
10
+ const result = await openDialog(LocalizeItemDialog, {
11
+ items,
12
+ editContext,
13
+ });
14
+ // If translation was started (result contains batchId), navigate to the batch view
15
+ if (result?.batchId) {
16
+ const translationManagementEnabled = editContext?.configuration?.localization
17
+ ?.translationManagement !== false;
18
+ if (translationManagementEnabled) {
19
+ // Synchronous URL update to avoid race with workspace URL sync
20
+ const params = new URLSearchParams(window.location.search);
21
+ params.set("workspace", "translation-management");
22
+ params.set("batchId", result.batchId);
23
+ const newUrl = `${window.location.pathname}?${params.toString()}`;
24
+ window.history.pushState(null, "", newUrl);
25
+ // Now switch the workspace; URL already contains batchId
26
+ editContext.switchWorkspace("translation-management");
27
+ window.setTimeout(() => {
28
+ dispatchTranslationBatchStarted(result.batchId);
29
+ }, 0);
30
+ }
31
+ }
32
+ return result;
33
+ }
8
34
  export const localizeItemCommand = {
9
35
  id: "localizeItem",
10
36
  label: "Translate",
@@ -26,25 +52,10 @@ export const localizeItemCommand = {
26
52
  if (!items || !items?.length) {
27
53
  return;
28
54
  }
29
- const result = await context.openDialog(LocalizeItemDialog, {
30
- items: items,
55
+ return openLocalizeItemDialog({
31
56
  editContext: context.editContext,
57
+ openDialog: context.openDialog,
58
+ items,
32
59
  });
33
- // If translation was started (result contains batchId), navigate to the batch view
34
- if (result?.batchId) {
35
- const translationManagementEnabled = context.editContext?.configuration?.localization
36
- ?.translationManagement !== false;
37
- if (translationManagementEnabled) {
38
- // Synchronous URL update to avoid race with workspace URL sync
39
- const params = new URLSearchParams(window.location.search);
40
- params.set("workspace", "translation-management");
41
- params.set("batchId", result.batchId);
42
- const newUrl = `${window.location.pathname}?${params.toString()}`;
43
- window.history.pushState(null, "", newUrl);
44
- // Now switch the workspace; URL already contains batchId
45
- context.editContext.switchWorkspace("translation-management");
46
- }
47
- }
48
- return result;
49
60
  },
50
61
  };
@@ -12,7 +12,7 @@ const LARGE_BATCH_WARNING_THRESHOLD = 100;
12
12
  const getNextButtonLabel = (step) => step?.nextButtonLabel || step?.name || "Next";
13
13
  // Note: DialogButtons is an internal component that might need to be added to core exports
14
14
  // For now, we'll implement it inline
15
- const DialogButtons = ({ children, ...props }) => (_jsx("div", { className: "mt-auto flex shrink-0 items-center gap-3 border-t border-border-default bg-background px-6 py-4", ...props, children: children }));
15
+ const DialogButtons = ({ children, ...props }) => (_jsx("div", { className: "mt-auto flex shrink-0 items-center gap-3 border-t border-border-default bg-white px-6 py-4", ...props, children: children }));
16
16
  export function LocalizeItemDialog(props) {
17
17
  const editContext = props.editContext;
18
18
  const configuration = editContext.configuration.translationWizard;
@@ -1 +1 @@
1
- {"version":3,"file":"ServiceLanguageSelectionStep.d.ts","sourceRoot":"","sources":["../../src/steps/ServiceLanguageSelectionStep.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,oBAAoB,EAAyB,MAAM,SAAS,CAAC;AAGtE;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,EAC3C,QAAe,EACf,IAAI,EACJ,OAAO,EACP,eAAe,EACf,WAAW,GACZ,EAAE,oBAAoB,2CA4OtB"}
1
+ {"version":3,"file":"ServiceLanguageSelectionStep.d.ts","sourceRoot":"","sources":["../../src/steps/ServiceLanguageSelectionStep.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,oBAAoB,EAAyB,MAAM,SAAS,CAAC;AAGtE;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,EAC3C,QAAe,EACf,IAAI,EACJ,OAAO,EACP,eAAe,EACf,WAAW,GACZ,EAAE,oBAAoB,2CAiRtB"}
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useMemo, useRef, useState } from "react";
3
- import { CountBadge } from "@parhelia/core";
3
+ import { CountBadge, getLanguages } from "@parhelia/core";
4
4
  import { WizardStepShell } from "./WizardStepShell";
5
5
  /**
6
6
  * Language-only step. Item selection lives in its own step
@@ -28,12 +28,40 @@ export function ServiceLanguageSelectionStep({ isActive = true, data, setData, o
28
28
  }
29
29
  }, [isActive, data.targetLanguages.length]);
30
30
  const editContextLanguages = useMemo(() => editContext?.itemLanguages || [], [editContext?.itemLanguages]);
31
+ const [siteLanguages, setSiteLanguages] = useState([]);
32
+ useEffect(() => {
33
+ if (editContextLanguages.length > 0 || data.languageData.size > 0)
34
+ return;
35
+ let cancelled = false;
36
+ const loadSiteLanguages = async () => {
37
+ try {
38
+ const res = await getLanguages();
39
+ const languages = (res?.data ?? res ?? []);
40
+ if (!cancelled && Array.isArray(languages)) {
41
+ setSiteLanguages(languages);
42
+ }
43
+ }
44
+ catch (error) {
45
+ console.error("Failed to load languages:", error);
46
+ }
47
+ };
48
+ loadSiteLanguages();
49
+ return () => {
50
+ cancelled = true;
51
+ };
52
+ }, [editContextLanguages.length, data.languageData.size]);
31
53
  const siteLanguageCodes = useMemo(() => {
32
54
  if (editContextLanguages.length > 0) {
33
55
  return editContextLanguages.map((lang) => lang.languageCode);
34
56
  }
57
+ if (data.languageData.size > 0) {
58
+ return Array.from(data.languageData.keys());
59
+ }
60
+ if (siteLanguages.length > 0) {
61
+ return siteLanguages.map((lang) => lang.languageCode);
62
+ }
35
63
  return Array.from(data.languageData.keys());
36
- }, [editContextLanguages, data.languageData]);
64
+ }, [editContextLanguages, data.languageData, siteLanguages]);
37
65
  const itemSourceLanguage = useMemo(() => {
38
66
  return (data.items[0]?.descriptor.language ||
39
67
  editContext?.item?.descriptor.language ||
@@ -42,16 +70,21 @@ export function ServiceLanguageSelectionStep({ isActive = true, data, setData, o
42
70
  }, [data.items, editContext?.item, editContext?.currentItemDescriptor]);
43
71
  const allLanguages = useMemo(() => {
44
72
  const editContextLanguageMap = new Map(editContextLanguages.map((lang) => [lang.languageCode, lang]));
73
+ const siteLanguageMap = new Map(siteLanguages.map((lang) => [lang.languageCode, lang]));
45
74
  const arr = siteLanguageCodes
46
75
  .map((code) => {
47
76
  const editContextLang = editContextLanguageMap.get(code);
77
+ const siteLanguage = siteLanguageMap.get(code);
48
78
  const languageDataLang = data.languageData.get(code);
49
79
  const sourceLanguage = languageDataLang?.translationStatus?.sourceLanguage ||
50
80
  itemSourceLanguage;
51
81
  return {
52
82
  code,
53
- name: editContextLang?.name || languageDataLang?.name || code,
54
- icon: editContextLang?.icon,
83
+ name: editContextLang?.name ||
84
+ languageDataLang?.name ||
85
+ siteLanguage?.name ||
86
+ code,
87
+ icon: editContextLang?.icon || siteLanguage?.icon,
55
88
  sourceLanguage,
56
89
  };
57
90
  })
@@ -61,6 +94,7 @@ export function ServiceLanguageSelectionStep({ isActive = true, data, setData, o
61
94
  }, [
62
95
  siteLanguageCodes,
63
96
  editContextLanguages,
97
+ siteLanguages,
64
98
  data.languageData,
65
99
  itemSourceLanguage,
66
100
  ]);
@@ -118,7 +152,7 @@ export function ServiceLanguageSelectionStep({ isActive = true, data, setData, o
118
152
  return allLanguages.some((lang) => languageSelection[lang.code]);
119
153
  }, [allLanguages, languageSelection]);
120
154
  const selectedCount = data.targetLanguages.length;
121
- return (_jsx(WizardStepShell, { fillHeight: true, testId: "language-selection-step", children: _jsxs("div", { className: "mx-auto flex min-h-0 w-full max-w-3xl flex-1 flex-col overflow-hidden rounded-xl border border-border-default bg-white", children: [_jsxs("div", { className: "flex h-[37px] shrink-0 items-center justify-between gap-2 border-b border-border-default bg-neutral-grey-5/50 px-3", children: [_jsxs("div", { className: "flex items-baseline gap-2", children: [_jsx("span", { className: "text-xs font-medium tracking-wider text-neutral-grey-50 ", children: "Target Languages" }), selectedCount > 0 && (_jsx(CountBadge, { children: selectedCount }))] }), allLanguages.length > 1 && (_jsxs("label", { className: "flex cursor-pointer items-center gap-1.5 text-[11px] font-medium text-neutral-grey-50 transition-colors hover:text-neutral-grey-100", children: [_jsx("input", { type: "checkbox", checked: areAllLanguagesSelected, ref: (input) => {
155
+ return (_jsx(WizardStepShell, { fillHeight: true, testId: "language-selection-step", children: _jsxs("div", { className: "mx-auto flex min-h-0 w-full max-w-3xl flex-1 flex-col overflow-hidden rounded-xl border border-border-default bg-white", children: [_jsxs("div", { className: "flex h-[37px] shrink-0 items-center justify-between gap-2 border-b border-border-default bg-neutral-grey-5/50 px-3", children: [_jsxs("div", { className: "flex items-baseline gap-2", children: [_jsx("span", { className: "text-xs font-medium tracking-wider text-neutral-grey-50 ", children: "Target Languages" }), selectedCount > 0 && _jsx(CountBadge, { children: selectedCount })] }), allLanguages.length > 1 && (_jsxs("label", { className: "flex cursor-pointer items-center gap-1.5 text-[11px] font-medium text-neutral-grey-50 transition-colors hover:text-neutral-grey-100", children: [_jsx("input", { type: "checkbox", checked: areAllLanguagesSelected, ref: (input) => {
122
156
  if (input) {
123
157
  input.indeterminate =
124
158
  areSomeLanguagesSelected && !areAllLanguagesSelected;
@@ -1 +1 @@
1
- {"version":3,"file":"TranslationBatches.d.ts","sourceRoot":"","sources":["../../src/translation-center/TranslationBatches.tsx"],"names":[],"mappings":"AAmPA,wBAAgB,kBAAkB,4CAgtCjC"}
1
+ {"version":3,"file":"TranslationBatches.d.ts","sourceRoot":"","sources":["../../src/translation-center/TranslationBatches.tsx"],"names":[],"mappings":"AAwPA,wBAAgB,kBAAkB,4CA6uCjC"}
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useRef, useState, useMemo } from "react";
3
3
  import React from "react";
4
- import { Button, Select, Input, UserPicker, Popover, PopoverContent, PopoverTrigger, ContentTree, fetchItemStubs, searchUsers as searchBackendUsers, useEditContext, getLanguages, SimpleIconButton, DeleteIcon, } from "@parhelia/core";
4
+ import { Button, Badge, Select, Input, UserPicker, Popover, PopoverContent, PopoverTrigger, ContentTree, fetchItemStubs, searchUsers as searchBackendUsers, useEditContext, getLanguages, SimpleIconButton, DeleteIcon, } from "@parhelia/core";
5
5
  import { listBatches, getTranslationProviders, listBatchTranslationJobs, retryBatchTranslation, abortBatch, deleteBatch, } from "../services/translationService";
6
+ import { TRANSLATION_BATCH_STARTED_EVENT, } from "../translationEvents";
6
7
  import { useDebouncedCallback } from "use-debounce";
7
8
  import { X, ChevronDown, Languages, Loader2, Calendar, CheckCircle2, User as UserIcon2, Cloud, Globe, FileText, FolderTree, ExternalLink, Check, AlertCircle, CircleStop, Hourglass, Copy, Sparkles, RotateCcw, } from "lucide-react";
8
9
  import { toast } from "sonner";
@@ -437,6 +438,25 @@ export function TranslationBatches() {
437
438
  useEffect(() => {
438
439
  loadBatchJobsRef.current = loadBatchJobs;
439
440
  }, [loadBatchJobs]);
441
+ useEffect(() => {
442
+ const handleTranslationBatchStarted = (event) => {
443
+ const detail = event
444
+ .detail;
445
+ const batchId = normalizeGuid(detail?.batchId);
446
+ void (async () => {
447
+ await loadRecentBatches(0, false, effectiveFilters);
448
+ if (!batchId)
449
+ return;
450
+ setExpandedBatchId(batchId);
451
+ setExpandedItems(new Set());
452
+ await loadBatchJobs(batchId);
453
+ })();
454
+ };
455
+ window.addEventListener(TRANSLATION_BATCH_STARTED_EVENT, handleTranslationBatchStarted);
456
+ return () => {
457
+ window.removeEventListener(TRANSLATION_BATCH_STARTED_EVENT, handleTranslationBatchStarted);
458
+ };
459
+ }, [effectiveFilters, loadBatchJobs, loadRecentBatches]);
440
460
  const toggleBatch = useCallback((batchId) => {
441
461
  setExpandedBatchId((prev) => {
442
462
  const next = prev === batchId ? null : batchId;
@@ -1146,7 +1166,7 @@ function LanguageJobChip({ job, language, batchStartedAtUtc, onOpen, isRetrying
1146
1166
  const emptyFieldText = statistics && statistics.emptyFieldCount > 0
1147
1167
  ? `${formatCount(statistics.emptyFieldCount)} empty`
1148
1168
  : null;
1149
- return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: `inline-flex items-center gap-1.5 rounded-md border badge-pad-sm text-[12px] transition-colors cursor-pointer ${chipCls}`, children: [language?.icon ? (_jsx("img", { src: language.icon, alt: langName, className: "h-3.5 shrink-0" })) : null, _jsx("span", { className: "font-mono tracking-tight", children: job.targetLanguage }), jobInProgress && (_jsx(LoaderIcon, { className: "h-3 w-3 shrink-0 animate-spin" })), jobPending && (_jsx(QueuedIcon, { className: "h-3 w-3 shrink-0", strokeWidth: 1.75, "aria-label": "Queued" })), jobError && _jsx(AlertCircleIcon, { className: "h-3 w-3 shrink-0" }), !jobInProgress && !jobError && !jobPending && !jobAborted && (_jsx(CheckIcon, { className: "h-3 w-3 shrink-0 text-feedback-green" }))] }) }), _jsxs(PopoverContent, { align: "start", sideOffset: 6, className: "w-[22rem] max-w-[calc(100vw-2rem)] p-0", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-border-default px-3 py-2", children: [language?.icon ? (_jsx("img", { src: language.icon, alt: langName, className: "h-4 shrink-0" })) : null, _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "text-[13px] font-medium text-neutral-grey-100 truncate", children: langName }), _jsxs("div", { className: "text-[11px] text-neutral-grey-50 font-mono truncate", children: [job.targetLanguage, job.sourceLanguage ? ` ← ${job.sourceLanguage}` : ""] })] }), _jsxs("span", { className: `inline-flex items-center gap-1 rounded-badge badge-pad-sm text-2xs font-medium ${statusBadgeCls}`, children: [jobInProgress && (_jsx(LoaderIcon, { className: "h-2.5 w-2.5 animate-spin" })), jobStatus || "Unknown"] })] }), _jsxs("dl", { className: "divide-y divide-border-default/60", children: [claimedAtValid && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Claimed" }), _jsx("dd", { className: "text-[12px] text-neutral-grey-100", children: claimedAt.toLocaleString() })] })), timestampValid && (jobInProgress || jobPending) && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Updated" }), _jsx("dd", { className: "text-[12px] text-neutral-grey-100", children: timestamp.toLocaleString() })] })), durationMs != null && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: jobInProgress || jobPending ? "Running" : "Duration" }), _jsxs("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", title: durationApproximated
1169
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: `inline-flex items-center gap-1.5 rounded-md border badge-pad-sm text-[12px] transition-colors cursor-pointer ${chipCls}`, children: [language?.icon ? (_jsx("img", { src: language.icon, alt: langName, className: "h-3.5 shrink-0" })) : null, _jsx("span", { className: "font-mono tracking-tight", children: job.targetLanguage }), jobInProgress && (_jsx(LoaderIcon, { className: "h-3 w-3 shrink-0 animate-spin" })), jobPending && (_jsx(QueuedIcon, { className: "h-3 w-3 shrink-0", strokeWidth: 1.75, "aria-label": "Queued" })), jobError && _jsx(AlertCircleIcon, { className: "h-3 w-3 shrink-0" }), !jobInProgress && !jobError && !jobPending && !jobAborted && (_jsx(CheckIcon, { className: "h-3 w-3 shrink-0 text-feedback-green" }))] }) }), _jsxs(PopoverContent, { align: "start", sideOffset: 6, className: "w-[22rem] max-w-[calc(100vw-2rem)] p-0", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-border-default px-3 py-2", children: [language?.icon ? (_jsx("img", { src: language.icon, alt: langName, className: "h-4 shrink-0" })) : null, _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "text-[13px] font-medium text-neutral-grey-100 truncate", children: langName }), _jsxs("div", { className: "text-[11px] text-neutral-grey-50 font-mono truncate", children: [job.targetLanguage, job.sourceLanguage ? ` ← ${job.sourceLanguage}` : ""] })] }), _jsxs(Badge, { size: "sm", className: statusBadgeCls, children: [jobInProgress && (_jsx(LoaderIcon, { className: "h-2.5 w-2.5 animate-spin" })), jobStatus || "Unknown"] })] }), _jsxs("dl", { className: "divide-y divide-border-default/60", children: [claimedAtValid && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Claimed" }), _jsx("dd", { className: "text-[12px] text-neutral-grey-100", children: claimedAt.toLocaleString() })] })), timestampValid && (jobInProgress || jobPending) && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Updated" }), _jsx("dd", { className: "text-[12px] text-neutral-grey-100", children: timestamp.toLocaleString() })] })), durationMs != null && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: jobInProgress || jobPending ? "Running" : "Duration" }), _jsxs("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", title: durationApproximated
1150
1170
  ? "Approximate — measured from batch start"
1151
1171
  : undefined, children: [durationApproximated ? "~" : "", formatDuration(durationMs)] })] })), statistics && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Fields" }), _jsxs("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", children: [fieldText, emptyFieldText ? (_jsxs("span", { className: "ml-1.5 text-neutral-grey-50", children: ["(", emptyFieldText, ")"] })) : null] })] })), statistics && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Text" }), _jsxs("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", children: [formatCount(statistics.sourceCharacterCount), " chars"] })] })), job.totalCost != null && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Cost" }), _jsx("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", children: formatUsdCost(job.totalCost) })] })), lastHeartbeatValid && (jobInProgress || jobPending) && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Heartbeat" }), _jsx("dd", { className: "text-[12px] text-neutral-grey-100", children: lastHeartbeat.toLocaleString() })] })), attemptCount > 1 && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Attempts" }), _jsx("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", children: attemptCount })] })), job.hash && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Hash" }), _jsx("dd", { className: "text-[11px] font-mono text-neutral-grey-100 break-all", children: job.hash })] })), job.message && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: jobError ? "Error" : jobAborted ? "Aborted" : "Message" }), _jsx("dd", { className: `text-[12px] break-words ${jobError ? "text-feedback-red" : jobAborted ? "text-feedback-orange" : "text-neutral-grey-100"}`, children: job.message })] })), job.batchId && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Batch" }), _jsx("dd", { className: "text-[11px] font-mono text-neutral-grey-50 break-all", children: job.batchId })] }))] }), _jsxs("div", { className: "flex justify-end gap-2 border-t border-border-default px-3 py-2", children: [jobError && onRetry && (_jsxs(Button, { size: "sm", variant: "outline", disabled: isRetrying, onClick: (e) => {
1152
1172
  e.stopPropagation();
@@ -1 +1 @@
1
- {"version":3,"file":"TranslationsTitlebar.d.ts","sourceRoot":"","sources":["../../src/translation-center/TranslationsTitlebar.tsx"],"names":[],"mappings":"AAIA;;;GAGG;AACH,wBAAgB,oBAAoB,mDAuBnC"}
1
+ {"version":3,"file":"TranslationsTitlebar.d.ts","sourceRoot":"","sources":["../../src/translation-center/TranslationsTitlebar.tsx"],"names":[],"mappings":"AAKA;;;GAGG;AACH,wBAAgB,oBAAoB,mDA8CnC"}
@@ -1,6 +1,7 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { SimpleIconButton, useEditContext } from "@parhelia/core";
3
- import { SquarePen } from "lucide-react";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, SimpleIconButton, useEditContext } from "@parhelia/core";
3
+ import { Globe, SquarePen } from "lucide-react";
4
+ import { openLocalizeItemDialog } from "../LocalizeItemCommand";
4
5
  /**
5
6
  * TranslationsTitlebar - Titlebar content for the Translation Management workspace.
6
7
  * Shows desktop actions for the Translation Management workspace.
@@ -12,5 +13,13 @@ export function TranslationsTitlebar() {
12
13
  if (editContext.isMobile)
13
14
  return null;
14
15
  const { showAgentsWorkspaceEditor, setShowAgentsWorkspaceEditor } = editContext;
15
- return (_jsx("div", { className: "flex w-full items-center justify-end pr-2", children: _jsx(SimpleIconButton, { icon: _jsx(SquarePen, { className: "h-5 w-5", strokeWidth: 1 }), label: showAgentsWorkspaceEditor ? "Hide Editor" : "Show Editor", size: "large", "data-testid": "translations-editor-panel-toggle", selected: showAgentsWorkspaceEditor, onClick: () => setShowAgentsWorkspaceEditor(!showAgentsWorkspaceEditor) }) }));
16
+ const multiItemEnabled = editContext.configuration?.localization?.multiItem !== false;
17
+ const currentItem = editContext.item;
18
+ const canTranslate = multiItemEnabled || !!currentItem;
19
+ return (_jsxs("div", { className: "flex w-full items-center justify-between gap-3 pr-2", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsx("div", { "aria-hidden": "true", className: "border-border-default h-7 shrink-0 border-r" }), _jsxs(Button, { size: "sm", title: canTranslate ? "Translate" : "Open an item to translate", "data-testid": "translations-titlebar-translate-button", disabled: !canTranslate, onClick: () => {
20
+ void openLocalizeItemDialog({
21
+ editContext,
22
+ items: currentItem ? [currentItem] : [],
23
+ });
24
+ }, children: [_jsx(Globe, { className: "h-4 w-4", strokeWidth: 1.5 }), "Translate"] })] }), _jsx(SimpleIconButton, { icon: _jsx(SquarePen, { className: "h-5 w-5", strokeWidth: 1 }), label: showAgentsWorkspaceEditor ? "Hide Editor" : "Show Editor", size: "large", "data-testid": "translations-editor-panel-toggle", selected: showAgentsWorkspaceEditor, onClick: () => setShowAgentsWorkspaceEditor(!showAgentsWorkspaceEditor) })] }));
16
25
  }
@@ -0,0 +1,6 @@
1
+ export declare const TRANSLATION_BATCH_STARTED_EVENT = "parhelia:translation-batch-started";
2
+ export type TranslationBatchStartedEventDetail = {
3
+ batchId: string;
4
+ };
5
+ export declare function dispatchTranslationBatchStarted(batchId: string): void;
6
+ //# sourceMappingURL=translationEvents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translationEvents.d.ts","sourceRoot":"","sources":["../src/translationEvents.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,+BAA+B,uCACN,CAAC;AAEvC,MAAM,MAAM,kCAAkC,GAAG;IAC/C,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,MAAM,QAO9D"}
@@ -0,0 +1,4 @@
1
+ export const TRANSLATION_BATCH_STARTED_EVENT = "parhelia:translation-batch-started";
2
+ export function dispatchTranslationBatchStarted(batchId) {
3
+ window.dispatchEvent(new CustomEvent(TRANSLATION_BATCH_STARTED_EVENT, { detail: { batchId } }));
4
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parhelia/localization",
3
- "version": "0.1.12901",
3
+ "version": "0.1.12903",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"