From 3852efbcfffbfd10b7f7cae7d1a15de2a5f3afee Mon Sep 17 00:00:00 2001 From: Leonardo Benini Date: Thu, 6 Nov 2025 13:42:32 +0100 Subject: [PATCH] feat(admin-*,dashboard): i18n labels for menu item extensions (#13843) * i18n menu item labels * changeset * changeset --- .changeset/eight-seahorses-bow.md | 7 +++ packages/admin/admin-sdk/src/config/types.ts | 11 ++++ .../__tests__/generate-menu-items.spec.ts | 54 +++++++++++++++++-- .../src/routes/generate-menu-items.ts | 20 ++++++- .../layout/main-layout/main-layout.tsx | 1 + .../components/layout/nav-item/nav-item.tsx | 18 +++++-- .../src/dashboard-app/dashboard-app.tsx | 1 + .../dashboard/src/dashboard-app/types.ts | 1 + 8 files changed, 104 insertions(+), 9 deletions(-) create mode 100644 .changeset/eight-seahorses-bow.md diff --git a/.changeset/eight-seahorses-bow.md b/.changeset/eight-seahorses-bow.md new file mode 100644 index 0000000000..2d88543407 --- /dev/null +++ b/.changeset/eight-seahorses-bow.md @@ -0,0 +1,7 @@ +--- +"@medusajs/admin-vite-plugin": patch +"@medusajs/admin-sdk": patch +"@medusajs/dashboard": patch +--- + +feat(admin-\*,dashboard): i18n labels for menu item extensions diff --git a/packages/admin/admin-sdk/src/config/types.ts b/packages/admin/admin-sdk/src/config/types.ts index a41b7737ad..d2bf36232e 100644 --- a/packages/admin/admin-sdk/src/config/types.ts +++ b/packages/admin/admin-sdk/src/config/types.ts @@ -30,6 +30,17 @@ export interface RouteConfig { * The nested route to display under existing route in the sidebar. */ nested?: NestedRoutePosition + + /** + * An optional i18n namespace for translating the label. When provided, the label will be treated as a translation key. + * @example + * ```ts + * label: "menuItems.customFeature" + * translationNs: "my-plugin" + * // Will translate using: t("menuItems.customFeature", { ns: "my-plugin" }) + * ``` + */ + translationNs?: string } export type CustomFormField< diff --git a/packages/admin/admin-vite-plugin/src/routes/__tests__/generate-menu-items.spec.ts b/packages/admin/admin-vite-plugin/src/routes/__tests__/generate-menu-items.spec.ts index 07f13b3cfb..0f509e003f 100644 --- a/packages/admin/admin-vite-plugin/src/routes/__tests__/generate-menu-items.spec.ts +++ b/packages/admin/admin-vite-plugin/src/routes/__tests__/generate-menu-items.spec.ts @@ -69,19 +69,22 @@ const expectedMenuItems = ` label: RouteConfig0.label, icon: RouteConfig0.icon, path: "/one", - nested: undefined + nested: undefined, + translationNs: undefined }, { label: RouteConfig1.label, icon: undefined, path: "/two", - nested: undefined + nested: undefined, + translationNs: undefined }, { label: RouteConfig2.label, icon: RouteConfig2.icon, path: "/three", - nested: "/products" + nested: "/products", + translationNs: undefined } ] ` @@ -137,4 +140,49 @@ describe("generateMenuItems", () => { utils.normalizeString(expectedMenuItems) ) }) + + it("should handle translationNs field", async () => { + const mockFileWithTranslation = ` + import { defineRouteConfig } from "@medusajs/admin-sdk" + + const Page = () => { + return
Custom Page
+ } + + export const config = defineRouteConfig({ + label: "menuItems.customFeature", + translationNs: "my-plugin", + }) + + export default Page + ` + + const mockFiles = ["Users/user/medusa/src/admin/routes/custom/page.tsx"] + vi.mocked(utils.crawl).mockResolvedValue(mockFiles) + vi.mocked(fs.readFile).mockResolvedValue(mockFileWithTranslation) + + const result = await generateMenuItems( + new Set(["Users/user/medusa/src/admin"]) + ) + + expect(result.imports).toEqual([ + `import { config as RouteConfig0 } from "Users/user/medusa/src/admin/routes/custom/page.tsx"`, + ]) + + const expectedOutput = ` + menuItems: [ + { + label: RouteConfig0.label, + icon: undefined, + path: "/custom", + nested: undefined, + translationNs: RouteConfig0.translationNs + } + ] + ` + + expect(utils.normalizeString(result.code)).toEqual( + utils.normalizeString(expectedOutput) + ) + }) }) diff --git a/packages/admin/admin-vite-plugin/src/routes/generate-menu-items.ts b/packages/admin/admin-vite-plugin/src/routes/generate-menu-items.ts index f6195b6dd5..aec298ff7a 100644 --- a/packages/admin/admin-vite-plugin/src/routes/generate-menu-items.ts +++ b/packages/admin/admin-vite-plugin/src/routes/generate-menu-items.ts @@ -28,6 +28,7 @@ type RouteConfig = { label: boolean icon: boolean nested?: string + translationNs?: string } type MenuItem = { @@ -35,6 +36,7 @@ type MenuItem = { label: string path: string nested?: string + translationNs?: string } type MenuItemResult = { @@ -64,12 +66,13 @@ function generateCode(results: MenuItemResult[]): string { } function formatMenuItem(route: MenuItem): string { - const { label, icon, path, nested } = route + const { label, icon, path, nested, translationNs } = route return `{ label: ${label}, icon: ${icon || "undefined"}, path: "${path}", - nested: ${nested ? `"${nested}"` : "undefined"} + nested: ${nested ? `"${nested}"` : "undefined"}, + translationNs: ${translationNs ? `${translationNs}` : "undefined"} }` } @@ -130,6 +133,7 @@ function generateMenuItem( icon: config.icon ? `${configName}.icon` : undefined, path: getRoute(file), nested: config.nested, + translationNs: config.translationNs ? `${configName}.translationNs` : undefined, } } @@ -240,10 +244,22 @@ function processConfigProperties( return null } + const translationNs = properties.find( + (prop) => + isObjectProperty(prop) && isIdentifier(prop.key, { name: "translationNs" }) + ) as ObjectProperty | undefined + + let translationNsValue: string | undefined = undefined + + if (isStringLiteral(translationNs?.value)) { + translationNsValue = translationNs.value.value + } + return { label: hasLabel, icon: hasProperty("icon"), nested: nestedValue, + translationNs: translationNsValue, } } diff --git a/packages/admin/dashboard/src/components/layout/main-layout/main-layout.tsx b/packages/admin/dashboard/src/components/layout/main-layout/main-layout.tsx index 5aa04ed5e9..01aa7b0701 100644 --- a/packages/admin/dashboard/src/components/layout/main-layout/main-layout.tsx +++ b/packages/admin/dashboard/src/components/layout/main-layout/main-layout.tsx @@ -348,6 +348,7 @@ const ExtensionRouteSection = () => { label={item.label} icon={item.icon ? item.icon : } items={item.items} + translationNs={item.translationNs} type="extension" /> ) diff --git a/packages/admin/dashboard/src/components/layout/nav-item/nav-item.tsx b/packages/admin/dashboard/src/components/layout/nav-item/nav-item.tsx index 5ef7be021c..6b79a7eb7f 100644 --- a/packages/admin/dashboard/src/components/layout/nav-item/nav-item.tsx +++ b/packages/admin/dashboard/src/components/layout/nav-item/nav-item.tsx @@ -17,6 +17,7 @@ type ItemType = "core" | "extension" | "setting" type NestedItemProps = { label: string to: string + translationNs?: string } export type INavItem = { @@ -27,6 +28,7 @@ export type INavItem = { type?: ItemType from?: string nested?: string + translationNs?: string } const BASE_NAV_LINK_CLASSES = @@ -90,10 +92,15 @@ export const NavItem = ({ items, type = "core", from, + translationNs, }: INavItem) => { + const { t } = useTranslation(translationNs as any) const { pathname } = useLocation() const [open, setOpen] = useState(getIsOpen(to, items, pathname)) + // Use translation if translationNs is provided, otherwise use label as-is + const displayLabel: string = translationNs ? t(label) : label + useEffect(() => { setOpen(getIsOpen(to, items, pathname)) }, [pathname, to, items]) @@ -150,7 +157,7 @@ export const NavItem = ({ )} - {label} + {displayLabel} @@ -166,7 +173,7 @@ export const NavItem = ({ - {label} + {displayLabel} @@ -189,12 +196,15 @@ export const NavItem = ({ }} > - {label} + {displayLabel} {items.map((item) => { + const { t: itemT } = useTranslation(item.translationNs as any) + const itemLabel: string = item.translationNs ? itemT(item.label) : item.label + return (
  • @@ -213,7 +223,7 @@ export const NavItem = ({ }} > - {item.label} + {itemLabel} diff --git a/packages/admin/dashboard/src/dashboard-app/dashboard-app.tsx b/packages/admin/dashboard/src/dashboard-app/dashboard-app.tsx index de3126a839..acc7fff552 100644 --- a/packages/admin/dashboard/src/dashboard-app/dashboard-app.tsx +++ b/packages/admin/dashboard/src/dashboard-app/dashboard-app.tsx @@ -187,6 +187,7 @@ export class DashboardApp { icon: item.icon ? : undefined, items: [], nested: item.nested, + translationNs: item.translationNs, } if (parentPath !== "/" && tempRegistry[parentPath]) { diff --git a/packages/admin/dashboard/src/dashboard-app/types.ts b/packages/admin/dashboard/src/dashboard-app/types.ts index 48cf34a34b..245fd2628e 100644 --- a/packages/admin/dashboard/src/dashboard-app/types.ts +++ b/packages/admin/dashboard/src/dashboard-app/types.ts @@ -24,6 +24,7 @@ export type MenuItemExtension = { path: string icon?: ComponentType nested?: NestedRoutePosition + translationNs?: string } export type WidgetExtension = {