@parhelia/localization 0.1.10745

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 (70) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +29 -0
  3. package/dist/core/src/editor/ui/DragPreview.d.ts +15 -0
  4. package/dist/core/src/editor/ui/DragPreview.d.ts.map +1 -0
  5. package/dist/core/src/editor/ui/DragPreview.js +32 -0
  6. package/dist/core/src/editor/ui/PerfectTree.d.ts +79 -0
  7. package/dist/core/src/editor/ui/PerfectTree.d.ts.map +1 -0
  8. package/dist/core/src/editor/ui/PerfectTree.js +857 -0
  9. package/dist/localization/src/LocalizeItemCommand.d.ts +8 -0
  10. package/dist/localization/src/LocalizeItemCommand.d.ts.map +1 -0
  11. package/dist/localization/src/LocalizeItemCommand.js +44 -0
  12. package/dist/localization/src/LocalizeItemDialog.d.ts +4 -0
  13. package/dist/localization/src/LocalizeItemDialog.d.ts.map +1 -0
  14. package/dist/localization/src/LocalizeItemDialog.js +126 -0
  15. package/dist/localization/src/LocalizeItemUtils.d.ts +17 -0
  16. package/dist/localization/src/LocalizeItemUtils.d.ts.map +1 -0
  17. package/dist/localization/src/LocalizeItemUtils.js +93 -0
  18. package/dist/localization/src/api/discovery.d.ts +36 -0
  19. package/dist/localization/src/api/discovery.d.ts.map +1 -0
  20. package/dist/localization/src/api/discovery.js +29 -0
  21. package/dist/localization/src/constants.d.ts +15 -0
  22. package/dist/localization/src/constants.d.ts.map +1 -0
  23. package/dist/localization/src/constants.js +21 -0
  24. package/dist/localization/src/hooks/useTranslationWizard.d.ts +6 -0
  25. package/dist/localization/src/hooks/useTranslationWizard.d.ts.map +1 -0
  26. package/dist/localization/src/hooks/useTranslationWizard.js +78 -0
  27. package/dist/localization/src/index.d.ts +69 -0
  28. package/dist/localization/src/index.d.ts.map +1 -0
  29. package/dist/localization/src/index.js +152 -0
  30. package/dist/localization/src/services/translationService.d.ts +102 -0
  31. package/dist/localization/src/services/translationService.d.ts.map +1 -0
  32. package/dist/localization/src/services/translationService.js +37 -0
  33. package/dist/localization/src/setup/LocalizationSetupStep.d.ts +3 -0
  34. package/dist/localization/src/setup/LocalizationSetupStep.d.ts.map +1 -0
  35. package/dist/localization/src/setup/LocalizationSetupStep.js +108 -0
  36. package/dist/localization/src/sidebar/TranslationSidebar.d.ts +2 -0
  37. package/dist/localization/src/sidebar/TranslationSidebar.d.ts.map +1 -0
  38. package/dist/localization/src/sidebar/TranslationSidebar.js +93 -0
  39. package/dist/localization/src/steps/MetadataInputStep.d.ts +4 -0
  40. package/dist/localization/src/steps/MetadataInputStep.d.ts.map +1 -0
  41. package/dist/localization/src/steps/MetadataInputStep.js +38 -0
  42. package/dist/localization/src/steps/ServiceLanguageSelectionStep.d.ts +3 -0
  43. package/dist/localization/src/steps/ServiceLanguageSelectionStep.d.ts.map +1 -0
  44. package/dist/localization/src/steps/ServiceLanguageSelectionStep.js +91 -0
  45. package/dist/localization/src/steps/SubitemDiscoveryStep.d.ts +3 -0
  46. package/dist/localization/src/steps/SubitemDiscoveryStep.d.ts.map +1 -0
  47. package/dist/localization/src/steps/SubitemDiscoveryStep.js +391 -0
  48. package/dist/localization/src/steps/index.d.ts +5 -0
  49. package/dist/localization/src/steps/index.d.ts.map +1 -0
  50. package/dist/localization/src/steps/index.js +4 -0
  51. package/dist/localization/src/steps/types.d.ts +68 -0
  52. package/dist/localization/src/steps/types.d.ts.map +1 -0
  53. package/dist/localization/src/steps/types.js +1 -0
  54. package/dist/localization/src/translation-center/BatchTranslationView.d.ts +7 -0
  55. package/dist/localization/src/translation-center/BatchTranslationView.d.ts.map +1 -0
  56. package/dist/localization/src/translation-center/BatchTranslationView.js +487 -0
  57. package/dist/localization/src/translation-center/RecentTranslations.d.ts +2 -0
  58. package/dist/localization/src/translation-center/RecentTranslations.d.ts.map +1 -0
  59. package/dist/localization/src/translation-center/RecentTranslations.js +199 -0
  60. package/dist/localization/src/translation-center/TranslationManagement.d.ts +2 -0
  61. package/dist/localization/src/translation-center/TranslationManagement.d.ts.map +1 -0
  62. package/dist/localization/src/translation-center/TranslationManagement.js +25 -0
  63. package/dist/localization/src/types.d.ts +18 -0
  64. package/dist/localization/src/types.d.ts.map +1 -0
  65. package/dist/localization/src/types.js +1 -0
  66. package/dist/localization/src/utils/createVersions.d.ts +14 -0
  67. package/dist/localization/src/utils/createVersions.d.ts.map +1 -0
  68. package/dist/localization/src/utils/createVersions.js +26 -0
  69. package/package.json +47 -0
  70. package/styles.css +1 -0
@@ -0,0 +1,91 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ // Version creation now offered via VersionOnlyTranslationProvider. No custom button here.
4
+ export function ServiceLanguageSelectionStep({ data, setData, onStepCompleted, editContext, setFooterActions, requestClose }) {
5
+ const [languageSelection, setLanguageSelection] = useState({});
6
+ const dataRef = useRef(data);
7
+ useEffect(() => { dataRef.current = data; }, [data]);
8
+ // Check if multi-item translation is enabled
9
+ const multiItemEnabled = editContext?.configuration?.localization?.multiItem !== false;
10
+ // Use a ref to avoid dependency on onStepCompleted
11
+ const onStepCompletedRef = useRef(onStepCompleted);
12
+ useEffect(() => {
13
+ onStepCompletedRef.current = onStepCompleted;
14
+ }, [onStepCompleted]);
15
+ useEffect(() => {
16
+ const hasProvider = !!data.translationProvider;
17
+ const hasLanguages = data.targetLanguages.length > 0;
18
+ onStepCompletedRef.current(hasProvider && hasLanguages);
19
+ }, [data.translationProvider, data.targetLanguages.length]);
20
+ // 1) Derive provider + available languages (lightweight, in-memory)
21
+ const provider = useMemo(() => data.translationProviders.find(p => p.name === data.translationProvider), [data.translationProviders, data.translationProvider]);
22
+ const siteLanguageCodes = useMemo(() => Array.from(data.languageData.keys()), [data.languageData]);
23
+ const availableLanguageCodes = useMemo(() => (provider?.supportedLanguages?.length ? provider.supportedLanguages : siteLanguageCodes), [provider?.supportedLanguages, siteLanguageCodes]);
24
+ const allLanguages = useMemo(() => {
25
+ const arr = availableLanguageCodes
26
+ .map(code => ({
27
+ code,
28
+ name: data.languageData.get(code)?.name || code,
29
+ hasVersions: (data.languageData.get(code)?.items.length || 0) > 0,
30
+ translationStatus: data.languageData.get(code)?.translationStatus,
31
+ }))
32
+ // Exclude languages where the provider/source language equals the language itself
33
+ // e.g., if sourceLanguage is 'en', don't allow translating to 'en'
34
+ .filter(lang => !lang.translationStatus || lang.translationStatus.sourceLanguage !== lang.code);
35
+ arr.sort((a, b) => a.name.localeCompare(b.name));
36
+ return arr;
37
+ }, [availableLanguageCodes, data.languageData]);
38
+ // Rehydrate UI selection from saved wizard data when returning to this step
39
+ useEffect(() => {
40
+ if (!allLanguages || allLanguages.length === 0)
41
+ return;
42
+ const initialSelection = {};
43
+ // Mark as selected any language that's in our saved targetLanguages array
44
+ for (const langCode of data.targetLanguages) {
45
+ const lang = allLanguages.find(l => l.code === langCode);
46
+ if (lang) {
47
+ initialSelection[langCode] = true;
48
+ }
49
+ }
50
+ setLanguageSelection(initialSelection);
51
+ }, [allLanguages, data.targetLanguages]);
52
+ // Update wizard data when language selection changes
53
+ useEffect(() => {
54
+ const selectedLanguages = Object.entries(languageSelection)
55
+ .filter(([, isSelected]) => isSelected)
56
+ .map(([code]) => code);
57
+ // Only update if the selection has actually changed
58
+ if (JSON.stringify(selectedLanguages.sort()) !== JSON.stringify(data.targetLanguages.sort())) {
59
+ const newData = {
60
+ ...data,
61
+ targetLanguages: selectedLanguages
62
+ };
63
+ setData(newData);
64
+ }
65
+ }, [languageSelection]);
66
+ const handleProviderChange = (e) => {
67
+ const newProvider = e.target.value;
68
+ const newData = {
69
+ ...data,
70
+ translationProvider: newProvider,
71
+ // Clear target languages when provider changes since language availability might change
72
+ targetLanguages: []
73
+ };
74
+ setData(newData);
75
+ // Clear UI selection too
76
+ setLanguageSelection({});
77
+ };
78
+ const handleLanguageToggle = (langCode) => {
79
+ setLanguageSelection(prev => ({ ...prev, [langCode]: !prev[langCode] }));
80
+ };
81
+ const handleSubitemsToggle = () => {
82
+ const newData = {
83
+ ...data,
84
+ includeSubitems: !data.includeSubitems
85
+ };
86
+ setData(newData);
87
+ };
88
+ return (_jsxs("div", { className: "p-6 space-y-6 h-full flex flex-col", "data-testid": "service-language-selection-step", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-xl font-semibold mb-2", children: "Configure Translation" }), _jsx("p", { className: "text-sm text-gray-600 mb-6", children: "Select translation provider and target languages for your content." })] }), _jsxs("div", { className: "space-y-6 flex-1", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-medium text-gray-900 mb-2", children: "Translation Provider" }), _jsx("p", { className: "text-xs text-gray-600 mb-3", children: "Choose how to translate your content. \"Create Versions\" will create new language versions without automatic translation." }), _jsxs("select", { value: data.translationProvider || "", onChange: handleProviderChange, className: "block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm", "data-testid": "translation-provider-select", children: [_jsx("option", { value: "", disabled: true, children: "Select a provider..." }), data.translationProviders.map((provider) => (_jsx("option", { value: provider.name, children: provider.displayName }, provider.name)))] })] }), multiItemEnabled && (_jsxs("div", { children: [_jsxs("label", { className: "flex items-center", children: [_jsx("input", { type: "checkbox", checked: data.includeSubitems, onChange: handleSubitemsToggle, className: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded", "data-testid": "include-subitems-checkbox" }), _jsx("span", { className: "ml-2 text-sm text-gray-900", children: "Include subitems" })] }), _jsx("p", { className: "text-xs text-gray-500 mt-1 ml-6", children: "Also translate any child components and nested content within this item." })] })), _jsxs("div", { children: [_jsx("h3", { className: "text-sm font-medium text-gray-900 mb-2", children: "Target Languages" }), _jsx("p", { className: "text-xs text-gray-600 mb-3", children: "Select the languages you want to translate this content into." }), _jsx("div", { className: "border border-gray-200 rounded-md min-h-[200px] max-h-64 overflow-y-auto", children: data.translationProviders.length === 0 || allLanguages.length === 0 ? (
89
+ // Loading skeleton
90
+ _jsx("div", { className: "p-3 space-y-2", children: [...Array(4)].map((_, i) => (_jsxs("div", { className: "flex items-center animate-pulse", children: [_jsx("div", { className: "h-4 w-4 bg-gray-200 rounded" }), _jsx("div", { className: "ml-2 h-4 bg-gray-200 rounded w-32" }), _jsx("div", { className: "ml-auto h-4 bg-gray-200 rounded w-16" })] }, i))) })) : (_jsx("div", { className: "p-3 grid grid-cols-1 gap-2", children: allLanguages.map((lang) => (_jsxs("label", { className: "flex items-center", children: [_jsx("input", { type: "checkbox", checked: languageSelection[lang.code] || false, onChange: () => handleLanguageToggle(lang.code), className: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded", "data-testid": `language-checkbox-${lang.code}` }), _jsxs("span", { className: "ml-2 text-sm text-gray-900", children: [lang.name, " (", lang.code, ")"] }), !lang.hasVersions && (_jsx("span", { className: "ml-auto text-xs text-orange-600 bg-orange-100 px-2 py-1 rounded", children: "No versions" }))] }, lang.code))) })) })] })] })] }));
91
+ }
@@ -0,0 +1,3 @@
1
+ import { TranslationStepProps } from "./types";
2
+ export declare function SubitemDiscoveryStep({ data, setData, editContext, onStepCompleted, setBeforeNextCallback, setFooterActions, requestClose }: TranslationStepProps): import("react/jsx-runtime").JSX.Element;
3
+ //# sourceMappingURL=SubitemDiscoveryStep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SubitemDiscoveryStep.d.ts","sourceRoot":"","sources":["../../../../src/steps/SubitemDiscoveryStep.tsx"],"names":[],"mappings":"AAOA,OAAO,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAY/C,wBAAgB,oBAAoB,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,YAAY,EAAE,EAAE,oBAAoB,2CAyfhK"}
@@ -0,0 +1,391 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState, useRef, useMemo, useCallback } from "react";
3
+ import { Button } from "@parhelia/core";
4
+ // Custom tree implementation (no rc-tree)
5
+ // NOTE: import PerfectTree from core package via dist path to avoid TS path issues in local package builds
6
+ import { PerfectTree } from "../../../core/src/editor/ui/PerfectTree";
7
+ import { convertFullItemToStub, convertStubToFullItem } from "@parhelia/core";
8
+ import { discoverItemsTree, convertBackendTreeToTreeNodes, flattenPagesFromBackendTrees } from "../api/discovery";
9
+ // We need to implement a basic Spinner component since it's not in core
10
+ const Spinner = ({ ...props }) => (_jsx("div", { className: "animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600", ...props }));
11
+ export function SubitemDiscoveryStep({ data, setData, editContext, onStepCompleted, setBeforeNextCallback, setFooterActions, requestClose }) {
12
+ const [isDiscovering, setIsDiscovering] = useState(false);
13
+ const [discoveredCount, setDiscoveredCount] = useState(0);
14
+ const [cancelled, setCancelled] = useState(false);
15
+ const [discoveryComplete, setDiscoveryComplete] = useState(false);
16
+ const [subitemsFound, setSubitemsFound] = useState(0);
17
+ const [selectedItemIds, setSelectedItemIds] = useState(new Set());
18
+ const [treeNodes, setTreeNodes] = useState([]);
19
+ const [allDiscoveredItems, setAllDiscoveredItems] = useState([]);
20
+ // Once the user interacts with selection, stop auto-selecting
21
+ const [userSelectionOverride, setUserSelectionOverride] = useState(false);
22
+ // Expanded state for custom tree
23
+ const [expandedIds, setExpandedIds] = useState(new Set());
24
+ // Track when tree structure is fully built and ready for display
25
+ const [treeInitialized, setTreeInitialized] = useState(false);
26
+ const cancelledRef = useRef(false);
27
+ const discoveredItemsRef = useRef([]);
28
+ const processedItemIds = useRef(new Set());
29
+ const dataRef = useRef(data);
30
+ // Track if initialization is complete to prevent reacting to selection changes during setup
31
+ const [initializationComplete, setInitializationComplete] = useState(false);
32
+ // Keep data ref updated
33
+ useEffect(() => {
34
+ dataRef.current = data;
35
+ }, [data]);
36
+ const contTokenRef = useRef(null);
37
+ const inFlightRef = useRef(false);
38
+ const aggregatedRef = useRef([]);
39
+ const backendTreesRef = useRef([]);
40
+ const shiftToggleRef = useRef(false);
41
+ const selectedItemIdsRef = useRef(new Set());
42
+ const allDiscoveredItemsRef = useRef([]);
43
+ const hasRegisteredBeforeNextRef = useRef(false);
44
+ // Track the last initialized item IDs to prevent re-initialization with same data
45
+ const lastInitializedItemIdsRef = useRef('');
46
+ const startDiscovery = useCallback(async () => {
47
+ if (inFlightRef.current)
48
+ return; // guard double-trigger (StrictMode, re-renders)
49
+ inFlightRef.current = true;
50
+ setIsDiscovering(true);
51
+ setCancelled(false);
52
+ setDiscoveryComplete(false);
53
+ // Only reset when starting fresh (no continuation token)
54
+ if (!contTokenRef.current) {
55
+ setAllDiscoveredItems([]);
56
+ setTreeNodes([]);
57
+ setTreeInitialized(false);
58
+ aggregatedRef.current = [];
59
+ }
60
+ cancelledRef.current = false;
61
+ const rootIds = data.items.map(i => i.descriptor.id);
62
+ try {
63
+ while (!cancelledRef.current) {
64
+ // Use new backend tree endpoint in one shot
65
+ const resp = await discoverItemsTree({
66
+ rootItemIds: rootIds,
67
+ language: editContext?.contentEditorItem?.language || "en",
68
+ }, editContext.sessionId ?? undefined);
69
+ const pages = flattenPagesFromBackendTrees(resp.trees);
70
+ aggregatedRef.current = pages;
71
+ backendTreesRef.current = resp.trees;
72
+ setDiscoveredCount(pages.length);
73
+ setSubitemsFound(Math.max(0, pages.length - data.items.length));
74
+ break;
75
+ }
76
+ if (!cancelledRef.current) {
77
+ setIsDiscovering(false);
78
+ setDiscoveryComplete(true);
79
+ // Keep a flat list of discovered pages for selection summary/commit
80
+ const stubs = aggregatedRef.current.map(x => ({
81
+ id: x.id,
82
+ name: x.name,
83
+ path: x.path,
84
+ parentId: "",
85
+ idPath: "",
86
+ database: "master",
87
+ descriptor: { id: x.id, language: editContext?.contentEditorItem?.language || "en", version: 0 },
88
+ icon: "",
89
+ largeIcon: "",
90
+ templateId: "",
91
+ templateName: "",
92
+ hasLayout: false,
93
+ hasChildren: x.hasChildren,
94
+ versions: 0,
95
+ language: editContext?.contentEditorItem?.language || "en",
96
+ version: 0,
97
+ }));
98
+ setAllDiscoveredItems(stubs);
99
+ const nodes = convertBackendTreeToTreeNodes(backendTreesRef.current);
100
+ setTreeNodes(nodes);
101
+ setExpandedIds(new Set(nodes.map(n => n.key)));
102
+ setTreeInitialized(true);
103
+ }
104
+ }
105
+ catch (error) {
106
+ console.error("Error during discovery:", error);
107
+ setIsDiscovering(false);
108
+ }
109
+ finally {
110
+ inFlightRef.current = false;
111
+ }
112
+ }, [data.items, editContext]);
113
+ // Initialize discovery state when component mounts or input items change
114
+ useEffect(() => {
115
+ const convertedItems = data.items.map(convertFullItemToStub);
116
+ // Create a stable key from item IDs and includeSubitems flag to detect actual changes
117
+ const itemKey = convertedItems.map(i => i.id).sort().join(',') + ':' + data.includeSubitems;
118
+ // Skip re-initialization if we've already initialized with these exact items
119
+ if (lastInitializedItemIdsRef.current === itemKey) {
120
+ return;
121
+ }
122
+ console.log('🔄 Initializing SubitemDiscoveryStep with items:', itemKey);
123
+ lastInitializedItemIdsRef.current = itemKey;
124
+ discoveredItemsRef.current = convertedItems;
125
+ // Don't set allDiscoveredItems until discovery is complete
126
+ setDiscoveredCount(data.items.length);
127
+ processedItemIds.current = new Set(); // Start empty - items should be processed to find children
128
+ // Auto-select original items
129
+ if (!userSelectionOverride) {
130
+ setSelectedItemIds(new Set(convertedItems.map(item => item.id)));
131
+ }
132
+ // Trigger discovery after state updates
133
+ if (data.includeSubitems && convertedItems.some(item => item.hasChildren)) {
134
+ // Clear any existing tree data before starting discovery
135
+ setAllDiscoveredItems([]);
136
+ setTreeNodes([]);
137
+ setTreeInitialized(false);
138
+ setDiscoveryComplete(false);
139
+ setTimeout(() => startDiscovery(), 0);
140
+ }
141
+ else {
142
+ // No discovery needed, show items immediately
143
+ setAllDiscoveredItems(convertedItems);
144
+ setDiscoveryComplete(true);
145
+ // Build tree structure
146
+ const nodes = buildTreeNodes(convertedItems);
147
+ setTreeNodes(nodes);
148
+ // Expand all root nodes by default
149
+ const rootNodeIds = nodes.map(node => node.key);
150
+ setExpandedIds(new Set(rootNodeIds));
151
+ // Mark tree as fully initialized
152
+ setTreeInitialized(true);
153
+ }
154
+ // Mark initialization as complete after all setup is done
155
+ setInitializationComplete(true);
156
+ // eslint-disable-next-line react-hooks/exhaustive-deps
157
+ }, [data.items, data.includeSubitems]);
158
+ // Keep refs in sync without causing parent rerenders
159
+ useEffect(() => { selectedItemIdsRef.current = selectedItemIds; }, [selectedItemIds]);
160
+ useEffect(() => { allDiscoveredItemsRef.current = allDiscoveredItems; }, [allDiscoveredItems]);
161
+ // Register the commit callback ONCE to avoid parent rerender on first selection
162
+ useEffect(() => {
163
+ if (!setBeforeNextCallback || hasRegisteredBeforeNextRef.current)
164
+ return;
165
+ const commit = async () => {
166
+ const selectedIds = selectedItemIdsRef.current;
167
+ const allStubs = allDiscoveredItemsRef.current;
168
+ const selectedItems = allStubs.filter(item => selectedIds.has(item.id));
169
+ const convertedItems = selectedItems.map(convertStubToFullItem);
170
+ const currentData = dataRef.current;
171
+ if (JSON.stringify(currentData.discoveredItems) !== JSON.stringify(convertedItems)) {
172
+ setData({ ...currentData, discoveredItems: convertedItems });
173
+ }
174
+ return true;
175
+ };
176
+ setBeforeNextCallback(commit);
177
+ hasRegisteredBeforeNextRef.current = true;
178
+ // eslint-disable-next-line react-hooks/exhaustive-deps
179
+ }, [setBeforeNextCallback]);
180
+ // Use a ref to avoid dependency on onStepCompleted
181
+ const onStepCompletedRef = useRef(onStepCompleted);
182
+ useEffect(() => {
183
+ onStepCompletedRef.current = onStepCompleted;
184
+ }, [onStepCompleted]);
185
+ // Update step completion based on selection
186
+ // Mark completion based on whether there is any selection
187
+ useEffect(() => {
188
+ if (!treeInitialized)
189
+ return;
190
+ onStepCompletedRef.current(selectedItemIds.size > 0);
191
+ }, [selectedItemIds.size, treeInitialized]);
192
+ const buildTreeNodes = useCallback((items) => {
193
+ // Fallback: Build simple tree from items for non-subitem discovery
194
+ const itemMap = new Map();
195
+ const childrenMap = new Map();
196
+ items.forEach(item => {
197
+ itemMap.set(item.id, item);
198
+ if (item.parentId) {
199
+ if (!childrenMap.has(item.parentId))
200
+ childrenMap.set(item.parentId, []);
201
+ childrenMap.get(item.parentId).push(item);
202
+ }
203
+ });
204
+ const buildNode = (item) => ({
205
+ key: item.id,
206
+ label: item.name || item.id,
207
+ data: { id: item.id, name: item.name, path: item.path, hasChildren: item.hasChildren },
208
+ children: (childrenMap.get(item.id) || []).map(buildNode)
209
+ });
210
+ const originalItemIds = new Set(data.items.map(item => item.descriptor.id));
211
+ const rootItems = items.filter(item => !item.parentId || !itemMap.has(item.parentId) || originalItemIds.has(item.id));
212
+ return rootItems.map(buildNode);
213
+ }, [data.items]);
214
+ // PerfectTree integrations
215
+ const expandedKeys = useMemo(() => Array.from(expandedIds), [expandedIds]);
216
+ const selectedKeys = useMemo(() => Array.from(selectedItemIds), [selectedItemIds]);
217
+ const getDescendantIds = (nodeKey) => {
218
+ const findNode = (nodes) => {
219
+ for (const node of nodes) {
220
+ if (node.key === nodeKey)
221
+ return node;
222
+ if (node.children) {
223
+ const found = findNode(node.children);
224
+ if (found)
225
+ return found;
226
+ }
227
+ }
228
+ return null;
229
+ };
230
+ const node = findNode(treeNodes);
231
+ if (!node?.children)
232
+ return [];
233
+ const descendants = [];
234
+ const traverse = (children) => {
235
+ children.forEach(child => {
236
+ descendants.push(child.key);
237
+ if (child.children)
238
+ traverse(child.children);
239
+ });
240
+ };
241
+ traverse(node.children);
242
+ return descendants;
243
+ };
244
+ const getDescendantPageIds = (nodeKey) => {
245
+ const findNode = (nodes) => {
246
+ for (const node of nodes) {
247
+ if (node.key === nodeKey)
248
+ return node;
249
+ if (node.children) {
250
+ const found = findNode(node.children);
251
+ if (found)
252
+ return found;
253
+ }
254
+ }
255
+ return null;
256
+ };
257
+ const root = findNode(treeNodes);
258
+ if (!root?.children)
259
+ return [];
260
+ const out = [];
261
+ const walk = (children) => {
262
+ children.forEach(child => {
263
+ const isPage = child?.data?.hasLayout === true;
264
+ if (isPage)
265
+ out.push(child.key);
266
+ if (child.children)
267
+ walk(child.children);
268
+ });
269
+ };
270
+ walk(root.children);
271
+ return out;
272
+ };
273
+ const handleCancel = () => {
274
+ setCancelled(true);
275
+ cancelledRef.current = true;
276
+ setIsDiscovering(false);
277
+ setDiscoveryComplete(true);
278
+ // Update allDiscoveredItems with current results
279
+ setAllDiscoveredItems([...discoveredItemsRef.current]);
280
+ // Build tree structure with current items
281
+ const nodes = buildTreeNodes(discoveredItemsRef.current);
282
+ setTreeNodes(nodes);
283
+ // Expand all root nodes by default
284
+ const rootNodeIds = nodes.map(node => node.key);
285
+ setExpandedIds(new Set(rootNodeIds));
286
+ // Mark tree as fully initialized
287
+ setTreeInitialized(true);
288
+ };
289
+ // Handle individual checkbox changes (no parent-child cascading)
290
+ const handleIndividualCheckboxChange = (itemId, checked) => {
291
+ setUserSelectionOverride(true);
292
+ const newSelection = new Set(selectedItemIds);
293
+ if (checked) {
294
+ newSelection.add(itemId);
295
+ }
296
+ else {
297
+ newSelection.delete(itemId);
298
+ }
299
+ setSelectedItemIds(newSelection);
300
+ };
301
+ return (_jsxs("div", { className: "flex flex-col gap-4 p-6 h-full", "data-testid": "subitem-discovery-step", children: [_jsxs("div", { className: "mb-2", children: [_jsx("h3", { className: "text-lg font-medium mb-1", children: "Discover Subitems" }), _jsx("p", { className: "text-sm text-gray-600", children: "Scanning for subitems to include in translation..." })] }), _jsxs("div", { className: "border rounded p-4 bg-gray-50 min-h-[120px]", "data-testid": "discovery-status-container", children: [_jsxs("div", { className: "flex items-center justify-between mb-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [isDiscovering && _jsx(Spinner, { "data-testid": "discovery-spinner" }), _jsx("span", { className: "font-medium", "data-testid": "discovery-status-text", children: isDiscovering ? 'Discovering subitems...' :
302
+ discoveryComplete ? 'Discovery Complete' :
303
+ 'Ready to discover' })] }), isDiscovering && (_jsx(Button, { size: "sm", onClick: handleCancel, "data-testid": "discovery-cancel-button", children: "Cancel" }))] }), _jsx("div", { className: "text-sm text-gray-600 mb-3", "data-testid": "discovery-summary", children: discoveredCount > 0 ? (_jsxs("p", { "data-testid": "discovery-total-count", children: [_jsxs("span", { className: "font-medium text-blue-600", children: [discoveredCount, " total items found"] }), _jsx("br", {}), _jsxs("span", { className: "text-xs", children: ["Original items: ", data.items.length, " | Subitems discovered: ", Math.max(0, discoveredCount - data.items.length)] }), isDiscovering && (_jsxs(_Fragment, { children: [_jsx("br", {}), _jsx("span", { className: "text-xs text-gray-500 italic", children: "Item tree will appear when discovery completes" })] }))] })) : (_jsx("p", { children: "No items discovered yet." })) }), !isDiscovering && !discoveryComplete && (_jsx(Button, { size: "sm", onClick: startDiscovery, "data-testid": "discovery-start-button", children: "Start Discovery" })), _jsx("div", { className: "mt-4 min-h-[400px]", children: !isDiscovering && discoveryComplete && treeInitialized && allDiscoveredItems.length > 0 && treeNodes.length > 0 ? (_jsx("div", { children: _jsxs("div", { className: "border-t pt-4", "data-testid": "item-selection-section", children: [_jsxs("div", { className: "flex items-center justify-between mb-3", children: [_jsx("h3", { className: "text-lg font-medium", children: "Select Items to Translate" }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("span", { className: "text-sm text-gray-600", "data-testid": "selection-summary-header", children: selectedItemIds.size > 0 ? `${selectedItemIds.size} selected` : "No items selected" }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => { setUserSelectionOverride(true); setSelectedItemIds(new Set(allDiscoveredItems.map(i => i.id))); }, "data-testid": "select-all-items-button", children: "Select All" }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => { setUserSelectionOverride(true); setSelectedItemIds(new Set()); }, "data-testid": "select-none-items-button", children: "Select None" })] })] }), _jsx("div", { className: "text-sm text-gray-600 mb-3", "data-testid": "selection-summary", children: _jsx("span", { className: "text-gray-500", children: "Tip: Hold Shift and click a checkbox to toggle all descendants." }) }), treeNodes.length > 0 && (_jsx("div", { className: "border rounded-lg h-80 overflow-auto", "data-testid": "item-tree-view", children: _jsx(PerfectTree, { nodes: treeNodes, expandedKeys: expandedKeys, selectedKeys: selectedKeys, onToggleExpand: (key) => {
304
+ setExpandedIds(prev => {
305
+ const next = new Set(prev);
306
+ if (next.has(key))
307
+ next.delete(key);
308
+ else
309
+ next.add(key);
310
+ return next;
311
+ });
312
+ }, onSelect: (key, event) => {
313
+ setUserSelectionOverride(true);
314
+ const targetNode = key;
315
+ const shift = event?.shiftKey === true;
316
+ const next = new Set(selectedItemIds);
317
+ // Only select pages
318
+ const findNode = (nodes) => {
319
+ for (const n of nodes) {
320
+ if (n.key === targetNode)
321
+ return n;
322
+ if (n.children) {
323
+ const f = findNode(n.children);
324
+ if (f)
325
+ return f;
326
+ }
327
+ }
328
+ return null;
329
+ };
330
+ const n = findNode(treeNodes);
331
+ const isPage = n?.data?.hasLayout === true;
332
+ if (isPage) {
333
+ const ids = shift ? [key, ...getDescendantIds(key)] : [key];
334
+ if (!next.has(key))
335
+ ids.forEach(id => next.add(id));
336
+ else
337
+ ids.forEach(id => next.delete(id));
338
+ }
339
+ else {
340
+ // Folder row click toggles all descendant pages
341
+ const pageIds = getDescendantPageIds(key);
342
+ const allSelected = pageIds.length > 0 && pageIds.every(id => next.has(id));
343
+ if (allSelected)
344
+ pageIds.forEach(id => next.delete(id));
345
+ else
346
+ pageIds.forEach(id => next.add(id));
347
+ }
348
+ setSelectedItemIds(next);
349
+ }, renderNode: (node) => {
350
+ const isPage = node?.data?.hasLayout === true;
351
+ const pageIds = !isPage ? getDescendantPageIds(node.key) : [];
352
+ const allSelected = !isPage ? (pageIds.length > 0 && pageIds.every(id => selectedItemIds.has(id))) : false;
353
+ const someSelected = !isPage ? (pageIds.some(id => selectedItemIds.has(id)) && !allSelected) : false;
354
+ const isChecked = isPage ? selectedItemIds.has(node.key) : allSelected;
355
+ const icon = node?.data?.icon;
356
+ return (_jsxs("div", { className: "flex items-center gap-2", children: [icon && (_jsx("img", { src: icon, alt: "", className: "w-4 h-4" })), _jsx("input", { type: "checkbox", checked: isChecked, ref: (el) => { if (el)
357
+ el.indeterminate = someSelected; }, onMouseDown: (e) => { shiftToggleRef.current = e.shiftKey; }, onChange: (e) => {
358
+ setUserSelectionOverride(true);
359
+ const next = new Set(selectedItemIds);
360
+ if (isPage) {
361
+ const withDesc = shiftToggleRef.current && (node.children?.length ?? 0) > 0;
362
+ const ids = withDesc ? [node.key, ...getDescendantIds(node.key)] : [node.key];
363
+ if (e.currentTarget.checked)
364
+ ids.forEach(id => next.add(id));
365
+ else
366
+ ids.forEach(id => next.delete(id));
367
+ }
368
+ else {
369
+ const ids = getDescendantPageIds(node.key);
370
+ const allSel = ids.length > 0 && ids.every(id => next.has(id));
371
+ if (e.currentTarget.checked && !allSel)
372
+ ids.forEach(id => next.add(id));
373
+ else if (!e.currentTarget.checked && allSel)
374
+ ids.forEach(id => next.delete(id));
375
+ else {
376
+ // Toggle based on current
377
+ if (allSel)
378
+ ids.forEach(id => next.delete(id));
379
+ else
380
+ ids.forEach(id => next.add(id));
381
+ }
382
+ }
383
+ setSelectedItemIds(next);
384
+ shiftToggleRef.current = false;
385
+ } }), _jsx("span", { children: node.label })] }));
386
+ } }) }))] }) })) : isDiscovering ? (
387
+ // Loading skeleton for discovery results
388
+ _jsxs("div", { children: [_jsx("div", { className: "p-3 bg-blue-50 border border-blue-200 rounded mb-4", children: _jsx("div", { className: "h-4 bg-blue-200 rounded w-64 animate-pulse" }) }), _jsxs("div", { className: "border-t pt-4", children: [_jsxs("div", { className: "flex items-center justify-between mb-3", children: [_jsx("div", { className: "h-6 bg-gray-200 rounded w-48 animate-pulse" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("div", { className: "h-8 bg-gray-200 rounded w-20 animate-pulse" }), _jsx("div", { className: "h-8 bg-gray-200 rounded w-20 animate-pulse" })] })] }), _jsx("div", { className: "h-4 bg-gray-200 rounded w-64 mb-3 animate-pulse" }), _jsx("div", { className: "border rounded-lg h-80 overflow-auto", children: _jsx("div", { className: "p-3 space-y-2", children: [...Array(8)].map((_, i) => (_jsxs("div", { className: "flex items-center gap-3 animate-pulse", children: [_jsx("div", { className: "h-4 w-4 bg-gray-200 rounded" }), _jsx("div", { className: "h-4 bg-gray-200 rounded w-48" })] }, i))) }) })] })] })) : (
389
+ // Empty state
390
+ _jsx("div", { className: "flex items-center justify-center h-80 border rounded-lg bg-gray-50", children: _jsxs("div", { className: "text-center text-gray-500", children: [_jsx("p", { children: "Ready to discover subitems" }), _jsx("p", { className: "text-sm", children: "Discovery will start automatically" })] }) })) })] })] }));
391
+ }
@@ -0,0 +1,5 @@
1
+ export { ServiceLanguageSelectionStep } from "./ServiceLanguageSelectionStep";
2
+ export { SubitemDiscoveryStep } from "./SubitemDiscoveryStep";
3
+ export { MetadataInputStep } from "./MetadataInputStep";
4
+ export * from "./types";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/steps/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,cAAc,SAAS,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { ServiceLanguageSelectionStep } from "./ServiceLanguageSelectionStep";
2
+ export { SubitemDiscoveryStep } from "./SubitemDiscoveryStep";
3
+ export { MetadataInputStep } from "./MetadataInputStep";
4
+ export * from "./types";
@@ -0,0 +1,68 @@
1
+ import { FullItem, ItemDescriptor, EditContextType } from "@parhelia/core";
2
+ export type TranslationProviderInfo = {
3
+ name: string;
4
+ displayName: string;
5
+ supportedLanguages: string[];
6
+ };
7
+ export type TranslationStatus = {
8
+ targetLanguage: string;
9
+ sourceLanguage?: string;
10
+ status: string;
11
+ timestamp: string;
12
+ hash?: string;
13
+ message?: string;
14
+ batchId?: string;
15
+ };
16
+ export type LanguageData = {
17
+ name: string;
18
+ languageCode: string;
19
+ items: ItemDescriptor[];
20
+ translationStatus?: TranslationStatus;
21
+ };
22
+ export type TranslationDialogResult = {
23
+ translationProvider: string;
24
+ targetLanguages: string[];
25
+ includeSubitems: boolean;
26
+ discoveredItems: FullItem[];
27
+ metadata?: any;
28
+ batchId?: string;
29
+ };
30
+ export type TranslationWizardConfiguration = {
31
+ skipLanguageSelection?: boolean;
32
+ predefinedTargetLanguages?: string[];
33
+ predefinedProvider?: string;
34
+ predefinedMetadata?: any;
35
+ };
36
+ export type TranslationWizardData = {
37
+ items: FullItem[];
38
+ targetLanguages: string[];
39
+ translationProvider: string;
40
+ includeSubitems: boolean;
41
+ discoveredItems: FullItem[];
42
+ languageData: Map<string, LanguageData>;
43
+ translationProviders: TranslationProviderInfo[];
44
+ itemMetadata: Map<string, Map<string, string>>;
45
+ metadata?: any;
46
+ [key: string]: any;
47
+ };
48
+ export type TranslationStepProps = {
49
+ data: TranslationWizardData;
50
+ setData: (data: TranslationWizardData) => void;
51
+ editContext: EditContextType;
52
+ onStepCompleted: (completed: boolean) => void;
53
+ setBeforeNextCallback?: (callback: (() => Promise<boolean>) | null) => void;
54
+ setFooterActions?: (actions: {
55
+ key: string;
56
+ label: string;
57
+ disabled?: boolean;
58
+ onClick: () => void;
59
+ signature?: string;
60
+ }[]) => void;
61
+ requestClose?: (result: TranslationDialogResult | null) => void;
62
+ };
63
+ export type LocalizeItemDialogProps = {
64
+ items: FullItem[];
65
+ editContext: EditContextType;
66
+ configuration?: TranslationWizardConfiguration;
67
+ };
68
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/steps/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ interface BatchTranslationViewProps {
2
+ batchId: string;
3
+ onBack?: () => void;
4
+ }
5
+ export declare function BatchTranslationView({ batchId, onBack }: BatchTranslationViewProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
7
+ //# sourceMappingURL=BatchTranslationView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BatchTranslationView.d.ts","sourceRoot":"","sources":["../../../../src/translation-center/BatchTranslationView.tsx"],"names":[],"mappings":"AAiBA,UAAU,yBAAyB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAoCD,wBAAgB,oBAAoB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,yBAAyB,2CAuxBlF"}