Skip to content

Commit 226984c

Browse files
authored
feat(admin-*,dashboard): add dashboard i18n extensions (medusajs#13763)
* virtual i18n module * changeset * fallback ns fallback to the default "translation" ns if the key isnt found. Allows to use a single "useTranslation("customNs")" hook for both custom and medusa-provided keys * simplify merges * optional for backward compat * fix HMR * fix generated deepMerge * test
1 parent 012e308 commit 226984c

File tree

25 files changed

+314
-9
lines changed

25 files changed

+314
-9
lines changed

.changeset/beige-shirts-work.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@medusajs/admin-vite-plugin": patch
3+
"@medusajs/admin-bundler": patch
4+
"@medusajs/admin-shared": patch
5+
"@medusajs/dashboard": patch
6+
---
7+
8+
feat(admin-\*,dashboard): add dashboard i18n extensions

packages/admin/admin-bundler/src/commands/plugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export async function plugin(options: PluginOptions) {
2424
"react",
2525
"react/jsx-runtime",
2626
"react-router-dom",
27+
"react-i18next",
2728
"@medusajs/js-sdk",
2829
"@medusajs/admin-sdk",
2930
"@tanstack/react-query",

packages/admin/admin-bundler/src/utils/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export async function getViteConfig(
3636
"react/jsx-runtime",
3737
"react-dom/client",
3838
"react-router-dom",
39+
"react-i18next",
3940
"@medusajs/ui",
4041
"@medusajs/dashboard",
4142
"@medusajs/js-sdk",

packages/admin/admin-shared/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from "./extensions/custom-fields"
22
export * from "./extensions/routes"
33
export * from "./extensions/widgets"
44
export * from "./virtual-modules"
5+
export * from "./utils"
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { isObject } from "./is-object"
2+
3+
export function deepMerge(target: any, source: any) {
4+
const recursive = (a:any, b:any) => {
5+
if (!isObject(a)) {
6+
return b
7+
}
8+
if (!isObject(b)) {
9+
return a
10+
}
11+
12+
Object.keys(b).forEach((key) => {
13+
if (isObject((b as any)[key])) {
14+
(a as any)[key] ??= {};
15+
(a as any)[key] = deepMerge((a as any)[key], (b as any)[key])
16+
} else {
17+
(a as any)[key] = (b as any)[key]
18+
}
19+
})
20+
21+
return a
22+
}
23+
24+
const copy = { ...target }
25+
return recursive(copy, source)
26+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./deep-merge"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function isObject(obj: any): obj is object {
2+
return obj != null && obj?.constructor?.name === "Object"
3+
}

packages/admin/admin-shared/src/virtual-modules/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export const DISPLAY_VIRTUAL_MODULE = `virtual:medusa/displays`
44
export const ROUTE_VIRTUAL_MODULE = `virtual:medusa/routes`
55
export const MENU_ITEM_VIRTUAL_MODULE = `virtual:medusa/menu-items`
66
export const WIDGET_VIRTUAL_MODULE = `virtual:medusa/widgets`
7+
export const I18N_VIRTUAL_MODULE = `virtual:medusa/i18n`
78

89
export const VIRTUAL_MODULES = [
910
LINK_VIRTUAL_MODULE,
@@ -12,4 +13,5 @@ export const VIRTUAL_MODULES = [
1213
ROUTE_VIRTUAL_MODULE,
1314
MENU_ITEM_VIRTUAL_MODULE,
1415
WIDGET_VIRTUAL_MODULE,
16+
I18N_VIRTUAL_MODULE,
1517
] as const
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { describe, expect, it, vi } from "vitest"
2+
3+
import * as utils from "../../utils"
4+
import { generateI18n } from "../generate-i18n"
5+
6+
// Mock the dependencies
7+
vi.mock("../../utils", async () => {
8+
const actual = await vi.importActual("../../utils")
9+
return {
10+
...actual,
11+
crawl: vi.fn(),
12+
}
13+
})
14+
15+
const expectedI18nSingleSource = `
16+
resources: i18nTranslations0
17+
`
18+
19+
const expectedI18nMultipleSources = `
20+
resources: deepMerge(deepMerge(i18nTranslations0, i18nTranslations1), i18nTranslations2)
21+
`
22+
23+
const expectedI18nNoSources = `
24+
resources: {}
25+
`
26+
27+
describe("generateI18n", () => {
28+
it("should generate i18n with single source", async () => {
29+
const mockFiles = ["Users/user/medusa/src/admin/i18n/index.ts"]
30+
vi.mocked(utils.crawl).mockResolvedValue(mockFiles)
31+
32+
const result = await generateI18n(
33+
new Set(["Users/user/medusa/src/admin"])
34+
)
35+
36+
expect(result.imports).toEqual([
37+
`import i18nTranslations0 from "Users/user/medusa/src/admin/i18n/index.ts"`,
38+
])
39+
expect(utils.normalizeString(result.code)).toEqual(
40+
utils.normalizeString(expectedI18nSingleSource)
41+
)
42+
})
43+
44+
it("should handle windows paths", async () => {
45+
const mockFiles = ["C:\\medusa\\src\\admin\\i18n\\index.ts"]
46+
vi.mocked(utils.crawl).mockResolvedValue(mockFiles)
47+
48+
const result = await generateI18n(new Set(["C:\\medusa\\src\\admin"]))
49+
50+
expect(result.imports).toEqual([
51+
`import i18nTranslations0 from "C:/medusa/src/admin/i18n/index.ts"`,
52+
])
53+
expect(utils.normalizeString(result.code)).toEqual(
54+
utils.normalizeString(expectedI18nSingleSource)
55+
)
56+
})
57+
58+
it("should generate i18n with multiple sources", async () => {
59+
vi.mocked(utils.crawl)
60+
.mockResolvedValueOnce(["Users/user/medusa/src/admin/i18n/index.ts"])
61+
.mockResolvedValueOnce(["Users/user/medusa/src/plugin1/i18n/index.ts"])
62+
.mockResolvedValueOnce(["Users/user/medusa/src/plugin2/i18n/index.ts"])
63+
64+
const result = await generateI18n(
65+
new Set([
66+
"Users/user/medusa/src/admin",
67+
"Users/user/medusa/src/plugin1",
68+
"Users/user/medusa/src/plugin2",
69+
])
70+
)
71+
72+
expect(result.imports).toEqual([
73+
`import i18nTranslations0 from "Users/user/medusa/src/admin/i18n/index.ts"`,
74+
`import i18nTranslations1 from "Users/user/medusa/src/plugin1/i18n/index.ts"`,
75+
`import i18nTranslations2 from "Users/user/medusa/src/plugin2/i18n/index.ts"`,
76+
])
77+
expect(utils.normalizeString(result.code)).toEqual(
78+
utils.normalizeString(expectedI18nMultipleSources)
79+
)
80+
})
81+
82+
it("should handle no i18n sources", async () => {
83+
vi.mocked(utils.crawl).mockResolvedValue([])
84+
85+
const result = await generateI18n(
86+
new Set(["Users/user/medusa/src/admin"])
87+
)
88+
89+
expect(result.imports).toEqual([])
90+
expect(utils.normalizeString(result.code)).toEqual(
91+
utils.normalizeString(expectedI18nNoSources)
92+
)
93+
})
94+
})
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import fs from "fs/promises"
2+
import { generateHash } from "../utils"
3+
import { getI18nIndexFilesFromSources } from "./helpers"
4+
5+
export async function generateI18nHash(sources: Set<string>): Promise<string> {
6+
const indexFiles = await getI18nIndexFilesFromSources(sources)
7+
8+
const contents = await Promise.all(
9+
indexFiles.map(file => fs.readFile(file, "utf-8"))
10+
)
11+
12+
const totalContent = contents.join("")
13+
14+
return generateHash(totalContent)
15+
}

0 commit comments

Comments
 (0)