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 = {