create-zudo-doc 5.13.1 → 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 (46) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +41 -2
  3. package/dist/api.d.ts +3 -0
  4. package/dist/api.js +12 -1
  5. package/dist/claude-md-gen.d.ts +2 -1
  6. package/dist/claude-md-gen.js +40 -17
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.js +27 -0
  9. package/dist/compose.d.ts +5 -4
  10. package/dist/compose.js +6 -6
  11. package/dist/constants.js +8 -1
  12. package/dist/features/asset-viewer.d.ts +10 -0
  13. package/dist/features/asset-viewer.js +12 -0
  14. package/dist/features/claude-resources.d.ts +3 -2
  15. package/dist/features/claude-resources.js +3 -2
  16. package/dist/features/codex-resources.d.ts +3 -2
  17. package/dist/features/codex-resources.js +3 -2
  18. package/dist/features/doc-tags.d.ts +2 -2
  19. package/dist/features/doc-tags.js +2 -2
  20. package/dist/features/i18n.d.ts +4 -4
  21. package/dist/features/i18n.js +4 -4
  22. package/dist/features/index.js +2 -0
  23. package/dist/features/tag-governance.js +18 -9
  24. package/dist/features/versioning.d.ts +3 -6
  25. package/dist/features/versioning.js +3 -6
  26. package/dist/index.js +2 -1
  27. package/dist/locale-plan.d.ts +20 -0
  28. package/dist/locale-plan.js +58 -0
  29. package/dist/preset.d.ts +1 -0
  30. package/dist/preset.js +20 -0
  31. package/dist/prompts.d.ts +2 -0
  32. package/dist/prompts.js +14 -1
  33. package/dist/scaffold.d.ts +2 -3
  34. package/dist/scaffold.js +109 -77
  35. package/dist/utils.d.ts +7 -1
  36. package/dist/utils.js +9 -2
  37. package/dist/zfb-config-gen.d.ts +2 -1
  38. package/dist/zfb-config-gen.js +30 -17
  39. package/package.json +1 -1
  40. package/templates/base/pages/docs/[[...slug]].tsx +6 -8
  41. package/templates/base/scripts/check-links.js +80 -23
  42. package/templates/base/scripts/setup-doc-skill.sh +359 -21
  43. package/templates/features/claudeSkills/files/.claude/skills/zudo-doc-translate/SKILL.md +76 -53
  44. package/templates/features/claudeSkills/files/.claude/skills/zudo-doc-version-bump/SKILL.md +60 -36
  45. package/templates/features/claudeSkillsWriting/files/.claude/skills/zudo-doc-writing/SKILL.md +13 -3
  46. package/templates/features/i18n/files/pages/[locale]/docs/[[...slug]].tsx +2 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,35 @@ All notable changes to `create-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 one ordered locale-plan contract across the API, CLI, presets, prompts, scaffold output, and generated guidance, allowing new projects to select and emit any supported set of locales. (`10e34ca0`, `5a990248`, `c9543772`)
12
+
13
+ ### Bug Fixes
14
+
15
+ - Newly generated multi-locale projects now localize generated Claude/Codex resource routes by default instead of keeping them limited to the default locale. (`26ecad11`)
16
+
17
+ ### Other Changes
18
+
19
+ - Updated newly generated projects to use the zfb 2.14.2 package family. (`1c1f8f26`)
20
+
21
+ ## [5.14.0] - 2026-08-31
22
+
23
+ ### Features
24
+
25
+ - Added the `--asset-viewer` feature flag. Generated projects now emit `assetViewer: true` in `zfb.config.ts`, enabling the package-owned viewer for files under `public/assets/` without adding a host route stub or extra template files. (`45dc50cdb`)
26
+ - Generated doc-history projects now use the package-owned `run-parallel` command for their concurrent development servers, removing `npm-run-all2` and reporting unsupported trailing arguments instead of silently ignoring them. (`9402ff157`, `3ebfa6e9c`)
27
+
28
+ ### Bug Fixes
29
+
30
+ - Updated the generated link checker to recognize valid unquoted HTML attributes, decode character references, resolve percent-encoded routes, and avoid serialized-markup false positives. (`db9a07018`)
31
+
32
+ ### Other Changes
33
+
34
+ - Updated newly generated projects to use the zfb 2.14.0 package family and `@takazudo/zdtp` 0.4.14. (`dbe8553c7`, `038870c94`)
35
+
7
36
  ## [5.13.1] - 2026-08-28
8
37
 
9
38
  ### Bug Fixes
package/README.md CHANGED
@@ -35,13 +35,49 @@ pnpm create zudo-doc my-docs --yes
35
35
  # Fully specified, non-interactive
36
36
  pnpm create zudo-doc my-docs \
37
37
  --lang ja \
38
+ --additional-langs en,de \
38
39
  --scheme "Default Dark" \
39
- --no-i18n \
40
40
  --search \
41
41
  --pm pnpm \
42
42
  --install
43
43
  ```
44
44
 
45
+ ### Locales and translations
46
+
47
+ `--lang` selects the primary locale. Its pages use the unprefixed
48
+ `/docs/...` routes and `src/content/docs/`. Add any number of additional
49
+ locales, in the order shown by the language switcher, with
50
+ `--additional-langs <code,...>`:
51
+
52
+ ```bash
53
+ pnpm create zudo-doc my-docs \
54
+ --lang en \
55
+ --additional-langs ja,de \
56
+ --yes
57
+ ```
58
+
59
+ An omitted or blank list creates a single-locale project. The same rule
60
+ applies to a preset that omits `additionalLangs`; an explicit non-empty list
61
+ is normalized to lowercase, validates each code for safe URL/path use, rejects
62
+ duplicates and the primary code, and creates `src/content/docs-<code>/` plus
63
+ the corresponding `/<code>/docs/...` routes. A legacy preset containing only
64
+ `i18n: true` keeps compatibility inference (`ja` for primary `en`, otherwise
65
+ `en`).
66
+
67
+ CLI locale flags replace the preset list rather than merging with it. An
68
+ explicit `--additional-langs` also enables i18n; if it overrides
69
+ `--no-i18n`, the CLI prints a warning so the precedence is visible.
70
+
71
+ The generated starter uses Japanese prose for `ja` and English placeholder
72
+ prose for every other additional locale. Translate those pages before
73
+ publishing. Labels are configuration-driven: the switcher uses each locale's
74
+ configured `label` and map order, so custom codes and labels are not tied to
75
+ hard-coded `JP` or `JA` links.
76
+
77
+ Built-in UI translations resolve in this order:
78
+
79
+ `requested locale → configured default locale → package English → raw UI-string key`
80
+
45
81
  ## Options
46
82
 
47
83
  ### Project basics
@@ -50,6 +86,7 @@ pnpm create zudo-doc my-docs \
50
86
  |------|-------------|---------|
51
87
  | `[project-name]` | Project name (positional arg or `--name`) | prompted |
52
88
  | `--lang <code>` | Default language: `en`, `ja`, `zh-cn`, `zh-tw`, `ko`, `es`, `fr`, `de`, `pt` | `en` |
89
+ | `--additional-langs <a,b>` | Ordered additional locale codes; implies i18n and replaces a preset list | none |
53
90
  | `--pm <manager>` | Package manager: `pnpm`, `npm`, `yarn`, `bun` | detected |
54
91
  | `--[no-]install` | Install dependencies after scaffolding | prompted |
55
92
  | `-y, --yes` | Use defaults for all unspecified options, skip prompts | — |
@@ -72,10 +109,11 @@ Each feature has a `--[no-]<flag>` form. Passing `--feature` enables it; `--no-f
72
109
 
73
110
  | Flag | Description | Default |
74
111
  |------|-------------|---------|
75
- | `--[no-]i18n` | Multi-language support (adds a secondary locale) | off |
112
+ | `--[no-]i18n` | Legacy multi-language toggle; with no explicit list, infers one additional locale | off |
76
113
  | `--[no-]search` | Full-text search | on |
77
114
  | `--[no-]sidebar-filter` | Real-time sidebar filter | on |
78
115
  | `--[no-]image-enlarge` | Click-to-enlarge for oversized images | on |
116
+ | `--[no-]asset-viewer` | Viewer pages for files under `public/assets/` | off |
79
117
  | `--[no-]tag-governance` | Vocabulary-aware tag audit + suggest scripts | off |
80
118
  | `--[no-]claude-resources` | Auto-generate Claude Code docs from `.claude/` | off |
81
119
  | `--[no-]codex-resources` | Auto-generate Codex docs from `.codex/` + `AGENTS.md` | off |
@@ -139,6 +177,7 @@ import { createZudoDoc } from "create-zudo-doc";
139
177
  await createZudoDoc({
140
178
  projectName: "my-docs",
141
179
  defaultLang: "en",
180
+ additionalLangs: ["ja", "de"],
142
181
  colorSchemeMode: "single",
143
182
  singleScheme: "Default Dark",
144
183
  features: ["search", "sidebarFilter", "tagGovernance"],
package/dist/api.d.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  import type { PresetHeaderRightItem, PresetMetaTagsConfig } from "./preset.js";
2
+ export { resolveLocalePlan, type LocalePlan, type LocalePlanInput, } from "./locale-plan.js";
2
3
  export type { UserChoices } from "./prompts.js";
3
4
  export interface CreateOptions {
4
5
  projectName: string;
5
6
  /** Default language code (default: "en") */
6
7
  defaultLang?: string;
8
+ /** Ordered additional locale codes. A non-empty list implies i18n. */
9
+ additionalLangs?: string[];
7
10
  colorSchemeMode: "single" | "light-dark";
8
11
  singleScheme?: string;
9
12
  lightScheme?: string;
package/dist/api.js CHANGED
@@ -3,6 +3,8 @@ import { SINGLE_SCHEMES, THEME_PACKS } from "./constants.js";
3
3
  import { parseChangelogPackages, validateChangelogPackages, validateHeaderRightItems, validateMetaTags, } from "./preset.js";
4
4
  import { scaffold } from "./scaffold.js";
5
5
  import { initGitRepo, installDependencies, validateProjectName } from "./utils.js";
6
+ import { resolveLocalePlan } from "./locale-plan.js";
7
+ export { resolveLocalePlan, } from "./locale-plan.js";
6
8
  export async function createZudoDoc(options) {
7
9
  const { install = false, git = false, ...rest } = options;
8
10
  const nameError = validateProjectName(rest.projectName);
@@ -52,9 +54,18 @@ export async function createZudoDoc(options) {
52
54
  throw new Error(err);
53
55
  changelogPackages = parseChangelogPackages(changelogPackages);
54
56
  }
57
+ const localePlan = resolveLocalePlan({
58
+ defaultLang: rest.defaultLang ?? "en",
59
+ additionalLangs: rest.additionalLangs,
60
+ i18n: rest.features.includes("i18n"),
61
+ });
55
62
  const choices = {
56
63
  ...rest,
57
- defaultLang: rest.defaultLang ?? "en",
64
+ defaultLang: localePlan.defaultLang,
65
+ additionalLangs: rest.additionalLangs === undefined ? undefined : localePlan.additionalLangs,
66
+ features: localePlan.i18n
67
+ ? [...new Set([...rest.features, "i18n"])]
68
+ : rest.features.filter((feature) => feature !== "i18n"),
58
69
  changelogPackages,
59
70
  };
60
71
  await scaffold(choices);
@@ -1,4 +1,5 @@
1
1
  import type { UserChoices } from "./prompts.js";
2
+ import type { LocalePlan } from "./locale-plan.js";
2
3
  /**
3
4
  * Generate the per-project `CLAUDE.md` (minimal-scaffold shape, epic
4
5
  * zudolab/zudo-doc#2651, Wave 6 #2660). Rewritten from scratch — the old
@@ -7,4 +8,4 @@ import type { UserChoices } from "./prompts.js";
7
8
  * longer exists. The scaffolded project is now ~13 files; almost
8
9
  * everything referenced here lives in `node_modules/@takazudo/zudo-doc`.
9
10
  */
10
- export declare function generateCLAUDEFile(choices: UserChoices): string;
11
+ export declare function generateCLAUDEFile(choices: UserChoices, localePlan?: LocalePlan): string;
@@ -1,4 +1,5 @@
1
- import { capitalize, pmRunCommand } from "./utils.js";
1
+ import { resolveLocalePlan } from "./locale-plan.js";
2
+ import { capitalize, getLangLabel, pmRunCommand } from "./utils.js";
2
3
  /**
3
4
  * Generate the per-project `CLAUDE.md` (minimal-scaffold shape, epic
4
5
  * zudolab/zudo-doc#2651, Wave 6 #2660). Rewritten from scratch — the old
@@ -7,9 +8,30 @@ import { capitalize, pmRunCommand } from "./utils.js";
7
8
  * longer exists. The scaffolded project is now ~13 files; almost
8
9
  * everything referenced here lives in `node_modules/@takazudo/zudo-doc`.
9
10
  */
10
- export function generateCLAUDEFile(choices) {
11
+ export function generateCLAUDEFile(choices, localePlan = resolveLocalePlan({
12
+ defaultLang: choices.defaultLang,
13
+ additionalLangs: choices.additionalLangs,
14
+ i18n: choices.features.includes("i18n"),
15
+ i18nExplicitlyDisabled: choices.explicitlyDisabledFeatures?.includes("i18n"),
16
+ })) {
11
17
  const siteName = capitalize(choices.projectName.replace(/-/g, " "));
12
18
  const lines = [];
19
+ const localeDisplayName = (locale) => {
20
+ if (locale === "en")
21
+ return "English";
22
+ if (locale === "ja")
23
+ return "Japanese";
24
+ return getLangLabel(locale);
25
+ };
26
+ const localeStarterNote = (locale) => {
27
+ if (locale === "ja")
28
+ return "Japanese starter prose";
29
+ if (locale === "en")
30
+ return "English starter prose";
31
+ return "English placeholder prose pending translation";
32
+ };
33
+ const defaultLocaleLabel = localeDisplayName(localePlan.defaultLang);
34
+ const additionalLocales = localePlan.i18n ? localePlan.additionalLangs : [];
13
35
  lines.push(`# ${siteName}`);
14
36
  lines.push(``);
15
37
  lines.push(`Documentation site built with [zudo-doc](https://github.com/zudolab/zudo-doc) — a zfb-based documentation framework with MDX, Tailwind CSS v4, and Preact islands. This project is intentionally minimal: one config file (\`zfb.config.ts\`) plus markdown content — layout, chrome, and islands all ship from \`@takazudo/zudo-doc\` in \`node_modules\`.`);
@@ -29,10 +51,10 @@ export function generateCLAUDEFile(choices) {
29
51
  lines.push(``);
30
52
  const pm = choices.packageManager;
31
53
  if (choices.features.includes("docHistory")) {
32
- lines.push(`- \`${pmRunCommand(pm, "dev")}\` — runs the zfb dev server (port 4321) and the doc-history API server (port 4322) concurrently via \`run-p\` (\`${pmRunCommand(pm, "dev:zfb")}\` / \`${pmRunCommand(pm, "dev:history")}\` individually)`);
54
+ lines.push(`- \`${pmRunCommand(pm, "dev")}\` — runs the zfb dev server (port 4321) and the doc-history API server (port 4322) concurrently via \`run-parallel\` (a bin from @takazudo/zudo-doc) (\`${pmRunCommand(pm, "dev:zfb")}\` / \`${pmRunCommand(pm, "dev:history")}\` individually)`);
33
55
  lines.push(`- \`${pmRunCommand(pm, "dev:network")}\` — same, but zfb binds \`--host 0.0.0.0\` for LAN access (\`${pmRunCommand(pm, "dev:zfb:network")}\` individually); the doc-history server stays loopback-only and LAN clients reach it through zfb's \`/doc-history/*\` dev proxy`);
34
56
  lines.push(`- **Trusted networks only:** this also serves your git doc-history — including UNPUBLISHED local commits — to anyone on the LAN via the \`/doc-history/*\` proxy`);
35
- lines.push(`- \`run-p\` swallows trailing args, so other zfb flags don't forward through \`${pmRunCommand(pm, "dev")}\` pass them directly instead: \`${pm} run dev:zfb -- <flags>\``);
57
+ lines.push(`- \`run-parallel\` does not forward trailing args it rejects them with an error rather than silently ignoring them, so pass other zfb flags directly instead: \`${pm} run dev:zfb -- <flags>\``);
36
58
  }
37
59
  else {
38
60
  lines.push(`- \`${pmRunCommand(pm, "dev")}\` — zfb dev server (port 4321)`);
@@ -49,16 +71,17 @@ export function generateCLAUDEFile(choices) {
49
71
  lines.push(`pages/`);
50
72
  lines.push(`├── index.tsx # 1-line re-export of the package home route`);
51
73
  lines.push(`└── docs/[[...slug]].tsx # self-contained doc-route stub (required for \`${pm} dev\`)`);
52
- if (choices.features.includes("i18n")) {
74
+ if (localePlan.i18n) {
53
75
  lines.push(` [locale]/docs/[[...slug]].tsx # same, for non-default locales`);
54
76
  }
55
77
  lines.push(`src/`);
56
78
  lines.push(`├── chrome-bindings.tsx # optional typed primary chrome / named header / MDX bindings`);
57
79
  lines.push(`├── content/`);
58
- lines.push(`│ └── docs/ # MDX content (this project's showcase docs)`);
59
- if (choices.features.includes("i18n")) {
60
- const secondaryLang = choices.defaultLang === "ja" ? "en" : "ja";
61
- lines.push(`│ └── docs-${secondaryLang}/ # ${secondaryLang === "ja" ? "Japanese" : "English"} MDX content (mirrors docs/)`);
80
+ const defaultBranch = additionalLocales.length === 0 ? "└──" : "├──";
81
+ lines.push(`│ ${defaultBranch} docs/ # ${defaultLocaleLabel} (default) MDX content (routes at /docs/; ${localeStarterNote(localePlan.defaultLang)})`);
82
+ for (const [index, locale] of additionalLocales.entries()) {
83
+ const branch = index === additionalLocales.length - 1 ? "└──" : "├──";
84
+ lines.push(`│ ${branch} docs-${locale}/ # ${localeDisplayName(locale)} MDX content (routes at /${locale}/docs/; ${localeStarterNote(locale)})`);
62
85
  }
63
86
  lines.push(`└── styles/`);
64
87
  lines.push(` └── global.css # @import chain + a token-override slot — that's it`);
@@ -105,16 +128,16 @@ export function generateCLAUDEFile(choices) {
105
128
  lines.push(`Admonitions (above), tabbed content (\`<Tabs>\` / \`<TabItem>\`, \`<CodeGroup>\`), and block math (\`<MathBlock>\`) work the same way — no import. Full reference: https://zudo-doc.takazudomodular.com/docs/components/`);
106
129
  lines.push(``);
107
130
  // i18n section
108
- if (choices.features.includes("i18n")) {
109
- const secondaryLang = choices.defaultLang === "ja" ? "en" : "ja";
110
- const defaultLabel = choices.defaultLang === "ja" ? "Japanese" : "English";
111
- const secondaryLabel = secondaryLang === "ja" ? "Japanese" : "English";
131
+ if (localePlan.i18n) {
112
132
  lines.push(`## i18n`);
113
133
  lines.push(``);
114
- lines.push(`- ${defaultLabel} (default): \`/docs/...\` — content in \`src/content/docs/\``);
115
- lines.push(`- ${secondaryLabel}: \`/${secondaryLang}/docs/...\` content in \`src/content/docs-${secondaryLang}/\``);
116
- lines.push(`- ${secondaryLabel} docs should mirror the ${defaultLabel} directory structure`);
117
- lines.push(`- Both \`pages/docs/[[...slug]].tsx\` and \`pages/[locale]/docs/[[...slug]].tsx\` are self-contained doc-route stubs shipped by the generator — required so \`${pm} dev\` doesn't 404 on doc pages (a zfb dev-mode limitation on package-injected dynamic routes). Don't delete them.`);
134
+ lines.push(`- ${defaultLocaleLabel} (default, \`${localePlan.defaultLang}\`): \`/docs/...\` — content in \`src/content/docs/\` (${localeStarterNote(localePlan.defaultLang)})`);
135
+ for (const locale of additionalLocales) {
136
+ lines.push(`- ${localeDisplayName(locale)} (\`${locale}\`): \`/${locale}/docs/...\` content in \`src/content/docs-${locale}/\` (${localeStarterNote(locale)})`);
137
+ }
138
+ lines.push(`- Every additional-locale directory should mirror the default directory structure`);
139
+ lines.push(`- The \`ja\` locale, when configured, receives Japanese starter prose and uses Japanese translation conventions. Other non-EN locale directories currently receive English placeholder prose pending translation; do not assume they are already translated.`);
140
+ lines.push(`- Both \`pages/docs/[[...slug]].tsx\` and \`pages/[locale]/docs/[[...slug]].tsx\` are self-contained doc-route stubs shipped by the generator as explicit host-owned seams. zfb 2.13.1 also renders package-injected dynamic routes in dev; keep these files so the generated project retains route ownership and customization.`);
118
141
  lines.push(``);
119
142
  }
120
143
  // Enabled features
package/dist/cli.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export interface CliArgs {
2
2
  name?: string;
3
3
  lang?: string;
4
+ additionalLangs?: string[];
4
5
  colorSchemeMode?: "single" | "light-dark";
5
6
  scheme?: string;
6
7
  lightScheme?: string;
@@ -43,6 +44,8 @@ export interface CliArgs {
43
44
  yes?: boolean;
44
45
  help?: boolean;
45
46
  }
47
+ /** Presets are the base layer; an explicitly supplied CLI list replaces it. */
48
+ export declare function layerAdditionalLangs(presetValue: string[] | undefined, cliValue: string[] | undefined): string[] | undefined;
46
49
  export declare function parseArgs(argv?: string[]): CliArgs;
47
50
  export declare function printHelp(): void;
48
51
  export declare function validateArgs(args: CliArgs): string | null;
package/dist/cli.js CHANGED
@@ -3,11 +3,17 @@ import pc from "picocolors";
3
3
  import { FEATURES, SINGLE_SCHEMES, SUPPORTED_LANGS, THEME_PACKS } from "./constants.js";
4
4
  import { parseChangelogPackages, validateChangelogPackages, } from "./preset.js";
5
5
  import { validateProjectName } from "./utils.js";
6
+ import { resolveLocalePlan } from "./locale-plan.js";
7
+ /** Presets are the base layer; an explicitly supplied CLI list replaces it. */
8
+ export function layerAdditionalLangs(presetValue, cliValue) {
9
+ return cliValue === undefined ? presetValue : cliValue;
10
+ }
6
11
  export function parseArgs(argv = process.argv.slice(2)) {
7
12
  const raw = minimist(argv, {
8
13
  string: [
9
14
  "name",
10
15
  "lang",
16
+ "additional-langs",
11
17
  "color-scheme-mode",
12
18
  "scheme",
13
19
  "light-scheme",
@@ -42,6 +48,10 @@ export function parseArgs(argv = process.argv.slice(2)) {
42
48
  }
43
49
  if (raw.lang)
44
50
  args.lang = raw.lang;
51
+ if (raw["additional-langs"] !== undefined) {
52
+ const value = raw["additional-langs"];
53
+ args.additionalLangs = (typeof value === "string" ? value : String(value)).split(",");
54
+ }
45
55
  if (raw["color-scheme-mode"])
46
56
  args.colorSchemeMode = raw["color-scheme-mode"];
47
57
  if (raw.scheme)
@@ -96,6 +106,7 @@ ${pc.bold("Options:")}
96
106
  --name <name> Project name (or first positional arg)
97
107
  --lang <code> Default language (${langList})
98
108
  Default: en
109
+ --additional-langs <a,b> Additional locale codes (ordered; implies i18n)
99
110
  --color-scheme-mode <mode> single | light-dark
100
111
  --scheme <name> Color scheme (single mode)
101
112
  --light-scheme <name> Light scheme (light-dark mode)
@@ -137,6 +148,22 @@ export function validateArgs(args) {
137
148
  return `Invalid language "${args.lang}". Supported: ${validLangs.join(", ")}`;
138
149
  }
139
150
  }
151
+ // A preset may supply the primary locale, so defer cross-field validation
152
+ // until the layered choices reach runPrompts() when --lang is omitted.
153
+ if (args.additionalLangs !== undefined &&
154
+ (args.preset === undefined || args.lang !== undefined)) {
155
+ try {
156
+ resolveLocalePlan({
157
+ defaultLang: args.lang ?? "en",
158
+ additionalLangs: args.additionalLangs,
159
+ i18n: args.i18n ?? false,
160
+ i18nExplicitlyDisabled: args.i18n === false,
161
+ });
162
+ }
163
+ catch (error) {
164
+ return error instanceof Error ? error.message : String(error);
165
+ }
166
+ }
140
167
  if (args.colorSchemeMode && !["single", "light-dark"].includes(args.colorSchemeMode)) {
141
168
  return `Invalid color-scheme-mode "${args.colorSchemeMode}". Must be "single" or "light-dark"`;
142
169
  }
package/dist/compose.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { UserChoices } from "./prompts.js";
2
+ import type { LocalePlan } from "./locale-plan.js";
2
3
  /** A single injection into a shared file at an anchor point. */
3
4
  export interface Injection {
4
5
  /** Target file path relative to project root */
@@ -29,13 +30,13 @@ export interface FeatureDefinition {
29
30
  * Post-processing hook for complex transformations that cannot be expressed
30
31
  * as simple file copies or anchor injections (e.g. i18n page patching).
31
32
  */
32
- postProcess?: (targetDir: string, choices: UserChoices) => Promise<void>;
33
+ postProcess?: (targetDir: string, choices: UserChoices, localePlan?: LocalePlan) => Promise<void>;
33
34
  }
34
35
  /**
35
36
  * A function that returns a FeatureDefinition based on user choices.
36
37
  * This allows injections to be conditional on other choices.
37
38
  */
38
- export type FeatureModule = (choices: UserChoices) => FeatureDefinition;
39
+ export type FeatureModule = (choices: UserChoices, localePlan?: LocalePlan) => FeatureDefinition;
39
40
  /**
40
41
  * Apply a list of injections to files in the target directory.
41
42
  *
@@ -62,7 +63,7 @@ export declare function copyFeatureFiles(featureFilesDir: string, targetDir: str
62
63
  * Resolve which features are selected based on UserChoices.
63
64
  * Handles special cases like the footer pseudo-feature.
64
65
  */
65
- export declare function resolveSelectedFeatures(choices: UserChoices, featureModules: Record<string, FeatureModule>): FeatureDefinition[];
66
+ export declare function resolveSelectedFeatures(choices: UserChoices, featureModules: Record<string, FeatureModule>, localePlan?: LocalePlan): FeatureDefinition[];
66
67
  /**
67
68
  * Validate that all feature dependencies are satisfied.
68
69
  * Throws if a selected feature depends on one that isn't selected.
@@ -96,4 +97,4 @@ export declare const ANCHOR_FILES: string[];
96
97
  * 5. Run post-processing hooks
97
98
  * 6. Clean up unused anchors
98
99
  */
99
- export declare function composeFeatures(targetDir: string, choices: UserChoices, featureModules: Record<string, FeatureModule>, featuresDir: string): Promise<void>;
100
+ export declare function composeFeatures(targetDir: string, choices: UserChoices, featureModules: Record<string, FeatureModule>, featuresDir: string, localePlan?: LocalePlan): Promise<void>;
package/dist/compose.js CHANGED
@@ -120,7 +120,7 @@ export async function copyFeatureFiles(featureFilesDir, targetDir) {
120
120
  * Resolve which features are selected based on UserChoices.
121
121
  * Handles special cases like the footer pseudo-feature.
122
122
  */
123
- export function resolveSelectedFeatures(choices, featureModules) {
123
+ export function resolveSelectedFeatures(choices, featureModules, localePlan) {
124
124
  const selected = [];
125
125
  for (const [name, moduleFn] of Object.entries(featureModules)) {
126
126
  // Special case: footer is activated by footerNavGroup, footerCopyright,
@@ -129,12 +129,12 @@ export function resolveSelectedFeatures(choices, featureModules) {
129
129
  if (choices.features.includes("footerNavGroup") ||
130
130
  choices.features.includes("footerCopyright") ||
131
131
  choices.features.includes("footerTaglist")) {
132
- selected.push(moduleFn(choices));
132
+ selected.push(moduleFn(choices, localePlan));
133
133
  }
134
134
  continue;
135
135
  }
136
136
  if (choices.features.includes(name)) {
137
- selected.push(moduleFn(choices));
137
+ selected.push(moduleFn(choices, localePlan));
138
138
  }
139
139
  }
140
140
  return selected;
@@ -182,9 +182,9 @@ export const ANCHOR_FILES = [];
182
182
  * 5. Run post-processing hooks
183
183
  * 6. Clean up unused anchors
184
184
  */
185
- export async function composeFeatures(targetDir, choices, featureModules, featuresDir) {
185
+ export async function composeFeatures(targetDir, choices, featureModules, featuresDir, localePlan) {
186
186
  // 1. Resolve
187
- const features = resolveSelectedFeatures(choices, featureModules);
187
+ const features = resolveSelectedFeatures(choices, featureModules, localePlan);
188
188
  const selectedNames = new Set(features.map((f) => f.name));
189
189
  // 2. Validate
190
190
  validateDependencies(features, selectedNames);
@@ -202,7 +202,7 @@ export async function composeFeatures(targetDir, choices, featureModules, featur
202
202
  // 5. Post-processing
203
203
  for (const feature of features) {
204
204
  if (feature.postProcess) {
205
- await feature.postProcess(targetDir, choices);
205
+ await feature.postProcess(targetDir, choices, localePlan);
206
206
  }
207
207
  }
208
208
  // 6. Clean up unused anchors
package/dist/constants.js CHANGED
@@ -187,7 +187,7 @@ export const FEATURES = [
187
187
  {
188
188
  value: "i18n",
189
189
  label: "i18n (multi-language)",
190
- hint: "Add a secondary language",
190
+ hint: "Configure additional locales",
191
191
  default: false,
192
192
  cliFlag: "i18n",
193
193
  },
@@ -331,6 +331,13 @@ export const FEATURES = [
331
331
  default: true,
332
332
  cliFlag: "image-enlarge",
333
333
  },
334
+ {
335
+ value: "assetViewer",
336
+ label: "Asset viewer",
337
+ hint: "Viewer pages for files under public/assets",
338
+ default: false,
339
+ cliFlag: "asset-viewer",
340
+ },
334
341
  {
335
342
  value: "dynamicPageTransition",
336
343
  label: "Dynamic page transition",
@@ -0,0 +1,10 @@
1
+ import type { FeatureModule } from "../compose.js";
2
+ /**
3
+ * Asset-viewer feature.
4
+ *
5
+ * Purely a `zudoDoc({ assetViewer: true })` field (see `zfb-config-gen.ts`).
6
+ * The scanner, viewer route, and all supporting UI are package-owned by
7
+ * `@takazudo/zudo-doc`; generated projects only need the config toggle.
8
+ * Nothing is injected or copied into the scaffold.
9
+ */
10
+ export declare const assetViewerFeature: FeatureModule;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Asset-viewer feature.
3
+ *
4
+ * Purely a `zudoDoc({ assetViewer: true })` field (see `zfb-config-gen.ts`).
5
+ * The scanner, viewer route, and all supporting UI are package-owned by
6
+ * `@takazudo/zudo-doc`; generated projects only need the config toggle.
7
+ * Nothing is injected or copied into the scaffold.
8
+ */
9
+ export const assetViewerFeature = () => ({
10
+ name: "assetViewer",
11
+ injections: [],
12
+ });
@@ -5,7 +5,8 @@ import type { FeatureModule } from "../compose.js";
5
5
  * Fully plugin-owned (`@takazudo/zudo-doc/plugins/claude-resources`,
6
6
  * `zudoDocPreset()` wires it whenever `settings.claudeResources` is
7
7
  * truthy). Generation is package-owned. This feature's touch points are now
8
- * just: `claudeResources` + `defaultLocaleOnlyPrefixes` fields
9
- * (`zfb-config-gen.ts`).
8
+ * just the `claudeResources` field (`zfb-config-gen.ts`). Resource routes are
9
+ * localized by default; projects can opt selected paths out through the
10
+ * general-purpose `defaultLocaleOnlyPrefixes` setting.
10
11
  */
11
12
  export declare const claudeResourcesFeature: FeatureModule;
@@ -4,8 +4,9 @@
4
4
  * Fully plugin-owned (`@takazudo/zudo-doc/plugins/claude-resources`,
5
5
  * `zudoDocPreset()` wires it whenever `settings.claudeResources` is
6
6
  * truthy). Generation is package-owned. This feature's touch points are now
7
- * just: `claudeResources` + `defaultLocaleOnlyPrefixes` fields
8
- * (`zfb-config-gen.ts`).
7
+ * just the `claudeResources` field (`zfb-config-gen.ts`). Resource routes are
8
+ * localized by default; projects can opt selected paths out through the
9
+ * general-purpose `defaultLocaleOnlyPrefixes` setting.
9
10
  */
10
11
  export const claudeResourcesFeature = () => ({
11
12
  name: "claudeResources",
@@ -5,7 +5,8 @@ import type { FeatureModule } from "../compose.js";
5
5
  * Fully plugin-owned (`@takazudo/zudo-doc/plugins/codex-resources`,
6
6
  * `zudoDocPreset()` wires it whenever `settings.codexResources` is
7
7
  * truthy). Generation is package-owned. This feature's touch points are now
8
- * just: `codexResources` + `defaultLocaleOnlyPrefixes` fields
9
- * (`zfb-config-gen.ts`).
8
+ * just the `codexResources` field (`zfb-config-gen.ts`). Resource routes are
9
+ * localized by default; projects can opt selected paths out through the
10
+ * general-purpose `defaultLocaleOnlyPrefixes` setting.
10
11
  */
11
12
  export declare const codexResourcesFeature: FeatureModule;
@@ -4,8 +4,9 @@
4
4
  * Fully plugin-owned (`@takazudo/zudo-doc/plugins/codex-resources`,
5
5
  * `zudoDocPreset()` wires it whenever `settings.codexResources` is
6
6
  * truthy). Generation is package-owned. This feature's touch points are now
7
- * just: `codexResources` + `defaultLocaleOnlyPrefixes` fields
8
- * (`zfb-config-gen.ts`).
7
+ * just the `codexResources` field (`zfb-config-gen.ts`). Resource routes are
8
+ * localized by default; projects can opt selected paths out through the
9
+ * general-purpose `defaultLocaleOnlyPrefixes` setting.
9
10
  */
10
11
  export const codexResourcesFeature = () => ({
11
12
  name: "codexResources",
@@ -9,7 +9,7 @@ import type { FeatureModule } from "../compose.js";
9
9
  * since the host catch-all stubs were retired in favor of package
10
10
  * injection — there is nothing left to copy or postProcess.
11
11
  *
12
- * Known limitation: same injected-DYNAMIC-route dev-mode gap noted on the
13
- * versioning feature the tag routes may 404 in `zfb dev` (build is fine).
12
+ * zfb 2.13.1 renders these injected tag routes in both dev and build. This
13
+ * feature has no host stubs because package injection owns the routes.
14
14
  */
15
15
  export declare const docTagsFeature: FeatureModule;
@@ -8,8 +8,8 @@
8
8
  * since the host catch-all stubs were retired in favor of package
9
9
  * injection — there is nothing left to copy or postProcess.
10
10
  *
11
- * Known limitation: same injected-DYNAMIC-route dev-mode gap noted on the
12
- * versioning feature the tag routes may 404 in `zfb dev` (build is fine).
11
+ * zfb 2.13.1 renders these injected tag routes in both dev and build. This
12
+ * feature has no host stubs because package injection owns the routes.
13
13
  */
14
14
  export const docTagsFeature = () => ({
15
15
  name: "docTags",
@@ -3,9 +3,9 @@ import type { FeatureModule } from "../compose.js";
3
3
  * i18n feature — gates the locale-prefixed page set.
4
4
  *
5
5
  * Locked manifest (#2653 Decision 4, i18n addendum): "i18n ON adds
6
- * `pages/[locale]/docs/[[...slug]].tsx` (a second doc stub, locale variant)
7
- * … No other pages." That ONE self-contained stub — required for the same
8
- * injected-DYNAMIC-route dev-mode 404 gap the default-locale stub fixes
6
+ * `pages/[locale]/docs/[[...slug]].tsx` (the additional-locale doc route stub)
7
+ * … No other pages." That ONE self-contained stub — retained as an explicit
8
+ * host-owned route seam; zfb 2.13.1 also serves injected dynamic routes in dev
9
9
  * is shipped under `templates/features/i18n/files/pages/[locale]/docs/`
10
10
  * and copied by `composeFeatures → copyFeatureFiles` whenever `i18n` is
11
11
  * selected. The old `pages/[locale]/index.tsx` home-route template is GONE
@@ -15,7 +15,7 @@ import type { FeatureModule } from "../compose.js";
15
15
  *
16
16
  * No injections: the stub iterates `settings.locales` at build/request
17
17
  * time, so no postProcess regex patching is required for non-default
18
- * languages. Secondary-language content mirrors under
18
+ * locales. Additional-locale content mirrors under
19
19
  * `src/content/docs-<lang>/` are seeded by `scaffold.ts`.
20
20
  *
21
21
  * Loud-failure check: per spec-lock Decision 8 (#1737), abort scaffolding
@@ -5,9 +5,9 @@ import { fileURLToPath } from "url";
5
5
  * i18n feature — gates the locale-prefixed page set.
6
6
  *
7
7
  * Locked manifest (#2653 Decision 4, i18n addendum): "i18n ON adds
8
- * `pages/[locale]/docs/[[...slug]].tsx` (a second doc stub, locale variant)
9
- * … No other pages." That ONE self-contained stub — required for the same
10
- * injected-DYNAMIC-route dev-mode 404 gap the default-locale stub fixes
8
+ * `pages/[locale]/docs/[[...slug]].tsx` (the additional-locale doc route stub)
9
+ * … No other pages." That ONE self-contained stub — retained as an explicit
10
+ * host-owned route seam; zfb 2.13.1 also serves injected dynamic routes in dev
11
11
  * is shipped under `templates/features/i18n/files/pages/[locale]/docs/`
12
12
  * and copied by `composeFeatures → copyFeatureFiles` whenever `i18n` is
13
13
  * selected. The old `pages/[locale]/index.tsx` home-route template is GONE
@@ -17,7 +17,7 @@ import { fileURLToPath } from "url";
17
17
  *
18
18
  * No injections: the stub iterates `settings.locales` at build/request
19
19
  * time, so no postProcess regex patching is required for non-default
20
- * languages. Secondary-language content mirrors under
20
+ * locales. Additional-locale content mirrors under
21
21
  * `src/content/docs-<lang>/` are seeded by `scaffold.ts`.
22
22
  *
23
23
  * Loud-failure check: per spec-lock Decision 8 (#1737), abort scaffolding
@@ -22,6 +22,7 @@ import { versioningFeature } from "./versioning.js";
22
22
  import { tauriFeature } from "./tauri.js";
23
23
  import { tauriDevFeature } from "./tauri-dev.js";
24
24
  import { imageEnlargeFeature } from "./image-enlarge.js";
25
+ import { assetViewerFeature } from "./asset-viewer.js";
25
26
  import { dynamicPageTransitionFeature } from "./dynamic-page-transition.js";
26
27
  import { tagGovernanceFeature } from "./tag-governance.js";
27
28
  import { footerTaglistFeature } from "./footer-taglist.js";
@@ -50,6 +51,7 @@ export const featureModules = {
50
51
  tauri: tauriFeature,
51
52
  tauriDev: tauriDevFeature,
52
53
  imageEnlarge: imageEnlargeFeature,
54
+ assetViewer: assetViewerFeature,
53
55
  dynamicPageTransition: dynamicPageTransitionFeature,
54
56
  // skillSymlinker — handled in scaffold.ts
55
57
  // claudeSkills — handled in scaffold.ts (copies zudo-doc-* skills from monorepo)