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:
Leonardo Benini
2025-10-23 15:16:43 -04:00
committed by GitHub
parent 012e30801e
commit 226984cf0f
25 changed files with 314 additions and 9 deletions
+2
View File
@@ -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
}
+2 -5
View File
@@ -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
View File
@@ -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
}