@parhelia/localization 0.1.11008 → 0.1.11049

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/README.md CHANGED
@@ -1,33 +1,33 @@
1
- # @parhelia/localization
2
-
3
- Localization and translation features for Parhelia visual editor.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- npm install @parhelia/localization
9
- ```
10
-
11
- ## Usage
12
-
13
- ```typescript
14
- import { LocalizeItemDialog, TranslationSidebar } from '@parhelia/localization';
15
- import '@parhelia/localization/styles.css';
16
- ```
17
-
18
- ## Features
19
-
20
- - Translation wizard
21
- - Localization setup
22
- - Translation center
23
- - Multi-language content management
24
- - Version creation for localized content
25
-
26
- ## License
27
-
28
- See LICENSE file for details.
29
-
30
- ## Documentation
31
-
32
- For complete documentation, visit [https://parhelia.ai](https://parhelia.ai)
33
-
1
+ # @parhelia/localization
2
+
3
+ Localization and translation features for Parhelia visual editor.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @parhelia/localization
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { LocalizeItemDialog, TranslationSidebar } from '@parhelia/localization';
15
+ import '@parhelia/localization/styles.css';
16
+ ```
17
+
18
+ ## Features
19
+
20
+ - Translation wizard
21
+ - Localization setup
22
+ - Translation center
23
+ - Multi-language content management
24
+ - Version creation for localized content
25
+
26
+ ## License
27
+
28
+ See LICENSE file for details.
29
+
30
+ ## Documentation
31
+
32
+ For complete documentation, visit [https://parhelia.ai](https://parhelia.ai)
33
+
@@ -1 +1 @@
1
- {"version":3,"file":"LocalizeItemDialog.d.ts","sourceRoot":"","sources":["../src/LocalizeItemDialog.tsx"],"names":[],"mappings":"AASA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAI7C,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACxB,MAAM,eAAe,CAAC;AAiBvB,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,uBAAuB,GAAG,WAAW,CAAC,uBAAuB,CAAC,2CA2PtE"}
1
+ {"version":3,"file":"LocalizeItemDialog.d.ts","sourceRoot":"","sources":["../src/LocalizeItemDialog.tsx"],"names":[],"mappings":"AASA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAI7C,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EAExB,MAAM,eAAe,CAAC;AAiBvB,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,uBAAuB,GAAG,WAAW,CAAC,uBAAuB,CAAC,2CAmVtE"}
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Dialog, DialogContent, DialogHeader, DialogTitle, Button, cn, } from "@parhelia/core";
3
- import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { ChevronRight as LucideChevronRight } from "lucide-react";
5
5
  import { useTranslationWizard } from "./hooks/useTranslationWizard";
6
6
  import { performDefaultTranslation, generateBatchId } from "./LocalizeItemUtils";
@@ -12,12 +12,15 @@ export function LocalizeItemDialog(props) {
12
12
  const editContext = props.editContext;
13
13
  const configuration = editContext.configuration.translationWizard;
14
14
  const [currentStepIndex, setCurrentStepIndex] = useState(0);
15
- const [completedByStepId, setCompletedByStepId] = useState({});
15
+ const [stepCompleted, setStepCompleted] = useState(-1);
16
16
  const [beforeNextCallback, setBeforeNextCallback] = useState(null);
17
17
  const { wizardData, setWizardData } = useTranslationWizard(props);
18
18
  // Keep a ref to the latest wizard data to avoid stale closure issues
19
19
  const latestWizardDataRef = useRef(wizardData);
20
20
  latestWizardDataRef.current = wizardData;
21
+ // Log render cycles to track infinite loops
22
+ const renderCountRef = useRef(0);
23
+ renderCountRef.current += 1;
21
24
  // Clear footer actions when step changes to avoid stale actions from previous steps
22
25
  const [footerActions, setFooterActions] = useState([]);
23
26
  useEffect(() => {
@@ -36,11 +39,40 @@ export function LocalizeItemDialog(props) {
36
39
  const requestCloseCb = useCallback((result) => {
37
40
  props.onClose?.(result);
38
41
  }, [props.onClose]);
39
- // Filter steps based on skip conditions
40
- const activeSteps = configuration.steps.filter((step) => !step.skipCondition || !step.skipCondition(wizardData));
42
+ // Memoize activeSteps to prevent unnecessary recalculations and new object references
43
+ // This prevents infinite loops when wizardData changes but skip conditions don't
44
+ // The skip condition for subitem-discovery step only checks includeSubitems
45
+ const activeSteps = useMemo(() => {
46
+ const steps = configuration.steps.filter((step) => !step.skipCondition || !step.skipCondition(wizardData));
47
+ return steps;
48
+ }, [configuration.steps, wizardData.includeSubitems]); // Only depend on what affects skip conditions
41
49
  const currentStep = activeSteps[currentStepIndex];
50
+ // Refs for stable callbacks - updated during render to avoid dependency issues
51
+ const currentStepIdRef = useRef(currentStep?.id);
52
+ const currentStepIndexRef = useRef(currentStepIndex);
53
+ const lastSetWizardDataRef = useRef('');
54
+ // Track step changes for logging
55
+ if (currentStepIdRef.current !== currentStep?.id) {
56
+ currentStepIdRef.current = currentStep?.id;
57
+ }
58
+ currentStepIndexRef.current = currentStepIndex;
59
+ // Ref to track beforeNextCallback for switchStep
60
+ const beforeNextCallbackRef = useRef(null);
61
+ useEffect(() => {
62
+ beforeNextCallbackRef.current = beforeNextCallback;
63
+ }, [beforeNextCallback]);
64
+ // Switch step helper that clears callbacks (similar to page wizard)
65
+ const switchStep = useCallback((index) => {
66
+ beforeNextCallbackRef.current = null; // Clear callback when switching steps
67
+ setBeforeNextCallback(null);
68
+ setCurrentStepIndex(index);
69
+ }, []);
70
+ // Stable callback setter to avoid infinite loops in child components
71
+ const setBeforeNextCallbackStable = useCallback((cb) => {
72
+ setBeforeNextCallback(() => cb);
73
+ }, []); // No dependencies - use ref to access current step index
42
74
  const isLastStep = currentStepIndex === activeSteps.length - 1;
43
- const canProceed = !!(currentStep && completedByStepId[currentStep.id]);
75
+ const canProceed = stepCompleted >= currentStepIndex;
44
76
  const handleNext = async () => {
45
77
  if (beforeNextCallback && typeof beforeNextCallback === 'function') {
46
78
  const canProceed = await beforeNextCallback();
@@ -56,14 +88,12 @@ export function LocalizeItemDialog(props) {
56
88
  await handleFinish();
57
89
  }
58
90
  else {
59
- setCurrentStepIndex(currentStepIndex + 1);
91
+ switchStep(currentStepIndex + 1);
60
92
  }
61
93
  };
62
94
  const handlePrevious = () => {
63
95
  if (currentStepIndex > 0) {
64
- // Clear the callback when moving to previous step
65
- setBeforeNextCallback(null);
66
- setCurrentStepIndex(currentStepIndex - 1);
96
+ switchStep(currentStepIndex - 1);
67
97
  }
68
98
  };
69
99
  const handleFinish = useCallback(async () => {
@@ -79,29 +109,62 @@ export function LocalizeItemDialog(props) {
79
109
  try {
80
110
  const batchId = generateBatchId();
81
111
  const translationResult = await performDefaultTranslation(currentWizardData, editContext, batchId);
82
- console.log("Translation completed successfully:", translationResult);
83
112
  // Include batchId in result for navigation to translation management
84
113
  const resultWithBatch = { ...result, batchId };
85
114
  // Close dialog and let parent component handle navigation to translation management
86
115
  props.onClose?.(resultWithBatch);
87
116
  }
88
117
  catch (error) {
89
- console.error("Translation failed:", error);
90
- // Still close the dialog even on error
118
+ // Handle specific error types
119
+ if (error.name === 'NoTranslationsNeededError') {
120
+ // Show user-friendly message and close dialog
121
+ alert('No translations needed. All selected target languages match the source languages of the items.');
122
+ props.onClose?.(result);
123
+ return;
124
+ }
125
+ // For other errors, still close the dialog
126
+ console.error('Translation failed:', error);
127
+ alert('Translation failed. Please check the console for details.');
91
128
  props.onClose?.(result);
92
129
  }
93
130
  }
94
131
  catch (error) {
95
- console.error("Error in handleFinish:", error);
96
132
  props.onClose?.(null);
97
133
  }
98
134
  }, [editContext, props]);
99
- const handleStepCompleted = useCallback((completed) => {
100
- if (!currentStep)
135
+ // Stable setData wrapper to prevent infinite loops
136
+ // Use refs to access current values without creating dependencies
137
+ const setWizardDataStable = useCallback((newData) => {
138
+ // Create a stable key from the data to detect actual changes
139
+ // Only compare the fields that matter for re-renders
140
+ const dataKey = JSON.stringify({
141
+ translationProvider: newData.translationProvider,
142
+ targetLanguages: [...newData.targetLanguages].sort(),
143
+ includeSubitems: newData.includeSubitems,
144
+ discoveredItemsCount: newData.discoveredItems?.length || 0,
145
+ itemsCount: newData.items?.length || 0,
146
+ });
147
+ // Skip if data hasn't actually changed
148
+ if (lastSetWizardDataRef.current === dataKey) {
101
149
  return;
102
- setCompletedByStepId(prev => ({ ...prev, [currentStep.id]: completed }));
103
- }, [currentStep?.id]);
104
- const StepComponent = currentStep?.component;
150
+ }
151
+ lastSetWizardDataRef.current = dataKey;
152
+ setWizardData(newData);
153
+ }, []); // No dependencies - use refs to access current values
154
+ // Simplified step completion handler - steps pass their stepIndex via closure
155
+ // Since steps are always mounted, we can use the step index directly from the map
156
+ const handleStepCompleted = useCallback((completed, stepIndex) => {
157
+ if (completed) {
158
+ setStepCompleted((prev) => {
159
+ const newValue = Math.max(prev, stepIndex);
160
+ return newValue;
161
+ });
162
+ }
163
+ else {
164
+ const newValue = Math.max(-1, stepIndex - 1);
165
+ setStepCompleted(newValue);
166
+ }
167
+ }, []);
105
168
  return (_jsx(Dialog, { open: true, onOpenChange: (open) => {
106
169
  // Only close when explicitly requested (e.g., close button clicked)
107
170
  if (!open) {
@@ -117,7 +180,11 @@ export function LocalizeItemDialog(props) {
117
180
  ? "text-blue-600 font-medium"
118
181
  : currentStepIndex > index
119
182
  ? "text-blue-600"
120
- : "text-gray-400"), "data-testid": `step-indicator-label-${step.id}`, children: step.name }), index < activeSteps.length - 1 && (_jsx(ChevronRightIcon, { className: "w-4 h-4 text-gray-400 mx-2" }))] }, step.id))) }) }), _jsx("div", { className: "flex-1 overflow-y-auto min-h-0", "data-testid": "translation-wizard-step-content", children: StepComponent && (_jsx("div", { className: "h-full", children: _jsx(StepComponent, { data: wizardData, setData: setWizardData, editContext: editContext, onStepCompleted: handleStepCompleted, setBeforeNextCallback: (cb) => {
121
- setBeforeNextCallback(() => cb);
122
- }, setFooterActions: provideFooterActions, requestClose: requestCloseCb }) })) }), _jsxs(DialogButtons, { "data-testid": "translation-wizard-dialog-buttons", children: [_jsx(Button, { onClick: () => props.onClose?.(null), variant: "outline", size: "default", "data-testid": "translation-wizard-cancel-button", children: "Cancel" }), currentStepIndex > 0 && (_jsx(Button, { onClick: handlePrevious, variant: "outline", size: "default", "data-testid": "translation-wizard-previous-button", children: "Previous" })), footerActions.map((a) => (_jsx(Button, { onClick: a.onClick, disabled: !!a.disabled, variant: "default", size: "default", "data-testid": `translation-wizard-footer-action-${a.key}`, children: a.label }, a.key))), _jsx(Button, { onClick: handleNext, disabled: !canProceed, variant: "default", size: "default", "data-testid": "translation-wizard-next-button", children: isLastStep ? "Start Translation" : "Next" })] })] })] }) }));
183
+ : "text-gray-400"), "data-testid": `step-indicator-label-${step.id}`, children: step.name }), index < activeSteps.length - 1 && (_jsx(ChevronRightIcon, { className: "w-4 h-4 text-gray-400 mx-2" }))] }, step.id))) }) }), _jsx("div", { className: "flex-1 overflow-y-auto min-h-0", "data-testid": "translation-wizard-step-content", children: _jsx("div", { className: "h-full relative", children: activeSteps.map((step, index) => {
184
+ const StepComponent = step.component;
185
+ const isActive = index === currentStepIndex;
186
+ if (!StepComponent)
187
+ return null;
188
+ return (_jsx("div", { className: "h-full", style: { display: isActive ? 'block' : 'none' }, "aria-hidden": !isActive, "data-testid": `step-content-${step.id}`, children: _jsx(StepComponent, { stepIndex: index, isActive: isActive, data: wizardData, setData: setWizardDataStable, editContext: editContext, onStepCompleted: (completed) => handleStepCompleted(completed, index), setBeforeNextCallback: isActive ? setBeforeNextCallbackStable : undefined, setFooterActions: isActive ? provideFooterActions : undefined, requestClose: isActive ? requestCloseCb : undefined }) }, step.id));
189
+ }) }) }), _jsxs(DialogButtons, { "data-testid": "translation-wizard-dialog-buttons", children: [_jsx(Button, { onClick: () => props.onClose?.(null), variant: "outline", size: "default", "data-testid": "translation-wizard-cancel-button", children: "Cancel" }), currentStepIndex > 0 && (_jsx(Button, { onClick: handlePrevious, variant: "outline", size: "default", "data-testid": "translation-wizard-previous-button", children: "Previous" })), footerActions.map((a) => (_jsx(Button, { onClick: a.onClick, disabled: !!a.disabled, variant: "default", size: "default", "data-testid": `translation-wizard-footer-action-${a.key}`, children: a.label }, a.key))), _jsx(Button, { onClick: handleNext, disabled: !canProceed, variant: "default", size: "default", "data-testid": "translation-wizard-next-button", children: isLastStep ? "Start Translation" : "Next" })] })] })] }) }));
123
190
  }
@@ -1 +1 @@
1
- {"version":3,"file":"LocalizeItemUtils.d.ts","sourceRoot":"","sources":["../src/LocalizeItemUtils.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAC,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAIjD,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,MAAM,MAAM,kBAAkB,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5F,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAIF,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,qBAAqB,EACjC,WAAW,EAAE,eAAe,EAC5B,iBAAiB,CAAC,EAAE,MAAM,GACzB,OAAO,CAAC,iBAAiB,CAAC,CAqE5B;AAGD,eAAO,MAAM,mBAAmB,GAAU,eAAe,MAAM,EAAE,EAAE,mBAAmB,iBAAiB,EAAE,EAAE,WAAW,MAAM,EAAE,MAAM,QAAQ,EAAE,qBAAqB,MAAM,+BA0BxK,CAAC"}
1
+ {"version":3,"file":"LocalizeItemUtils.d.ts","sourceRoot":"","sources":["../src/LocalizeItemUtils.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAC,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAIjD,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,MAAM,MAAM,kBAAkB,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5F,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAIF,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,qBAAqB,EACjC,WAAW,EAAE,eAAe,EAC5B,iBAAiB,CAAC,EAAE,MAAM,GACzB,OAAO,CAAC,iBAAiB,CAAC,CAgF5B;AAGD,eAAO,MAAM,mBAAmB,GAAU,eAAe,MAAM,EAAE,EAAE,mBAAmB,iBAAiB,EAAE,EAAE,WAAW,MAAM,EAAE,MAAM,QAAQ,EAAE,qBAAqB,MAAM,+BA0BxK,CAAC"}
@@ -15,15 +15,17 @@ export async function performDefaultTranslation(wizardData, editContext, predefi
15
15
  for (const item of itemsToTranslate) {
16
16
  for (const language of wizardData.targetLanguages) {
17
17
  const languageInfo = wizardData.languageData.get(language);
18
- if (!languageInfo?.translationStatus)
19
- continue;
20
- const status = languageInfo.translationStatus;
21
- if (status.sourceLanguage === language)
18
+ // Determine source language for this target language from translation status
19
+ // The UI already filters out languages where sourceLanguage === targetLanguage,
20
+ // so we can trust that if a language is selected, it has a valid source
21
+ const sourceLanguage = languageInfo?.translationStatus?.sourceLanguage || "en";
22
+ // Skip if target language equals source language (already filtered in UI, but double-check here)
23
+ if (language === sourceLanguage)
22
24
  continue;
23
25
  const translationItem = {
24
26
  itemId: item.descriptor.id,
25
27
  targetLanguage: language,
26
- sourceLanguage: status.sourceLanguage || "en",
28
+ sourceLanguage: sourceLanguage,
27
29
  };
28
30
  // Get metadata for this specific item and language
29
31
  const itemLanguageMetadata = wizardData.itemMetadata.get(item.descriptor.id)?.get(language);
@@ -33,6 +35,12 @@ export async function performDefaultTranslation(wizardData, editContext, predefi
33
35
  translationRequests.push(translationItem);
34
36
  }
35
37
  }
38
+ // Validate that we have translation requests to process
39
+ if (translationRequests.length === 0) {
40
+ const error = new Error('No translations needed. All selected target languages match the source languages of the items.');
41
+ error.name = 'NoTranslationsNeededError';
42
+ throw error;
43
+ }
36
44
  try {
37
45
  const batchResult = await requestBatchTranslation({
38
46
  sessionId: editContext.sessionId,
@@ -1,4 +1,4 @@
1
1
  import { TranslationStepProps } from "./types";
2
2
  /** Example Step for metadata input testing */
3
- export declare function MetadataInputStep({ data, setData, onStepCompleted, }: TranslationStepProps): import("react/jsx-runtime").JSX.Element;
3
+ export declare function MetadataInputStep({ stepIndex, isActive, data, setData, onStepCompleted, }: TranslationStepProps): import("react/jsx-runtime").JSX.Element;
4
4
  //# sourceMappingURL=MetadataInputStep.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"MetadataInputStep.d.ts","sourceRoot":"","sources":["../../src/steps/MetadataInputStep.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAE/C,8CAA8C;AAC9C,wBAAgB,iBAAiB,CAAC,EAChC,IAAI,EACJ,OAAO,EACP,eAAe,GAChB,EAAE,oBAAoB,2CA0EtB"}
1
+ {"version":3,"file":"MetadataInputStep.d.ts","sourceRoot":"","sources":["../../src/steps/MetadataInputStep.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAE/C,8CAA8C;AAC9C,wBAAgB,iBAAiB,CAAC,EAChC,SAAS,EACT,QAAe,EACf,IAAI,EACJ,OAAO,EACP,eAAe,GAChB,EAAE,oBAAoB,2CA4EtB"}
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useEffect } from "react";
3
3
  /** Example Step for metadata input testing */
4
- export function MetadataInputStep({ data, setData, onStepCompleted, }) {
4
+ export function MetadataInputStep({ stepIndex, isActive = true, data, setData, onStepCompleted, }) {
5
5
  // Convert Map<string, Map<string, string>> to a flat object for local state management
6
6
  const [itemMetadata, setItemMetadata] = useState(() => {
7
7
  const result = {};
@@ -30,9 +30,12 @@ export function MetadataInputStep({ data, setData, onStepCompleted, }) {
30
30
  };
31
31
  // Data is saved automatically on change
32
32
  useEffect(() => {
33
+ // Only update completion when this step is active
34
+ if (!isActive)
35
+ return;
33
36
  // Mark step as completed since metadata is optional
34
37
  onStepCompleted(true);
35
- }, [onStepCompleted]);
38
+ }, [isActive, onStepCompleted]); // Check when active state changes
36
39
  // No footer actions needed since data is saved automatically
37
40
  return (_jsxs("div", { className: "flex flex-col gap-4 p-4", "data-testid": "metadata-input-step", children: [_jsx("h3", { className: "text-lg font-medium", children: "Enter Metadata Per Item Per Language" }), _jsx("p", { className: "text-sm text-gray-600", children: "Provide custom metadata for each item and language combination (optional)." }), _jsx("div", { className: "max-h-64 overflow-y-auto space-y-4", "data-testid": "metadata-input-container", children: allItems.map((item) => (_jsxs("div", { className: "border rounded p-3", "data-testid": `metadata-item-${item.id}`, children: [_jsx("label", { className: "block text-sm font-medium mb-2", "data-testid": `metadata-item-label-${item.id}`, children: item.name || item.id }), data.targetLanguages?.map((language) => (_jsxs("div", { className: "mb-2", "data-testid": `metadata-field-${item.id}-${language}`, children: [_jsxs("label", { className: "block text-xs font-medium mb-1 text-gray-600", "data-testid": `metadata-language-label-${item.id}-${language}`, children: [language.toUpperCase(), ":"] }), _jsx("textarea", { className: "w-full border rounded p-2 text-sm", rows: 2, value: itemMetadata[item.id]?.[language] || "", onChange: (e) => handleMetadataChange(item.id, language, e.target.value), placeholder: `Enter metadata for ${language}...`, "data-testid": `metadata-textarea-${item.id}-${language}` })] }, `${item.id}-${language}`)))] }, item.id))) })] }));
38
41
  }
@@ -1,3 +1,3 @@
1
1
  import { TranslationStepProps } from "./types";
2
- export declare function ServiceLanguageSelectionStep({ data, setData, onStepCompleted, editContext, setFooterActions, requestClose }: TranslationStepProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function ServiceLanguageSelectionStep({ stepIndex, isActive, data, setData, onStepCompleted, editContext, setFooterActions, requestClose }: TranslationStepProps): import("react/jsx-runtime").JSX.Element;
3
3
  //# sourceMappingURL=ServiceLanguageSelectionStep.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ServiceLanguageSelectionStep.d.ts","sourceRoot":"","sources":["../../src/steps/ServiceLanguageSelectionStep.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAyB,MAAM,SAAS,CAAC;AAGtE,wBAAgB,4BAA4B,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,gBAAgB,EAAE,YAAY,EAAE,EAAE,oBAAoB,2CAsPjJ"}
1
+ {"version":3,"file":"ServiceLanguageSelectionStep.d.ts","sourceRoot":"","sources":["../../src/steps/ServiceLanguageSelectionStep.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAyB,MAAM,SAAS,CAAC;AAGtE,wBAAgB,4BAA4B,CAAC,EAAE,SAAS,EAAE,QAAe,EAAE,IAAI,EAAE,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,gBAAgB,EAAE,YAAY,EAAE,EAAE,oBAAoB,2CAyU7K"}
@@ -1,44 +1,88 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useMemo, useRef, useState } from "react";
3
3
  // Version creation now offered via VersionOnlyTranslationProvider. No custom button here.
4
- export function ServiceLanguageSelectionStep({ data, setData, onStepCompleted, editContext, setFooterActions, requestClose }) {
4
+ export function ServiceLanguageSelectionStep({ stepIndex, isActive = true, data, setData, onStepCompleted, editContext, setFooterActions, requestClose }) {
5
5
  const [languageSelection, setLanguageSelection] = useState({});
6
6
  const dataRef = useRef(data);
7
7
  useEffect(() => { dataRef.current = data; }, [data]);
8
8
  // Check if multi-item translation is enabled
9
9
  const multiItemEnabled = editContext?.configuration?.localization?.multiItem !== false;
10
- // Use a ref to avoid dependency on onStepCompleted
10
+ // Use ref to track onStepCompleted to avoid dependency issues
11
11
  const onStepCompletedRef = useRef(onStepCompleted);
12
12
  useEffect(() => {
13
13
  onStepCompletedRef.current = onStepCompleted;
14
14
  }, [onStepCompleted]);
15
+ // Track last completion state to prevent unnecessary calls
16
+ const lastCompletionStateRef = useRef(null);
17
+ // Call completion check when component mounts or when returning to this step
18
+ // Also check when data changes (but only if we're actually the active step)
15
19
  useEffect(() => {
20
+ // Only update completion when this step is active
21
+ if (!isActive)
22
+ return;
16
23
  const hasProvider = !!data.translationProvider;
17
24
  const hasLanguages = data.targetLanguages.length > 0;
18
- onStepCompletedRef.current(hasProvider && hasLanguages);
19
- }, [data.translationProvider, data.targetLanguages.length]);
25
+ const isCompleted = hasProvider && hasLanguages;
26
+ // Only call if completion state actually changed
27
+ if (lastCompletionStateRef.current !== isCompleted) {
28
+ lastCompletionStateRef.current = isCompleted;
29
+ onStepCompletedRef.current(isCompleted);
30
+ }
31
+ }, [isActive, data.translationProvider, data.targetLanguages.length]);
20
32
  // 1) Derive provider + available languages (lightweight, in-memory)
21
33
  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]);
34
+ // Use editContext.itemLanguages (like TranslationSidebar) to get all languages including source language
35
+ // This is not item-specific - it shows all available languages for the site
36
+ const editContextLanguages = useMemo(() => editContext?.itemLanguages || [], [editContext?.itemLanguages]);
37
+ // Get language codes from editContext languages, or fallback to languageData keys
38
+ const siteLanguageCodes = useMemo(() => {
39
+ if (editContextLanguages.length > 0) {
40
+ return editContextLanguages.map(lang => lang.languageCode);
41
+ }
42
+ return Array.from(data.languageData.keys());
43
+ }, [editContextLanguages, data.languageData]);
23
44
  const availableLanguageCodes = useMemo(() => (provider?.supportedLanguages?.length ? provider.supportedLanguages : siteLanguageCodes), [provider?.supportedLanguages, siteLanguageCodes]);
45
+ // Determine the source language from the items being translated
46
+ // Use the first item's language as the source language (all items should have the same source language)
47
+ const itemSourceLanguage = useMemo(() => {
48
+ return data.items[0]?.descriptor.language || editContext?.contentEditorItem?.descriptor.language || "en";
49
+ }, [data.items, editContext?.contentEditorItem]);
24
50
  const allLanguages = useMemo(() => {
51
+ // Create a map of language codes to Language objects from editContext for quick lookup
52
+ const editContextLanguageMap = new Map(editContextLanguages.map(lang => [lang.languageCode, lang]));
25
53
  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);
54
+ .map(code => {
55
+ // Prefer language info from editContext (like TranslationSidebar), fallback to languageData
56
+ const editContextLang = editContextLanguageMap.get(code);
57
+ const languageDataLang = data.languageData.get(code);
58
+ // Determine source language: prefer from translationStatus, fallback to item's source language
59
+ const sourceLanguage = languageDataLang?.translationStatus?.sourceLanguage || itemSourceLanguage;
60
+ return {
61
+ code,
62
+ name: editContextLang?.name || languageDataLang?.name || code,
63
+ hasVersions: (editContextLang?.versions || 0) > 0 || (languageDataLang?.items.length || 0) > 0,
64
+ translationStatus: languageDataLang?.translationStatus,
65
+ sourceLanguage, // Add source language to the language object
66
+ };
67
+ })
68
+ // Filter out languages where sourceLanguage equals targetLanguage
69
+ .filter(lang => lang.sourceLanguage !== lang.code);
35
70
  arr.sort((a, b) => a.name.localeCompare(b.name));
36
71
  return arr;
37
- }, [availableLanguageCodes, data.languageData]);
72
+ }, [availableLanguageCodes, editContextLanguages, data.languageData, itemSourceLanguage]);
73
+ // Track last processed targetLanguages to prevent unnecessary updates
74
+ const lastProcessedTargetLanguagesRef = useRef('');
38
75
  // Rehydrate UI selection from saved wizard data when returning to this step
39
76
  useEffect(() => {
40
77
  if (!allLanguages || allLanguages.length === 0)
41
78
  return;
79
+ // Create a stable key from targetLanguages to detect actual changes
80
+ const targetLanguagesKey = JSON.stringify([...data.targetLanguages].sort());
81
+ // Skip if we've already processed this exact set of target languages
82
+ if (lastProcessedTargetLanguagesRef.current === targetLanguagesKey) {
83
+ return;
84
+ }
85
+ lastProcessedTargetLanguagesRef.current = targetLanguagesKey;
42
86
  const initialSelection = {};
43
87
  // Mark as selected any language that's in our saved targetLanguages array
44
88
  for (const langCode of data.targetLanguages) {
@@ -49,20 +93,32 @@ export function ServiceLanguageSelectionStep({ data, setData, onStepCompleted, e
49
93
  }
50
94
  setLanguageSelection(initialSelection);
51
95
  }, [allLanguages, data.targetLanguages]);
96
+ // Track last set targetLanguages to prevent unnecessary setData calls
97
+ const lastSetTargetLanguagesRef = useRef('');
52
98
  // Update wizard data when language selection changes
53
99
  useEffect(() => {
54
100
  const selectedLanguages = Object.entries(languageSelection)
55
101
  .filter(([, isSelected]) => isSelected)
56
102
  .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);
103
+ const selectedLanguagesKey = JSON.stringify([...selectedLanguages].sort());
104
+ const currentTargetLanguagesKey = JSON.stringify([...data.targetLanguages].sort());
105
+ // Only update if the selection has actually changed AND we haven't already set this value
106
+ if (selectedLanguagesKey === currentTargetLanguagesKey) {
107
+ // Selection matches current data, no update needed
108
+ return;
109
+ }
110
+ if (lastSetTargetLanguagesRef.current === selectedLanguagesKey) {
111
+ // We've already set this value, skip to prevent loops
112
+ return;
64
113
  }
65
- }, [languageSelection]);
114
+ lastSetTargetLanguagesRef.current = selectedLanguagesKey;
115
+ const newData = {
116
+ ...dataRef.current,
117
+ targetLanguages: selectedLanguages
118
+ };
119
+ setData(newData);
120
+ // Completion will be updated by the useEffect that watches data.targetLanguages.length
121
+ }, [languageSelection, setData, data.targetLanguages]);
66
122
  const handleProviderChange = (e) => {
67
123
  const newProvider = e.target.value;
68
124
  const newData = {
@@ -74,6 +130,7 @@ export function ServiceLanguageSelectionStep({ data, setData, onStepCompleted, e
74
130
  setData(newData);
75
131
  // Clear UI selection too
76
132
  setLanguageSelection({});
133
+ // Completion will be updated by the useEffect that watches data.translationProvider
77
134
  };
78
135
  const handleLanguageToggle = (langCode) => {
79
136
  setLanguageSelection(prev => ({ ...prev, [langCode]: !prev[langCode] }));
@@ -110,11 +167,11 @@ export function ServiceLanguageSelectionStep({ data, setData, onStepCompleted, e
110
167
  const areSomeLanguagesSelected = useMemo(() => {
111
168
  return allLanguages.some(lang => languageSelection[lang.code]);
112
169
  }, [allLanguages, languageSelection]);
113
- 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: [_jsxs("div", { className: "flex items-center justify-between mb-2", children: [_jsx("h3", { className: "text-sm font-medium text-gray-900", children: "Target Languages" }), allLanguages.length > 1 && (_jsxs("label", { className: "flex items-center cursor-pointer", children: [_jsx("input", { type: "checkbox", checked: areAllLanguagesSelected, ref: (input) => {
170
+ 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 }, 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: [_jsxs("div", { className: "flex items-center justify-between mb-2", children: [_jsx("h3", { className: "text-sm font-medium text-gray-900", children: "Target Languages" }), allLanguages.length > 1 && (_jsxs("label", { className: "flex items-center cursor-pointer", children: [_jsx("input", { type: "checkbox", checked: areAllLanguagesSelected, ref: (input) => {
114
171
  if (input) {
115
172
  input.indeterminate = areSomeLanguagesSelected && !areAllLanguagesSelected;
116
173
  }
117
174
  }, onChange: handleSelectAllLanguages, className: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded", "data-testid": "select-all-languages-checkbox" }), _jsx("span", { className: "ml-2 text-xs text-gray-600", children: "Select All" })] }))] }), _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 ? (
118
175
  // Loading skeleton
119
- _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))) })) })] })] })] }));
176
+ _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 flex items-center gap-2", children: [_jsxs("span", { children: [lang.name, " (", lang.code, ")"] }), lang.sourceLanguage && (_jsxs("span", { className: "text-xs text-gray-500", children: ["from: ", lang.sourceLanguage] }))] }), !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))) })) })] })] })] }));
120
177
  }
@@ -1,3 +1,3 @@
1
1
  import { TranslationStepProps } from "./types";
2
- export declare function SubitemDiscoveryStep({ data, setData, editContext, onStepCompleted, setBeforeNextCallback, setFooterActions, requestClose }: TranslationStepProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function SubitemDiscoveryStep({ stepIndex, isActive, data, setData, editContext, onStepCompleted, setBeforeNextCallback, setFooterActions, requestClose }: TranslationStepProps): import("react/jsx-runtime").JSX.Element;
3
3
  //# sourceMappingURL=SubitemDiscoveryStep.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SubitemDiscoveryStep.d.ts","sourceRoot":"","sources":["../../src/steps/SubitemDiscoveryStep.tsx"],"names":[],"mappings":"AAKA,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,2CAshBhK"}
1
+ {"version":3,"file":"SubitemDiscoveryStep.d.ts","sourceRoot":"","sources":["../../src/steps/SubitemDiscoveryStep.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAW/C,wBAAgB,oBAAoB,CAAC,EAAE,SAAS,EAAE,QAAe,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,YAAY,EAAE,EAAE,oBAAoB,2CAqb5L"}