= {};
const LOOP_HEADER_ADORNMENT_GAP = 8;
const LOOP_HEADER_ADORNMENT_PADDING =
@@ -190,6 +191,7 @@ function LoopNodeComponent(props: LoopNodeProps) {
iterationState: iterationStateProp,
} = props;
const nodeTypeRegistry = useOptionalNodeTypeRegistry();
+ const { _ } = useSafeLingui();
const [isHovered, setIsHovered] = useState(false);
const [isResizing, setIsResizing] = useState(false);
@@ -239,9 +241,16 @@ function LoopNodeComponent(props: LoopNodeProps) {
[manifest?.display, id, resolvedData]
);
- const displayTitle = display.label ?? DEFAULT_LOOP_TITLE;
+ const displayTitle = display.label ?? _({ id: 'loop-node.title', message: 'Loop' });
const displayIcon = display.icon ?? DEFAULT_LOOP_ICON;
const isParallel = resolvedData.parallel === true;
+ const label = isParallel
+ ? _({ id: 'loop-node.mode.parallel', message: 'Parallel' })
+ : _({ id: 'loop-node.mode.sequential', message: 'Sequential' });
+ const addNodeToLoopLabel = _({
+ id: 'loop-node.add-node',
+ message: 'Add node to loop',
+ });
const isDropTarget = resolvedData.isDropTarget === true;
const containerWidth = width || DEFAULT_CONTAINER_WIDTH;
const containerHeight = height || DEFAULT_CONTAINER_HEIGHT;
@@ -393,6 +402,7 @@ function LoopNodeComponent(props: LoopNodeProps) {
icon={displayIcon}
loading={isLoading}
isParallel={isParallel}
+ label={label}
iterationState={iterationStateProp}
hasTopLeftAdornment={hasTopLeftAdornment}
hasTopRightAdornment={hasTopRightAdornment}
@@ -405,7 +415,7 @@ function LoopNodeComponent(props: LoopNodeProps) {
'-translate-x-1/2 -translate-y-1/2'
)}
>
-
+
) : null}
{toolbarConfig && (
@@ -441,6 +451,7 @@ function Header({
icon,
loading,
isParallel,
+ label,
iterationState,
hasTopLeftAdornment,
hasTopRightAdornment,
@@ -449,6 +460,7 @@ function Header({
icon?: string;
loading: boolean;
isParallel: boolean;
+ label: string;
iterationState?: LoopIterationState;
hasTopLeftAdornment: boolean;
hasTopRightAdornment: boolean;
@@ -474,7 +486,6 @@ function Header({
paddingRight: hasTopRightAdornment ? LOOP_HEADER_ADORNMENT_PADDING : undefined,
}
: undefined;
- const executionModeIcon = isParallel ? 'columns-3' : 'rows-3';
return (
{iterationState ? : null}
-
-
+
+
- {isParallel ? 'Parallel' : 'Sequential'}
+ {label}
);
}
-function EmptyState({ onAddFirstChild }: { onAddFirstChild: () => void }) {
+function EmptyState({ label, onAddFirstChild }: { label: string; onAddFirstChild: () => void }) {
return (
-
+
+
+
);
}
diff --git a/packages/apollo-react/src/canvas/locales/en.json b/packages/apollo-react/src/canvas/locales/en.json
index 9cbfb0bbc..b6ce5eaa4 100644
--- a/packages/apollo-react/src/canvas/locales/en.json
+++ b/packages/apollo-react/src/canvas/locales/en.json
@@ -34,5 +34,13 @@
"sticky-note.toolbar.color": "Color",
"sticky-note.toolbar.delete": "Delete",
"sticky-note.toolbar.edit": "Edit",
- "toolbox.search": "Search"
+ "toolbox.search": "Search",
+ "loop-node.add-node": "Add node to loop",
+ "loop-node.iteration.label": "Loop iteration",
+ "loop-node.iteration.next": "Next loop iteration",
+ "loop-node.iteration.previous": "Previous loop iteration",
+ "loop-node.iteration.status": "{label}: {visibleIndex} of {total}",
+ "loop-node.mode.parallel": "Parallel",
+ "loop-node.mode.sequential": "Sequential",
+ "loop-node.title": "Loop"
}
diff --git a/packages/apollo-react/src/i18n/useSafeLingui.test.tsx b/packages/apollo-react/src/i18n/useSafeLingui.test.tsx
new file mode 100644
index 000000000..c83396cde
--- /dev/null
+++ b/packages/apollo-react/src/i18n/useSafeLingui.test.tsx
@@ -0,0 +1,65 @@
+import { setupI18n } from '@lingui/core';
+import { I18nProvider } from '@lingui/react';
+import { render, screen } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+
+import { useSafeLingui } from './useSafeLingui';
+
+type TranslationDescriptor = { id: string; message?: string; values?: Record };
+type TranslationInput = TranslationDescriptor | string;
+
+function TranslationProbe({ descriptor }: { descriptor: TranslationInput }) {
+ const { _ } = useSafeLingui();
+ const label = typeof descriptor === 'string' ? _(descriptor) : _(descriptor);
+
+ return {label};
+}
+
+describe('useSafeLingui', () => {
+ it('returns string ids when no provider is mounted', () => {
+ render();
+
+ expect(screen.getByText('test.message')).toBeInTheDocument();
+ });
+
+ it('returns descriptor messages when no provider is mounted', () => {
+ render();
+
+ expect(screen.getByText('Hello world')).toBeInTheDocument();
+ });
+
+ it('formats descriptor values when no provider is mounted', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('Loop iteration: 1 of 3')).toBeInTheDocument();
+ });
+
+ it('uses the mounted Lingui provider when available', () => {
+ const testI18n = setupI18n({
+ locale: 'es',
+ messages: {
+ es: {
+ 'test.greeting': 'Hola {name}',
+ },
+ },
+ });
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByText('Hola Ada')).toBeInTheDocument();
+ });
+});
diff --git a/packages/apollo-react/src/i18n/useSafeLingui.ts b/packages/apollo-react/src/i18n/useSafeLingui.ts
index db85c1b9a..6d86e333e 100644
--- a/packages/apollo-react/src/i18n/useSafeLingui.ts
+++ b/packages/apollo-react/src/i18n/useSafeLingui.ts
@@ -1,3 +1,4 @@
+import { setupI18n } from '@lingui/core';
import { LinguiContext } from '@lingui/react';
import { useContext, useMemo } from 'react';
@@ -7,10 +8,21 @@ type Translate = {
(id: string): string;
};
-// Returns the `message` (English default baked into the macro call) when no
+const fallbackI18n = setupI18n({ locale: 'en', messages: { en: {} } });
+
+// Formats the `message` (English default baked into the macro call) when no
// I18nProvider is mounted upstream. Falls back to `id` if no message is given.
-const fallbackTranslate = ((arg: Descriptor | string): string =>
- typeof arg === 'string' ? arg : (arg.message ?? arg.id)) as Translate;
+const fallbackTranslate = ((arg: Descriptor | string): string => {
+ if (typeof arg === 'string') {
+ return arg;
+ }
+
+ if (!arg.message) {
+ return arg.id;
+ }
+
+ return fallbackI18n._(arg);
+}) as Translate;
// Drop-in replacement for `useLingui()` from `@lingui/react` that does NOT
// throw when there is no I18nProvider upstream. Reads `LinguiContext` directly