feat(admin-*,dashboard): add dashboard i18n extensions (#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
This commit is contained in:
1 parent
012e30801e
commit
226984cf0f
25 files changed
+314
-9
No files matched your search
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/admin-vite-plugin": patch
|
||||||
|
"@medusajs/admin-bundler": patch
|
||||||
|
"@medusajs/admin-shared": patch
|
||||||
|
"@medusajs/dashboard": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(admin-\*,dashboard): add dashboard i18n extensions
|
||||||
@@ -24,6 +24,7 @@ export async function plugin(options: PluginOptions) {
|
|||||||
"react",
|
"react",
|
||||||
"react/jsx-runtime",
|
"react/jsx-runtime",
|
||||||
"react-router-dom",
|
"react-router-dom",
|
||||||
|
"react-i18next",
|
||||||
"@medusajs/js-sdk",
|
"@medusajs/js-sdk",
|
||||||
"@medusajs/admin-sdk",
|
"@medusajs/admin-sdk",
|
||||||
"@tanstack/react-query",
|
"@tanstack/react-query",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export async function getViteConfig(
|
|||||||
"react/jsx-runtime",
|
"react/jsx-runtime",
|
||||||
"react-dom/client",
|
"react-dom/client",
|
||||||
"react-router-dom",
|
"react-router-dom",
|
||||||
|
"react-i18next",
|
||||||
"@medusajs/ui",
|
"@medusajs/ui",
|
||||||
"@medusajs/dashboard",
|
"@medusajs/dashboard",
|
||||||
"@medusajs/js-sdk",
|
"@medusajs/js-sdk",
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ export * from "./extensions/custom-fields"
|
|||||||
export * from "./extensions/routes"
|
export * from "./extensions/routes"
|
||||||
export * from "./extensions/widgets"
|
export * from "./extensions/widgets"
|
||||||
export * from "./virtual-modules"
|
export * from "./virtual-modules"
|
||||||
|
export * from "./utils"
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { isObject } from "./is-object"
|
||||||
|
|
||||||
|
export function deepMerge(target: any, source: any) {
|
||||||
|
const recursive = (a:any, b:any) => {
|
||||||
|
if (!isObject(a)) {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
if (!isObject(b)) {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(b).forEach((key) => {
|
||||||
|
if (isObject((b as any)[key])) {
|
||||||
|
(a as any)[key] ??= {};
|
||||||
|
(a as any)[key] = deepMerge((a as any)[key], (b as any)[key])
|
||||||
|
} else {
|
||||||
|
(a as any)[key] = (b as any)[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
const copy = { ...target }
|
||||||
|
return recursive(copy, source)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./deep-merge"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function isObject(obj: any): obj is object {
|
||||||
|
return obj != null && obj?.constructor?.name === "Object"
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ export const DISPLAY_VIRTUAL_MODULE = `virtual:medusa/displays`
|
|||||||
export const ROUTE_VIRTUAL_MODULE = `virtual:medusa/routes`
|
export const ROUTE_VIRTUAL_MODULE = `virtual:medusa/routes`
|
||||||
export const MENU_ITEM_VIRTUAL_MODULE = `virtual:medusa/menu-items`
|
export const MENU_ITEM_VIRTUAL_MODULE = `virtual:medusa/menu-items`
|
||||||
export const WIDGET_VIRTUAL_MODULE = `virtual:medusa/widgets`
|
export const WIDGET_VIRTUAL_MODULE = `virtual:medusa/widgets`
|
||||||
|
export const I18N_VIRTUAL_MODULE = `virtual:medusa/i18n`
|
||||||
|
|
||||||
export const VIRTUAL_MODULES = [
|
export const VIRTUAL_MODULES = [
|
||||||
LINK_VIRTUAL_MODULE,
|
LINK_VIRTUAL_MODULE,
|
||||||
@@ -12,4 +13,5 @@ export const VIRTUAL_MODULES = [
|
|||||||
ROUTE_VIRTUAL_MODULE,
|
ROUTE_VIRTUAL_MODULE,
|
||||||
MENU_ITEM_VIRTUAL_MODULE,
|
MENU_ITEM_VIRTUAL_MODULE,
|
||||||
WIDGET_VIRTUAL_MODULE,
|
WIDGET_VIRTUAL_MODULE,
|
||||||
|
I18N_VIRTUAL_MODULE,
|
||||||
] as const
|
] as const
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest"
|
||||||
|
|
||||||
|
import * as utils from "../../utils"
|
||||||
|
import { generateI18n } from "../generate-i18n"
|
||||||
|
|
||||||
|
// Mock the dependencies
|
||||||
|
vi.mock("../../utils", async () => {
|
||||||
|
const actual = await vi.importActual("../../utils")
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
crawl: vi.fn(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const expectedI18nSingleSource = `
|
||||||
|
resources: i18nTranslations0
|
||||||
|
`
|
||||||
|
|
||||||
|
const expectedI18nMultipleSources = `
|
||||||
|
resources: deepMerge(deepMerge(i18nTranslations0, i18nTranslations1), i18nTranslations2)
|
||||||
|
`
|
||||||
|
|
||||||
|
const expectedI18nNoSources = `
|
||||||
|
resources: {}
|
||||||
|
`
|
||||||
|
|
||||||
|
describe("generateI18n", () => {
|
||||||
|
it("should generate i18n with single source", async () => {
|
||||||
|
const mockFiles = ["Users/user/medusa/src/admin/i18n/index.ts"]
|
||||||
|
vi.mocked(utils.crawl).mockResolvedValue(mockFiles)
|
||||||
|
|
||||||
|
const result = await generateI18n(
|
||||||
|
new Set(["Users/user/medusa/src/admin"])
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.imports).toEqual([
|
||||||
|
`import i18nTranslations0 from "Users/user/medusa/src/admin/i18n/index.ts"`,
|
||||||
|
])
|
||||||
|
expect(utils.normalizeString(result.code)).toEqual(
|
||||||
|
utils.normalizeString(expectedI18nSingleSource)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle windows paths", async () => {
|
||||||
|
const mockFiles = ["C:\\medusa\\src\\admin\\i18n\\index.ts"]
|
||||||
|
vi.mocked(utils.crawl).mockResolvedValue(mockFiles)
|
||||||
|
|
||||||
|
const result = await generateI18n(new Set(["C:\\medusa\\src\\admin"]))
|
||||||
|
|
||||||
|
expect(result.imports).toEqual([
|
||||||
|
`import i18nTranslations0 from "C:/medusa/src/admin/i18n/index.ts"`,
|
||||||
|
])
|
||||||
|
expect(utils.normalizeString(result.code)).toEqual(
|
||||||
|
utils.normalizeString(expectedI18nSingleSource)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should generate i18n with multiple sources", async () => {
|
||||||
|
vi.mocked(utils.crawl)
|
||||||
|
.mockResolvedValueOnce(["Users/user/medusa/src/admin/i18n/index.ts"])
|
||||||
|
.mockResolvedValueOnce(["Users/user/medusa/src/plugin1/i18n/index.ts"])
|
||||||
|
.mockResolvedValueOnce(["Users/user/medusa/src/plugin2/i18n/index.ts"])
|
||||||
|
|
||||||
|
const result = await generateI18n(
|
||||||
|
new Set([
|
||||||
|
"Users/user/medusa/src/admin",
|
||||||
|
"Users/user/medusa/src/plugin1",
|
||||||
|
"Users/user/medusa/src/plugin2",
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.imports).toEqual([
|
||||||
|
`import i18nTranslations0 from "Users/user/medusa/src/admin/i18n/index.ts"`,
|
||||||
|
`import i18nTranslations1 from "Users/user/medusa/src/plugin1/i18n/index.ts"`,
|
||||||
|
`import i18nTranslations2 from "Users/user/medusa/src/plugin2/i18n/index.ts"`,
|
||||||
|
])
|
||||||
|
expect(utils.normalizeString(result.code)).toEqual(
|
||||||
|
utils.normalizeString(expectedI18nMultipleSources)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle no i18n sources", async () => {
|
||||||
|
vi.mocked(utils.crawl).mockResolvedValue([])
|
||||||
|
|
||||||
|
const result = await generateI18n(
|
||||||
|
new Set(["Users/user/medusa/src/admin"])
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.imports).toEqual([])
|
||||||
|
expect(utils.normalizeString(result.code)).toEqual(
|
||||||
|
utils.normalizeString(expectedI18nNoSources)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import fs from "fs/promises"
|
||||||
|
import { generateHash } from "../utils"
|
||||||
|
import { getI18nIndexFilesFromSources } from "./helpers"
|
||||||
|
|
||||||
|
export async function generateI18nHash(sources: Set<string>): Promise<string> {
|
||||||
|
const indexFiles = await getI18nIndexFilesFromSources(sources)
|
||||||
|
|
||||||
|
const contents = await Promise.all(
|
||||||
|
indexFiles.map(file => fs.readFile(file, "utf-8"))
|
||||||
|
)
|
||||||
|
|
||||||
|
const totalContent = contents.join("")
|
||||||
|
|
||||||
|
return generateHash(totalContent)
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { outdent } from "outdent"
|
||||||
|
import { normalizePath } from "../utils"
|
||||||
|
import { getI18nIndexFilesFromSources } from "./helpers"
|
||||||
|
|
||||||
|
export async function generateI18n(sources: Set<string>) {
|
||||||
|
const indexFiles = await getI18nIndexFilesFromSources(sources)
|
||||||
|
|
||||||
|
const imports = indexFiles.map((file, index) => {
|
||||||
|
const normalizedPath = normalizePath(file)
|
||||||
|
return `import i18nTranslations${index} from "${normalizedPath}"`
|
||||||
|
})
|
||||||
|
|
||||||
|
let mergeCode = '{}'
|
||||||
|
if (indexFiles.length === 1) {
|
||||||
|
mergeCode = 'i18nTranslations0'
|
||||||
|
} else if (indexFiles.length > 1) {
|
||||||
|
// Only happens in dev mode if there are 2+ plugins linked with plugin:develop
|
||||||
|
// Chain deepMerge calls since it only accepts 2 arguments
|
||||||
|
mergeCode = indexFiles.slice(1).reduce((acc, _, index) => {
|
||||||
|
return `deepMerge(${acc}, i18nTranslations${index + 1})`
|
||||||
|
}, 'i18nTranslations0')
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = outdent`
|
||||||
|
resources: ${mergeCode}
|
||||||
|
`
|
||||||
|
|
||||||
|
return {
|
||||||
|
imports,
|
||||||
|
code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { crawl } from "../utils"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get i18n index files from sources
|
||||||
|
* Looks for src/admin/i18n/index.ts in each source
|
||||||
|
*/
|
||||||
|
export async function getI18nIndexFilesFromSources(
|
||||||
|
sources: Set<string>
|
||||||
|
): Promise<string[]> {
|
||||||
|
return (await Promise.all(
|
||||||
|
Array.from(sources).map(async (source) =>
|
||||||
|
crawl(`${source}/i18n`, "index", { min: 0, max: 0 })
|
||||||
|
)
|
||||||
|
)).flat()
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { generateI18nHash } from "./generate-i18n-hash"
|
||||||
|
export { getI18nIndexFilesFromSources } from "./helpers"
|
||||||
|
export { generateI18n } from "./generate-i18n"
|
||||||
@@ -3,12 +3,14 @@ import { rm, writeFile } from "node:fs/promises"
|
|||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import type * as Vite from "vite"
|
import type * as Vite from "vite"
|
||||||
import { generateCustomFieldHashes } from "./custom-fields"
|
import { generateCustomFieldHashes } from "./custom-fields"
|
||||||
|
import { generateI18nHash } from "./i18n"
|
||||||
import { generateRouteHashes } from "./routes"
|
import { generateRouteHashes } from "./routes"
|
||||||
import { MedusaVitePlugin } from "./types"
|
import { MedusaVitePlugin } from "./types"
|
||||||
import { AdminSubdirectory, isFileInAdminSubdirectory } from "./utils"
|
import { AdminSubdirectory, isFileInAdminSubdirectory } from "./utils"
|
||||||
import {
|
import {
|
||||||
generateVirtualDisplayModule,
|
generateVirtualDisplayModule,
|
||||||
generateVirtualFormModule,
|
generateVirtualFormModule,
|
||||||
|
generateVirtualI18nModule,
|
||||||
generateVirtualLinkModule,
|
generateVirtualLinkModule,
|
||||||
generateVirtualMenuItemModule,
|
generateVirtualMenuItemModule,
|
||||||
generateVirtualRouteModule,
|
generateVirtualRouteModule,
|
||||||
@@ -84,6 +86,7 @@ export const medusaVitePlugin: MedusaVitePlugin = (options) => {
|
|||||||
const menuItemModule = await generateVirtualMenuItemModule(sources, true)
|
const menuItemModule = await generateVirtualMenuItemModule(sources, true)
|
||||||
const formModule = await generateVirtualFormModule(sources, true)
|
const formModule = await generateVirtualFormModule(sources, true)
|
||||||
const displayModule = await generateVirtualDisplayModule(sources, true)
|
const displayModule = await generateVirtualDisplayModule(sources, true)
|
||||||
|
const i18nModule = await generateVirtualI18nModule(sources, true)
|
||||||
|
|
||||||
// Create the index.js content that re-exports everything
|
// Create the index.js content that re-exports everything
|
||||||
return `
|
return `
|
||||||
@@ -93,13 +96,15 @@ export const medusaVitePlugin: MedusaVitePlugin = (options) => {
|
|||||||
${menuItemModule.code}
|
${menuItemModule.code}
|
||||||
${formModule.code}
|
${formModule.code}
|
||||||
${displayModule.code}
|
${displayModule.code}
|
||||||
|
${i18nModule.code}
|
||||||
|
|
||||||
const plugin = {
|
const plugin = {
|
||||||
widgetModule,
|
widgetModule,
|
||||||
routeModule,
|
routeModule,
|
||||||
menuItemModule,
|
menuItemModule,
|
||||||
formModule,
|
formModule,
|
||||||
displayModule
|
displayModule,
|
||||||
|
i18nModule
|
||||||
}
|
}
|
||||||
|
|
||||||
export default plugin
|
export default plugin
|
||||||
@@ -229,6 +234,11 @@ const loadConfigs: Record<string, ModuleConfig> = {
|
|||||||
moduleGenerator: async (sources) => generateVirtualMenuItemModule(sources),
|
moduleGenerator: async (sources) => generateVirtualMenuItemModule(sources),
|
||||||
hashKey: vmod.virtual.menuItem,
|
hashKey: vmod.virtual.menuItem,
|
||||||
},
|
},
|
||||||
|
[vmod.resolved.i18n]: {
|
||||||
|
hashGenerator: async (sources) => generateI18nHash(sources),
|
||||||
|
moduleGenerator: async (sources) => generateVirtualI18nModule(sources),
|
||||||
|
hashKey: vmod.virtual.i18n,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type WatcherConfig = {
|
type WatcherConfig = {
|
||||||
@@ -292,4 +302,17 @@ const watcherConfigs: WatcherConfig[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
subdirectory: "i18n",
|
||||||
|
hashGenerator: async (sources) => ({
|
||||||
|
i18nHash: await generateI18nHash(sources),
|
||||||
|
}),
|
||||||
|
modules: [
|
||||||
|
{
|
||||||
|
virtualModule: vmod.virtual.i18n,
|
||||||
|
resolvedModule: vmod.resolved.i18n,
|
||||||
|
hashKey: "i18nHash",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
@@ -49,7 +49,7 @@ export function generateModule(code: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VALID_FILE_EXTENSIONS = [".tsx", ".jsx", ".js"]
|
export const VALID_FILE_EXTENSIONS = [".tsx", ".jsx", ".js", ".ts"]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Crawls a directory and returns all files that match the criteria.
|
* Crawls a directory and returns all files that match the criteria.
|
||||||
@@ -176,7 +176,7 @@ export function generateHash(content: string) {
|
|||||||
return crypto.createHash("md5").update(content).digest("hex")
|
return crypto.createHash("md5").update(content).digest("hex")
|
||||||
}
|
}
|
||||||
|
|
||||||
const ADMIN_SUBDIRECTORIES = ["routes", "custom-fields", "widgets"] as const
|
const ADMIN_SUBDIRECTORIES = ["routes", "custom-fields", "widgets", "i18n"] as const
|
||||||
|
|
||||||
export type AdminSubdirectory = (typeof ADMIN_SUBDIRECTORIES)[number]
|
export type AdminSubdirectory = (typeof ADMIN_SUBDIRECTORIES)[number]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import outdent from "outdent"
|
||||||
|
import { generateModule } from "../utils"
|
||||||
|
import { generateI18n } from "../i18n"
|
||||||
|
|
||||||
|
export async function generateVirtualI18nModule(
|
||||||
|
sources: Set<string>,
|
||||||
|
pluginMode = false
|
||||||
|
) {
|
||||||
|
const i18n = await generateI18n(sources)
|
||||||
|
|
||||||
|
const imports = [
|
||||||
|
'import { deepMerge } from "@medusajs/admin-shared"',
|
||||||
|
...i18n.imports
|
||||||
|
]
|
||||||
|
|
||||||
|
const code = outdent`
|
||||||
|
${imports.join("\n")}
|
||||||
|
|
||||||
|
${pluginMode
|
||||||
|
? `const i18nModule = { ${i18n.code} }`
|
||||||
|
: `export default { ${i18n.code} }`
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
return generateModule(code)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -4,3 +4,4 @@ export * from "./generate-virtual-link-module"
|
|||||||
export * from "./generate-virtual-menu-item-module"
|
export * from "./generate-virtual-menu-item-module"
|
||||||
export * from "./generate-virtual-route-module"
|
export * from "./generate-virtual-route-module"
|
||||||
export * from "./generate-virtual-widget-module"
|
export * from "./generate-virtual-widget-module"
|
||||||
|
export * from "./generate-virtual-i18n-module"
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
DISPLAY_VIRTUAL_MODULE,
|
DISPLAY_VIRTUAL_MODULE,
|
||||||
FORM_VIRTUAL_MODULE,
|
FORM_VIRTUAL_MODULE,
|
||||||
|
I18N_VIRTUAL_MODULE,
|
||||||
LINK_VIRTUAL_MODULE,
|
LINK_VIRTUAL_MODULE,
|
||||||
MENU_ITEM_VIRTUAL_MODULE,
|
MENU_ITEM_VIRTUAL_MODULE,
|
||||||
ROUTE_VIRTUAL_MODULE,
|
ROUTE_VIRTUAL_MODULE,
|
||||||
@@ -13,6 +14,7 @@ const RESOLVED_DISPLAY_VIRTUAL_MODULE = `\0${DISPLAY_VIRTUAL_MODULE}`
|
|||||||
const RESOLVED_ROUTE_VIRTUAL_MODULE = `\0${ROUTE_VIRTUAL_MODULE}`
|
const RESOLVED_ROUTE_VIRTUAL_MODULE = `\0${ROUTE_VIRTUAL_MODULE}`
|
||||||
const RESOLVED_MENU_ITEM_VIRTUAL_MODULE = `\0${MENU_ITEM_VIRTUAL_MODULE}`
|
const RESOLVED_MENU_ITEM_VIRTUAL_MODULE = `\0${MENU_ITEM_VIRTUAL_MODULE}`
|
||||||
const RESOLVED_WIDGET_VIRTUAL_MODULE = `\0${WIDGET_VIRTUAL_MODULE}`
|
const RESOLVED_WIDGET_VIRTUAL_MODULE = `\0${WIDGET_VIRTUAL_MODULE}`
|
||||||
|
const RESOLVED_I18N_VIRTUAL_MODULE = `\0${I18N_VIRTUAL_MODULE}`
|
||||||
|
|
||||||
const VIRTUAL_MODULES = [
|
const VIRTUAL_MODULES = [
|
||||||
LINK_VIRTUAL_MODULE,
|
LINK_VIRTUAL_MODULE,
|
||||||
@@ -21,6 +23,7 @@ const VIRTUAL_MODULES = [
|
|||||||
ROUTE_VIRTUAL_MODULE,
|
ROUTE_VIRTUAL_MODULE,
|
||||||
MENU_ITEM_VIRTUAL_MODULE,
|
MENU_ITEM_VIRTUAL_MODULE,
|
||||||
WIDGET_VIRTUAL_MODULE,
|
WIDGET_VIRTUAL_MODULE,
|
||||||
|
I18N_VIRTUAL_MODULE,
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
const RESOLVED_VIRTUAL_MODULES = [
|
const RESOLVED_VIRTUAL_MODULES = [
|
||||||
@@ -30,6 +33,7 @@ const RESOLVED_VIRTUAL_MODULES = [
|
|||||||
RESOLVED_ROUTE_VIRTUAL_MODULE,
|
RESOLVED_ROUTE_VIRTUAL_MODULE,
|
||||||
RESOLVED_MENU_ITEM_VIRTUAL_MODULE,
|
RESOLVED_MENU_ITEM_VIRTUAL_MODULE,
|
||||||
RESOLVED_WIDGET_VIRTUAL_MODULE,
|
RESOLVED_WIDGET_VIRTUAL_MODULE,
|
||||||
|
RESOLVED_I18N_VIRTUAL_MODULE,
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export function resolveVirtualId(id: string) {
|
export function resolveVirtualId(id: string) {
|
||||||
@@ -55,6 +59,8 @@ export type VirtualModule =
|
|||||||
| typeof ROUTE_VIRTUAL_MODULE
|
| typeof ROUTE_VIRTUAL_MODULE
|
||||||
| typeof MENU_ITEM_VIRTUAL_MODULE
|
| typeof MENU_ITEM_VIRTUAL_MODULE
|
||||||
| typeof WIDGET_VIRTUAL_MODULE
|
| typeof WIDGET_VIRTUAL_MODULE
|
||||||
|
| typeof I18N_VIRTUAL_MODULE
|
||||||
|
|
||||||
const resolvedVirtualModuleIds = {
|
const resolvedVirtualModuleIds = {
|
||||||
link: RESOLVED_LINK_VIRTUAL_MODULE,
|
link: RESOLVED_LINK_VIRTUAL_MODULE,
|
||||||
form: RESOLVED_FORM_VIRTUAL_MODULE,
|
form: RESOLVED_FORM_VIRTUAL_MODULE,
|
||||||
@@ -62,6 +68,7 @@ const resolvedVirtualModuleIds = {
|
|||||||
route: RESOLVED_ROUTE_VIRTUAL_MODULE,
|
route: RESOLVED_ROUTE_VIRTUAL_MODULE,
|
||||||
menuItem: RESOLVED_MENU_ITEM_VIRTUAL_MODULE,
|
menuItem: RESOLVED_MENU_ITEM_VIRTUAL_MODULE,
|
||||||
widget: RESOLVED_WIDGET_VIRTUAL_MODULE,
|
widget: RESOLVED_WIDGET_VIRTUAL_MODULE,
|
||||||
|
i18n: RESOLVED_I18N_VIRTUAL_MODULE,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
const virtualModuleIds = {
|
const virtualModuleIds = {
|
||||||
@@ -71,6 +78,7 @@ const virtualModuleIds = {
|
|||||||
route: ROUTE_VIRTUAL_MODULE,
|
route: ROUTE_VIRTUAL_MODULE,
|
||||||
menuItem: MENU_ITEM_VIRTUAL_MODULE,
|
menuItem: MENU_ITEM_VIRTUAL_MODULE,
|
||||||
widget: WIDGET_VIRTUAL_MODULE,
|
widget: WIDGET_VIRTUAL_MODULE,
|
||||||
|
i18n: I18N_VIRTUAL_MODULE,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const vmod = {
|
export const vmod = {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { DashboardPlugin } from "./dashboard-app/types"
|
|||||||
|
|
||||||
import displayModule from "virtual:medusa/displays"
|
import displayModule from "virtual:medusa/displays"
|
||||||
import formModule from "virtual:medusa/forms"
|
import formModule from "virtual:medusa/forms"
|
||||||
|
import i18nModule from "virtual:medusa/i18n"
|
||||||
import menuItemModule from "virtual:medusa/menu-items"
|
import menuItemModule from "virtual:medusa/menu-items"
|
||||||
import routeModule from "virtual:medusa/routes"
|
import routeModule from "virtual:medusa/routes"
|
||||||
import widgetModule from "virtual:medusa/widgets"
|
import widgetModule from "virtual:medusa/widgets"
|
||||||
@@ -15,6 +16,7 @@ const localPlugin = {
|
|||||||
displayModule,
|
displayModule,
|
||||||
formModule,
|
formModule,
|
||||||
menuItemModule,
|
menuItemModule,
|
||||||
|
i18nModule,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppProps {
|
interface AppProps {
|
||||||
|
|||||||
@@ -3,12 +3,16 @@ import LanguageDetector from "i18next-browser-languagedetector"
|
|||||||
import { initReactI18next } from "react-i18next"
|
import { initReactI18next } from "react-i18next"
|
||||||
|
|
||||||
import { defaultI18nOptions } from "../../../i18n/config"
|
import { defaultI18nOptions } from "../../../i18n/config"
|
||||||
|
import { useExtension } from "../../../providers/extension-provider"
|
||||||
|
|
||||||
export const I18n = () => {
|
export const I18n = () => {
|
||||||
|
const { getI18nResources } = useExtension()
|
||||||
|
|
||||||
if (i18n.isInitialized) {
|
if (i18n.isInitialized) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resources = getI18nResources()
|
||||||
i18n
|
i18n
|
||||||
.use(
|
.use(
|
||||||
new LanguageDetector(null, {
|
new LanguageDetector(null, {
|
||||||
@@ -17,7 +21,11 @@ export const I18n = () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.use(initReactI18next)
|
.use(initReactI18next)
|
||||||
.init(defaultI18nOptions)
|
.init({
|
||||||
|
...defaultI18nOptions,
|
||||||
|
resources,
|
||||||
|
supportedLngs: Object.keys(resources),
|
||||||
|
})
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
CustomFieldFormTab,
|
CustomFieldFormTab,
|
||||||
CustomFieldFormZone,
|
CustomFieldFormZone,
|
||||||
CustomFieldModel,
|
CustomFieldModel,
|
||||||
|
deepMerge,
|
||||||
InjectionZone,
|
InjectionZone,
|
||||||
NESTED_ROUTE_POSITIONS,
|
NESTED_ROUTE_POSITIONS,
|
||||||
} from "@medusajs/admin-shared"
|
} from "@medusajs/admin-shared"
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
} from "react-router-dom"
|
} from "react-router-dom"
|
||||||
import { INavItem } from "../components/layout/nav-item"
|
import { INavItem } from "../components/layout/nav-item"
|
||||||
import { Providers } from "../providers"
|
import { Providers } from "../providers"
|
||||||
|
import coreTranslations from "../i18n/translations"
|
||||||
import { getRouteMap } from "./routes/get-route.map"
|
import { getRouteMap } from "./routes/get-route.map"
|
||||||
import { createRouteMap, getRouteExtensions } from "./routes/utils"
|
import { createRouteMap, getRouteExtensions } from "./routes/utils"
|
||||||
import {
|
import {
|
||||||
@@ -28,6 +30,7 @@ import {
|
|||||||
FormFieldExtension,
|
FormFieldExtension,
|
||||||
FormFieldMap,
|
FormFieldMap,
|
||||||
FormZoneMap,
|
FormZoneMap,
|
||||||
|
I18nExtension,
|
||||||
MenuItemExtension,
|
MenuItemExtension,
|
||||||
MenuItemKey,
|
MenuItemKey,
|
||||||
MenuMap,
|
MenuMap,
|
||||||
@@ -47,6 +50,7 @@ export class DashboardApp {
|
|||||||
private displays: DisplayMap
|
private displays: DisplayMap
|
||||||
private coreRoutes: RouteObject[]
|
private coreRoutes: RouteObject[]
|
||||||
private settingsRoutes: RouteObject[]
|
private settingsRoutes: RouteObject[]
|
||||||
|
private i18nResources: I18nExtension
|
||||||
|
|
||||||
constructor({ plugins }: DashboardAppProps) {
|
constructor({ plugins }: DashboardAppProps) {
|
||||||
this.widgets = this.populateWidgets(plugins)
|
this.widgets = this.populateWidgets(plugins)
|
||||||
@@ -60,6 +64,7 @@ export class DashboardApp {
|
|||||||
this.fields = fields
|
this.fields = fields
|
||||||
this.configs = configs
|
this.configs = configs
|
||||||
this.displays = this.populateDisplays(plugins)
|
this.displays = this.populateDisplays(plugins)
|
||||||
|
this.i18nResources = this.populateI18n(plugins)
|
||||||
}
|
}
|
||||||
|
|
||||||
private populateRoutes(plugins: DashboardPlugin[]) {
|
private populateRoutes(plugins: DashboardPlugin[]) {
|
||||||
@@ -378,6 +383,18 @@ export class DashboardApp {
|
|||||||
return displays
|
return displays
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private populateI18n(
|
||||||
|
plugins: DashboardPlugin[]
|
||||||
|
): I18nExtension {
|
||||||
|
let resources: I18nExtension = { ...coreTranslations }
|
||||||
|
|
||||||
|
for (const plugin of plugins) {
|
||||||
|
resources = deepMerge(resources, plugin.i18nModule?.resources)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources
|
||||||
|
}
|
||||||
|
|
||||||
private processDisplays(
|
private processDisplays(
|
||||||
displays: DisplayExtension[]
|
displays: DisplayExtension[]
|
||||||
): Map<CustomFieldContainerZone, React.ComponentType<{ data: any }>[]> {
|
): Map<CustomFieldContainerZone, React.ComponentType<{ data: any }>[]> {
|
||||||
@@ -431,6 +448,10 @@ export class DashboardApp {
|
|||||||
return this.displays.get(model)?.get(zone) || []
|
return this.displays.get(model)?.get(zone) || []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getI18nResources() {
|
||||||
|
return this.i18nResources
|
||||||
|
}
|
||||||
|
|
||||||
get api() {
|
get api() {
|
||||||
return {
|
return {
|
||||||
getMenu: this.getMenu.bind(this),
|
getMenu: this.getMenu.bind(this),
|
||||||
@@ -438,6 +459,7 @@ export class DashboardApp {
|
|||||||
getFormFields: this.getFormFields.bind(this),
|
getFormFields: this.getFormFields.bind(this),
|
||||||
getFormConfigs: this.getFormConfigs.bind(this),
|
getFormConfigs: this.getFormConfigs.bind(this),
|
||||||
getDisplays: this.getDisplays.bind(this),
|
getDisplays: this.getDisplays.bind(this),
|
||||||
|
getI18nResources: this.getI18nResources.bind(this),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ export type ConfigExtension = {
|
|||||||
fields: Record<string, ConfigFieldExtension>
|
fields: Record<string, ConfigFieldExtension>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type I18nExtension = Record<string, Record<string, any>>
|
||||||
|
|
||||||
export type LinkModule = {
|
export type LinkModule = {
|
||||||
links: Record<CustomFieldModel, (string | string[])[]>
|
links: Record<CustomFieldModel, (string | string[])[]>
|
||||||
}
|
}
|
||||||
@@ -90,6 +92,10 @@ export type MenuItemModule = {
|
|||||||
menuItems: MenuItemExtension[]
|
menuItems: MenuItemExtension[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type I18nModule = {
|
||||||
|
resources: I18nExtension
|
||||||
|
}
|
||||||
|
|
||||||
export type MenuItemKey = "coreExtensions" | "settingsExtensions"
|
export type MenuItemKey = "coreExtensions" | "settingsExtensions"
|
||||||
|
|
||||||
export type FormField = FormFieldExtension & {
|
export type FormField = FormFieldExtension & {
|
||||||
@@ -131,4 +137,5 @@ export type DashboardPlugin = {
|
|||||||
menuItemModule: MenuItemModule
|
menuItemModule: MenuItemModule
|
||||||
widgetModule: WidgetModule
|
widgetModule: WidgetModule
|
||||||
routeModule: RouteModule
|
routeModule: RouteModule
|
||||||
|
i18nModule?: I18nModule
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import { InitOptions } from "i18next"
|
import { InitOptions } from "i18next"
|
||||||
|
|
||||||
import translations from "./translations"
|
|
||||||
|
|
||||||
export const defaultI18nOptions: InitOptions = {
|
export const defaultI18nOptions: InitOptions = {
|
||||||
debug: process.env.NODE_ENV === "development",
|
debug: process.env.NODE_ENV === "development",
|
||||||
detection: {
|
detection: {
|
||||||
@@ -11,9 +9,8 @@ export const defaultI18nOptions: InitOptions = {
|
|||||||
order: ["cookie", "localStorage", "header"],
|
order: ["cookie", "localStorage", "header"],
|
||||||
},
|
},
|
||||||
fallbackLng: "en",
|
fallbackLng: "en",
|
||||||
|
fallbackNS: "translation",
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false,
|
escapeValue: false,
|
||||||
},
|
}
|
||||||
resources: translations,
|
|
||||||
supportedLngs: Object.keys(translations),
|
|
||||||
}
|
}
|
||||||
+6
@@ -33,3 +33,9 @@ declare module "virtual:medusa/widgets" {
|
|||||||
const widgetModule: WidgetModule
|
const widgetModule: WidgetModule
|
||||||
export default widgetModule
|
export default widgetModule
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module "virtual:medusa/i18n" {
|
||||||
|
import type { I18nModule } from "./extensions"
|
||||||
|
const i18nModule: I18nModule
|
||||||
|
export default i18nModule
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ export default defineConfig({
|
|||||||
"virtual:medusa/links",
|
"virtual:medusa/links",
|
||||||
"virtual:medusa/menu-items",
|
"virtual:medusa/menu-items",
|
||||||
"virtual:medusa/widgets",
|
"virtual:medusa/widgets",
|
||||||
|
"virtual:medusa/i18n",
|
||||||
],
|
],
|
||||||
tsconfig: "tsconfig.build.json",
|
tsconfig: "tsconfig.build.json",
|
||||||
clean: true,
|
clean: true,
|
||||||
|
|||||||
Reference in new issue
Block a user