Skip to content

Commit 73c084e

Browse files
feat(apollo-react): add i18n support for sticky note formatting toolbar tooltips
Add Lingui localization to the formatting toolbar tooltips with platform-dependent keyboard shortcuts (⌘ on Mac, Ctrl+ elsewhere). Includes a new canvas locale catalog with 13 supported locales and configures the Lingui babel plugin for vitest. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fe4c2c3 commit 73c084e

File tree

23 files changed

+342
-30
lines changed

23 files changed

+342
-30
lines changed

apps/storybook/.storybook/main.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { StorybookConfig } from "@storybook/react-vite";
2+
import react from "@vitejs/plugin-react";
23
import { readFileSync } from "node:fs";
34
import { dirname, resolve } from "node:path";
45
import { fileURLToPath } from "node:url";
@@ -31,6 +32,34 @@ const reactScanHook = isDev
3132
: "";
3233

3334
const config: StorybookConfig = {
35+
viteFinal(config) {
36+
config.plugins ??= [];
37+
38+
// Lingui macros (`@lingui/core/macro`) are compile-time Babel transforms.
39+
// Without this plugin, they run uncompiled in the browser and crash (process is not defined).
40+
// Only process source files — dist/ is already compiled and doesn't contain macros.
41+
config.plugins.push(
42+
react({
43+
include: /packages\/apollo-react\/src\/.*\.[tj]sx?$/,
44+
babel: {
45+
plugins: ['@lingui/babel-plugin-lingui-macro'],
46+
},
47+
})
48+
);
49+
50+
// Lingui compiles locale catalogs to CommonJS (.js with module.exports).
51+
// Vite's dev server serves .js as ESM, breaking default imports from CJS.
52+
// This plugin converts CJS → ESM on the fly for locale files.
53+
config.plugins.push({
54+
name: 'lingui-cjs-to-esm',
55+
transform(code, id) {
56+
if (id.includes('/locales/') && /\.js(\?|$)/.test(id) && code.includes('module.exports')) {
57+
return code.replace(/module\.exports\s*=\s*/, 'export default ');
58+
}
59+
},
60+
});
61+
return config;
62+
},
3463
stories: [
3564
// For now only include canvas stories
3665
{

apps/storybook/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@
2121
"react-dom": "19.2.3"
2222
},
2323
"devDependencies": {
24+
"@lingui/babel-plugin-lingui-macro": "^5.9.4",
2425
"@storybook/addon-a11y": "^10.2.15",
2526
"@storybook/addon-docs": "^10.2.15",
2627
"@storybook/addon-links": "^10.2.15",
2728
"@storybook/react": "^10.2.15",
2829
"@storybook/react-vite": "^10.2.15",
30+
"@vitejs/plugin-react": "^4.7.0",
2931
"prettier": "^3.8.1",
3032
"react-scan": "^0.5.3",
3133
"storybook": "^10.2.15",

lingui.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* Root-level Lingui config for tooling that searches from cwd (e.g. Storybook).
3+
* The canonical config lives in packages/apollo-react/lingui.config.ts —
4+
* this re-exports it so the Babel macro plugin can find it from any workspace.
5+
*/
6+
export { default } from './packages/apollo-react/lingui.config';

packages/apollo-react/lingui.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ const config: LinguiConfig = {
1313
path: 'src/material/components/ap-tool-call/locales/{locale}',
1414
include: ['src/material/components/ap-tool-call'],
1515
},
16+
{
17+
path: 'src/canvas/locales/{locale}',
18+
include: ['src/canvas'],
19+
},
1620
],
1721
format: formatter({ style: 'minimal' }),
1822
};

packages/apollo-react/src/canvas/components/StickyNoteNode/FormattingToolbar.test.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { fireEvent, render, screen } from '@testing-library/react';
22
import { describe, expect, it, vi } from 'vitest';
3+
import { ApI18nProvider } from '../../../i18n';
34
import { FormattingToolbar } from './FormattingToolbar';
45
import type { ActiveFormats } from './markdown-formatting';
56

@@ -21,12 +22,20 @@ function createMockTextArea(value = '', selectionStart = 0, selectionEnd = 0) {
2122
return { current: textarea };
2223
}
2324

25+
function renderWithI18n(ui: React.ReactElement) {
26+
return render(
27+
<ApI18nProvider component="canvas" locale="en">
28+
{ui}
29+
</ApI18nProvider>
30+
);
31+
}
32+
2433
describe('FormattingToolbar', () => {
2534
it('calls onFormat when a button is clicked', () => {
2635
const onFormat = vi.fn();
2736
const ref = createMockTextArea('hello world', 6, 11);
2837

29-
render(
38+
renderWithI18n(
3039
<FormattingToolbar
3140
textAreaRef={ref}
3241
borderColor="#42A1FF"
@@ -46,7 +55,7 @@ describe('FormattingToolbar', () => {
4655
const onFormat = vi.fn();
4756
const ref = { current: null } as React.RefObject<HTMLTextAreaElement | null>;
4857

49-
render(
58+
renderWithI18n(
5059
<FormattingToolbar
5160
textAreaRef={ref}
5261
borderColor="#42A1FF"
@@ -61,7 +70,7 @@ describe('FormattingToolbar', () => {
6170

6271
it('prevents textarea blur on mousedown', () => {
6372
const ref = { current: null } as React.RefObject<HTMLTextAreaElement | null>;
64-
const { container } = render(
73+
const { container } = renderWithI18n(
6574
<FormattingToolbar
6675
textAreaRef={ref}
6776
borderColor="#42A1FF"
@@ -70,7 +79,7 @@ describe('FormattingToolbar', () => {
7079
/>
7180
);
7281

73-
const toolbarContainer = container.firstChild as HTMLElement;
82+
const toolbarContainer = container.querySelector('[class]') as HTMLElement;
7483
const mouseDownEvent = new MouseEvent('mousedown', { bubbles: true, cancelable: true });
7584
const prevented = !toolbarContainer.dispatchEvent(mouseDownEvent);
7685
expect(prevented).toBe(true);

packages/apollo-react/src/canvas/components/StickyNoteNode/FormattingToolbar.tsx

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { msg } from '@lingui/core/macro';
2+
import { useLingui } from '@lingui/react';
13
import { NodeIcon } from '@uipath/apollo-react/canvas';
24
import { ApTooltip } from '@uipath/apollo-react/material/components';
3-
import { memo, type RefObject, useCallback } from 'react';
5+
import { memo, type RefObject, useCallback, useMemo } from 'react';
46
import type { ActiveFormats } from './markdown-formatting';
57
import {
68
toggleBold,
@@ -16,6 +18,10 @@ import {
1618
} from './StickyNoteNode.styles';
1719
import type { TextSelection } from './StickyNoteNode.types';
1820

21+
const isMac = () => typeof navigator !== 'undefined' && /Mac/.test(navigator.userAgent);
22+
23+
const getModifierKey = () => (isMac() ? '⌘' : 'Ctrl+');
24+
1925
interface FormattingToolbarProps {
2026
textAreaRef: RefObject<HTMLTextAreaElement | null>;
2127
borderColor: string;
@@ -29,6 +35,9 @@ const FormattingToolbarComponent = ({
2935
activeFormats,
3036
onFormat,
3137
}: FormattingToolbarProps) => {
38+
const { _ } = useLingui();
39+
const mod = useMemo(() => getModifierKey(), []);
40+
3241
const applyFormat = useCallback(
3342
(formatFn: (input: TextSelection) => TextSelection) => {
3443
const textarea = textAreaRef.current;
@@ -52,36 +61,75 @@ const FormattingToolbarComponent = ({
5261
const handleBulletList = useCallback(() => applyFormat(toggleBulletList), [applyFormat]);
5362
const handleNumberedList = useCallback(() => applyFormat(toggleNumberedList), [applyFormat]);
5463

64+
const boldShortcut = `${mod}B`;
65+
const italicShortcut = `${mod}I`;
66+
const strikethroughShortcut = `${mod}⇧X`;
67+
5568
return (
5669
<FormattingToolbarContainer
5770
borderColor={borderColor}
5871
onMouseDown={(e) => e.preventDefault()}
5972
className="nodrag nowheel"
6073
>
61-
<ApTooltip content="Bold (⌘B)" placement="top" delay>
74+
<ApTooltip
75+
content={_(
76+
msg({
77+
id: 'sticky-note.formatting.bold',
78+
message: `Bold (${boldShortcut})`,
79+
})
80+
)}
81+
placement="top"
82+
delay
83+
>
6284
<FormattingButton isActive={activeFormats.bold} onClick={handleBold}>
6385
<NodeIcon icon="bold" size={14} />
6486
</FormattingButton>
6587
</ApTooltip>
66-
<ApTooltip content="Italic (⌘I)" placement="top" delay>
88+
<ApTooltip
89+
content={_(
90+
msg({
91+
id: 'sticky-note.formatting.italic',
92+
message: `Italic (${italicShortcut})`,
93+
})
94+
)}
95+
placement="top"
96+
delay
97+
>
6798
<FormattingButton isActive={activeFormats.italic} onClick={handleItalic}>
6899
<NodeIcon icon="italic" size={14} />
69100
</FormattingButton>
70101
</ApTooltip>
71-
<ApTooltip content="Strikethrough (⌘⇧X)" placement="top" delay>
102+
<ApTooltip
103+
content={_(
104+
msg({
105+
id: 'sticky-note.formatting.strikethrough',
106+
message: `Strikethrough (${strikethroughShortcut})`,
107+
})
108+
)}
109+
placement="top"
110+
delay
111+
>
72112
<FormattingButton isActive={activeFormats.strikethrough} onClick={handleStrikethrough}>
73113
<NodeIcon icon="strikethrough" size={14} />
74114
</FormattingButton>
75115
</ApTooltip>
76116

77117
<ToolbarSeparator />
78118

79-
<ApTooltip content="Bullet list" placement="top" delay>
119+
<ApTooltip
120+
content={_(msg({ id: 'sticky-note.formatting.bullet-list', message: 'Bullet list' }))}
121+
placement="top"
122+
delay
123+
>
80124
<FormattingButton isActive={activeFormats.bulletList} onClick={handleBulletList}>
81125
<NodeIcon icon="list" size={14} />
82126
</FormattingButton>
83127
</ApTooltip>
84-
<ApTooltip content="Numbered list" placement="top" delay>
128+
<ApTooltip
129+
content={_(msg({ id: 'sticky-note.formatting.numbered-list', message: 'Numbered list' }))}
130+
placement="top"
131+
delay
132+
>
85133
<FormattingButton isActive={activeFormats.numberedList} onClick={handleNumberedList}>
86134
<NodeIcon icon="list-ordered" size={14} />
87135
</FormattingButton>

packages/apollo-react/src/canvas/components/StickyNoteNode/StickyNoteNode.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { NodeProps } from '@uipath/apollo-react/canvas/xyflow/react';
44
import { NodeResizeControl, useReactFlow } from '@uipath/apollo-react/canvas/xyflow/react';
55
import { AnimatePresence } from 'motion/react';
66
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
7+
import { ApI18nProvider } from '../../../i18n';
78
import ReactMarkdown from 'react-markdown';
89
import remarkBreaks from 'remark-breaks';
910
import remarkGfm from 'remark-gfm';
@@ -335,7 +336,7 @@ const StickyNoteNodeComponent = ({
335336
<TopCornerIndicators selected={selected} />
336337
<BottomCornerIndicators selected={selected} />
337338
{isEditing ? (
338-
<>
339+
<ApI18nProvider component="canvas">
339340
<FormattingToolbar
340341
textAreaRef={textAreaRef}
341342
borderColor={color}
@@ -354,7 +355,7 @@ const StickyNoteNodeComponent = ({
354355
isEditing={isEditing}
355356
className="nodrag nowheel"
356357
/>
357-
</>
358+
</ApI18nProvider>
358359
) : (
359360
<StickyNoteMarkdown ref={markdownRef} {...scrollCaptureProps}>
360361
{localContent ? (
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"sticky-note.formatting.bold": "",
3+
"sticky-note.formatting.italic": "",
4+
"sticky-note.formatting.strikethrough": "",
5+
"sticky-note.formatting.bullet-list": "",
6+
"sticky-note.formatting.numbered-list": ""
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"sticky-note.formatting.bold": "Bold ({boldShortcut})",
3+
"sticky-note.formatting.italic": "Italic ({italicShortcut})",
4+
"sticky-note.formatting.strikethrough": "Strikethrough ({strikethroughShortcut})",
5+
"sticky-note.formatting.bullet-list": "Bullet list",
6+
"sticky-note.formatting.numbered-list": "Numbered list"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"sticky-note.formatting.bold": "",
3+
"sticky-note.formatting.italic": "",
4+
"sticky-note.formatting.strikethrough": "",
5+
"sticky-note.formatting.bullet-list": "",
6+
"sticky-note.formatting.numbered-list": ""
7+
}

0 commit comments

Comments
 (0)