Skip to content

Commit 112cb9d

Browse files
feat(dashboard): Support localization for dashboard extensions (#3962)
1 parent 3881d46 commit 112cb9d

File tree

10 files changed

+365
-86
lines changed

10 files changed

+365
-86
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
---
2+
title: 'Localization'
3+
---
4+
5+
:::note
6+
Support for localization of Dashboard extensions was added in v3.5.1
7+
:::
8+
9+
The Dashboard uses [Lingui](https://lingui.dev/), which provides a powerful i18n solution for React:
10+
11+
- ICU MessageFormat support
12+
- Automatic message extraction
13+
- TypeScript integration
14+
- Pluralization support
15+
- Compile-time optimization
16+
17+
## Wrap your strings
18+
19+
First you'll need to wrap any strings that need to be localized:
20+
21+
```tsx
22+
import { Trans, useLingui } from '@lingui/react/macro';
23+
24+
function MyComponent() {
25+
const { t } = useLingui();
26+
27+
return (
28+
<div>
29+
<h1>
30+
// highlight-next-line
31+
<Trans>Welcome to Dashboard</Trans>
32+
</h1>
33+
// highlight-next-line
34+
<p>{t`Click here to continue`}</p>
35+
</div>
36+
);
37+
}
38+
```
39+
40+
You will mainly make use of the [Trans component](https://lingui.dev/ref/react#trans)
41+
and the [useLingui hook](https://lingui.dev/ref/react#uselingui).
42+
43+
## Extract translations
44+
45+
Create a `lingui.config.js` file in your project root, with references to any plugins that need to be localized:
46+
47+
```js title="lingui.config.js
48+
import { defineConfig } from '@lingui/cli';
49+
50+
export default defineConfig({
51+
sourceLocale: 'en',
52+
// Add any locales you wish to support
53+
locales: ['en', 'de'],
54+
catalogs: [
55+
// For each plugin you want to localize, add a catalog entry
56+
{
57+
// This is the output location of the generated .po files
58+
path: '<rootDir>/src/plugins/reviews/dashboard/i18n/{locale}',
59+
// This is the pattern that tells Lingui which files to scan
60+
// to extract translation strings
61+
include: ['<rootDir>/src/plugins/reviews/dashboard/**'],
62+
},
63+
],
64+
});
65+
```
66+
67+
Then extract the translations:
68+
69+
```bash
70+
npx lingui extract
71+
```
72+
73+
This will output the given locale files in the directories specified in the config file above.
74+
In this case:
75+
76+
```
77+
src/
78+
└── plugins/
79+
└── reviews/
80+
└── dashboard/
81+
└── i18n/
82+
├── en.po
83+
└── de.po
84+
```
85+
86+
Since we set the "sourceLocale" to be "en", the `en.po` file will already be complete. You'll then need to
87+
open up the `de.po` file and add German translations for each of the strings, by filling out the empty `msgstr` values:
88+
89+
```po title="de.po"
90+
#: test-plugins/reviews/dashboard/review-list.tsx:51
91+
msgid "Welcome to Dashboard"
92+
// highlight-next-line
93+
msgstr "Willkommen zum Dashboard"
94+
```

docs/sidebars.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ const sidebars = {
177177
'guides/extending-the-dashboard/alerts/index',
178178
'guides/extending-the-dashboard/data-fetching/index',
179179
'guides/extending-the-dashboard/theming/index',
180+
'guides/extending-the-dashboard/localization/index',
180181
'guides/extending-the-dashboard/deployment/index',
181182
'guides/extending-the-dashboard/tech-stack/index',
182183
'guides/extending-the-dashboard/migration/index',

packages/dashboard/src/lib/lib/load-i18n-messages.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ export async function loadI18nMessages(locale: string): Promise<Messages> {
1212
} else {
1313
// In dev mode we allow the dynamic import behaviour
1414
const { messages } = await import(`../../i18n/locales/${locale}.po`);
15-
return messages;
15+
const pluginTranslations = await import('virtual:plugin-translations');
16+
const safeLocale = locale.replace(/-/g, '_');
17+
const pluginTranslationsForLocale = pluginTranslations[safeLocale] ?? {};
18+
return { ...messages, ...pluginTranslationsForLocale };
1619
}
1720
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import path from 'path';
2+
3+
import { PluginInfo } from '../types.js';
4+
5+
/**
6+
* Returns an array of the paths to plugins, based on the info provided by the ConfigLoaderApi.
7+
*/
8+
export function getDashboardPaths(pluginInfo: PluginInfo[]) {
9+
return (
10+
pluginInfo
11+
?.flatMap(({ dashboardEntryPath, sourcePluginPath, pluginPath }) => {
12+
if (!dashboardEntryPath) {
13+
return [];
14+
}
15+
const sourcePaths = [];
16+
if (sourcePluginPath) {
17+
sourcePaths.push(
18+
path.join(path.dirname(sourcePluginPath), path.dirname(dashboardEntryPath)),
19+
);
20+
}
21+
if (pluginPath) {
22+
sourcePaths.push(path.join(path.dirname(pluginPath), path.dirname(dashboardEntryPath)));
23+
}
24+
return sourcePaths;
25+
})
26+
.filter(x => x != null) ?? []
27+
);
28+
}

packages/dashboard/vite/vite-plugin-tailwind-source.ts

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import path from 'path';
21
import { Plugin } from 'vite';
32

43
import { CompileResult } from './utils/compiler.js';
4+
import { getDashboardPaths } from './utils/get-dashboard-paths.js';
55
import { ConfigLoaderApi, getConfigLoaderApi } from './vite-plugin-config-loader.js';
66

77
/**
@@ -25,29 +25,7 @@ export function dashboardTailwindSourcePlugin(): Plugin {
2525
loadVendureConfigResult = await configLoaderApi.getVendureConfig();
2626
}
2727
const { pluginInfo } = loadVendureConfigResult;
28-
const dashboardExtensionDirs =
29-
pluginInfo
30-
?.flatMap(({ dashboardEntryPath, sourcePluginPath, pluginPath }) => {
31-
if (!dashboardEntryPath) {
32-
return [];
33-
}
34-
const sourcePaths = [];
35-
if (sourcePluginPath) {
36-
sourcePaths.push(
37-
path.join(
38-
path.dirname(sourcePluginPath),
39-
path.dirname(dashboardEntryPath),
40-
),
41-
);
42-
}
43-
if (pluginPath) {
44-
sourcePaths.push(
45-
path.join(path.dirname(pluginPath), path.dirname(dashboardEntryPath)),
46-
);
47-
}
48-
return sourcePaths;
49-
})
50-
.filter(x => x != null) ?? [];
28+
const dashboardExtensionDirs = getDashboardPaths(pluginInfo);
5129
const sources = dashboardExtensionDirs
5230
.map(extension => {
5331
return `@source '${extension}';`;

0 commit comments

Comments
 (0)