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:
8
.changeset/beige-shirts-work.md
Normal file
8
.changeset/beige-shirts-work.md
Normal file
@@ -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/jsx-runtime",
|
||||
"react-router-dom",
|
||||
"react-i18next",
|
||||
"@medusajs/js-sdk",
|
||||
"@medusajs/admin-sdk",
|
||||
"@tanstack/react-query",
|
||||
|
||||
@@ -36,6 +36,7 @@ export async function getViteConfig(
|
||||
"react/jsx-runtime",
|
||||
"react-dom/client",
|
||||
"react-router-dom",
|
||||
"react-i18next",
|
||||
"@medusajs/ui",
|
||||
"@medusajs/dashboard",
|
||||
"@medusajs/js-sdk",
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from "./extensions/custom-fields"
|
||||
export * from "./extensions/routes"
|
||||
export * from "./extensions/widgets"
|
||||
export * from "./virtual-modules"
|
||||
export * from "./utils"
|
||||
|
||||
26
packages/admin/admin-shared/src/utils/deep-merge.ts
Normal file
26
packages/admin/admin-shared/src/utils/deep-merge.ts
Normal file
@@ -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)
|
||||
}
|
||||
1
packages/admin/admin-shared/src/utils/index.ts
Normal file
1
packages/admin/admin-shared/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./deep-merge"
|
||||
3
packages/admin/admin-shared/src/utils/is-object.ts
Normal file
3
packages/admin/admin-shared/src/utils/is-object.ts
Normal file
@@ -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 MENU_ITEM_VIRTUAL_MODULE = `virtual:medusa/menu-items`
|
||||
export const WIDGET_VIRTUAL_MODULE = `virtual:medusa/widgets`
|
||||
export const I18N_VIRTUAL_MODULE = `virtual:medusa/i18n`
|
||||
|
||||
export const VIRTUAL_MODULES = [
|
||||
LINK_VIRTUAL_MODULE,
|
||||
@@ -12,4 +13,5 @@ export const VIRTUAL_MODULES = [
|
||||
ROUTE_VIRTUAL_MODULE,
|
||||
MENU_ITEM_VIRTUAL_MODULE,
|
||||
WIDGET_VIRTUAL_MODULE,
|
||||
I18N_VIRTUAL_MODULE,
|
||||
] 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)
|
||||
}
|
||||
33
packages/admin/admin-vite-plugin/src/i18n/generate-i18n.ts
Normal file
33
packages/admin/admin-vite-plugin/src/i18n/generate-i18n.ts
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
15
packages/admin/admin-vite-plugin/src/i18n/helpers.ts
Normal file
15
packages/admin/admin-vite-plugin/src/i18n/helpers.ts
Normal file
@@ -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()
|
||||
}
|
||||
3
packages/admin/admin-vite-plugin/src/i18n/index.ts
Normal file
3
packages/admin/admin-vite-plugin/src/i18n/index.ts
Normal file
@@ -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 type * as Vite from "vite"
|
||||
import { generateCustomFieldHashes } from "./custom-fields"
|
||||
import { generateI18nHash } from "./i18n"
|
||||
import { generateRouteHashes } from "./routes"
|
||||
import { MedusaVitePlugin } from "./types"
|
||||
import { AdminSubdirectory, isFileInAdminSubdirectory } from "./utils"
|
||||
import {
|
||||
generateVirtualDisplayModule,
|
||||
generateVirtualFormModule,
|
||||
generateVirtualI18nModule,
|
||||
generateVirtualLinkModule,
|
||||
generateVirtualMenuItemModule,
|
||||
generateVirtualRouteModule,
|
||||
@@ -84,6 +86,7 @@ export const medusaVitePlugin: MedusaVitePlugin = (options) => {
|
||||
const menuItemModule = await generateVirtualMenuItemModule(sources, true)
|
||||
const formModule = await generateVirtualFormModule(sources, true)
|
||||
const displayModule = await generateVirtualDisplayModule(sources, true)
|
||||
const i18nModule = await generateVirtualI18nModule(sources, true)
|
||||
|
||||
// Create the index.js content that re-exports everything
|
||||
return `
|
||||
@@ -93,13 +96,15 @@ export const medusaVitePlugin: MedusaVitePlugin = (options) => {
|
||||
${menuItemModule.code}
|
||||
${formModule.code}
|
||||
${displayModule.code}
|
||||
${i18nModule.code}
|
||||
|
||||
const plugin = {
|
||||
widgetModule,
|
||||
routeModule,
|
||||
menuItemModule,
|
||||
formModule,
|
||||
displayModule
|
||||
displayModule,
|
||||
i18nModule
|
||||
}
|
||||
|
||||
export default plugin
|
||||
@@ -229,6 +234,11 @@ const loadConfigs: Record<string, ModuleConfig> = {
|
||||
moduleGenerator: async (sources) => generateVirtualMenuItemModule(sources),
|
||||
hashKey: vmod.virtual.menuItem,
|
||||
},
|
||||
[vmod.resolved.i18n]: {
|
||||
hashGenerator: async (sources) => generateI18nHash(sources),
|
||||
moduleGenerator: async (sources) => generateVirtualI18nModule(sources),
|
||||
hashKey: vmod.virtual.i18n,
|
||||
},
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -176,7 +176,7 @@ export function generateHash(content: string) {
|
||||
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]
|
||||
|
||||
|
||||
@@ -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-route-module"
|
||||
export * from "./generate-virtual-widget-module"
|
||||
export * from "./generate-virtual-i18n-module"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
DISPLAY_VIRTUAL_MODULE,
|
||||
FORM_VIRTUAL_MODULE,
|
||||
I18N_VIRTUAL_MODULE,
|
||||
LINK_VIRTUAL_MODULE,
|
||||
MENU_ITEM_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_MENU_ITEM_VIRTUAL_MODULE = `\0${MENU_ITEM_VIRTUAL_MODULE}`
|
||||
const RESOLVED_WIDGET_VIRTUAL_MODULE = `\0${WIDGET_VIRTUAL_MODULE}`
|
||||
const RESOLVED_I18N_VIRTUAL_MODULE = `\0${I18N_VIRTUAL_MODULE}`
|
||||
|
||||
const VIRTUAL_MODULES = [
|
||||
LINK_VIRTUAL_MODULE,
|
||||
@@ -21,6 +23,7 @@ const VIRTUAL_MODULES = [
|
||||
ROUTE_VIRTUAL_MODULE,
|
||||
MENU_ITEM_VIRTUAL_MODULE,
|
||||
WIDGET_VIRTUAL_MODULE,
|
||||
I18N_VIRTUAL_MODULE,
|
||||
] as const
|
||||
|
||||
const RESOLVED_VIRTUAL_MODULES = [
|
||||
@@ -30,6 +33,7 @@ const RESOLVED_VIRTUAL_MODULES = [
|
||||
RESOLVED_ROUTE_VIRTUAL_MODULE,
|
||||
RESOLVED_MENU_ITEM_VIRTUAL_MODULE,
|
||||
RESOLVED_WIDGET_VIRTUAL_MODULE,
|
||||
RESOLVED_I18N_VIRTUAL_MODULE,
|
||||
] as const
|
||||
|
||||
export function resolveVirtualId(id: string) {
|
||||
@@ -55,6 +59,8 @@ export type VirtualModule =
|
||||
| typeof ROUTE_VIRTUAL_MODULE
|
||||
| typeof MENU_ITEM_VIRTUAL_MODULE
|
||||
| typeof WIDGET_VIRTUAL_MODULE
|
||||
| typeof I18N_VIRTUAL_MODULE
|
||||
|
||||
const resolvedVirtualModuleIds = {
|
||||
link: RESOLVED_LINK_VIRTUAL_MODULE,
|
||||
form: RESOLVED_FORM_VIRTUAL_MODULE,
|
||||
@@ -62,6 +68,7 @@ const resolvedVirtualModuleIds = {
|
||||
route: RESOLVED_ROUTE_VIRTUAL_MODULE,
|
||||
menuItem: RESOLVED_MENU_ITEM_VIRTUAL_MODULE,
|
||||
widget: RESOLVED_WIDGET_VIRTUAL_MODULE,
|
||||
i18n: RESOLVED_I18N_VIRTUAL_MODULE,
|
||||
} as const
|
||||
|
||||
const virtualModuleIds = {
|
||||
@@ -71,6 +78,7 @@ const virtualModuleIds = {
|
||||
route: ROUTE_VIRTUAL_MODULE,
|
||||
menuItem: MENU_ITEM_VIRTUAL_MODULE,
|
||||
widget: WIDGET_VIRTUAL_MODULE,
|
||||
i18n: I18N_VIRTUAL_MODULE,
|
||||
} as const
|
||||
|
||||
export const vmod = {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { DashboardPlugin } from "./dashboard-app/types"
|
||||
|
||||
import displayModule from "virtual:medusa/displays"
|
||||
import formModule from "virtual:medusa/forms"
|
||||
import i18nModule from "virtual:medusa/i18n"
|
||||
import menuItemModule from "virtual:medusa/menu-items"
|
||||
import routeModule from "virtual:medusa/routes"
|
||||
import widgetModule from "virtual:medusa/widgets"
|
||||
@@ -15,6 +16,7 @@ const localPlugin = {
|
||||
displayModule,
|
||||
formModule,
|
||||
menuItemModule,
|
||||
i18nModule,
|
||||
}
|
||||
|
||||
interface AppProps {
|
||||
|
||||
@@ -3,12 +3,16 @@ import LanguageDetector from "i18next-browser-languagedetector"
|
||||
import { initReactI18next } from "react-i18next"
|
||||
|
||||
import { defaultI18nOptions } from "../../../i18n/config"
|
||||
import { useExtension } from "../../../providers/extension-provider"
|
||||
|
||||
export const I18n = () => {
|
||||
const { getI18nResources } = useExtension()
|
||||
|
||||
if (i18n.isInitialized) {
|
||||
return null
|
||||
}
|
||||
|
||||
const resources = getI18nResources()
|
||||
i18n
|
||||
.use(
|
||||
new LanguageDetector(null, {
|
||||
@@ -17,7 +21,11 @@ export const I18n = () => {
|
||||
})
|
||||
)
|
||||
.use(initReactI18next)
|
||||
.init(defaultI18nOptions)
|
||||
.init({
|
||||
...defaultI18nOptions,
|
||||
resources,
|
||||
supportedLngs: Object.keys(resources),
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
CustomFieldFormTab,
|
||||
CustomFieldFormZone,
|
||||
CustomFieldModel,
|
||||
deepMerge,
|
||||
InjectionZone,
|
||||
NESTED_ROUTE_POSITIONS,
|
||||
} from "@medusajs/admin-shared"
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
} from "react-router-dom"
|
||||
import { INavItem } from "../components/layout/nav-item"
|
||||
import { Providers } from "../providers"
|
||||
import coreTranslations from "../i18n/translations"
|
||||
import { getRouteMap } from "./routes/get-route.map"
|
||||
import { createRouteMap, getRouteExtensions } from "./routes/utils"
|
||||
import {
|
||||
@@ -28,6 +30,7 @@ import {
|
||||
FormFieldExtension,
|
||||
FormFieldMap,
|
||||
FormZoneMap,
|
||||
I18nExtension,
|
||||
MenuItemExtension,
|
||||
MenuItemKey,
|
||||
MenuMap,
|
||||
@@ -47,6 +50,7 @@ export class DashboardApp {
|
||||
private displays: DisplayMap
|
||||
private coreRoutes: RouteObject[]
|
||||
private settingsRoutes: RouteObject[]
|
||||
private i18nResources: I18nExtension
|
||||
|
||||
constructor({ plugins }: DashboardAppProps) {
|
||||
this.widgets = this.populateWidgets(plugins)
|
||||
@@ -60,6 +64,7 @@ export class DashboardApp {
|
||||
this.fields = fields
|
||||
this.configs = configs
|
||||
this.displays = this.populateDisplays(plugins)
|
||||
this.i18nResources = this.populateI18n(plugins)
|
||||
}
|
||||
|
||||
private populateRoutes(plugins: DashboardPlugin[]) {
|
||||
@@ -378,6 +383,18 @@ export class DashboardApp {
|
||||
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(
|
||||
displays: DisplayExtension[]
|
||||
): Map<CustomFieldContainerZone, React.ComponentType<{ data: any }>[]> {
|
||||
@@ -431,6 +448,10 @@ export class DashboardApp {
|
||||
return this.displays.get(model)?.get(zone) || []
|
||||
}
|
||||
|
||||
private getI18nResources() {
|
||||
return this.i18nResources
|
||||
}
|
||||
|
||||
get api() {
|
||||
return {
|
||||
getMenu: this.getMenu.bind(this),
|
||||
@@ -438,6 +459,7 @@ export class DashboardApp {
|
||||
getFormFields: this.getFormFields.bind(this),
|
||||
getFormConfigs: this.getFormConfigs.bind(this),
|
||||
getDisplays: this.getDisplays.bind(this),
|
||||
getI18nResources: this.getI18nResources.bind(this),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,8 @@ export type ConfigExtension = {
|
||||
fields: Record<string, ConfigFieldExtension>
|
||||
}
|
||||
|
||||
export type I18nExtension = Record<string, Record<string, any>>
|
||||
|
||||
export type LinkModule = {
|
||||
links: Record<CustomFieldModel, (string | string[])[]>
|
||||
}
|
||||
@@ -90,6 +92,10 @@ export type MenuItemModule = {
|
||||
menuItems: MenuItemExtension[]
|
||||
}
|
||||
|
||||
export type I18nModule = {
|
||||
resources: I18nExtension
|
||||
}
|
||||
|
||||
export type MenuItemKey = "coreExtensions" | "settingsExtensions"
|
||||
|
||||
export type FormField = FormFieldExtension & {
|
||||
@@ -131,4 +137,5 @@ export type DashboardPlugin = {
|
||||
menuItemModule: MenuItemModule
|
||||
widgetModule: WidgetModule
|
||||
routeModule: RouteModule
|
||||
i18nModule?: I18nModule
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { InitOptions } from "i18next"
|
||||
|
||||
import translations from "./translations"
|
||||
|
||||
export const defaultI18nOptions: InitOptions = {
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
detection: {
|
||||
@@ -11,9 +9,8 @@ export const defaultI18nOptions: InitOptions = {
|
||||
order: ["cookie", "localStorage", "header"],
|
||||
},
|
||||
fallbackLng: "en",
|
||||
fallbackNS: "translation",
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
resources: translations,
|
||||
supportedLngs: Object.keys(translations),
|
||||
}
|
||||
}
|
||||
|
||||
6
packages/admin/dashboard/src/module.d.ts
vendored
6
packages/admin/dashboard/src/module.d.ts
vendored
@@ -33,3 +33,9 @@ declare module "virtual:medusa/widgets" {
|
||||
const widgetModule: 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/menu-items",
|
||||
"virtual:medusa/widgets",
|
||||
"virtual:medusa/i18n",
|
||||
],
|
||||
tsconfig: "tsconfig.build.json",
|
||||
clean: true,
|
||||
|
||||
Reference in New Issue
Block a user