Skip to content

Commit 4e1b22b

Browse files
feat(apollo-react): add i18n support for sticky note formatting toolbar tooltips
1 parent 1415c76 commit 4e1b22b

27 files changed

+419
-154
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ storybook-static/
2424

2525
# Compiled locale files (generated by lingui compile)
2626
**/locales/*.js
27+
**/locales/*.mjs
2728

2829
# Playwright MCP
2930
.playwright-mcp

apps/storybook/.storybook/main.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import type { StorybookConfig } from "@storybook/react-vite";
2-
import { readFileSync } from "node:fs";
3-
import { dirname, resolve } from "node:path";
4-
import { fileURLToPath } from "node:url";
1+
import { readFileSync } from 'node:fs';
2+
import { dirname, resolve } from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
import type { StorybookConfig } from '@storybook/react-vite';
5+
import react from '@vitejs/plugin-react';
56

67
const __filename = fileURLToPath(import.meta.url);
78
const __dirname = dirname(__filename);
89

910
// react-scan is a dev-only tool — skip it in production Storybook builds.
1011
// main.ts runs in Node (Storybook CLI) so we use process.env.
1112
// preview.tsx runs in the browser (Vite) so it uses import.meta.env.MODE instead.
12-
const isDev = process.env.NODE_ENV !== "production";
13+
const isDev = process.env.NODE_ENV !== 'production';
1314

1415
// react-scan must install its devtools hook before React initializes.
1516
// Two Storybook behaviors interfere with this:
@@ -22,26 +23,40 @@ const isDev = process.env.NODE_ENV !== "production";
2223
// (React is loaded via type="module" scripts, which are deferred).
2324
const reactScanHook = isDev
2425
? readFileSync(
25-
resolve(
26-
__dirname,
27-
"../node_modules/react-scan/dist/install-hook.global.js",
28-
),
29-
"utf-8",
26+
resolve(__dirname, '../node_modules/react-scan/dist/install-hook.global.js'),
27+
'utf-8'
3028
)
31-
: "";
29+
: '';
3230

3331
const config: StorybookConfig = {
32+
viteFinal(config) {
33+
config.plugins ??= [];
34+
35+
// Lingui macros (`@lingui/core/macro`) are compile-time Babel transforms.
36+
// Without this plugin, they run uncompiled in the browser and crash (process is not defined).
37+
// Only process source files — dist/ is already compiled and doesn't contain macros.
38+
config.plugins.push(
39+
react({
40+
include: /packages\/apollo-react\/src\/.*\.[tj]sx?$/,
41+
babel: {
42+
plugins: ['@lingui/babel-plugin-lingui-macro'],
43+
},
44+
})
45+
);
46+
47+
return config;
48+
},
3449
stories: [
3550
// For now only include canvas stories
3651
{
37-
directory: "../../../packages/apollo-react/src/canvas",
38-
files: "**/*.stories.@(tsx|ts|jsx|js|mdx)",
39-
titlePrefix: "Canvas",
52+
directory: '../../../packages/apollo-react/src/canvas',
53+
files: '**/*.stories.@(tsx|ts|jsx|js|mdx)',
54+
titlePrefix: 'Canvas',
4055
},
4156
],
4257
addons: [],
4358
framework: {
44-
name: "@storybook/react-vite",
59+
name: '@storybook/react-vite',
4560
options: {},
4661
},
4762

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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@ 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' }),
22+
compileNamespace: 'es',
1823
};
1924

2025
export default config;

packages/apollo-react/rslib.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export default defineConfig({
5858
'!./src/test/**',
5959
'!./src/icons/.cache',
6060
'!./src/**/*.md',
61-
'!./src/**/locales/*.js',
61+
'!./src/**/locales/*.mjs',
6262
'!./src/**/locales/*.json',
6363
'!./src/**/.DS_Store',
6464
],

packages/apollo-react/scripts/plugin-copy-locales.ts

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,25 @@
1-
import {
2-
existsSync,
3-
mkdirSync,
4-
readFileSync,
5-
readdirSync,
6-
statSync,
7-
writeFileSync,
8-
} from 'fs';
9-
import {
10-
dirname,
11-
join,
12-
} from 'path';
1+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'fs';
2+
import { dirname, join } from 'path';
133
import { fileURLToPath } from 'url';
144

155
const __dirname = dirname(fileURLToPath(import.meta.url));
166
const packageRoot = dirname(__dirname);
177

188
/**
19-
* Convert CommonJS locale module to ESM format
9+
* Convert CommonJS locale module to ESM named export format
2010
* From: module.exports={messages:JSON.parse("{...}")};
21-
* To: export default {messages:JSON.parse("{...}")};
11+
* To: export const messages=JSON.parse("{...}");
12+
*
13+
* The locale-registry imports `{ messages }` as a named export,
14+
* so we must produce a named export, not `export default`.
2215
*/
2316
function convertToESM(content: string): string {
17+
const prefix = 'module.exports={messages:';
18+
const start = content.indexOf(prefix);
19+
if (start !== -1) {
20+
const value = content.slice(start + prefix.length, -2); // strip trailing "};"
21+
return `/*eslint-disable*/export const messages=${value};`;
22+
}
2423
return content.replace(/module\.exports\s*=\s*/, 'export default ');
2524
}
2625

@@ -56,10 +55,33 @@ export function pluginCopyLocales() {
5655
const cjsFile = join(distLocales, file.replace('.js', '.cjs'));
5756
writeFileSync(cjsFile, content, 'utf-8');
5857

59-
// Write .js (ESM format)
58+
// Write .js (ESM named export format)
6059
const esmFile = join(distLocales, file);
6160
const esmContent = convertToESM(content);
6261
writeFileSync(esmFile, esmContent, 'utf-8');
62+
} else if (file.endsWith('.mjs')) {
63+
// ESM compiled files (from compileNamespace: 'es') — already correct format
64+
const srcFile = join(srcLocales, file);
65+
const content = readFileSync(srcFile, 'utf-8');
66+
const baseName = file.replace('.mjs', '');
67+
68+
// Write .js only if not already created from a .js source above
69+
const jsFile = join(distLocales, `${baseName}.js`);
70+
if (!existsSync(jsFile)) {
71+
writeFileSync(jsFile, content, 'utf-8');
72+
}
73+
74+
// Write .cjs only if not already created from a .js source above
75+
const cjsFile = join(distLocales, `${baseName}.cjs`);
76+
if (!existsSync(cjsFile)) {
77+
const cjsContent = content
78+
.replace(
79+
/\/\*eslint-disable\*\/export const messages=/,
80+
'/*eslint-disable*/module.exports={messages:'
81+
)
82+
.replace(/;$/, '};');
83+
writeFileSync(cjsFile, cjsContent, 'utf-8');
84+
}
6385
} else if (file.endsWith('.json')) {
6486
// Copy JSON files as-is
6587
const srcFile = join(srcLocales, file);

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: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
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';
35
import { memo, type RefObject, useCallback } from 'react';
@@ -30,6 +32,11 @@ const FormattingToolbarComponent = ({
3032
activeFormats,
3133
onFormat,
3234
}: FormattingToolbarProps) => {
35+
const { _ } = useLingui();
36+
37+
const mod = getModifierKey();
38+
const shift = isMac() ? '⇧' : '+Shift+';
39+
3340
const applyFormat = useCallback(
3441
(formatFn: (input: TextSelection) => TextSelection) => {
3542
const textarea = textAreaRef.current;
@@ -53,39 +60,71 @@ const FormattingToolbarComponent = ({
5360
const handleBulletList = useCallback(() => applyFormat(toggleBulletList), [applyFormat]);
5461
const handleNumberedList = useCallback(() => applyFormat(toggleNumberedList), [applyFormat]);
5562

56-
const mod = getModifierKey();
57-
const shift = isMac() ? '⇧' : '+Shift+';
58-
5963
return (
6064
<FormattingToolbarContainer
6165
borderColor={borderColor}
6266
onMouseDown={(e) => e.preventDefault()}
6367
className="nodrag nowheel"
6468
>
65-
<ApTooltip content={`Bold (${mod}+B)`} placement="top" delay>
69+
<ApTooltip
70+
content={_(
71+
msg({
72+
id: 'sticky-note.formatting.bold',
73+
message: `Bold (${mod}+B)`,
74+
})
75+
)}
76+
placement="top"
77+
delay
78+
>
6679
<FormattingButton isActive={activeFormats.bold} onClick={handleBold}>
6780
<NodeIcon icon="bold" size={14} />
6881
</FormattingButton>
6982
</ApTooltip>
70-
<ApTooltip content={`Italic (${mod}+I)`} placement="top" delay>
83+
<ApTooltip
84+
content={_(
85+
msg({
86+
id: 'sticky-note.formatting.italic',
87+
message: `Italic (${mod}+I)`,
88+
})
89+
)}
90+
placement="top"
91+
delay
92+
>
7193
<FormattingButton isActive={activeFormats.italic} onClick={handleItalic}>
7294
<NodeIcon icon="italic" size={14} />
7395
</FormattingButton>
7496
</ApTooltip>
75-
<ApTooltip content={`Strikethrough (${mod}${shift}X)`} placement="top" delay>
97+
<ApTooltip
98+
content={_(
99+
msg({
100+
id: 'sticky-note.formatting.strikethrough',
101+
message: `Strikethrough (${mod}${shift}X)`,
102+
})
103+
)}
104+
placement="top"
105+
delay
106+
>
76107
<FormattingButton isActive={activeFormats.strikethrough} onClick={handleStrikethrough}>
77108
<NodeIcon icon="strikethrough" size={14} />
78109
</FormattingButton>
79110
</ApTooltip>
80111

81112
<ToolbarSeparator />
82113

83-
<ApTooltip content="Bullet list" placement="top" delay>
114+
<ApTooltip
115+
content={_(msg({ id: 'sticky-note.formatting.bullet-list', message: 'Bullet list' }))}
116+
placement="top"
117+
delay
118+
>
84119
<FormattingButton isActive={activeFormats.bulletList} onClick={handleBulletList}>
85120
<NodeIcon icon="list" size={14} />
86121
</FormattingButton>
87122
</ApTooltip>
88-
<ApTooltip content="Numbered list" placement="top" delay>
123+
<ApTooltip
124+
content={_(msg({ id: 'sticky-note.formatting.numbered-list', message: 'Numbered list' }))}
125+
placement="top"
126+
delay
127+
>
89128
<FormattingButton isActive={activeFormats.numberedList} onClick={handleNumberedList}>
90129
<NodeIcon icon="list-ordered" size={14} />
91130
</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 ? (

0 commit comments

Comments
 (0)