Skip to content

Commit 9067864

Browse files
committed
feat: add i18n to storybook
1 parent 4c19a5c commit 9067864

6 files changed

Lines changed: 1245 additions & 571 deletions

File tree

.storybook/languageAddon.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import { Addon, types, useGlobals } from "storybook/manager-api";
9+
import { WithTooltip, IconButton, TooltipLinkList } from "storybook/internal/components";
10+
import React from "react";
11+
import { GlobeIcon } from "@storybook/icons";
12+
13+
// We can't import `shared/i18n.tsx` directly here.
14+
// The storybook addon doesn't seem to benefit the vite config of storybook and we can't resolve the alias in i18n.tsx.
15+
import json from "../webapp/i18n/languages.json";
16+
const languages = Object.keys(json).filter((lang) => lang !== "default");
17+
18+
/**
19+
* Returns the title of a language in the user's locale.
20+
*/
21+
function languageTitle(language: string): string {
22+
return new Intl.DisplayNames([language], { type: "language", style: "short" }).of(language) || language;
23+
}
24+
25+
export const languageAddon: Addon = {
26+
title: "Language Selector",
27+
type: types.TOOL,
28+
render: ({ active }) => {
29+
const [globals, updateGlobals] = useGlobals();
30+
const selectedLanguage = globals.language || "en";
31+
32+
return (
33+
<WithTooltip
34+
placement="top"
35+
trigger="click"
36+
closeOnOutsideClick
37+
tooltip={({ onHide }) => {
38+
return (
39+
<TooltipLinkList
40+
links={languages.map((language) => ({
41+
id: language,
42+
title: languageTitle(language),
43+
active: selectedLanguage === language,
44+
onClick: async () => {
45+
// Update the global state with the selected language
46+
updateGlobals({ language });
47+
onHide();
48+
},
49+
}))}
50+
/>
51+
);
52+
}}
53+
>
54+
<IconButton title="Language">
55+
<GlobeIcon />
56+
{languageTitle(selectedLanguage)}
57+
</IconButton>
58+
</WithTooltip>
59+
);
60+
},
61+
};

.storybook/main.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,17 @@ Please see LICENSE files in the repository root for full details.
66
*/
77

88
import type { StorybookConfig } from "@storybook/react-vite";
9+
import path from "node:path";
10+
import { fileURLToPath } from "node:url";
11+
import { nodePolyfills } from "vite-plugin-node-polyfills";
12+
import { mergeConfig } from "vite";
13+
14+
const __filename = fileURLToPath(import.meta.url);
15+
const __dirname = path.dirname(__filename);
916

1017
const config: StorybookConfig = {
1118
stories: ["../src/shared-components/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
19+
staticDirs: ["../webapp"],
1220
addons: ["@storybook/addon-docs", "@storybook/addon-designs"],
1321
framework: "@storybook/react-vite",
1422
core: {
@@ -17,5 +25,17 @@ const config: StorybookConfig = {
1725
typescript: {
1826
reactDocgen: "react-docgen-typescript",
1927
},
28+
async viteFinal(config) {
29+
return mergeConfig(config, {
30+
resolve: {
31+
alias: {
32+
// Alias used by i18n.tsx
33+
$webapp: path.resolve(__dirname, "../webapp"),
34+
},
35+
},
36+
// Needed for counterpart to work
37+
plugins: [nodePolyfills({ include: ["process", "util"] })],
38+
});
39+
},
2040
};
2141
export default config;

.storybook/manager.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
55
Please see LICENSE files in the repository root for full details.
66
*/
77

8+
import React from "react";
9+
810
import { addons } from "storybook/manager-api";
911
import ElementTheme from "./ElementTheme";
12+
import { languageAddon } from "./languageAddon";
1013

1114
addons.setConfig({
1215
theme: ElementTheme,
1316
});
17+
18+
addons.register("elementhq/language", () => addons.add("language", languageAddon));

.storybook/preview.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import type { ArgTypes, Preview, Decorator } from "@storybook/react-vite";
2+
import { addons } from "storybook/preview-api";
23

34
import "../res/css/shared.pcss";
45
import "./preview.css";
56
import React, { useLayoutEffect } from "react";
7+
import { FORCE_RE_RENDER } from "storybook/internal/core-events";
8+
import { setLanguage } from "../src/shared-components/i18n";
69

710
export const globalTypes = {
811
theme: {
912
name: "Theme",
10-
defaultValue: "system",
1113
description: "Global theme for components",
1214
toolbar: {
1315
icon: "circlehollow",
@@ -21,6 +23,14 @@ export const globalTypes = {
2123
],
2224
},
2325
},
26+
language: {
27+
name: "Language",
28+
description: "Global language for components",
29+
},
30+
initialGlobals: {
31+
theme: "system",
32+
language: "en",
33+
},
2434
} satisfies ArgTypes;
2535

2636
const allThemesClasses = globalTypes.theme.toolbar.items.map(({ value }) => `cpd-theme-${value}`);
@@ -48,9 +58,33 @@ const withThemeProvider: Decorator = (Story, context) => {
4858
);
4959
};
5060

61+
const LanguageSwitcher: React.FC<{
62+
language: string;
63+
}> = ({ language }) => {
64+
useLayoutEffect(() => {
65+
const changeLanguage = async (language: string) => {
66+
await setLanguage(language);
67+
// Force the component to re-render to apply the new language
68+
addons.getChannel().emit(FORCE_RE_RENDER);
69+
};
70+
changeLanguage(language);
71+
}, [language]);
72+
73+
return null;
74+
};
75+
76+
export const withLanguageProvider: Decorator = (Story, context) => {
77+
return (
78+
<>
79+
<LanguageSwitcher language={context.globals.language} />
80+
<Story />
81+
</>
82+
);
83+
};
84+
5185
const preview: Preview = {
5286
tags: ["autodocs"],
53-
decorators: [withThemeProvider],
87+
decorators: [withThemeProvider, withLanguageProvider],
5488
};
5589

5690
export default preview;

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@
193193
"@sentry/webpack-plugin": "^3.0.0",
194194
"@storybook/addon-designs": "^10.0.1",
195195
"@storybook/addon-docs": "^9.0.12",
196+
"@storybook/icons": "^1.4.0",
196197
"@storybook/react-vite": "^9.0.15",
197198
"@storybook/test-runner": "^0.23.0",
198199
"@stylistic/eslint-plugin": "^5.0.0",
@@ -306,6 +307,7 @@
306307
"typescript": "5.8.3",
307308
"util": "^0.12.5",
308309
"vite": "^7.0.1",
310+
"vite-plugin-node-polyfills": "^0.24.0",
309311
"web-streams-polyfill": "^4.0.0",
310312
"webpack": "^5.89.0",
311313
"webpack-bundle-analyzer": "^4.8.0",

0 commit comments

Comments
 (0)