@takazudo/zudo-doc 5.14.0 → 5.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/asset-components/asset-card.js +3 -1
  3. package/dist/asset-components/asset-code.js +2 -0
  4. package/dist/asset-components/shared.d.ts +5 -0
  5. package/dist/asset-components/shared.js +7 -0
  6. package/dist/asset-index-page/index.d.ts +1 -0
  7. package/dist/asset-index-page/index.js +9 -8
  8. package/dist/asset-page/index.d.ts +20 -2
  9. package/dist/asset-page/index.js +42 -23
  10. package/dist/asset-path/index.d.ts +2 -0
  11. package/dist/asset-path/index.js +11 -1
  12. package/dist/chrome/derive.js +31 -6
  13. package/dist/compiled.css +12 -0
  14. package/dist/content/content-link.d.ts +5 -1
  15. package/dist/content/content-link.js +4 -1
  16. package/dist/factory-context/index.d.ts +7 -3
  17. package/dist/header-with-defaults/index.js +2 -0
  18. package/dist/html-preview-wrapper/html-preview-wrapper.d.ts +41 -7
  19. package/dist/html-preview-wrapper/html-preview-wrapper.js +86 -2
  20. package/dist/html-preview-wrapper/html-preview.d.ts +29 -4
  21. package/dist/html-preview-wrapper/html-preview.js +64 -6
  22. package/dist/html-preview-wrapper/index.d.ts +1 -1
  23. package/dist/html-preview-wrapper/preview-auto-height.d.ts +24 -0
  24. package/dist/html-preview-wrapper/preview-auto-height.js +120 -0
  25. package/dist/html-preview-wrapper/preview-base.d.ts +21 -1
  26. package/dist/html-preview-wrapper/preview-base.js +99 -50
  27. package/dist/i18n-defaults/index.d.ts +5 -3
  28. package/dist/i18n-defaults/index.js +89 -0
  29. package/dist/i18n-version/language-switcher.d.ts +10 -5
  30. package/dist/i18n-version/language-switcher.js +115 -22
  31. package/dist/i18n-version/types.d.ts +1 -1
  32. package/dist/mdx-components/index.d.ts +4 -0
  33. package/dist/mdx-components/index.js +11 -2
  34. package/dist/plugins/claude-resources.js +15 -1
  35. package/dist/plugins/codex-resources.js +15 -1
  36. package/dist/plugins/internal/claude-resources/generate.d.ts +9 -0
  37. package/dist/plugins/internal/claude-resources/generate.js +188 -18
  38. package/dist/plugins/internal/claude-resources/index.d.ts +9 -0
  39. package/dist/plugins/internal/claude-resources/index.js +13 -1
  40. package/dist/plugins/internal/codex-resources/agents-md.js +15 -2
  41. package/dist/plugins/internal/codex-resources/agents.js +15 -2
  42. package/dist/plugins/internal/codex-resources/config.js +15 -2
  43. package/dist/plugins/internal/codex-resources/generate.d.ts +9 -0
  44. package/dist/plugins/internal/codex-resources/generate.js +19 -7
  45. package/dist/plugins/internal/codex-resources/hooks.js +15 -2
  46. package/dist/plugins/internal/codex-resources/index.d.ts +9 -0
  47. package/dist/plugins/internal/codex-resources/index.js +13 -1
  48. package/dist/plugins/internal/codex-resources/overview.js +136 -7
  49. package/dist/plugins/internal/codex-resources/rules.js +15 -2
  50. package/dist/plugins/internal/codex-resources/skills.js +16 -3
  51. package/dist/plugins/internal/resource-docs-shared/fs.d.ts +33 -0
  52. package/dist/plugins/internal/resource-docs-shared/fs.js +82 -1
  53. package/dist/plugins/internal/resource-docs-shared/index.d.ts +4 -2
  54. package/dist/plugins/internal/resource-docs-shared/index.js +21 -1
  55. package/dist/plugins/internal/resource-docs-shared/labels.d.ts +24 -0
  56. package/dist/plugins/internal/resource-docs-shared/labels.js +14 -0
  57. package/dist/plugins/internal/resource-docs-shared/locale-routes.d.ts +16 -0
  58. package/dist/plugins/internal/resource-docs-shared/locale-routes.js +14 -0
  59. package/dist/plugins/internal/resource-docs-shared/mdx.d.ts +7 -1
  60. package/dist/plugins/internal/resource-docs-shared/mdx.js +2 -2
  61. package/dist/plugins/routes.js +14 -0
  62. package/dist/preset.d.ts +4 -0
  63. package/dist/preset.js +10 -2
  64. package/dist/route-context/index.js +5 -17
  65. package/dist/routes/locale-files-path.d.ts +29 -0
  66. package/dist/routes/locale-files-path.js +52 -0
  67. package/dist/safelist.css +1 -1
  68. package/eject/asset-components/asset-card.tsx +2 -0
  69. package/eject/asset-components/asset-code.tsx +2 -0
  70. package/eject/asset-components/shared.tsx +16 -0
  71. package/package.json +13 -9
  72. package/routes-src/locale-files-path.tsx +73 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@ All notable changes to `@takazudo/zudo-doc` are documented in this file.
4
4
 
5
5
  The format is based on Keep a Changelog, and release notes are generated from the changelog MDX pages.
6
6
 
7
+ ## [5.15.0] - 2026-09-01
8
+
9
+ ### Features
10
+
11
+ - Added an accessible language dropdown that scales to every configured locale instead of assuming a two-language site. (`607121e9`)
12
+ - Generated locale-owned Claude/Codex resource indexes and Asset Viewer routes for every configured locale, with translated page chrome, locale-preserving links, and safe handling of generated versus authored indexes. (`781fb820`, `8ceca53e`, `44ae7812`, `1e558854`, `5d682bc1`)
13
+ - Localized all seven `HtmlPreview` control labels and added per-preview `labels`, `showSource`, and `showViewportControls` overrides while preserving host-provided MDX bindings. (`ffee673f`, `6538184a`)
14
+ - Added `loading="visible"` for route-bound `HtmlPreview` instances, deferring iframe markup, scripts, and external resources until the reserved preview area enters the viewport. (`df63d981`)
15
+ - Kept auto-height previews synchronized after delayed content changes and viewport reflows, including both growth and shrink. (`df3d4b56`)
16
+ - Added escaped, nonblank language and title metadata to generated preview documents, with explicit author `<title>` content retaining precedence. (`2b8b0ee3`, `55e66300`)
17
+
18
+ ### Bug Fixes
19
+
20
+ - Retained package English strings when a configured locale supplies only partial translations. (`0a3619cb`)
21
+ - Preserved an explicit empty `sandbox` attribute in generated HTML so strict opaque previews remain sandboxed as authored. (`c8b1cac7`)
22
+ - Increased interactive `HtmlPreview` controls to accessible minimum hit-target dimensions. (`a35f1573`)
23
+
24
+ ### Other Changes
25
+
26
+ - Updated the zfb peer family to 2.14.2. (`5c5bfd2f`)
27
+
7
28
  ## [5.14.0] - 2026-08-31
8
29
 
9
30
  ### Features
@@ -23,6 +44,8 @@ The format is based on Keep a Changelog, and release notes are generated from th
23
44
 
24
45
  ### Other Changes
25
46
 
47
+ - Enabled Asset Viewer routes now localize automatically for existing multi-locale consumers after a package bump, unless `/${assetViewerRoutePrefix}/` is explicitly present in `defaultLocaleOnlyPrefixes`. Existing apps that already carry the legacy Claude/Codex resource prefixes remain default-locale-only after upgrading; remove the prefixes for resource sections you want generated in every configured locale. (#3820)
48
+ - Migration: before building with 5.14.0, remove or rename any authored locale resource overview/category `index.mdx` at a generator target and move custom titles, descriptions, and labels to `ZudoDocConfig.translations`. The generator refuses to overwrite an existing file unless its frontmatter contains `generated: true`. (#3820)
26
49
  - Updated the zfb peer family to 2.14.0, `@takazudo/zdtp` to 0.4.14, and the doc-history-server peer floor to 5.13.1. (`dbe8553c7`, `038870c94`, `95a82f15f`)
27
50
 
28
51
  ## [5.13.1] - 2026-08-28
@@ -2,6 +2,7 @@ import { jsx, jsxs } from "preact/jsx-runtime";
2
2
  import { assetRawHref, assetViewerHref } from "../asset-path/index.js";
3
3
  import {
4
4
  AssetFileIcon,
5
+ assetComponentViewerLocale,
5
6
  assetComponentText,
6
7
  formatAssetBytes,
7
8
  formatAssetLanguage,
@@ -23,7 +24,8 @@ function createAssetCard(context) {
23
24
  const viewerHref = assetViewerHref({
24
25
  base: context.base,
25
26
  routePrefix: context.routePrefix,
26
- path
27
+ path,
28
+ locale: assetComponentViewerLocale(context, path)
27
29
  });
28
30
  const rawHref = assetRawHref({ base: context.base, dir: context.dir, path });
29
31
  const details = [
@@ -2,6 +2,7 @@ import { jsx, jsxs } from "preact/jsx-runtime";
2
2
  import { assetViewerHref } from "../asset-path/index.js";
3
3
  import {
4
4
  AssetFileIcon,
5
+ assetComponentViewerLocale,
5
6
  assetComponentText,
6
7
  MissingAssetWarning,
7
8
  resolveAssetEntry
@@ -43,6 +44,7 @@ function createAssetCode(context) {
43
44
  base: context.base,
44
45
  routePrefix: context.routePrefix,
45
46
  path: resolved.path,
47
+ locale: assetComponentViewerLocale(context, resolved.path),
46
48
  fragment: excerpt.viewerLineAvailable ? `L${excerpt.startLine}` : void 0
47
49
  });
48
50
  const shown = Math.max(0, excerpt.endLine - excerpt.startLine + 1);
@@ -7,9 +7,14 @@ export interface AssetComponentContext {
7
7
  assetManifest: AssetManifest | null;
8
8
  routePrefix: string;
9
9
  dir: string;
10
+ /** Non-default locale segment for viewer links. */
11
+ locale?: string;
12
+ /** Match viewer paths that intentionally remain default-locale-only. */
13
+ isDefaultLocaleOnlyPath?: (path: string) => boolean;
10
14
  /** Locale-bound package translator. Omitted callers retain English fallbacks. */
11
15
  t?: (key: string) => string;
12
16
  }
17
+ export declare function assetComponentViewerLocale(context: AssetComponentContext, path: string): string | undefined;
13
18
  export declare function assetComponentText(context: AssetComponentContext, key: string, fallback: string, values?: Record<string, string | number>): string;
14
19
  export declare function resolveAssetEntry(src: string, context: AssetComponentContext): {
15
20
  path: string;
@@ -1,5 +1,11 @@
1
1
  import { jsx, jsxs } from "preact/jsx-runtime";
2
2
  import { decodeAuthoredHref } from "../asset-path/index.js";
3
+ function assetComponentViewerLocale(context, path) {
4
+ if (context.isDefaultLocaleOnlyPath?.(`/${context.routePrefix}/${path}`)) {
5
+ return void 0;
6
+ }
7
+ return context.locale;
8
+ }
3
9
  function assetComponentText(context, key, fallback, values = {}) {
4
10
  let text = context.t?.(key) ?? fallback;
5
11
  for (const [name, value] of Object.entries(values)) {
@@ -83,6 +89,7 @@ export {
83
89
  AssetFileIcon,
84
90
  MissingAssetWarning,
85
91
  assetComponentText,
92
+ assetComponentViewerLocale,
86
93
  formatAssetBytes,
87
94
  formatAssetLanguage,
88
95
  resolveAssetEntry
@@ -14,6 +14,7 @@ export interface AssetTreeNode {
14
14
  }
15
15
  export interface AssetIndexPageViewProps {
16
16
  entries: AssetIndexEntry[];
17
+ locale?: string;
17
18
  }
18
19
  /** Build a sorted directory tree and aggregate every directory's subtree totals. */
19
20
  export declare function buildAssetTree(entries: AssetIndexEntry[]): AssetTreeNode;
@@ -86,7 +86,7 @@ function iconFor(asset) {
86
86
  function countLabel(count, plural, single) {
87
87
  return (count === 1 ? single : plural).replace("{count}", String(count));
88
88
  }
89
- function AssetTree({ node, base, routePrefix, fileCountLabel, fileCountSingleLabel, linesLabel, root = false }) {
89
+ function AssetTree({ node, base, routePrefix, locale, fileCountLabel, fileCountSingleLabel, linesLabel, root = false }) {
90
90
  return /* @__PURE__ */ jsxs("ul", { "data-zd-asset-tree": root ? true : void 0, children: [
91
91
  node.dirs.map((dir) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("details", { open: true, children: [
92
92
  /* @__PURE__ */ jsxs("summary", { class: "flex cursor-pointer items-center gap-hsp-xs rounded px-hsp-sm py-vsp-3xs text-small hover:bg-accent/10 focus-visible:bg-accent/10", children: [
@@ -103,13 +103,13 @@ function AssetTree({ node, base, routePrefix, fileCountLabel, fileCountSingleLab
103
103
  formatAssetBytes(dir.bytes)
104
104
  ] })
105
105
  ] }),
106
- /* @__PURE__ */ jsx(AssetTree, { node: dir, base, routePrefix, fileCountLabel, fileCountSingleLabel, linesLabel })
106
+ /* @__PURE__ */ jsx(AssetTree, { node: dir, base, routePrefix, locale, fileCountLabel, fileCountSingleLabel, linesLabel })
107
107
  ] }) })),
108
108
  node.files.map((asset) => {
109
109
  const Icon = iconFor(asset);
110
110
  const facet = facetLabel(asset, linesLabel);
111
111
  const meta = [kindLabel(asset), facet, formatAssetBytes(asset.bytes)].filter(Boolean).join(" \xB7 ");
112
- return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("a", { href: assetViewerHref({ base, routePrefix, path: asset.path }), title: asset.name, class: "flex min-w-0 items-center gap-hsp-xs rounded px-hsp-sm py-vsp-3xs text-small text-fg hover:bg-accent/10 hover:text-accent hover:underline focus-visible:bg-accent/10 focus-visible:text-accent focus-visible:underline", children: [
112
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("a", { href: assetViewerHref({ base, routePrefix, path: asset.path, locale }), title: asset.name, class: "flex min-w-0 items-center gap-hsp-xs rounded px-hsp-sm py-vsp-3xs text-small text-fg hover:bg-accent/10 hover:text-accent hover:underline focus-visible:bg-accent/10 focus-visible:text-accent focus-visible:underline", children: [
113
113
  /* @__PURE__ */ jsx(Icon, { className: "h-icon-sm w-icon-sm shrink-0 text-muted" }),
114
114
  /* @__PURE__ */ jsx("span", { class: "min-w-0 truncate font-mono", children: basename(asset.path) }),
115
115
  /* @__PURE__ */ jsx("span", { class: "ml-auto hidden whitespace-nowrap pl-hsp-lg text-caption text-muted sm:block", children: meta })
@@ -120,17 +120,18 @@ function AssetTree({ node, base, routePrefix, fileCountLabel, fileCountSingleLab
120
120
  function createAssetIndexPageView(ctx) {
121
121
  assertChromeContext(ctx, "createAssetIndexPageView");
122
122
  const settings = ctx.settings;
123
- const locale = ctx.defaultLocale;
124
123
  const t = ctx.t;
125
124
  const composeMetaTitle = deriveComposeMetaTitle(ctx);
126
125
  const HeadWithDefaults = createHeadWithDefaults(ctx);
127
126
  const { Header: HeaderWithDefaults, Footer: FooterWithDefaults, Breadcrumb: BreadcrumbWithDefaults } = derivePrimaryChromeSlots(ctx);
128
127
  const BodyEndIslands = deriveBodyEndIslands(ctx);
129
128
  const dataThemePack = resolveThemePackSsrSlug(ctx.themePackRegistry, settings);
130
- return function AssetIndexPageView({ entries }) {
129
+ return function AssetIndexPageView({ entries, locale = ctx.defaultLocale }) {
130
+ const localeSegment = locale === ctx.defaultLocale ? void 0 : locale;
131
131
  const routePrefix = ctx.assetManifest?.routePrefix ?? settings.assetViewerRoutePrefix;
132
132
  const dir = ctx.assetManifest?.dir ?? settings.assetViewerDir;
133
- const indexUrl = ctx.withBase(`/${routePrefix}/`);
133
+ const indexUrl = ctx.withBase(`/${localeSegment ? `${localeSegment}/` : ""}${routePrefix}/`);
134
+ const homeUrl = ctx.withBase(`/${localeSegment ? `${localeSegment}/` : ""}`);
134
135
  const tree = buildAssetTree(entries);
135
136
  const folders = folderCount(tree);
136
137
  const fileCountLabel = t("asset.fileCount", locale);
@@ -138,7 +139,7 @@ function createAssetIndexPageView(ctx) {
138
139
  const folderCountLabel = t("asset.folderCount", locale);
139
140
  const folderCountSingleLabel = t("asset.folderCountSingle", locale);
140
141
  const title = t("asset.crumb", locale);
141
- return /* @__PURE__ */ jsx(DocLayoutWithDefaults, { title: composeMetaTitle(title), head: /* @__PURE__ */ jsx(HeadWithDefaults, { title, description: t("asset.indexDescription", locale), canonical: ctx.absoluteUrl(indexUrl) }), lang: locale, dataThemePack, noindex: settings.noindex, hideSidebar: true, hideToc: true, sidebarOverride: false, contentWide: true, breadcrumbOverride: /* @__PURE__ */ jsx(BreadcrumbWithDefaults, { items: [{ label: "", href: ctx.withBase("/") }, { label: title }] }), headerOverride: /* @__PURE__ */ jsx(HeaderWithDefaults, { lang: locale, currentPath: indexUrl, hideSidebarToggle: true }), footerOverride: /* @__PURE__ */ jsx(FooterWithDefaults, { lang: locale }), bodyEndComponents: /* @__PURE__ */ jsx(BodyEndIslands, { basePath: settings.base ?? "/" }), enableClientRouter: settings.dynamicPageTransition, children: /* @__PURE__ */ jsxs("div", { "data-zd-asset-index-page": true, children: [
142
+ return /* @__PURE__ */ jsx(DocLayoutWithDefaults, { title: composeMetaTitle(title), head: /* @__PURE__ */ jsx(HeadWithDefaults, { title, description: t("asset.indexDescription", locale), canonical: ctx.absoluteUrl(indexUrl) }), lang: locale, dataThemePack, noindex: settings.noindex, hideSidebar: true, hideToc: true, sidebarOverride: false, contentWide: true, breadcrumbOverride: /* @__PURE__ */ jsx(BreadcrumbWithDefaults, { items: [{ label: "", href: homeUrl }, { label: title }] }), headerOverride: /* @__PURE__ */ jsx(HeaderWithDefaults, { lang: locale, currentPath: indexUrl, hideSidebarToggle: true }), footerOverride: /* @__PURE__ */ jsx(FooterWithDefaults, { lang: locale }), bodyEndComponents: /* @__PURE__ */ jsx(BodyEndIslands, { basePath: settings.base ?? "/" }), enableClientRouter: settings.dynamicPageTransition, children: /* @__PURE__ */ jsxs("div", { "data-zd-asset-index-page": true, children: [
142
143
  /* @__PURE__ */ jsxs("header", { children: [
143
144
  /* @__PURE__ */ jsxs("div", { class: "mb-vsp-xs flex flex-wrap items-center gap-hsp-xs text-micro tracking-wide uppercase", children: [
144
145
  /* @__PURE__ */ jsx("span", { class: "rounded-full border border-muted px-hsp-sm py-vsp-3xs text-fg", children: title }),
@@ -163,7 +164,7 @@ function createAssetIndexPageView(ctx) {
163
164
  /* @__PURE__ */ jsx("button", { type: "button", disabled: true, "data-zd-asset-index-action": "collapse", class: "text-fg hover:text-accent focus-visible:text-accent", children: t("asset.collapseAll", locale) })
164
165
  ] })
165
166
  ] }),
166
- entries.length > 0 ? /* @__PURE__ */ jsx(AssetTree, { root: true, node: tree, base: settings.base, routePrefix, fileCountLabel, fileCountSingleLabel, linesLabel: t("asset.lines", locale) }) : /* @__PURE__ */ jsx("p", { class: "text-small text-muted", "data-zd-asset-index-empty": true, children: t("asset.indexEmpty", locale) }),
167
+ entries.length > 0 ? /* @__PURE__ */ jsx(AssetTree, { root: true, node: tree, base: settings.base, routePrefix, locale: localeSegment, fileCountLabel, fileCountSingleLabel, linesLabel: t("asset.lines", locale) }) : /* @__PURE__ */ jsx("p", { class: "text-small text-muted", "data-zd-asset-index-empty": true, children: t("asset.indexEmpty", locale) }),
167
168
  /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: ASSET_INDEX_PAGE_SCRIPT } })
168
169
  ] }) });
169
170
  };
@@ -8,6 +8,7 @@ export { ASSET_PAGE_SCRIPT } from "./script.js";
8
8
  export type { AssetRecord } from "../plugins/internal/asset-viewer/types.js";
9
9
  export interface AssetPageViewProps {
10
10
  entry: AssetRecord;
11
+ locale?: string;
11
12
  }
12
13
  export declare function AssetEyebrow({ asset, badge }: {
13
14
  asset: AssetRecord;
@@ -36,9 +37,17 @@ export declare function AssetCodeBody({ asset, copyLabel, wrapLabel, truncatedLa
36
37
  truncatedLabel: string;
37
38
  linesLabel?: string;
38
39
  }): VNode;
39
- export declare function AssetImageStage({ asset, rawUrl }: {
40
+ export interface AssetImageStageLabels {
41
+ fit: string;
42
+ actualSize: string;
43
+ checker: string;
44
+ dark: string;
45
+ enlarge: string;
46
+ }
47
+ export declare function AssetImageStage({ asset, rawUrl, labels }: {
40
48
  asset: AssetRecord;
41
49
  rawUrl: string;
50
+ labels: AssetImageStageLabels;
42
51
  }): VNode;
43
52
  export declare function AssetVideoStage({ asset, rawUrl }: {
44
53
  asset: AssetRecord;
@@ -60,8 +69,17 @@ export declare function AssetLinkedFrom({ asset, label }: {
60
69
  asset: AssetRecord;
61
70
  label: string;
62
71
  }): VNode | null;
63
- export declare function AssetDetails({ asset }: {
72
+ export interface AssetDetailsLabels {
73
+ heading: string;
74
+ type: string;
75
+ size: string;
76
+ path: string;
77
+ dimensions: string;
78
+ updated: string;
79
+ }
80
+ export declare function AssetDetails({ asset, labels }: {
64
81
  asset: AssetRecord;
82
+ labels: AssetDetailsLabels;
65
83
  }): VNode;
66
84
  /** Build the package-owned wide asset viewer page from a chrome context. */
67
85
  export declare function createAssetPageView<S extends Settings = Settings>(ctx: ChromeContext<S>): (props: AssetPageViewProps) => JSX.Element;
@@ -93,21 +93,21 @@ function AssetCodeBody({ asset, copyLabel, wrapLabel, truncatedLabel, linesLabel
93
93
  function EnlargeIcon() {
94
94
  return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { fill: "none", stroke: "currentColor", "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M8 3H3v5m13-5h5v5M8 21H3v-5m13 5h5v-5M3 8l6-6m12 6-6-6M3 16l6 6m12-6-6 6" }) });
95
95
  }
96
- function AssetImageStage({ asset, rawUrl }) {
96
+ function AssetImageStage({ asset, rawUrl, labels }) {
97
97
  return /* @__PURE__ */ jsxs("section", { children: [
98
98
  /* @__PURE__ */ jsxs("div", { class: "mb-vsp-xs flex flex-wrap gap-hsp-sm text-caption", children: [
99
99
  /* @__PURE__ */ jsxs("div", { class: "flex rounded border border-muted", children: [
100
- /* @__PURE__ */ jsx("button", { type: "button", "aria-pressed": "true", "data-zd-asset-action": "fit", class: "px-hsp-sm py-vsp-3xs", children: "Fit" }),
101
- /* @__PURE__ */ jsx("button", { type: "button", "aria-pressed": "false", "data-zd-asset-action": "1to1", class: "border-l border-muted px-hsp-sm py-vsp-3xs", children: "1:1" })
100
+ /* @__PURE__ */ jsx("button", { type: "button", "aria-pressed": "true", "data-zd-asset-action": "fit", class: "px-hsp-sm py-vsp-3xs", children: labels.fit }),
101
+ /* @__PURE__ */ jsx("button", { type: "button", "aria-pressed": "false", "data-zd-asset-action": "1to1", class: "border-l border-muted px-hsp-sm py-vsp-3xs", children: labels.actualSize })
102
102
  ] }),
103
103
  /* @__PURE__ */ jsxs("div", { class: "flex rounded border border-muted", children: [
104
- /* @__PURE__ */ jsx("button", { type: "button", "aria-pressed": "true", "data-zd-asset-action": "checker", class: "px-hsp-sm py-vsp-3xs", children: "Checker" }),
105
- /* @__PURE__ */ jsx("button", { type: "button", "aria-pressed": "false", "data-zd-asset-action": "dark", class: "border-l border-muted px-hsp-sm py-vsp-3xs", children: "Dark" })
104
+ /* @__PURE__ */ jsx("button", { type: "button", "aria-pressed": "true", "data-zd-asset-action": "checker", class: "px-hsp-sm py-vsp-3xs", children: labels.checker }),
105
+ /* @__PURE__ */ jsx("button", { type: "button", "aria-pressed": "false", "data-zd-asset-action": "dark", class: "border-l border-muted px-hsp-sm py-vsp-3xs", children: labels.dark })
106
106
  ] })
107
107
  ] }),
108
108
  /* @__PURE__ */ jsxs("figure", { class: "zd-enlargeable zd-asset-stage is-checker flex min-h-[20rem] items-center justify-center overflow-auto rounded border border-muted bg-surface p-hsp-lg", children: [
109
109
  /* @__PURE__ */ jsx("img", { src: rawUrl, alt: asset.description ?? asset.name, width: asset.width, height: asset.height }),
110
- /* @__PURE__ */ jsx("button", { type: "button", class: "zd-enlarge-btn", hidden: true, "aria-label": "Enlarge image", children: /* @__PURE__ */ jsx(EnlargeIcon, {}) })
110
+ /* @__PURE__ */ jsx("button", { type: "button", class: "zd-enlarge-btn", hidden: true, "aria-label": labels.enlarge, children: /* @__PURE__ */ jsx(EnlargeIcon, {}) })
111
111
  ] })
112
112
  ] });
113
113
  }
@@ -147,12 +147,12 @@ function AssetLinkedFrom({ asset, label }) {
147
147
  ] })) })
148
148
  ] });
149
149
  }
150
- function AssetDetails({ asset }) {
151
- const rows = [["Type", asset.mime], ["Size", formatAssetBytes(asset.bytes)], ["Path", asset.path]];
152
- if (asset.width !== void 0 && asset.height !== void 0) rows.splice(1, 0, ["Dimensions", `${asset.width} \xD7 ${asset.height}`]);
153
- if (asset.updatedDate) rows.push(["Updated", asset.updatedDate]);
150
+ function AssetDetails({ asset, labels }) {
151
+ const rows = [[labels.type, asset.mime], [labels.size, formatAssetBytes(asset.bytes)], [labels.path, asset.path]];
152
+ if (asset.width !== void 0 && asset.height !== void 0) rows.splice(1, 0, [labels.dimensions, `${asset.width} \xD7 ${asset.height}`]);
153
+ if (asset.updatedDate) rows.push([labels.updated, asset.updatedDate]);
154
154
  return /* @__PURE__ */ jsxs("section", { children: [
155
- /* @__PURE__ */ jsx("h2", { class: "mb-vsp-xs text-title font-bold", children: "Details" }),
155
+ /* @__PURE__ */ jsx("h2", { class: "mb-vsp-xs text-title font-bold", children: labels.heading }),
156
156
  /* @__PURE__ */ jsx("dl", { class: "grid grid-cols-[auto_1fr] gap-x-hsp-md gap-y-vsp-2xs text-caption", children: rows.map(([term, value]) => /* @__PURE__ */ jsxs(Fragment, { children: [
157
157
  /* @__PURE__ */ jsx("dt", { class: "font-medium text-muted", children: term }),
158
158
  /* @__PURE__ */ jsx("dd", { class: "min-w-0 break-words text-fg", children: value })
@@ -171,53 +171,72 @@ function MediaLayout({ stage, details, linked }) {
171
171
  function createAssetPageView(ctx) {
172
172
  assertChromeContext(ctx, "createAssetPageView");
173
173
  const settings = ctx.settings;
174
- const locale = ctx.defaultLocale;
175
174
  const t = ctx.t;
176
175
  const composeMetaTitle = deriveComposeMetaTitle(ctx);
177
176
  const HeadWithDefaults = createHeadWithDefaults(ctx);
178
177
  const { Header: HeaderWithDefaults, Footer: FooterWithDefaults, Breadcrumb: BreadcrumbWithDefaults } = derivePrimaryChromeSlots(ctx);
179
178
  const BodyEndIslands = deriveBodyEndIslands(ctx);
180
179
  const dataThemePack = resolveThemePackSsrSlug(ctx.themePackRegistry, settings);
181
- return function AssetPageView({ entry: asset }) {
180
+ return function AssetPageView({ entry: asset, locale = ctx.defaultLocale }) {
181
+ const localeSegment = locale === ctx.defaultLocale ? void 0 : locale;
182
182
  const routePrefix = ctx.assetManifest?.routePrefix ?? settings.assetViewerRoutePrefix;
183
183
  const dir = ctx.assetManifest?.dir ?? settings.assetViewerDir;
184
- const viewerUrl = assetViewerHref({ base: settings.base, routePrefix, path: asset.path });
184
+ const viewerUrl = assetViewerHref({ base: settings.base, routePrefix, path: asset.path, locale: localeSegment });
185
185
  const rawUrl = assetRawHref({ base: settings.base, dir, path: asset.path });
186
186
  const linesLabel = t("asset.lines", locale).replace(
187
187
  "{count}",
188
188
  String(asset.lines ?? 0)
189
189
  );
190
190
  const dirSegments = asset.dir.split("/").filter(Boolean);
191
- const indexUrl = ctx.withBase(`/${routePrefix}/`);
191
+ const indexUrl = ctx.withBase(`/${localeSegment ? `${localeSegment}/` : ""}${routePrefix}/`);
192
+ const homeUrl = ctx.withBase(`/${localeSegment ? `${localeSegment}/` : ""}`);
193
+ const backLink = locale === ctx.defaultLocale ? asset.linkedFrom[0] : asset.linkedFrom.find((link) => link.locale === locale) ?? asset.linkedFrom[0];
192
194
  const breadcrumbItems = [
193
- { label: "", href: ctx.withBase("/") },
195
+ { label: "", href: homeUrl },
194
196
  { label: t("asset.crumb", locale), ...settings.assetViewerIndex ? { href: indexUrl } : {} },
195
197
  ...dirSegments.map((label) => ({ label })),
196
198
  { label: asset.name }
197
199
  ];
198
200
  const linked = /* @__PURE__ */ jsx(AssetLinkedFrom, { asset, label: t("asset.linkedFrom", locale) });
199
- const details = /* @__PURE__ */ jsx(AssetDetails, { asset });
201
+ const detailsLabels = {
202
+ heading: t("asset.details", locale),
203
+ type: t("asset.type", locale),
204
+ size: t("asset.size", locale),
205
+ path: t("asset.path", locale),
206
+ dimensions: t("asset.dimensions", locale),
207
+ updated: t("doc.updated", locale)
208
+ };
209
+ const imageStageLabels = {
210
+ fit: t("asset.fit", locale),
211
+ actualSize: t("asset.actualSize", locale),
212
+ checker: t("asset.checker", locale),
213
+ dark: t("asset.dark", locale),
214
+ enlarge: t("asset.enlarge", locale)
215
+ };
216
+ const details = /* @__PURE__ */ jsx(AssetDetails, { asset, labels: detailsLabels });
200
217
  const downloadPanel = /* @__PURE__ */ jsx(AssetDownloadPanel, { asset, rawUrl, noPreview: t("asset.noPreview", locale), downloadLabel: t("asset.download", locale), copyLabel: t("asset.copy", locale) });
201
218
  let body;
202
219
  const isMedia = asset.previewable && asset.sniffOk && ["image", "video", "pdf"].includes(asset.kind);
203
220
  if (!asset.previewable || !asset.sniffOk) body = /* @__PURE__ */ jsxs(Fragment, { children: [
204
221
  downloadPanel,
205
- /* @__PURE__ */ jsx(AssetDetails, { asset }),
222
+ /* @__PURE__ */ jsx(AssetDetails, { asset, labels: detailsLabels }),
206
223
  linked
207
224
  ] });
208
- else if (asset.kind === "image") body = /* @__PURE__ */ jsx(MediaLayout, { stage: /* @__PURE__ */ jsx(AssetImageStage, { asset, rawUrl }), details, linked });
225
+ else if (asset.kind === "image") body = /* @__PURE__ */ jsx(MediaLayout, { stage: /* @__PURE__ */ jsx(AssetImageStage, { asset, rawUrl, labels: imageStageLabels }), details, linked });
209
226
  else if (asset.kind === "video") body = /* @__PURE__ */ jsx(MediaLayout, { stage: /* @__PURE__ */ jsx(AssetVideoStage, { asset, rawUrl }), details, linked });
210
227
  else if (asset.kind === "pdf") body = /* @__PURE__ */ jsx(MediaLayout, { stage: /* @__PURE__ */ jsx(AssetPdfStage, { asset, rawUrl, children: downloadPanel }), details, linked });
211
228
  else body = /* @__PURE__ */ jsxs(Fragment, { children: [
212
229
  /* @__PURE__ */ jsx(AssetCodeBody, { asset, copyLabel: t("asset.copy", locale), wrapLabel: t("asset.wrap", locale), truncatedLabel: t("asset.truncated", locale), linesLabel }),
213
- /* @__PURE__ */ jsx(AssetDetails, { asset }),
230
+ /* @__PURE__ */ jsx(AssetDetails, { asset, labels: detailsLabels }),
214
231
  linked
215
232
  ] });
216
233
  const showSource = settings.bodyFootUtilArea !== false && settings.bodyFootUtilArea.viewSourceLink !== false;
217
234
  return /* @__PURE__ */ jsx(DocLayoutWithDefaults, { title: composeMetaTitle(asset.name), head: /* @__PURE__ */ jsx(HeadWithDefaults, { title: asset.name, description: asset.description, canonical: ctx.absoluteUrl(viewerUrl) }), lang: locale, dataThemePack, noindex: settings.noindex, hideSidebar: true, hideToc: true, sidebarOverride: false, contentWide: true, breadcrumbOverride: /* @__PURE__ */ jsx(BreadcrumbWithDefaults, { items: breadcrumbItems }), headerOverride: /* @__PURE__ */ jsx(HeaderWithDefaults, { lang: locale, currentPath: viewerUrl, hideSidebarToggle: true }), footerOverride: /* @__PURE__ */ jsx(FooterWithDefaults, { lang: locale }), bodyEndComponents: /* @__PURE__ */ jsx(BodyEndIslands, { basePath: settings.base ?? "/", forceImageEnlarge: asset.kind === "image" && asset.previewable && asset.sniffOk }), enableClientRouter: settings.dynamicPageTransition, children: /* @__PURE__ */ jsxs("div", { class: "zd-asset-page", "data-zd-asset-page": true, children: [
218
- asset.linkedFrom[0] && /* @__PURE__ */ jsx("p", { class: "mb-vsp-xs text-caption", children: /* @__PURE__ */ jsxs("a", { href: asset.linkedFrom[0].href, class: "text-muted hover:text-accent focus-visible:text-accent hover:underline focus-visible:underline", children: [
219
- "\u2190 Back to ",
220
- asset.linkedFrom[0].title
235
+ backLink && /* @__PURE__ */ jsx("p", { class: "mb-vsp-xs text-caption", children: /* @__PURE__ */ jsxs("a", { href: backLink.href, class: "text-muted hover:text-accent focus-visible:text-accent hover:underline focus-visible:underline", children: [
236
+ "\u2190 ",
237
+ t("asset.backTo", locale),
238
+ " ",
239
+ backLink.title
221
240
  ] }) }),
222
241
  /* @__PURE__ */ jsx(AssetHeader, { asset, locale, badge: t("asset.badge", locale), updatedLabel: t("doc.updated", locale), linesLabel }),
223
242
  /* @__PURE__ */ jsx(AssetActions, { rawUrl, downloadLabel: t("asset.download", locale), openRawLabel: t("asset.openRaw", locale), copyLabel: t("asset.copy", locale), wrapLabel: t("asset.wrap", locale), code: !isMedia && asset.previewable && asset.sniffOk }),
@@ -9,6 +9,8 @@ export interface AssetViewerHrefOptions {
9
9
  base: string;
10
10
  routePrefix: string;
11
11
  path: string;
12
+ /** Non-default locale segment. Omit for the unprefixed default locale. */
13
+ locale?: string;
12
14
  /** A fragment with or without its leading `#`. */
13
15
  fragment?: string;
14
16
  }
@@ -59,9 +59,10 @@ function decodeAuthoredHref(href, options) {
59
59
  }
60
60
  function assetViewerHref(options) {
61
61
  const routePrefix = normalizeSettingPath(options.routePrefix, "routePrefix");
62
+ const localePrefix = options.locale === void 0 ? "" : `/${encodeURIComponent(normalizeLocaleSegment(options.locale))}`;
62
63
  const href = withBase(
63
64
  options.base,
64
- `/${routePrefix}/${encodeAssetPathForUrl(options.path)}/`
65
+ `${localePrefix}/${routePrefix}/${encodeAssetPathForUrl(options.path)}/`
65
66
  );
66
67
  if (options.fragment == null || options.fragment === "") return href;
67
68
  if (CONTROL_CHARS.test(options.fragment)) {
@@ -69,6 +70,15 @@ function assetViewerHref(options) {
69
70
  }
70
71
  return href + (options.fragment.startsWith("#") ? options.fragment : `#${options.fragment}`);
71
72
  }
73
+ function normalizeLocaleSegment(locale) {
74
+ if (locale.length === 0 || locale.includes("/") || locale.includes("\\")) {
75
+ throw new Error("Asset viewer locale must be a non-empty URL segment");
76
+ }
77
+ if (locale === "." || locale === ".." || CONTROL_CHARS.test(locale)) {
78
+ throw new Error("Asset viewer locale must be a safe URL segment");
79
+ }
80
+ return locale.normalize("NFC");
81
+ }
72
82
  function assetRawHref(options) {
73
83
  const dir = normalizeSettingPath(options.dir, "dir");
74
84
  return withBase(options.base, `/${dir}/${encodeAssetPathForUrl(options.path)}`);
@@ -314,12 +314,6 @@ function deriveMdxComponents(ctx) {
314
314
  t: (key, locale) => ctx.t(key, locale),
315
315
  versionedDocsUrl: ctx.versionedDocsUrl
316
316
  });
317
- function HtmlPreviewBound(props) {
318
- return HtmlPreviewWrapper({
319
- globalConfig: ctx.settings.htmlPreview ?? null,
320
- ...props
321
- });
322
- }
323
317
  const assetComponentContext = {
324
318
  base: ctx.settings.base,
325
319
  assetManifest: ctx.assetManifest,
@@ -329,8 +323,37 @@ function deriveMdxComponents(ctx) {
329
323
  function createMdxComponentsBound(lang = ctx.defaultLocale, currentVersion, currentSlug = "") {
330
324
  const localizedAssetContext = {
331
325
  ...assetComponentContext,
326
+ locale: lang === ctx.defaultLocale ? void 0 : lang,
327
+ isDefaultLocaleOnlyPath: ctx.isDefaultLocaleOnlyPath,
332
328
  t: (key) => ctx.t(key, lang)
333
329
  };
330
+ const localizedHtmlPreviewLabels = {
331
+ mobile: ctx.t("htmlPreview.viewport.mobile", lang),
332
+ tablet: ctx.t("htmlPreview.viewport.tablet", lang),
333
+ full: ctx.t("htmlPreview.viewport.full", lang),
334
+ viewportSize: ctx.t("htmlPreview.viewport.label", lang),
335
+ showCode: ctx.t("htmlPreview.source.show", lang),
336
+ hideCode: ctx.t("htmlPreview.source.hide", lang),
337
+ preview: ctx.t("htmlPreview.iframe.title", lang)
338
+ };
339
+ function HtmlPreviewBound(props) {
340
+ const labels = props.labels;
341
+ const effectiveLang = props.lang?.trim() ? props.lang : lang.trim() ? lang : "en";
342
+ return HtmlPreviewWrapper({
343
+ globalConfig: ctx.settings.htmlPreview ?? null,
344
+ ...props,
345
+ lang: effectiveLang,
346
+ labels: {
347
+ mobile: labels?.mobile ?? localizedHtmlPreviewLabels.mobile,
348
+ tablet: labels?.tablet ?? localizedHtmlPreviewLabels.tablet,
349
+ full: labels?.full ?? localizedHtmlPreviewLabels.full,
350
+ viewportSize: labels?.viewportSize ?? localizedHtmlPreviewLabels.viewportSize,
351
+ showCode: labels?.showCode ?? localizedHtmlPreviewLabels.showCode,
352
+ hideCode: labels?.hideCode ?? localizedHtmlPreviewLabels.hideCode,
353
+ preview: labels?.preview ?? localizedHtmlPreviewLabels.preview
354
+ }
355
+ });
356
+ }
334
357
  const mdxExtrasDefault = {
335
358
  Asset: createAssetCard(localizedAssetContext),
336
359
  AssetCode: createAssetCode(localizedAssetContext),
@@ -348,6 +371,8 @@ function deriveMdxComponents(ctx) {
348
371
  return createMdxComponents({
349
372
  settings: ctx.settings,
350
373
  assetManifest: ctx.assetManifest,
374
+ assetViewerLocale: lang === ctx.defaultLocale ? void 0 : lang,
375
+ isDefaultLocaleOnlyPath: ctx.isDefaultLocaleOnlyPath,
351
376
  locale: lang,
352
377
  currentVersion,
353
378
  currentSlug,
package/dist/compiled.css CHANGED
@@ -658,6 +658,9 @@
658
658
  .min-h-\[20rem\] {
659
659
  min-height: 20rem;
660
660
  }
661
+ .min-h-\[44px\] {
662
+ min-height: 44px;
663
+ }
661
664
  .min-h-\[60vh\] {
662
665
  min-height: 60vh;
663
666
  }
@@ -754,6 +757,9 @@
754
757
  .w-icon-xs {
755
758
  width: var(--spacing-icon-xs);
756
759
  }
760
+ .max-w-\[16rem\] {
761
+ max-width: 16rem;
762
+ }
757
763
  .max-w-\[64rem\] {
758
764
  max-width: 64rem;
759
765
  }
@@ -772,6 +778,9 @@
772
778
  .max-w-\[calc\(100vw-2rem\)\] {
773
779
  max-width: calc(100vw - 2rem);
774
780
  }
781
+ .max-w-\[calc\(100vw-var\(--spacing-hsp-xl\)\)\] {
782
+ max-width: calc(100vw - var(--spacing-hsp-xl));
783
+ }
775
784
  .max-w-\[clamp\(50rem\,75vw\,90rem\)\] {
776
785
  max-width: clamp(50rem, 75vw, 90rem);
777
786
  }
@@ -793,6 +802,9 @@
793
802
  .min-w-\[10rem\] {
794
803
  min-width: 10rem;
795
804
  }
805
+ .min-w-\[44px\] {
806
+ min-width: 44px;
807
+ }
796
808
  .flex-1 {
797
809
  flex: 1;
798
810
  }
@@ -9,10 +9,14 @@ export interface CreateContentLinkOptions {
9
9
  assetManifest: AssetManifest | null;
10
10
  routePrefix: string;
11
11
  dir: string;
12
+ /** Non-default locale segment used by asset-viewer links. */
13
+ locale?: string;
14
+ /** Match viewer paths that intentionally remain default-locale-only. */
15
+ isDefaultLocaleOnlyPath?: (path: string) => boolean;
12
16
  }
13
17
  /**
14
18
  * Bind ContentLink to an asset manifest. Only exact manifest entries are
15
19
  * decorated and redirected; every other anchor follows ContentLink verbatim.
16
20
  */
17
- export declare function createContentLink({ base, assetManifest, routePrefix, dir, }: CreateContentLinkOptions): (props: Props) => JSX.Element;
21
+ export declare function createContentLink({ base, assetManifest, routePrefix, dir, locale, isDefaultLocaleOnlyPath, }: CreateContentLinkOptions): (props: Props) => JSX.Element;
18
22
  export {};
@@ -24,7 +24,9 @@ function createContentLink({
24
24
  base,
25
25
  assetManifest,
26
26
  routePrefix,
27
- dir
27
+ dir,
28
+ locale,
29
+ isDefaultLocaleOnlyPath
28
30
  }) {
29
31
  return function ManifestAwareContentLink(props) {
30
32
  const { href, className, children } = props;
@@ -43,6 +45,7 @@ function createContentLink({
43
45
  base,
44
46
  routePrefix,
45
47
  path: entry.path,
48
+ locale: isDefaultLocaleOnlyPath?.(`/${routePrefix}/${entry.path}`) ? void 0 : locale,
46
49
  fragment: decoded.fragment
47
50
  }),
48
51
  children: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-baseline gap-x-hsp-xs font-mono", children: [
@@ -27,8 +27,9 @@ export interface FactoryI18n {
27
27
  /** Display label for a locale (e.g. `"EN"`, `"日本語"`). */
28
28
  getLocaleLabel: (locale: string) => string;
29
29
  /**
30
- * Translate a UI string key for a locale, falling back to the default locale
31
- * then the key itself. Optional: factories that emit no UI strings omit it.
30
+ * Translate a UI string key for a locale, falling back through the configured
31
+ * default locale, package English, then the raw key. Optional: factories
32
+ * that emit no UI strings omit it.
32
33
  */
33
34
  t?: (key: string, locale?: string) => string;
34
35
  }
@@ -136,7 +137,10 @@ export interface RouteContext<S = Settings> extends UrlHelpers, NavSourceDocsAPI
136
137
  } | undefined;
137
138
  /** Display label for a locale. */
138
139
  getLocaleLabel(locale: string): string;
139
- /** Translate a UI string key for a locale (falls back to default then key). */
140
+ /**
141
+ * Translate a UI string key for a locale (falls back through the configured
142
+ * default locale, package English, then the raw key).
143
+ */
140
144
  t(key: string, locale?: string): string;
141
145
  /** The full URL-helper bag (also spread as own members via `extends`). */
142
146
  urlHelpers: UrlHelpers;
@@ -120,6 +120,8 @@ function createHeaderWithDefaults(ctx) {
120
120
  LanguageSwitcher,
121
121
  {
122
122
  links: localeLinks,
123
+ accessibleLabel: t("language.switcher.label", lang),
124
+ idSuffix: "header",
123
125
  config: {
124
126
  base: settings.base.replace(/\/+$/, ""),
125
127
  defaultLocale,