From 1086197af06c0d7419cadf57f976256b0a7f5bfc Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Tue, 29 Sep 2026 17:27:04 +0900 Subject: [PATCH] fix(sidebar): resolve localized item titles --- .changeset/sunny-titles-resolve.md | 5 +++++ docs-manifest.json | 14 ++++++------- .../components/sidebar-item.docs.outline.md | 12 +++++++++-- .../sidebar-navigation.docs.outline.md | 14 ++++++------- docs/components/sidebar-item.md | 12 +++++++++-- docs/concepts/sidebar-navigation.md | 14 ++++++------- .../src/components/sidebar/sidebar-item.tsx | 14 ++++++++++--- .../src/components/sidebar/sidebar.test.tsx | 20 +++++++++++++++++-- 8 files changed, 75 insertions(+), 30 deletions(-) create mode 100644 .changeset/sunny-titles-resolve.md diff --git a/.changeset/sunny-titles-resolve.md b/.changeset/sunny-titles-resolve.md new file mode 100644 index 000000000..315d1416c --- /dev/null +++ b/.changeset/sunny-titles-resolve.md @@ -0,0 +1,5 @@ +--- +"@tailor-platform/app-shell": patch +--- + +Fix `SidebarItem` title overrides accepting localized strings. Pass a value from `defineI18nLabels` directly to `title` and it resolves with the active AppShell locale. diff --git a/docs-manifest.json b/docs-manifest.json index 718997a22..70509a247 100644 --- a/docs-manifest.json +++ b/docs-manifest.json @@ -1147,10 +1147,10 @@ "claims": [], "symbols": ["SidebarItem", "SidebarItemProps"], "hashes": { - "typeSurface": "7be21739c231ede3", - "outline": "1cc674b956d09fab", + "typeSurface": "37530cafa374d799", + "outline": "d1bbcfac50071047", "snapshot": null, - "outputMd": "d55d38352040ef2c", + "outputMd": "9236c03e53aa79f2", "examples": null } }, @@ -1182,9 +1182,9 @@ "symbols": [], "hashes": { "typeSurface": null, - "outline": "74f353988f15c82b", + "outline": "91f0fa065f3a3192", "snapshot": null, - "outputMd": "96f1cf893c231861", + "outputMd": "dc6b3afc049b4148", "examples": null } }, @@ -1587,7 +1587,7 @@ "packages/core/skills/app-shell-patterns/references/concepts/graphql.md": "ba5107604e4f42e8", "packages/core/skills/app-shell-patterns/references/concepts/modules-and-resources.md": "bf15b5460f22d165", "packages/core/skills/app-shell-patterns/references/concepts/routing-navigation.md": "2f3ff3f095c2b1cc", - "packages/core/skills/app-shell-patterns/references/concepts/sidebar-navigation.md": "96f1cf893c231861", + "packages/core/skills/app-shell-patterns/references/concepts/sidebar-navigation.md": "dc6b3afc049b4148", "packages/core/skills/app-shell-patterns/references/concepts/styling-theming.md": "0133fdae405f467c", "packages/core/skills/app-shell-patterns/references/components/action-panel.md": "71794d4b561e4d4b", "packages/core/skills/app-shell-patterns/references/components/activity-card.md": "4f386e49380fb0b0", @@ -1622,7 +1622,7 @@ "packages/core/skills/app-shell-patterns/references/components/select.md": "ae287e2bb5abc1c6", "packages/core/skills/app-shell-patterns/references/components/sheet.md": "1f81ebd562c50371", "packages/core/skills/app-shell-patterns/references/components/sidebar-group.md": "7288fdf8bb5c19b9", - "packages/core/skills/app-shell-patterns/references/components/sidebar-item.md": "d55d38352040ef2c", + "packages/core/skills/app-shell-patterns/references/components/sidebar-item.md": "9236c03e53aa79f2", "packages/core/skills/app-shell-patterns/references/components/sidebar-layout.md": "e50b2d649289d772", "packages/core/skills/app-shell-patterns/references/components/spinner.md": "e05d94ba9c3d17da", "packages/core/skills/app-shell-patterns/references/components/table.md": "f85ec64ceb7f4acf", diff --git a/docs-src/components/sidebar-item.docs.outline.md b/docs-src/components/sidebar-item.docs.outline.md index 3abc60271..9823d083d 100644 --- a/docs-src/components/sidebar-item.docs.outline.md +++ b/docs-src/components/sidebar-item.docs.outline.md @@ -9,7 +9,7 @@ sources: # SidebarItem -`SidebarItem` represents an individual navigation link in the sidebar. It automatically resolves titles and icons from resource definitions and provides active state highlighting. +`SidebarItem` represents an individual navigation link in the sidebar. It automatically resolves titles and icons from resource definitions, supports localized title overrides, and provides active state highlighting. ## Import @@ -36,7 +36,7 @@ When you only provide `to`, the title and icon are automatically resolved from t | Prop | Type | Default | Description | | ------------- | ---------------------- | ------------- | ---------------------------------------------- | | `to` | `string` | **Required** | Target URL (internal or external) | -| `title` | `string` | Auto-resolved | Override the display title | +| `title` | `LocalizedString` | Auto-resolved | Override the display title (i18n supported) | | `icon` | `React.ReactNode` | Auto-resolved | Override the icon | | `external` | `boolean` | `false` | Opens link in new tab with external icon | | `activeMatch` | `"exact" \| "prefix"` | `"prefix"` | How to match the current path for active state | @@ -106,6 +106,14 @@ const modules = [ /> ``` +### Localized Override + +Pass a `LocalizedString`, such as a value from `defineI18nLabels`, to resolve the override using the current AppShell locale: + +```tsx + +``` + ### Override Icon ```tsx diff --git a/docs-src/concepts/sidebar-navigation.docs.outline.md b/docs-src/concepts/sidebar-navigation.docs.outline.md index c8020c22d..853d9eace 100644 --- a/docs-src/concepts/sidebar-navigation.docs.outline.md +++ b/docs-src/concepts/sidebar-navigation.docs.outline.md @@ -72,13 +72,13 @@ A navigation item that automatically resolves title and icon from resource defin #### Props -| Prop | Type | Required | Description | -| ---------- | ----------- | -------- | --------------------------------------------------------------------- | -| `to` | `string` | Yes | Target URL. External URLs (http://...) are rendered as external links | -| `title` | `string` | No | Override title. When omitted, auto-resolved from resource meta | -| `icon` | `ReactNode` | No | Override icon. When omitted, auto-resolved from resource meta | -| `external` | `boolean` | No | Opens link in new tab with external link icon | -| `render` | `function` | No | Custom render function for full UI control | +| Prop | Type | Required | Description | +| ---------- | ----------------- | -------- | ------------------------------------------------------------------------------- | +| `to` | `string` | Yes | Target URL. External URLs (http://...) are rendered as external links | +| `title` | `LocalizedString` | No | Override title (i18n supported). When omitted, auto-resolved from resource meta | +| `icon` | `ReactNode` | No | Override icon. When omitted, auto-resolved from resource meta | +| `external` | `boolean` | No | Opens link in new tab with external link icon | +| `render` | `function` | No | Custom render function for full UI control | #### Examples diff --git a/docs/components/sidebar-item.md b/docs/components/sidebar-item.md index be600e416..160b757b1 100644 --- a/docs/components/sidebar-item.md +++ b/docs/components/sidebar-item.md @@ -7,7 +7,7 @@ description: Individual navigation item for customizing sidebar with icons, acti # SidebarItem -`SidebarItem` represents an individual navigation link in the sidebar. It automatically resolves titles and icons from resource definitions and provides active state highlighting. +`SidebarItem` represents an individual navigation link in the sidebar. It automatically resolves titles and icons from resource definitions, supports localized title overrides, and provides active state highlighting. ## Import @@ -34,7 +34,7 @@ When you only provide `to`, the title and icon are automatically resolved from t | Prop | Type | Default | Description | | ------------- | ---------------------- | ------------- | ---------------------------------------------- | | `to` | `string` | **Required** | Target URL (internal or external) | -| `title` | `string` | Auto-resolved | Override the display title | +| `title` | `LocalizedString` | Auto-resolved | Override the display title (i18n supported) | | `icon` | `React.ReactNode` | Auto-resolved | Override the icon | | `external` | `boolean` | `false` | Opens link in new tab with external icon | | `activeMatch` | `"exact" \| "prefix"` | `"prefix"` | How to match the current path for active state | @@ -104,6 +104,14 @@ const modules = [ /> ``` +### Localized Override + +Pass a `LocalizedString`, such as a value from `defineI18nLabels`, to resolve the override using the current AppShell locale: + +```tsx + +``` + ### Override Icon ```tsx diff --git a/docs/concepts/sidebar-navigation.md b/docs/concepts/sidebar-navigation.md index 843f937fb..4c3924431 100644 --- a/docs/concepts/sidebar-navigation.md +++ b/docs/concepts/sidebar-navigation.md @@ -72,13 +72,13 @@ A navigation item that automatically resolves title and icon from resource defin #### Props -| Prop | Type | Required | Description | -| ---------- | ----------- | -------- | --------------------------------------------------------------------- | -| `to` | `string` | Yes | Target URL. External URLs (http://...) are rendered as external links | -| `title` | `string` | No | Override title. When omitted, auto-resolved from resource meta | -| `icon` | `ReactNode` | No | Override icon. When omitted, auto-resolved from resource meta | -| `external` | `boolean` | No | Opens link in new tab with external link icon | -| `render` | `function` | No | Custom render function for full UI control | +| Prop | Type | Required | Description | +| ---------- | ----------------- | -------- | ------------------------------------------------------------------------------- | +| `to` | `string` | Yes | Target URL. External URLs (http://...) are rendered as external links | +| `title` | `LocalizedString` | No | Override title (i18n supported). When omitted, auto-resolved from resource meta | +| `icon` | `ReactNode` | No | Override icon. When omitted, auto-resolved from resource meta | +| `external` | `boolean` | No | Opens link in new tab with external link icon | +| `render` | `function` | No | Custom render function for full UI control | #### Examples diff --git a/packages/core/src/components/sidebar/sidebar-item.tsx b/packages/core/src/components/sidebar/sidebar-item.tsx index f8edaf9a5..2fb641b29 100644 --- a/packages/core/src/components/sidebar/sidebar-item.tsx +++ b/packages/core/src/components/sidebar/sidebar-item.tsx @@ -3,6 +3,8 @@ import { useLocation, Link } from "react-router"; import { ExternalLink } from "lucide-react"; import { SidebarMenuItem, SidebarMenuButton } from "./primitives"; import { usePageMeta } from "@/hooks/use-page-meta"; +import { useAppShellConfig } from "@/contexts/appshell-context"; +import { buildLocaleResolver, type LocalizedString } from "@/lib/i18n"; export type SidebarItemRenderProps = { /** Title auto-resolved from resource meta */ @@ -23,10 +25,10 @@ export type SidebarItemProps = { to: string; /** - * Override title. + * Override title (i18n supported). * When omitted, title is auto-resolved from resource meta. */ - title?: string; + title?: LocalizedString; /** * Override icon. @@ -96,13 +98,19 @@ export const SidebarItem = (props: SidebarItemProps) => { icon: iconOverride, } = props; const { pathname: currentPath } = useLocation(); + const { configurations } = useAppShellConfig(); const pageMeta = usePageMeta(to); + const resolve = buildLocaleResolver(configurations.locale); const isActive = activeMatch === "exact" ? currentPath === to : currentPath === to || currentPath.startsWith(`${to}/`); - const title = titleOverride ?? pageMeta?.title ?? extractTitleFromUrl(to); + const fallbackTitle = pageMeta?.title ?? extractTitleFromUrl(to); + const title = + typeof titleOverride === "function" + ? resolve(titleOverride, fallbackTitle) + : (titleOverride ?? fallbackTitle); const icon = iconOverride ?? pageMeta?.icon; const renderProps: SidebarItemRenderProps = { diff --git a/packages/core/src/components/sidebar/sidebar.test.tsx b/packages/core/src/components/sidebar/sidebar.test.tsx index bb495e437..b0d5f6a60 100644 --- a/packages/core/src/components/sidebar/sidebar.test.tsx +++ b/packages/core/src/components/sidebar/sidebar.test.tsx @@ -90,11 +90,15 @@ const testConfig: RootConfiguration = { /** * Wrapper to render sidebar components with all required providers. */ -const renderWithProviders = (ui: React.ReactNode, initialPath = "/dashboard/overview") => { +const renderWithProviders = ( + ui: React.ReactNode, + initialPath = "/dashboard/overview", + locale = "en", +) => { stubDesktopViewport(); return render( - + {ui} @@ -118,6 +122,18 @@ describe("SidebarItem", () => { expect(menu.getByRole("link", { name: /dashboard/i })).toBeDefined(); }); + it("resolves localized title overrides", () => { + renderWithProviders(, undefined, "ja"); + + expect(getSidebarMenu().getByRole("link", { name: "製品" })).toBeDefined(); + }); + + it("preserves empty string title overrides", () => { + renderWithProviders(); + + expect(getSidebarMenu().getByRole("link").textContent).toBe(""); + }); + it("renders with custom render function", () => { renderWithProviders(