diff --git a/docs/fr/how-to/customize-bmad.md b/docs/fr/how-to/customize-bmad.md
index f6a481235..c8975cc55 100644
--- a/docs/fr/how-to/customize-bmad.md
+++ b/docs/fr/how-to/customize-bmad.md
@@ -84,7 +84,7 @@ Ajouter un contexte persistant que l'agent gardera toujours en mémoire :
```yaml
memories:
- 'Travaille au Krusty Krab'
- - 'Célébrité préférée : David Hasslehoff'
+ - 'Célébrité préférée : David Hasselhoff'
- 'Appris dans l’Epic 1 que ce n’est pas cool de faire semblant que les tests ont passé'
```
diff --git a/docs/how-to/customize-bmad.md b/docs/how-to/customize-bmad.md
index cfb75333c..15832df89 100644
--- a/docs/how-to/customize-bmad.md
+++ b/docs/how-to/customize-bmad.md
@@ -85,7 +85,7 @@ Add persistent context the agent will always remember:
```yaml
memories:
- 'Works at Krusty Krab'
- - 'Favorite Celebrity: David Hasslehoff'
+ - 'Favorite Celebrity: David Hasselhoff'
- 'Learned in Epic 1 that it is not cool to just pretend that tests have passed'
```
diff --git a/docs/zh-cn/how-to/customize-bmad.md b/docs/zh-cn/how-to/customize-bmad.md
index 5f762ba20..5ed2d44c3 100644
--- a/docs/zh-cn/how-to/customize-bmad.md
+++ b/docs/zh-cn/how-to/customize-bmad.md
@@ -85,7 +85,7 @@ persona:
```yaml
memories:
- 'Works at Krusty Krab'
- - 'Favorite Celebrity: David Hasslehoff'
+ - 'Favorite Celebrity: David Hasselhoff'
- 'Learned in Epic 1 that it is not cool to just pretend that tests have passed'
```
diff --git a/tools/build-docs.mjs b/tools/build-docs.mjs
index 7d916b515..cada7c0e1 100644
--- a/tools/build-docs.mjs
+++ b/tools/build-docs.mjs
@@ -14,6 +14,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { getSiteUrl } from '../website/src/lib/site-url.mjs';
+import { translatedLocales } from '../website/src/lib/locales.mjs';
// =============================================================================
// Configuration
@@ -288,6 +289,9 @@ function shouldExcludeFromLlm(filePath) {
const pathParts = filePath.split(path.sep);
if (pathParts.some((part) => part.startsWith('_'))) return true;
+ // Exclude non-root locale directories (translations duplicate English content)
+ if (translatedLocales.some((locale) => filePath.startsWith(`${locale}/`) || filePath.startsWith(`${locale}${path.sep}`))) return true;
+
// Check configured patterns
return LLM_EXCLUDE_PATTERNS.some((pattern) => filePath.includes(pattern));
}
diff --git a/website/astro.config.mjs b/website/astro.config.mjs
index b0f44d492..9d7efd99e 100644
--- a/website/astro.config.mjs
+++ b/website/astro.config.mjs
@@ -5,6 +5,7 @@ import sitemap from '@astrojs/sitemap';
import rehypeMarkdownLinks from './src/rehype-markdown-links.js';
import rehypeBasePaths from './src/rehype-base-paths.js';
import { getSiteUrl } from './src/lib/site-url.mjs';
+import { locales } from './src/lib/locales.mjs';
const siteUrl = getSiteUrl();
const urlParts = new URL(siteUrl);
@@ -45,22 +46,9 @@ export default defineConfig({
title: 'BMAD Method',
tagline: 'AI-driven agile development with specialized agents and workflows that scale from bug fixes to enterprise platforms.',
- // i18n: English as root (no URL prefix), Chinese at /zh-cn/, French at /fr/
+ // i18n: locale config from shared module (website/src/lib/locales.mjs)
defaultLocale: 'root',
- locales: {
- root: {
- label: 'English',
- lang: 'en',
- },
- 'zh-cn': {
- label: '简体中文',
- lang: 'zh-CN',
- },
- fr: {
- label: 'Français',
- lang: 'fr-FR',
- },
- },
+ locales,
logo: {
light: './public/img/bmad-light.png',
diff --git a/website/src/lib/locales.mjs b/website/src/lib/locales.mjs
new file mode 100644
index 000000000..ef7e273e9
--- /dev/null
+++ b/website/src/lib/locales.mjs
@@ -0,0 +1,32 @@
+/**
+ * Shared i18n locale configuration.
+ *
+ * Single source of truth for locale definitions used by:
+ * - website/astro.config.mjs (Starlight i18n)
+ * - tools/build-docs.mjs (llms-full.txt locale exclusion)
+ * - website/src/pages/404.astro (client-side locale redirect)
+ *
+ * The root locale (English) uses Starlight's 'root' key convention
+ * (no URL prefix). All other locales get a URL prefix matching their key.
+ */
+
+export const locales = {
+ root: {
+ label: 'English',
+ lang: 'en',
+ },
+ 'zh-cn': {
+ label: '简体中文',
+ lang: 'zh-CN',
+ },
+ fr: {
+ label: 'Français',
+ lang: 'fr-FR',
+ },
+};
+
+/**
+ * Non-root locale keys (the URL prefixes for translated content).
+ * @type {string[]}
+ */
+export const translatedLocales = Object.keys(locales).filter((k) => k !== 'root');
diff --git a/website/src/pages/404.astro b/website/src/pages/404.astro
index 46065d04c..6ae826ab7 100644
--- a/website/src/pages/404.astro
+++ b/website/src/pages/404.astro
@@ -1,6 +1,7 @@
---
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
import { getEntry } from 'astro:content';
+import { translatedLocales } from '../lib/locales.mjs';
const entry = await getEntry('docs', '404');
const { Content } = await entry.render();
@@ -9,3 +10,18 @@ const { Content } = await entry.render();
+
+
+