This is an automated email from the ASF dual-hosted git repository.
morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-website.git
The following commit(s) were added to refs/heads/master by this push:
new bb7e86a7ff7b [feat](i18n) Add the unified content identity and
translation manifest (#4490)
bb7e86a7ff7b is described below
commit bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Sat Oct 24 07:03:30 2026 +0100
[feat](i18n) Add the unified content identity and translation manifest
(#4490)
Existing docs state, page allowlists and blog directory detection cannot
describe partial translation availability consistently. Add a versioned
build-only contract that joins native docs/page/blog identities and
routes, separates navigable fallback routes from indexable translations,
and binds every decision to the source SHA and input digests.
Preserve the existing docs state, hash/ownership decisions, legacy
manifest and browser behavior. Register the 21 ordinary page IDs and
Slack utility, define independent manual page/blog ledgers, and expose a
compact per-page routing payload. Manual decisions explicitly carry
`renderIntegrated: false` until the page/blog renderers adopt them in
T03/T14; the empty ledgers make no new translation coverage claims. Full
blog lifecycle and SEO integration remain separate tasks.
Validation:
- 237 i18n tests passed; one existing opt-in production-render test
skipped.
- 22 workflow boundary tests passed; workflow YAML and `git diff
--check` passed.
- Real native input: 1,680 Dev docs, 21 pages, 206 blog posts and one
utility. The full English manifest is 3,020,067 bytes; largest per-page
routing payload is 1,063 bytes, with zero added global browser payload.
- Sequential real production fixtures passed for all five languages and
with Russian disabled, covering zero/one/partial/full manual coverage,
native routes, emitted HTML, docs fallback, manual source drift and
payload sizes.
- Repository typecheck still reports the same 176 diagnostics as the
unmodified baseline, with no new diagnostics.
The shared contract and manual ledgers select the complete optimized
five-language, all-active-version CI build. That CI/publishing
validation is pending; this PR does not publish the global language
switcher or whole-page fallback.
---------
Co-authored-by: morningman <[email protected]>
---
.github/workflows/build-check.yml | 7 +
docusaurus.config.js | 26 ++--
scripts/i18n/__tests__/content-manifest.test.cjs | 104 +++++++++++++++
.../i18n/__tests__/fixtures/content-manifest.cjs | 51 ++++++++
scripts/i18n/__tests__/fixtures/multi-locale.cjs | 6 +
scripts/i18n/build-scope.cjs | 7 +-
scripts/i18n/content-manifest.cjs | 87 +++++++++++++
scripts/i18n/content-manifest.schema.json | 141 +++++++++++++++++++++
scripts/i18n/docs-plugin.cjs | 67 +++++++---
scripts/i18n/manual-plugins.cjs | 102 +++++++++++++++
scripts/i18n/manual-state.cjs | 45 +++++++
scripts/i18n/native-context.cjs | 12 ++
scripts/i18n/status-plugin.cjs | 23 +++-
scripts/i18n/verify-content-fixture.cjs | 63 +++++++++
src/utils/translation.ts | 23 ++++
translations/CONTENT_MANIFEST.md | 133 +++++++++++++++++++
translations/blog/state.json | 4 +
translations/pages/registry.json | 115 +++++++++++++++++
translations/pages/state.json | 4 +
19 files changed, 986 insertions(+), 34 deletions(-)
diff --git a/.github/workflows/build-check.yml
b/.github/workflows/build-check.yml
index 628d933204a4..d6ec352771b8 100644
--- a/.github/workflows/build-check.yml
+++ b/.github/workflows/build-check.yml
@@ -116,6 +116,7 @@ jobs:
node --test scripts/i18n/__tests__/marketing-pages.test.cjs
node --test scripts/i18n/__tests__/locale-registry.test.cjs
node --test scripts/i18n/__tests__/multi-locale.test.cjs
+ node --test scripts/i18n/__tests__/content-manifest.test.cjs
node --test scripts/i18n/__tests__/inventory.test.cjs
node --test scripts/i18n/__tests__/planner.test.cjs
node --test
scripts/i18n/__tests__/translation-engine.test.cjs
@@ -126,6 +127,10 @@ jobs:
node --test
scripts/i18n/__tests__/translate-document.test.cjs
node --test
scripts/i18n/__tests__/incremental-document.test.cjs
+ - name: Verify native content manifest fixture
+ if: steps.detect.outputs.optimize_publish == 'true'
+ run: node scripts/i18n/verify-content-fixture.cjs
+
- name: Workflow security boundary tests
run: PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s
.github/scripts -p 'test_*.py' -v
@@ -229,6 +234,8 @@ jobs:
path: |
.i18n-build/publish-result.json
.i18n-build/build-result.json
+ .i18n-build/content-fixture-verification.json
+ .i18n-build/content-manifests/*.sizes.json
.i18n-build/ci-*.txt
.i18n-build/browser-*-time.txt
.i18n-build/browser-*.json
diff --git a/docusaurus.config.js b/docusaurus.config.js
index 6589a9d04fa0..cc4a944ff50d 100644
--- a/docusaurus.config.js
+++ b/docusaurus.config.js
@@ -10,6 +10,7 @@ const REDIRECTS_4X = require('./config/redirects-4.x.json');
const path = require('path');
const translationDocsPlugin = require('./scripts/i18n/docs-plugin.cjs');
const translationStatusPlugin = require('./scripts/i18n/status-plugin.cjs');
+const manualPlugins = require('./scripts/i18n/manual-plugins.cjs');
const contentTranslationPlugin = require('./scripts/i18n/content-plugin.cjs');
const {translatedContentPages} = require('./scripts/i18n/source-groups.cjs');
const searchPlugin = require('./scripts/i18n/search-plugin.cjs');
@@ -382,7 +383,18 @@ const config = {
plugins: [
contentTranslationPlugin,
SKIP_DOCS ? null : [translationDocsPlugin, MAIN_DOCS_OPTIONS],
- MANAGED_LOCALES.length ? translationStatusPlugin : null,
+ [translationStatusPlugin, {browserData: MANAGED_LOCALES.length > 0}],
+ [manualPlugins.pages, {}],
+ SKIP_BLOG ? null : [manualPlugins.blog, {
+ blogTitle: 'Apache Doris - Blog | Latest news and events ',
+ blogDescription:
+ 'Explore how Doris empower lakehouse, adhoc analysis,
customer-facing analysis and various scenarios',
+ postsPerPage: 'ALL',
+ blogSidebarCount: 0,
+ showReadingTime: false,
+ onUntruncatedBlogPosts: 'ignore',
+ onInlineAuthors: 'ignore',
+ }],
'docusaurus-plugin-sass',
'docusaurus-plugin-matomo',
// Use custom blog plugin
@@ -505,16 +517,8 @@ const config = {
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: false, // The native docs plugin is wrapped below for
translation state.
- blog: SKIP_BLOG ? false : {
- blogTitle: 'Apache Doris - Blog | Latest news and events ',
- blogDescription:
- 'Explore how Doris empower lakehouse, adhoc analysis,
customer-facing analysis and various scenarios',
- postsPerPage: 'ALL',
- blogSidebarCount: 0,
- showReadingTime: false,
- onUntruncatedBlogPosts: 'ignore',
- onInlineAuthors: 'ignore',
- },
+ blog: false, // Native blog metadata is wrapped for the
content contract.
+ pages: false,
theme: {
customCss: require.resolve('./src/scss/custom.scss'),
},
diff --git a/scripts/i18n/__tests__/content-manifest.test.cjs
b/scripts/i18n/__tests__/content-manifest.test.cjs
new file mode 100644
index 000000000000..58d6c83fbfcb
--- /dev/null
+++ b/scripts/i18n/__tests__/content-manifest.test.cjs
@@ -0,0 +1,104 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const os = require('node:os');
+const {createContentFixture} = require('./fixtures/content-manifest.cjs');
+const {loadManual, decideManual} = require('../manual-state.cjs');
+const {sourceSha, validateManifest, buildManifest, routeData} =
require('../content-manifest.cjs');
+const {buildScope} = require('../build-scope.cjs');
+function directory(t) {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(),
'doris-content-manifest-'));
+ t.after(() => fs.rmSync(root, {recursive: true, force: true}));
+ return root;
+}
+async function collect(root, locale, options) {
+ const {loadContext} = require('@docusaurus/core/lib/server/site.js');
+ const {loadPluginConfigs} =
require('@docusaurus/core/lib/server/plugins/configs.js');
+ const {initPluginsConfigs} =
require('@docusaurus/core/lib/server/plugins/init.js');
+ const context = await loadContext({siteDir: root, locale});
+ const plugins = (await initPluginsConfigs(context, await
loadPluginConfigs(context))).map(item => item.plugin);
+ const allContent = {};
+ for (const plugin of plugins) {
+ if (!plugin.loadContent) continue;
+ const content = await plugin.loadContent();
+ if (!allContent[plugin.name]) allContent[plugin.name] = {};
+ allContent[plugin.name][plugin.options.id] = content;
+ }
+ let globalData;
+ await require('../status-plugin.cjs')(context,
options).allContentLoaded({allContent,
+ actions: {setGlobalData: value => {globalData = value;}}});
+ return {manifest: JSON.parse(fs.readFileSync(path.join(root,
'.i18n-build/content-manifests', locale + '.json'))), globalData};
+}
+test('real native plugin output binds stable identities, routes and all four
coverage levels to one source', async t => {
+ const {root} = await createContentFixture(directory(t));
+ const old = process.env.NODE_ENV; process.env.NODE_ENV = 'production';
+ t.after(() => old === undefined ? delete process.env.NODE_ENV :
process.env.NODE_ENV = old);
+ for (const locale of ['en', 'zh-CN', 'ja', 'ko', 'ru']) {
+ const {manifest, globalData} = await collect(root, locale);
+ validateManifest(manifest);
+ const validateShape = new
(require('ajv'))().compile(require('../content-manifest.schema.json'));
+ assert(validateShape(manifest), JSON.stringify(validateShape.errors));
+ assert.equal(manifest.sourceSha, sourceSha(root));
+ const page = manifest.pages.find(page => page.contentId ===
'page:sample-0');
+ assert.deepEqual(page.availableTranslations, ['en', 'ja', 'ko', 'ru']);
+ assert.deepEqual(Object.keys(page.navigationRoutes), ['en', 'zh-CN',
'ja', 'ko', 'ru']);
+ assert.equal(page.fallback, locale === 'zh-CN');
+ const blog = manifest.pages.find(page => page.contentId ===
'blog:2026-01-01-sample.md');
+ assert.equal(blog.state, locale === 'en' ? 'source' : locale ===
'zh-CN' ? 'missing' : 'needs-update');
+ assert.equal(blog.route, `${locale === 'en' ? '' : '/' +
locale}/blog/native-0/`);
+ const full = manifest.pages.find(page => page.contentId ===
'blog:2026-01-03-sample.md');
+ assert.deepEqual(full.availableTranslations, ['en', 'ru']);
+ assert(!manifest.pages.find(page => page.contentId ===
'utility:slack').indexable);
+ assert(manifest.reports.some(report => report.kind === 'orphan_target'
&& report.locale === 'ja'));
+ const ids = manifest.pages.filter(page => page.docId ===
'shared').map(page => page.contentId);
+ assert.equal(new Set(ids).size, 3, 'docs identity includes plugin and
version');
+ const missing = manifest.pages.find(page => page.kind === 'docs' &&
page.docId === 'missing');
+ assert.equal(missing.navigationRoutes['zh-CN'],
'/zh-CN/docs/dev/chinese-missing/');
+ assert(!JSON.stringify(globalData).includes('sourceHash'));
+ assert(!JSON.stringify(routeData(page)).includes('sourceHash'));
+ assert(Buffer.byteLength(JSON.stringify(routeData(page))) < 2048);
+ }
+ const buildOnly = await collect(root, 'en', {browserData: false});
+ assert.equal(buildOnly.globalData, undefined);
+ assert(buildOnly.manifest.pages.length > 0);
+ const previous = process.env.FIXTURE_DISABLE_RU;
process.env.FIXTURE_DISABLE_RU = 'true';
+ try {
+ const {manifest} = await collect(root, 'ja');
+ assert(!manifest.enabledLocales.includes('ru'));
+ assert(manifest.pages.every(page => !page.navigationRoutes.ru &&
!page.availableTranslations.includes('ru')));
+ assert(!manifest.reports.some(report => report.locale === 'ru'));
+ } finally {previous === undefined ? delete process.env.FIXTURE_DISABLE_RU
: process.env.FIXTURE_DISABLE_RU = previous;}
+});
+test('manual targets reject corruption, retain source drift, and honor blocked
publication', async t => {
+ const {root} = await createContentFixture(directory(t));
+ const record = loadManual(root, 'blog').find(record => record.locale ===
'ja');
+ const decide = entry => decideManual({root, record: entry, sourceFiles:
entry.sourceFiles});
+ assert.equal(decide(record).state, 'needs-update');
+ assert.equal(decide(record).fallback, false);
+ assert.equal(decide({...record, state: 'blocked'}).fallback, true);
+ fs.appendFileSync(path.join(root, record.targetFiles[0]), 'Modified
target.');
+ assert.throws(() => decide(record), /target digest mismatch/);
+});
+test('validator rejects mixed source and fallback indexing; missing English
has an explicit report', () => {
+ const entry = {contentId: 'page:one', kind: 'page', updatePolicy:
'manual', renderIntegrated: false, siteLocale: 'ja', contentLocale: 'en',
state: 'missing',
+ route: '/ja/one/', englishRoute: '/one/', fallback: true, indexable:
false};
+ const base = {sourceSha: 'a'.repeat(40), locale: 'ja', enabledLocales:
['en', 'ja']};
+ const absent = buildManifest({...base, entries: [entry]});
+ assert.equal(absent.pages.length, 0);
+ assert.equal(absent.reports[0].kind, 'missing_english_identity');
+ const manifest = buildManifest({...base, entries: [entry, {...entry,
siteLocale: 'en', route: '/one/', fallback: false, indexable: true}]});
+ const corrupt = structuredClone(manifest); corrupt.pages[0].sourceSha =
'b'.repeat(40);
+ assert.throws(() => validateManifest(corrupt), /Mixed source/);
+ manifest.pages[0].indexable = true;
+ assert.throws(() => validateManifest(manifest), /fallback/);
+});
+test('CI recognizes the new contract and manual state as shared inputs', () =>
{
+ for (const file of ['scripts/i18n/content-manifest.cjs',
'scripts/i18n/manual-state.cjs', 'scripts/i18n/manual-plugins.cjs',
+ 'scripts/i18n/native-context.cjs', 'translations/pages/state.json',
'translations/pages/registry.json', 'translations/blog/state.json']) {
+ const scope = buildScope([file], ['4.x'], ['ja', 'ko', 'ru']);
+ assert.deepEqual(scope.locales, ['en', 'zh-CN', 'ja', 'ko', 'ru'],
file);
+ assert.equal(scope.optimize_publish, true);
+ assert.equal(scope.docs_versions, '');
+ }
+});
diff --git a/scripts/i18n/__tests__/fixtures/content-manifest.cjs
b/scripts/i18n/__tests__/fixtures/content-manifest.cjs
new file mode 100644
index 000000000000..a109a1f6e94b
--- /dev/null
+++ b/scripts/i18n/__tests__/fixtures/content-manifest.cjs
@@ -0,0 +1,51 @@
+const fs = require('node:fs');
+const path = require('node:path');
+const {execFileSync} = require('node:child_process');
+const {createFixture} = require('./multi-locale.cjs');
+const {digestFiles} = require('../../manual-state.cjs');
+const repo = path.resolve(__dirname, '../../../..');
+async function createContentFixture(root) {
+ const fixture = await createFixture(root);
+ const {write} = fixture;
+ const registry = [{id: 'home', kind: 'page', sourcePath:
'src/pages/index.tsx'},
+ {id: 'slack', kind: 'utility', sourcePath: 'src/pages/slack.tsx'}];
+ write('src/pages/slack.tsx', "import React from 'react'; export default
function Slack(){return <main>Slack utility</main>;}\n");
+ const states = {pages: [], blog: []};
+ const counts = {'zh-CN': 0, ja: 1, ko: 2, ru: 3};
+ for (let i = 0; i < 3; i++) {
+ const file = `src/pages/sample-${i}.tsx`;
+ registry.push({id: `sample-${i}`, kind: 'page', sourcePath: file});
+ write(file, `import React from 'react'; import Translate from
'@docusaurus/Translate';
+export default function Page(){return <main><Translate
id="fixture.${i}">English sample ${i}</Translate></main>;}\n`);
+ write(`blog/2026-01-0${i + 1}-sample.md`, `---\ntitle: English article
${i}\nslug: /native-${i}\n---\n\nEnglish article ${i}.\n`);
+ }
+ for (const [locale, count] of Object.entries(counts)) {
+ const messages = Object.fromEntries(Array.from({length: count}, (_, i)
=> [`fixture.${i}`, {message: `${locale} translated sample ${i}`} ]));
+ write(`i18n/${locale}/code.json`, JSON.stringify(messages));
+ for (let i = 0; i < count; i++) {
+ const sourceFiles = [`src/pages/sample-${i}.tsx`];
+ const targetFiles = [`i18n/${locale}/code.json`];
+ states.pages.push({contentId: `page:sample-${i}`, locale, state:
'ready', messageNamespaces: [`fixture.${i}`], sourceFiles, targetFiles,
+ sourceHash: digestFiles(root, sourceFiles), targetHash:
digestFiles(root, targetFiles), reviewStatus: 'unverified'});
+ const blogFile = `2026-01-0${i + 1}-sample.md`;
+ const target =
`i18n/${locale}/docusaurus-plugin-content-blog/${blogFile}`;
+ write(target, fs.readFileSync(path.join(root, 'blog', blogFile),
'utf8').replaceAll('English article', `${locale} translated article`));
+ states.blog.push({contentId: `blog:${blogFile}`, locale, state:
'ready', sourceFiles: [`blog/${blogFile}`], targetFiles: [target],
+ sourceHash: digestFiles(root, [`blog/${blogFile}`]),
targetHash: digestFiles(root, [target]), reviewStatus: 'unverified'});
+ }
+ }
+ // Source drift preserves the published manual target; docs drift above
falls back.
+ fs.appendFileSync(path.join(root, 'blog/2026-01-01-sample.md'), '\nSource
update.\n');
+ write('translations/pages/registry.json', JSON.stringify({schemaVersion:
1, pages: registry}));
+ for (const kind of ['pages', 'blog'])
write(`translations/${kind}/state.json`, JSON.stringify({schemaVersion: 1,
pages: states[kind]}));
+ write('i18n/ja/docusaurus-plugin-content-blog/2026-01-10-orphan.md',
'---\ntitle: Orphan\n---\n\nOrphan.\n');
+ let config = fs.readFileSync(path.join(root, 'docusaurus.config.js'),
'utf8');
+ config = config.replace(" presets:
[['classic',{docs:false,blog:false}]],", ` presets:
[['classic',{docs:false,blog:false,pages:false}]],`)
+ .replace(" plugins: [", ` plugins:
[[require(${JSON.stringify(path.join(repo,
'scripts/i18n/manual-plugins.cjs'))}).pages, {}],
+ [require(${JSON.stringify(path.join(repo,
'scripts/i18n/manual-plugins.cjs'))}).blog, {onUntruncatedBlogPosts:'ignore',
onInlineAuthors:'ignore'}],`);
+ write('docusaurus.config.js', config);
+ execFileSync('git', ['add', '.'], {cwd: root});
+ execFileSync('git', ['-c', 'user.name=Fixture', '-c',
'[email protected]', 'commit', '-qm', 'Five-language content
source'], {cwd: root});
+ return fixture;
+}
+module.exports = {createContentFixture};
diff --git a/scripts/i18n/__tests__/fixtures/multi-locale.cjs
b/scripts/i18n/__tests__/fixtures/multi-locale.cjs
index ae30c2b6588d..6f29dbf12eb9 100644
--- a/scripts/i18n/__tests__/fixtures/multi-locale.cjs
+++ b/scripts/i18n/__tests__/fixtures/multi-locale.cjs
@@ -80,6 +80,12 @@ export default function Layout(props) {
function fixtureCache(){return
{name:'fixture-cache',configureWebpack:()=>({cache:false})}}],
presets: [['classic',{docs:false,blog:false}]],
});\n`);
+ write('.gitignore', 'node_modules\n.i18n-build/\n.docusaurus/\nbuild/\n');
+ const {execFileSync} = require('node:child_process');
+ const git = (...args) => execFileSync('git', args, {cwd: root, stdio:
'pipe'});
+ git('init', '-q');
+ git('add', '.');
+ git('-c', 'user.name=Fixture', '-c', '[email protected]',
'commit', '-qm', 'Native fixture source');
return {root, write};
}
module.exports = {createFixture};
diff --git a/scripts/i18n/build-scope.cjs b/scripts/i18n/build-scope.cjs
index 12d47e7c51bf..3ecb8cc04d4c 100644
--- a/scripts/i18n/build-scope.cjs
+++ b/scripts/i18n/build-scope.cjs
@@ -40,7 +40,8 @@ function buildScope(files, versions, managed = []) {
else if (/^sidebars(Community|Releases|Course)\./.test(file)) shared();
else if (/^(src|config)\//.test(file) ||
['docusaurus.config.js','package.json','yarn.lock','tailwind.config.js','tsconfig.json'].includes(file)
|| /^scripts\/(key-features|last-update|publish)\//.test(file) ||
/^scripts\/(patch-search-tokenize.js|build-locales.cjs|build-locale.cjs)$/.test(file)
- ||
/^scripts\/i18n\/(docs-plugin|status-plugin|state|structure|content-plugin|content-loader|source-groups|code-prose|protection)\.cjs$/.test(file))
shared();
+ ||
/^scripts\/i18n\/(docs-plugin|status-plugin|state|structure|content-plugin|content-loader|source-groups|code-prose|protection|content-manifest|manual-state|manual-plugins|native-context)\.cjs$/.test(file))
shared();
+ else if (/^translations\/(pages|blog)\//.test(file)) {shared(); if
(file.endsWith('.json')) json.add(file);}
else if ((match =
file.match(/^translations\/(scope|glossary)\/([^/]+)\.json$/))) {
if (!locales.includes(match[2])) throw new Error(`Unconfigured
locale: ${match[2]}`);
json.add(file); // Offline configuration checks cover these
without a site build.
@@ -68,7 +69,9 @@ function buildScope(files, versions, managed = []) {
||
/^src\/utils\/(searchVersion|searchContexts|searchInput|searchNormalization|tokenize|smartQueries|smartTerms)\.[jt]s$/.test(file));
const navigationChanged = files.some(file =>
/^src\/theme\/NavbarItem\/(LocaleDropdownNavbarItem\/index|DocsVersionDropdownNavbarItem)\.tsx$/.test(file)
|| /^scripts\/i18n\/verify-(doc-navigation|search)\.cjs$/.test(file));
- const optimizePublish = searchChanged || navigationChanged ||
files.some(file =>
/^(scripts\/publish\/|config\/(custom-docusaurus-plugin|ssrTemplate)\.js$|scripts\/build-locales\.cjs$|static\/js\/(brand-theme|course-rail)-bootstrap\.js$|\.github\/workflows\/(cron|manual)-deploy-website\.yml$)/.test(file));
+ const contractChanged = files.some(file =>
/^scripts\/i18n\/(content-manifest|manual-state|manual-plugins|native-context|docs-plugin|status-plugin)\.cjs$/.test(file)
+ || /^translations\/(pages|blog)\//.test(file));
+ const optimizePublish = contractChanged || searchChanged ||
navigationChanged || files.some(file =>
/^(scripts\/publish\/|config\/(custom-docusaurus-plugin|ssrTemplate)\.js$|scripts\/build-locales\.cjs$|static\/js\/(brand-theme|course-rail)-bootstrap\.js$|\.github\/workflows\/(cron|manual)-deploy-website\.yml$)/.test(file));
// Publication changes need the same complete artifact as deployment, on
// the CI runner. Keep local checks scoped to fixtures and Dev versions.
if (optimizePublish) {tier = 'full'; shared();}
diff --git a/scripts/i18n/content-manifest.cjs
b/scripts/i18n/content-manifest.cjs
new file mode 100644
index 000000000000..48aa0859dfb4
--- /dev/null
+++ b/scripts/i18n/content-manifest.cjs
@@ -0,0 +1,87 @@
+// Build-only contract. Keep source hashes, ownership and reports out of
browser data.
+const assert = require('node:assert/strict');
+const {execFileSync} = require('node:child_process');
+const {hash} = require('./state.cjs');
+const schemaVersion = 1;
+const docsId = ({pluginId, version, docId}) =>
`docs:${JSON.stringify([pluginId, version, docId])}`;
+const key = entry => JSON.stringify([entry.contentId, entry.siteLocale]);
+const isRoute = route => typeof route === 'string' && route.startsWith('/') &&
!route.startsWith('//') && !/[?#]/.test(route);
+
+function sourceSha(root) {
+ const sha = execFileSync('git', ['rev-parse', 'HEAD'], {cwd: root,
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']}).trim();
+ assert(/^[a-f0-9]{40}$/.test(sha), 'Manifest requires a Git source SHA');
+ return sha;
+}
+
+// Automatic docs state has already made the hash/technical/ownership decision.
+// This adapter never reclassifies stale_prose, human protection, or conflicts.
+function adaptDoc(page) {
+ return {contentId: docsId(page), kind: 'docs', renderIntegrated: true,
pluginId: page.pluginId, version: page.version, docId: page.docId,
+ siteLocale: page.locale, contentLocale: page.contentLocale, state:
page.status,
+ route: page.permalink, englishRoute: page.englishPermalink, fallback:
!page.useTarget,
+ indexable: page.indexable, sourceHash: page.sourceHash, targetHash:
page.targetHash,
+ englishPath: page.englishPath, sourcePath: page.sourcePath,
targetPath: page.targetPath,
+ sourceLocale: page.sourceLocale, reviewStatus: page.reviewStatus,
updatePolicy: 'automatic-docs'};
+}
+
+function buildManifest({sourceSha: sha, locale, enabledLocales, entries,
reports = []}) {
+ assert(enabledLocales.includes('en') && enabledLocales.includes(locale),
'English and current locale must be enabled');
+ const selected = entries.filter(entry =>
enabledLocales.includes(entry.siteLocale));
+ const seen = new Set(), routes = new Set(), groups = new Map();
+ for (const entry of selected) {
+ assert(!seen.has(key(entry)), `Duplicate content identity:
${key(entry)}`);
+ assert(!routes.has(entry.siteLocale + ':' + entry.route), `Duplicate
content route: ${entry.route}`);
+ seen.add(key(entry)); routes.add(entry.siteLocale + ':' + entry.route);
+ if (!groups.has(entry.contentId)) groups.set(entry.contentId, []);
+ groups.get(entry.contentId).push(entry);
+ }
+ const pages = [];
+ for (const group of groups.values()) {
+ const english = group.find(entry => entry.siteLocale === 'en');
+ if (!english) {
+ reports.push({kind: 'missing_english_identity', contentId:
group[0].contentId});
+ continue;
+ }
+ const navigationRoutes = Object.fromEntries(group.map(entry =>
[entry.siteLocale, entry.route]));
+ const availableTranslations = group.filter(entry => entry.indexable &&
!entry.fallback).map(entry => entry.siteLocale);
+ for (const entry of group.filter(item => item.siteLocale === locale)) {
+ assert(entry.englishRoute === english.route, `English route
mismatch: ${entry.contentId}`);
+ pages.push({...entry, sourceSha: sha, navigationRoutes,
availableTranslations});
+ }
+ }
+ pages.sort((a, b) => a.contentId.localeCompare(b.contentId));
+ const manifest = {schemaVersion, sourceSha: sha, locale, enabledLocales,
pages,
+ reports: reports.filter(report => !report.locale ||
enabledLocales.includes(report.locale))};
+ manifest.snapshotHash = hash(JSON.stringify({pages, reports:
manifest.reports}));
+ validateManifest(manifest);
+ return manifest;
+}
+
+function validateManifest(manifest) {
+ assert(manifest.schemaVersion === schemaVersion &&
/^[a-f0-9]{40}$/.test(manifest.sourceSha), 'Invalid manifest version/source
SHA');
+ assert(new Set(manifest.enabledLocales).size ===
manifest.enabledLocales.length &&
manifest.enabledLocales.includes(manifest.locale), 'Invalid enabled locales');
+ const ids = new Set();
+ for (const page of manifest.pages) {
+ assert(['docs', 'page', 'blog', 'utility'].includes(page.kind) &&
page.contentId && !ids.has(page.contentId), 'Invalid/duplicate content
identity');
+ ids.add(page.contentId);
+ assert(page.siteLocale === manifest.locale && page.sourceSha ===
manifest.sourceSha, 'Mixed source/locale manifest');
+ assert(isRoute(page.route) && isRoute(page.englishRoute), 'Invalid
native route');
+ assert(typeof page.state === 'string' && typeof page.indexable ===
'boolean' && typeof page.fallback === 'boolean' && typeof page.renderIntegrated
=== 'boolean', 'Invalid content decision');
+ assert(manifest.enabledLocales.includes(page.contentLocale) &&
(page.fallback ? page.contentLocale === 'en' && !page.indexable :
page.contentLocale === page.siteLocale), 'Invalid fallback decision');
+ assert(page.kind !== 'utility' || !page.indexable, 'Utility must not
be indexed');
+ assert(['automatic-docs', 'native-manual-docs',
'manual'].includes(page.updatePolicy), 'Invalid update policy');
+ assert(Object.entries(page.navigationRoutes).every(([locale, route])
=> manifest.enabledLocales.includes(locale) && isRoute(route)),
'Disabled/invalid navigation route');
+ assert(page.navigationRoutes[page.siteLocale] === page.route &&
page.navigationRoutes.en === page.englishRoute, 'Missing self/English
navigation route');
+ assert(new Set(page.availableTranslations).size ===
page.availableTranslations.length && page.availableTranslations.every(locale =>
page.navigationRoutes[locale]), 'Invalid available translations');
+ assert(page.availableTranslations.includes(page.siteLocale) ===
(page.indexable && !page.fallback), 'Fallback advertised as a translation');
+ }
+ return manifest;
+}
+
+// Consumers request one content identity; the full registry never enters
globalData.
+function routeData(page) {
+ return {schemaVersion, sourceSha: page.sourceSha, contentId:
page.contentId,
+ siteLocale: page.siteLocale, contentLocale: page.contentLocale,
fallback: page.fallback, renderIntegrated: page.renderIntegrated,
+ indexable: page.indexable, navigationRoutes: page.navigationRoutes,
availableTranslations: page.availableTranslations};
+}
+module.exports = {schemaVersion, docsId, sourceSha, adaptDoc, buildManifest,
validateManifest, routeData};
diff --git a/scripts/i18n/content-manifest.schema.json
b/scripts/i18n/content-manifest.schema.json
new file mode 100644
index 000000000000..86bd762249aa
--- /dev/null
+++ b/scripts/i18n/content-manifest.schema.json
@@ -0,0 +1,141 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Doris build content manifest v1",
+ "type": "object",
+ "required": [
+ "schemaVersion",
+ "sourceSha",
+ "snapshotHash",
+ "locale",
+ "enabledLocales",
+ "pages",
+ "reports"
+ ],
+ "properties": {
+ "schemaVersion": {
+ "const": 1
+ },
+ "sourceSha": {
+ "type": "string",
+ "pattern": "^[a-f0-9]{40}$"
+ },
+ "snapshotHash": {
+ "type": "string",
+ "pattern": "^[a-f0-9]{64}$"
+ },
+ "locale": {
+ "type": "string"
+ },
+ "enabledLocales": {
+ "type": "array",
+ "uniqueItems": true,
+ "items": {
+ "type": "string"
+ }
+ },
+ "pages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "contentId",
+ "kind",
+ "siteLocale",
+ "contentLocale",
+ "state",
+ "route",
+ "englishRoute",
+ "fallback",
+ "indexable",
+ "sourceSha",
+ "navigationRoutes",
+ "availableTranslations",
+ "updatePolicy",
+ "renderIntegrated"
+ ],
+ "properties": {
+ "contentId": {
+ "type": "string",
+ "minLength": 1
+ },
+ "kind": {
+ "enum": [
+ "docs",
+ "page",
+ "blog",
+ "utility"
+ ]
+ },
+ "siteLocale": {
+ "type": "string"
+ },
+ "contentLocale": {
+ "type": "string"
+ },
+ "state": {
+ "type": "string"
+ },
+ "route": {
+ "type": "string",
+ "pattern": "^/(?!/)"
+ },
+ "englishRoute": {
+ "type": "string",
+ "pattern": "^/(?!/)"
+ },
+ "fallback": {
+ "type": "boolean"
+ },
+ "indexable": {
+ "type": "boolean"
+ },
+ "sourceSha": {
+ "type": "string",
+ "pattern": "^[a-f0-9]{40}$"
+ },
+ "navigationRoutes": {
+ "type": "object",
+ "required": [
+ "en"
+ ],
+ "additionalProperties": {
+ "type": "string",
+ "pattern": "^/(?!/)"
+ }
+ },
+ "availableTranslations": {
+ "type": "array",
+ "uniqueItems": true,
+ "items": {
+ "type": "string"
+ }
+ },
+ "updatePolicy": {
+ "enum": [
+ "automatic-docs",
+ "native-manual-docs",
+ "manual"
+ ]
+ },
+ "renderIntegrated": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "reports": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "kind"
+ ],
+ "properties": {
+ "kind": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/scripts/i18n/docs-plugin.cjs b/scripts/i18n/docs-plugin.cjs
index 99bcd84d0efc..e332642699f1 100644
--- a/scripts/i18n/docs-plugin.cjs
+++ b/scripts/i18n/docs-plugin.cjs
@@ -3,7 +3,7 @@ const path = require('node:path');
const native = require('@docusaurus/plugin-content-docs');
const {readVersionsMetadata} =
require('@docusaurus/plugin-content-docs/lib/versions/index.js');
const {processDocMetadata} =
require('@docusaurus/plugin-content-docs/lib/docs.js');
-const {Globby, getPluginI18nPath, normalizeUrl, readLastUpdateData,
aliasedSitePath, resolveMarkdownLinkPathname} = require('@docusaurus/utils');
+const {Globby, getPluginI18nPath, readLastUpdateData, aliasedSitePath,
resolveMarkdownLinkPathname} = require('@docusaurus/utils');
const {applyTrailingSlash} = require('@docusaurus/utils-common');
const {hash, relative, identity, loadConfig, managedLocales, loadRecords,
sourceLocaleFor, decide} = require('./state.cjs');
const {mergeAlternates} = require('./alternates.cjs');
@@ -11,15 +11,8 @@ const {structure, alignHeadings} =
require('./structure.cjs');
async function exists(file) { try { await fs.access(file); return true; }
catch { return false; } }
async function read(file) { return await exists(file) ? fs.readFile(file,
'utf8') : undefined; }
-function localizedContext(context, locale) {
- const defaultLocale = context.i18n.defaultLocale;
- const rootBaseUrl = context.i18n.currentLocale === defaultLocale ?
context.baseUrl
- : context.baseUrl.replace(`/${context.i18n.currentLocale}/`, '/');
- const baseUrl = locale === context.i18n.currentLocale ? context.baseUrl
- : normalizeUrl([rootBaseUrl, locale === defaultLocale ? '' : locale,
'/']);
- return {...context, baseUrl, localizationDir: path.join(context.siteDir,
'i18n', locale),
- i18n: {...context.i18n, currentLocale: locale}};
-}
+const {localizedContext} = require('./native-context.cjs');
+const {adaptDoc, docsId} = require('./content-manifest.cjs');
async function metadata(context, options, versionMetadata, source, filePath,
content) {
return processDocMetadata({context, options: {...options,
showLastUpdateTime: false, showLastUpdateAuthor: false},
@@ -32,10 +25,15 @@ async function metadata(context, options, versionMetadata,
source, filePath, con
// to its generated view and adds translation state.
module.exports = async function translationDocsPlugin(context, options) {
// Managed locales come from the translation configuration, restricted to
the
- // locales this build serves. With none of them active the adapter is inert
- // and the native plugin runs untouched.
+ // locales this build serves. Without managed locales the native render
path
+ // stays untouched, while English/Chinese identities still enter the
registry.
+ const metadataCache = new Map();
+ const readMetadata = (ctx, opts, version, source, file, content) => {
+ const key = JSON.stringify([ctx.i18n.currentLocale,
version.versionName, file, hash(content)]);
+ if (!metadataCache.has(key)) metadataCache.set(key, metadata(ctx,
opts, version, source, file, content));
+ return metadataCache.get(key);
+ };
const managed = managedLocales(context.siteDir, context.i18n.locales);
- if (!managed.length) return native.default(context, options);
const settings = loadConfig(context.siteDir).locales;
const records = Object.fromEntries(managed.map(locale => [locale,
loadRecords(context.siteDir, locale)]));
// Source locales are always present; a managed locale that repeats one of
@@ -58,6 +56,7 @@ module.exports = async function
translationDocsPlugin(context, options) {
if (await exists(originalPluginDir(activeLocale))) await
fs.cp(originalPluginDir(activeLocale), viewPluginDir(activeLocale), {recursive:
true});
}
const pages = [];
+ const sourceEntries = [];
const reports = [];
const route = permalink => applyTrailingSlash(permalink, {trailingSlash:
context.siteConfig.trailingSlash, baseUrl: context.baseUrl});
const versionInputs = await Promise.all(versions.en.map(async
(englishVersion, index) => {
@@ -65,6 +64,35 @@ module.exports = async function
translationDocsPlugin(context, options) {
return {index, englishVersion, filenames, registered: new
Set(filenames),
versionDir: englishVersion.versionName === 'current' ? 'current' :
`version-${englishVersion.versionName}`};
}));
+ // Native source metadata also exists when all optional locales are
disabled.
+ // It supplies real Chinese slugs and per-file fallback, independent of
docs state.
+ for (const {index, englishVersion, filenames} of versionInputs) {
+ for (const filename of filenames) {
+ const englishFile = path.join(englishVersion.contentPath,
filename);
+ const englishContent = await fs.readFile(englishFile, 'utf8');
+ const englishDoc = await readMetadata(contexts.en, options,
englishVersion, filename, englishFile, englishContent);
+ if (englishDoc.draft) continue;
+ for (const locale of ['en', 'zh-CN'].filter(lang =>
context.i18n.locales.includes(lang))) {
+ const file = locale === 'en' ? englishFile :
path.join(versions[locale][index].contentPathLocalized, filename);
+ const content = locale === 'en' ? englishContent : await
read(file);
+ const doc = locale === 'en' ? englishDoc : await
readMetadata(contexts[locale], options, versions[locale][index], filename,
+ content === undefined ? englishFile : file, content ??
englishContent);
+ if (doc.draft) continue;
+ if (doc.id !== englishDoc.id) {
+ reports.push({kind: 'source_identity_conflict', pluginId:
options.id, version: englishVersion.versionName,
+ sourceLocale: locale, docId: doc.id, englishDocId:
englishDoc.id, sourcePath: relative(context.siteDir, file)});
+ continue;
+ }
+ sourceEntries.push({contentId: docsId({pluginId: options.id,
version: englishVersion.versionName, docId: englishDoc.id}),
+ kind: 'docs', renderIntegrated: true, pluginId:
options.id, version: englishVersion.versionName, docId: englishDoc.id,
siteLocale: locale,
+ contentLocale: content === undefined ? 'en' : locale,
state: content === undefined ? 'untranslated' : 'source',
+ fallback: content === undefined, indexable: content !==
undefined && !doc.unlisted && !versions[locale][index].noIndex,
+ route: route(doc.permalink), englishRoute:
route(englishDoc.permalink),
+ englishPath: relative(context.siteDir, englishFile),
sourcePath: relative(context.siteDir, content === undefined ? englishFile :
file),
+ sourceHash: hash(content ?? englishContent), targetHash:
null, updatePolicy: 'native-manual-docs'});
+ }
+ }
+ }
for (const locale of managed) {
for (const {index, englishVersion, versionDir, filenames, registered} of
versionInputs) {
const targetDir = path.join(originalPluginDir(locale), versionDir);
@@ -72,7 +100,7 @@ module.exports = async function
translationDocsPlugin(context, options) {
for (const filename of filenames) {
const englishFile = path.join(englishVersion.contentPath,
filename);
const englishContent = await fs.readFile(englishFile, 'utf8');
- const englishDoc = await metadata(contexts.en, options,
englishVersion, filename, englishFile, englishContent);
+ const englishDoc = await readMetadata(contexts.en, options,
englishVersion, filename, englishFile, englishContent);
seenIds.add(englishDoc.id);
const page = {pluginId: options.id, version:
englishVersion.versionName, docId: englishDoc.id, locale};
const requestedSourceLocale = sourceLocaleFor(settings[locale],
page);
@@ -96,12 +124,12 @@ module.exports = async function
translationDocsPlugin(context, options) {
}
// Parse all counterparts with the framework, including custom IDs,
// number prefixes and localized slugs. Never invent a missing
node.
- const chineseDoc = await metadata(contexts['zh-CN'], options,
versions['zh-CN'][index], filename,
+ const chineseDoc = await readMetadata(contexts['zh-CN'], options,
versions['zh-CN'][index], filename,
chineseContent === undefined ? englishFile : chineseFile,
chineseContent ?? englishContent);
const nativeSourceDoc = sourceLocale === 'en' ? englishDoc :
chineseDoc;
let localizedContent;
if (state.useTarget) {
- const targetDoc = await metadata(contexts[locale], options,
versions[locale][index], filename, targetFile, target).catch(() => null);
+ const targetDoc = await readMetadata(contexts[locale],
options, versions[locale][index], filename, targetFile, target).catch(() =>
null);
if (!targetDoc || targetDoc.id !== englishDoc.id ||
targetDoc.slug !== englishDoc.slug || targetDoc.draft !== englishDoc.draft
|| targetDoc.unlisted !== englishDoc.unlisted) {
state = {...state, status: 'state_conflict', useTarget:
false, indexable: false, contentLocale: 'en'};
@@ -119,7 +147,7 @@ module.exports = async function
translationDocsPlugin(context, options) {
reports.push({kind: 'source_divergence', ...page, reason:
'source_route_conflict'});
}
const selectedContent = state.useTarget ? localizedContent :
englishContent;
- const localizedDoc = await metadata(contexts[locale], options,
versions[locale][index], filename, targetFile, selectedContent);
+ const localizedDoc = await readMetadata(contexts[locale], options,
versions[locale][index], filename, targetFile, selectedContent);
const indexable = state.indexable && !localizedDoc.draft &&
!localizedDoc.unlisted && !versions[locale][index].noIndex;
state = {...state, indexable, draft: localizedDoc.draft, unlisted:
localizedDoc.unlisted,
permalink: route(localizedDoc.permalink), englishPermalink:
route(englishDoc.permalink),
@@ -201,11 +229,12 @@ module.exports = async function
translationDocsPlugin(context, options) {
}
}
}
- return {...content, translationManifest: {schemaVersion: 1,
pluginId: options.id, locale: context.i18n.currentLocale, pages, reports}};
+ return {...content, contentIdentity: {entries: [...sourceEntries,
...pages.filter(page => !page.draft).map(adaptDoc)], reports},
+ ...(managed.length && {translationManifest: {schemaVersion: 1,
pluginId: options.id, locale: context.i18n.currentLocale, pages, reports}})};
},
translateContent({content, translationFiles}) {
// Docusaurus's native translateLoadedContent returns only
versions.
- return {...plugin.translateContent({content, translationFiles}),
translationManifest: content.translationManifest};
+ return {...plugin.translateContent({content, translationFiles}),
translationManifest: content.translationManifest, contentIdentity:
content.contentIdentity};
},
async contentLoaded(args) {
await plugin.contentLoaded(args);
diff --git a/scripts/i18n/manual-plugins.cjs b/scripts/i18n/manual-plugins.cjs
new file mode 100644
index 000000000000..c28b959920a7
--- /dev/null
+++ b/scripts/i18n/manual-plugins.cjs
@@ -0,0 +1,102 @@
+// Read routes from native plugins. Rendering, blog lifecycle and SEO remain
+// native until the page/blog consumers adopt the contract in T03/T14.
+const path = require('node:path');
+const {Globby, getPluginI18nPath} = require('@docusaurus/utils');
+const {applyTrailingSlash} = require('@docusaurus/utils-common');
+const {relative} = require('./state.cjs');
+const {readJson, loadManual, decideManual} = require('./manual-state.cjs');
+const {localizedContext} = require('./native-context.cjs');
+
+function wrap(kind, native) {
+ async function adapter(context, options) {
+ const plugin = await native.default(context, options);
+ return {...plugin,
+ getPathsToWatch() {return [...plugin.getPathsToWatch(),
path.join(context.siteDir, `translations/${kind}/**/*.json`),
+ ...loadManual(context.siteDir, kind).flatMap(record =>
[...record.sourceFiles, ...record.targetFiles])
+ .map(file => path.join(context.siteDir, file))];},
+ async loadContent() {
+ const snapshots = new Map();
+ for (const locale of context.i18n.locales) {
+ const ctx = localizedContext(context, locale);
+ const instance = locale === context.i18n.currentLocale ?
plugin : await native.default(ctx, options);
+ snapshots.set(locale, await instance.loadContent());
+ }
+ const records = loadManual(context.siteDir, kind);
+ const registry = readJson(context.siteDir,
'translations/pages/registry.json', {schemaVersion: 1, pages: []});
+ if (registry.schemaVersion !== 1 ||
!Array.isArray(registry.pages)) throw new Error('Invalid page registry');
+ const registeredIds = new Set(), registeredPaths = new Set();
+ for (const page of registry.pages) {
+ if (!page.id || !page.sourcePath || !['page',
'utility'].includes(page.kind)
+ || registeredIds.has(page.id) ||
registeredPaths.has(page.sourcePath)) throw new Error('Invalid/duplicate page
registry identity');
+ registeredIds.add(page.id);
registeredPaths.add(page.sourcePath);
+ }
+ const report = [], entries = [];
+ const list = value => kind === 'blog' ?
value?.blogPosts.map(post => post.metadata) ?? [] : value ?? [];
+ const route = permalink => applyTrailingSlash(permalink,
{trailingSlash: context.siteConfig.trailingSlash, baseUrl: context.baseUrl});
+ function sourcePath(item, locale) {
+ const source = path.resolve(context.siteDir,
item.source.replace(/^@site\//, ''));
+ const localized = getPluginI18nPath({localizationDir:
path.join(context.siteDir, 'i18n', locale),
+ pluginName: `docusaurus-plugin-content-${kind}`,
pluginId: options.id});
+ const base = source.startsWith(localized + path.sep) ?
localized : path.resolve(context.siteDir, options.path);
+ return relative(base, source);
+ }
+ const english = new Map(list(snapshots.get('en')).map(item =>
[sourcePath(item, 'en'), item]));
+ const locales = new Map([...snapshots].map(([locale, value])
=> [locale,
+ new Map(list(value).map(item => [sourcePath(item, locale),
item]))]));
+ const known = new Set();
+ for (const [file, source] of english) {
+ const englishPath = relative(context.siteDir,
path.resolve(context.siteDir, options.path, file));
+ const registration = kind === 'pages' ?
registry.pages.find(page => page.sourcePath === englishPath) : undefined;
+ const contentId = kind === 'blog' ? `blog:${options.id ===
'default' ? '' : options.id + ':'}${file}`
+ : `${registration?.kind ?? 'page'}:${registration?.id
?? englishPath}`;
+ known.add(contentId);
+ if (kind === 'pages' && !registration) report.push({kind:
'unregistered_page_identity', contentId, englishPath});
+ for (const locale of context.i18n.locales) {
+ const item = locales.get(locale).get(file);
+ if (!item) {report.push({kind: 'missing_native_route',
contentId, locale}); continue;}
+ const record = records.find(page => page.contentId ===
contentId && page.locale === locale);
+ if (record &&
(!record.sourceFiles.includes(englishPath) || (kind === 'blog'
+ &&
!record.targetFiles.includes(`i18n/${locale}/docusaurus-plugin-content-blog${options.id
=== 'default' ? '' : '-' + options.id}/${file}`)))) {
+ throw new Error(`Manual state does not cover
native source/target: ${contentId}/${locale}`);
+ }
+ const decision = decideManual({root: context.siteDir,
record: locale === 'en' ? undefined : record, sourceFiles: [englishPath]});
+ if (locale === 'en') Object.assign(decision, {state:
'source', contentLocale: 'en', fallback: false, indexable: true});
+ if (decision.state === 'needs-update')
report.push({kind: 'manual_source_changed', contentId, locale,
+ sourceHash: decision.sourceHash,
publishedSourceHash: record.sourceHash});
+ const utility = registration?.kind === 'utility';
+ entries.push({contentId, kind: utility ? 'utility' :
kind === 'pages' ? 'page' : 'blog',
+ pluginId: options.id, englishPath,
renderIntegrated: locale === 'en', siteLocale: locale, englishRoute:
route(source.permalink), route: route(item.permalink),
+ ...decision, indexable: !utility && !item.unlisted
&& decision.indexable, updatePolicy: 'manual'});
+ }
+ }
+ for (const [locale, items] of locales) {
+ for (const file of items.keys()) if (!english.has(file))
report.push({kind: 'orphan_target', locale, targetPath: file});
+ if (locale === 'en') continue;
+ const ctx = localizedContext(context, locale);
+ const dir = getPluginI18nPath({localizationDir:
ctx.localizationDir, pluginName: `docusaurus-plugin-content-${kind}`, pluginId:
options.id});
+ for (const file of await Globby(options.include, {cwd:
dir, ignore: options.exclude})) {
+ if (!english.has(file)) report.push({kind:
'orphan_target_file', locale, targetPath: relative(context.siteDir,
path.join(dir, file))});
+ }
+ }
+ if (kind === 'pages') {
+ const englishPaths = new Set(entries.map(entry =>
entry.englishPath));
+ for (const page of registry.pages) if
(!englishPaths.has(page.sourcePath)) report.push({
+ kind: 'missing_english_identity', contentId:
`${page.kind}:${page.id}`, englishPath: page.sourcePath});
+ }
+ for (const record of records) if
(!known.has(record.contentId)) report.push({kind: 'source_deleted', ...record});
+ const content = snapshots.get(context.i18n.currentLocale);
+ if (content) content.contentIdentity = {entries, reports:
report};
+ return content;
+ },
+ ...(plugin.translateContent && {translateContent(args) {
+ const result = plugin.translateContent(args);
+ result.contentIdentity = args.content.contentIdentity;
+ return result;
+ }}),
+ };
+ }
+ adapter.validateOptions = native.validateOptions;
+ return adapter;
+}
+module.exports = {pages: wrap('pages',
require('@docusaurus/plugin-content-pages')),
+ blog: wrap('blog', require('@docusaurus/plugin-content-blog'))};
diff --git a/scripts/i18n/manual-state.cjs b/scripts/i18n/manual-state.cjs
new file mode 100644
index 000000000000..f1e8508c123a
--- /dev/null
+++ b/scripts/i18n/manual-state.cjs
@@ -0,0 +1,45 @@
+const fs = require('node:fs');
+const path = require('node:path');
+const assert = require('node:assert/strict');
+const {hash} = require('./state.cjs');
+const safePath = value => typeof value === 'string' && value &&
!path.isAbsolute(value) && !value.split(/[\\/]/).includes('..');
+function readJson(root, file, fallback) {
+ const full = path.join(root, file);
+ return fs.existsSync(full) ? JSON.parse(fs.readFileSync(full, 'utf8')) :
fallback;
+}
+function loadManual(root, kind) {
+ const data = readJson(root, `translations/${kind}/state.json`,
{schemaVersion: 1, pages: []});
+ assert(data.schemaVersion === 1 && Array.isArray(data.pages), `Invalid
manual ${kind} state`);
+ const seen = new Set();
+ for (const page of data.pages) {
+ const key = JSON.stringify([page.contentId, page.locale]);
+ assert(page.contentId && page.locale !== 'en' && page.locale &&
!seen.has(key), 'Invalid/duplicate manual identity');
+ assert(['missing', 'ready', 'needs-update',
'blocked'].includes(page.state), 'Invalid manual state');
+ assert(Array.isArray(page.sourceFiles) && page.sourceFiles.length &&
page.sourceFiles.every(safePath), 'Manual source dependencies required');
+ assert(Array.isArray(page.targetFiles) &&
page.targetFiles.every(safePath), 'Invalid manual target dependencies');
+ if (['ready', 'needs-update'].includes(page.state)) {
+ assert(page.targetFiles.length &&
/^[a-f0-9]{64}$/.test(page.sourceHash) &&
/^[a-f0-9]{64}$/.test(page.targetHash), 'Published manual content needs both
digests');
+ assert(['machine', 'human',
'unverified'].includes(page.reviewStatus), 'Manual review identity required');
+ }
+ seen.add(key);
+ }
+ return data.pages;
+}
+function digestFiles(root, files) {
+ return hash(JSON.stringify([...new Set(files)].sort().map(file => {
+ assert(safePath(file), `Invalid dependency: ${file}`);
+ return [file, hash(fs.readFileSync(path.join(root, file), 'utf8'))];
+ })));
+}
+function decideManual({root, record, sourceFiles, targetFiles = []}) {
+ const sourceHash = digestFiles(root, record?.sourceFiles ?? sourceFiles);
+ if (!record || ['missing', 'blocked'].includes(record.state)) return
{state: record?.state ?? 'missing', sourceHash,
+ targetHash: null, contentLocale: 'en', fallback: true, indexable:
false, reviewStatus: record?.reviewStatus ?? 'unverified'};
+ const targetHash = digestFiles(root, record.targetFiles ?? targetFiles);
+ // A broken/edited target needs an explicit new publication decision. Do
not
+ // silently mask invalid translations with English during a release build.
+ assert(record.targetHash === targetHash, `Manual target digest mismatch:
${record.contentId}/${record.locale}`);
+ return {state: record.sourceHash === sourceHash ? record.state :
'needs-update', sourceHash, targetHash,
+ contentLocale: record.locale, fallback: false, indexable: true,
reviewStatus: record.reviewStatus};
+}
+module.exports = {readJson, loadManual, digestFiles, decideManual};
diff --git a/scripts/i18n/native-context.cjs b/scripts/i18n/native-context.cjs
new file mode 100644
index 000000000000..356da738716d
--- /dev/null
+++ b/scripts/i18n/native-context.cjs
@@ -0,0 +1,12 @@
+const path = require('node:path');
+const {normalizeUrl} = require('@docusaurus/utils');
+function localizedContext(context, locale) {
+ const defaultLocale = context.i18n.defaultLocale;
+ const rootBaseUrl = context.i18n.currentLocale === defaultLocale ?
context.baseUrl
+ : context.baseUrl.replace(`/${context.i18n.currentLocale}/`, '/');
+ const baseUrl = locale === context.i18n.currentLocale ? context.baseUrl
+ : normalizeUrl([rootBaseUrl, locale === defaultLocale ? '' : locale,
'/']);
+ return {...context, baseUrl, siteConfig: {...context.siteConfig, baseUrl},
localizationDir: path.join(context.siteDir, 'i18n', locale),
+ i18n: {...context.i18n, currentLocale: locale}};
+}
+module.exports = {localizedContext};
diff --git a/scripts/i18n/status-plugin.cjs b/scripts/i18n/status-plugin.cjs
index c60d03713bf3..192f9bfbe7f1 100644
--- a/scripts/i18n/status-plugin.cjs
+++ b/scripts/i18n/status-plugin.cjs
@@ -2,8 +2,9 @@ const fs = require('node:fs/promises');
const path = require('node:path');
const {managedLocales} = require('./state.cjs');
const {mergeAlternates} = require('./alternates.cjs');
+const {sourceSha, buildManifest, routeData} =
require('./content-manifest.cjs');
-module.exports = function translationStatusPlugin(context) {
+module.exports = function translationStatusPlugin(context, {browserData =
true} = {}) {
const locale = context.i18n.currentLocale;
// The browser needs to know which locales carry translation state, so the
// UI can decide from data instead of naming a language.
@@ -34,7 +35,25 @@ module.exports = function translationStatusPlugin(context) {
// localized documentation, search, or other plugin-owned UI.
const pageRoutes = [...new
Set(Object.values(allContent['docusaurus-plugin-content-pages'] ?? {})
.flatMap(content => content ?? []).map(page =>
page.permalink.replace(/\/$/, '') + '/'))];
- actions.setGlobalData({alternates, prefixes, locales, pageRoutes});
+ if (browserData) actions.setGlobalData({alternates, prefixes,
locales, pageRoutes});
+ const inputs = Object.values(allContent).flatMap(instances =>
Object.values(instances))
+ .map(content => content?.contentIdentity).filter(Boolean);
+ if (inputs.length) {
+ const manifest = buildManifest({sourceSha:
sourceSha(context.siteDir), locale,
+ enabledLocales: context.i18n.locales, entries:
inputs.flatMap(input => input.entries),
+ reports: inputs.flatMap(input => input.reports)});
+ const directory = path.join(context.siteDir,
'.i18n-build/content-manifests');
+ await fs.mkdir(directory, {recursive: true});
+ const serialized = JSON.stringify(manifest, null, 2) + '\n';
+ await fs.writeFile(path.join(directory, `${locale}.json`),
serialized);
+ // A separate per-page payload is available to T02/T03
consumers;
+ // no full content registry or source digests are put in
globalData.
+ const sizes = manifest.pages.map(page => ({contentId:
page.contentId,
+ bytes:
Buffer.byteLength(JSON.stringify(routeData(page)))}));
+ await fs.writeFile(path.join(directory,
`${locale}.sizes.json`), JSON.stringify({
+ sourceSha: manifest.sourceSha, fullBytes:
Buffer.byteLength(serialized), pages: sizes,
+ maxRouteBytes: Math.max(0, ...sizes.map(page =>
page.bytes)), clientGlobalBytesAdded: 0}, null, 2) + '\n');
+ }
// Reports that belong to no single locale describe the repository,
// so they stay in every locale's manifest.
const reports = manifests.flatMap(manifest => manifest.reports)
diff --git a/scripts/i18n/verify-content-fixture.cjs
b/scripts/i18n/verify-content-fixture.cjs
new file mode 100644
index 000000000000..ccf874697eeb
--- /dev/null
+++ b/scripts/i18n/verify-content-fixture.cjs
@@ -0,0 +1,63 @@
+// Real sequential builds: five languages, then Russian disabled. The manifests
+// come from plugin loadContent output, never from hand-written result JSON.
+const assert = require('node:assert/strict');
+const fs = require('node:fs/promises');
+const path = require('node:path');
+const {spawnSync, execFileSync} = require('node:child_process');
+const {hash} = require('./state.cjs');
+const cheerio = require('cheerio');
+const {createContentFixture} =
require('./__tests__/fixtures/content-manifest.cjs');
+const {validateManifest, sourceSha, routeData} =
require('./content-manifest.cjs');
+const root = path.resolve(__dirname, '../..');
+async function main() {
+ const implementation = () => ({sha: sourceSha(root), trackedDiffHash:
hash(execFileSync('git', ['diff', 'HEAD', '--binary'], {cwd: root, encoding:
'utf8'}))});
+ const initialImplementation = implementation();
+ const results = [];
+ for (const disabled of [false, true]) {
+ const fixture = path.join(root, '.i18n-build', disabled ?
'content-disabled-fixture' : 'content-fixture');
+ await fs.rm(fixture, {recursive: true, force: true});
+ await createContentFixture(fixture);
+ const result = spawnSync(process.execPath, [path.join(root,
'scripts/build-locales.cjs'), '--site-dir', fixture],
+ {stdio: 'inherit', env: {...process.env, FIXTURE_DISABLE_RU:
disabled ? 'true' : '', NODE_OPTIONS: '--max-old-space-size=4096'}});
+ assert.equal(result.status, 0, 'Content fixture build failed');
+ const locales = ['en', 'zh-CN', 'ja', 'ko', ...(disabled ? [] :
['ru'])];
+ const manifests = [];
+ for (const locale of locales) {
+ const manifest = validateManifest(JSON.parse(await
fs.readFile(path.join(fixture, '.i18n-build/content-manifests', locale +
'.json'), 'utf8')));
+ assert.equal(manifest.sourceSha, sourceSha(fixture));
+ let checked = 0;
+ for (const page of manifest.pages) {
+ for (const route of Object.values(page.navigationRoutes))
await fs.access(path.join(fixture, 'build', decodeURIComponent(route),
'index.html'));
+ if (disabled) assert(!page.navigationRoutes.ru &&
!page.availableTranslations.includes('ru'));
+ const html = await fs.readFile(path.join(fixture, 'build',
decodeURIComponent(page.route), 'index.html'), 'utf8');
+ const $ = cheerio.load(html);
+ if (page.kind === 'docs' && ['ja', 'ko',
'ru'].includes(locale)) assert.equal($('.theme-doc-markdown').attr('lang'),
page.contentLocale);
+ if (page.contentId.startsWith('page:sample-')) {
+ const i = page.contentId.slice(-1);
+ assert($('main').text().includes(page.fallback || locale
=== 'en' ? `English sample ${i}` : `${locale} translated sample ${i}`));
+ }
+ if (page.kind === 'blog') {
+ const i = page.contentId.match(/01-0(\d)/)[1] - 1;
+ assert($('article').text().includes(page.fallback ||
locale === 'en' ? `English article ${i}` : `${locale} translated article
${i}`));
+ }
+ checked++;
+ }
+ const sizes = JSON.parse(await fs.readFile(path.join(fixture,
'.i18n-build/content-manifests', locale + '.sizes.json'), 'utf8'));
+ assert(sizes.maxRouteBytes < 2048);
+ assert.equal(sizes.clientGlobalBytesAdded, 0);
+ const first = manifest.pages.find(page => page.contentId ===
'blog:2026-01-01-sample.md');
+ assert.deepEqual(first.availableTranslations, locales.filter(lang
=> lang !== 'zh-CN'));
+ manifests.push({locale, checked, fullBytes: sizes.fullBytes,
maxRouteBytes: sizes.maxRouteBytes,
+ fallback: manifest.pages.filter(page => page.fallback).length,
+ reportKinds: [...new Set(manifest.reports.map(report =>
report.kind))]});
+ // One page payload can be consumed independently of the full
manifest.
+ assert.equal(routeData(first).contentId, first.contentId);
+ }
+ if (disabled) await assert.rejects(fs.access(path.join(fixture,
'build/ru/index.html')));
+ assert.deepEqual(implementation(), initialImplementation,
'Implementation changed during fixture builds');
+ results.push({disabled, fixture, sourceSha: sourceSha(fixture),
implementation: initialImplementation, manifests});
+ }
+ await fs.writeFile(path.join(root,
'.i18n-build/content-fixture-verification.json'), JSON.stringify(results, null,
2) + '\n');
+ console.log(JSON.stringify(results, null, 2));
+}
+if (require.main === module) main().catch(error => {console.error(error);
process.exitCode = 1;});
diff --git a/src/utils/translation.ts b/src/utils/translation.ts
index 2b51ef1a7b18..651e23f5fe9c 100644
--- a/src/utils/translation.ts
+++ b/src/utils/translation.ts
@@ -48,3 +48,26 @@ export function useTranslationRoutes() {
managed: ordinaryPage || !!data?.prefixes.some(prefix => route ===
prefix || route.startsWith(prefix.replace(/\/$/, '') + '/')),
pageFallback};
}
+
+// T01 consumers accept one page's routing payload. The build-only registry
+// (source/target digests, state and reports) must not be imported by the
browser.
+export type ContentRouteData = {
+ schemaVersion: 1;
+ renderIntegrated: boolean;
+ sourceSha: string;
+ contentId: string;
+ siteLocale: string;
+ contentLocale: string;
+ fallback: boolean;
+ indexable: boolean;
+ navigationRoutes: Record<string, string>;
+ availableTranslations: string[];
+};
+
+export function contentRouteGroups(page: ContentRouteData) {
+ return {
+ navigationRoutes: page.navigationRoutes,
+ indexableTranslations: Object.fromEntries(page.availableTranslations
+ .map(locale => [locale, page.navigationRoutes[locale]])),
+ };
+}
diff --git a/translations/CONTENT_MANIFEST.md b/translations/CONTENT_MANIFEST.md
new file mode 100644
index 000000000000..0714a8ff3818
--- /dev/null
+++ b/translations/CONTENT_MANIFEST.md
@@ -0,0 +1,133 @@
+# Content manifest v1
+
+The build contract joins native content identity, routes, translation
availability,
+body language and indexing decisions. It does not replace `translations/state/`
+or change existing page rendering. The status plugin writes
+`.i18n-build/content-manifests/<locale>.json` and `<locale>.sizes.json`.
+The legacy `.i18n-build/manifests/<locale>.json` and its browser API remain
intact.
+
+## Identity and routing
+
+| Kind | Stable identity | Route authority |
+| --- | --- | --- |
+| docs | `docs:` + JSON `[pluginId, version, docId]` | Native docs metadata,
including localized Chinese slugs |
+| page | `page:<registry-id>` | Native pages metadata |
+| blog | `blog:<English-source-relative-path>` | Native blog metadata,
including date/custom permalink |
+| utility | `utility:<registry-id>` | Native pages metadata; always excluded
from indexing |
+
+`pages/registry.json` pins the 21 existing page IDs P01–P21 and the Slack
utility.
+Keep the ID when moving a source file. Update its source path and redirects in
the
+same change. Unregistered native pages get a source-path ID and an explicit
report;
+assign a registry ID before adopting them into the stable public contract.
Search
+and 404 belong to their native plugins; add their utility identities when those
+plugins expose route metadata in T05. They are not inferred from URL strings.
+Blog plugin instances other than `default` namespace their source paths with
the
+plugin ID. An English source move needs a deliberate migration and redirects in
+T14. A localized-only file does not establish an English identity.
+
+Each page records `sourceSha` (the checkout's Git HEAD), `siteLocale`,
+`contentLocale`, `state`, `route`, `englishRoute`, `fallback`, `indexable` and
an
+`updatePolicy`. All pages in an output share the source SHA. Source/target
hashes
+bind the selected input bytes; `snapshotHash` binds the complete output
decision
+set. Local uncommitted work still has the checkout's HEAD: retain the patch and
+its digest with validation evidence. The publishing builder also rejects
tracked
+source changes between locale builds.
+
+`navigationRoutes` contains only enabled, native routes for the same identity,
+including English fallback routes. `availableTranslations` contains only
+indexable, non-fallback members. Draft docs are absent from this registry;
+unlisted/noIndex content remains navigable but is excluded from translations.
+Unknown identities and orphan targets are reported rather than assigned an
+invented counterpart. Chinese-only docs remain explicit unsupported source
+reports. Disabled languages are removed from routes, translation lists and
+locale-specific reports.
+
+## Rendering adoption
+
+`renderIntegrated` is true for the docs adapter and English sources. The manual
+page/blog decisions have `renderIntegrated: false` until T03/T14 connects their
+renderers. Those decisions describe declared publication availability, not an
+assertion that the existing renderer already applies whole-page fallback. Do
not
+use pending manual decisions to change live SEO. In particular, the old seven
+marketing translations have not been promoted to `ready` by this task. Their
+manual audit and migration remain in T10. No translated content is generated
here.
+
+## Independent update policies
+
+The docs adapter copies the existing state decision without changing its hash,
+technical-content, source override, review or ownership rules. `current`,
+`stale_prose`, conflicts and other states retain their existing meaning.
Chinese
+manual docs keep native per-file selection. English fallback is never converted
+to `current` or `ready` by a compatibility reader.
+
+`pages/state.json` and `blog/state.json` contain `{schemaVersion: 1, pages:
[]}`.
+Each manual record uses:
+
+```json
+{
+ "contentId": "page:P01",
+ "locale": "ja",
+ "state": "ready",
+ "sourceFiles": ["src/pages/index.tsx", "src/components/home/index.tsx"],
+ "targetFiles": ["i18n/ja/code.json"],
+ "messageNamespaces": ["homepage"],
+ "sourceHash": "<digestFiles result at publication>",
+ "targetHash": "<digestFiles result at publication>",
+ "reviewStatus": "unverified"
+}
+```
+
+This illustrates the shape; it is not a valid ready record until paths exist
and
+both hashes are computed. Use `digestFiles(root, files)` from
+`scripts/i18n/manual-state.cjs`: it sorts unique relative paths, hashes each
file
+with the existing CRLF normalization, then hashes the ordered path/hash pairs.
+Explicit dependencies must include the entry module, shared body components,
+English messages/data, target messages/data and resources that affect
publication.
+The adapter requires the native English entry in `sourceFiles`; blog records
must
+also include the native localized file in `targetFiles`. Whole dictionaries are
+valid dependencies but produce broader drift reports. `messageNamespaces` and
+reviewer metadata may document the publication unit; neither grants automatic
+ownership or human review. These records never enter the docs scheduler.
+
+| State / event | Selected body | Index policy |
+| --- | --- | --- |
+| no record / missing / blocked | English | Not indexable in the target locale
|
+| ready, matching hashes | Published target | Indexable unless native metadata
excludes it |
+| source hash changed / needs-update | Published target; report pending update
| Preserve indexability |
+| missing dependency / changed target hash / invalid native metadata | Fail
build | Require explicit repair or publication update |
+
+A source update must not overwrite the old `sourceHash` just to clear drift.
+Re-review, update the target as needed, then record both current digests and
the
+actual review identity. A target edit requires a new explicit publication
record;
+it cannot silently fall back. The initial empty ledgers deliberately make no
new
+claims about translated body coverage. Complete blog route, draft, tag, author,
+feed, anchor and lifecycle validation remains in T14.
+
+## Consumers and validation
+
+`scripts/i18n/content-manifest.schema.json` describes the serialized shape.
+`validateManifest()` checks identity uniqueness, source/locale consistency,
+native routes, enabled-language closure and fallback/indexing invariants.
+`buildManifest()` joins all native inputs before producing the current locale.
+Reports cover deleted English sources, isolated targets, conflicting Chinese
+identities and unregistered page IDs. They do not delete files or change URLs.
+
+Full manifests are build artifacts. Do not import them into React or put them
in
+`setGlobalData`. `routeData(page)` produces one compact payload;
+`ContentRouteData` and `contentRouteGroups()` in `src/utils/translation.ts`
keep
+navigation and indexing consumers distinct. T02/T03 will attach only the
current
+page or required family to its route. The current compatibility browser data is
+unchanged; source-only builds use `browserData: false`, preserving the previous
+absence of status globals when optional locales are disabled. The size report
records full bytes, maximum per-page route bytes and
+zero added global payload bytes. Existing global alternate data can be retired
+when T02 consumers migrate.
+
+Run `node --test scripts/i18n/__tests__/content-manifest.test.cjs` for native
+metadata and state boundaries, then `node
scripts/i18n/verify-content-fixture.cjs`
+for sequential five-language and disabled-Russian production builds. The
fixture
+uses real docs (two versions/two plugins), React pages, native blog posts and a
+utility route; target coverage is zero/one/partial/full. It checks emitted
HTML,
+route existence, body selection, source binding and payload sizes. This fixture
+does not certify full production publishing, page-wide SEO or blog lifecycle.
+CI selects the complete optimized five-language, all-active-version build for
+changes to the contract, adapters or manual ledgers, and archives size reports.
diff --git a/translations/blog/state.json b/translations/blog/state.json
new file mode 100644
index 000000000000..f2350ea88165
--- /dev/null
+++ b/translations/blog/state.json
@@ -0,0 +1,4 @@
+{
+ "schemaVersion": 1,
+ "pages": []
+}
diff --git a/translations/pages/registry.json b/translations/pages/registry.json
new file mode 100644
index 000000000000..86f3e3365bc1
--- /dev/null
+++ b/translations/pages/registry.json
@@ -0,0 +1,115 @@
+{
+ "schemaVersion": 1,
+ "pages": [
+ {
+ "id": "P01",
+ "kind": "page",
+ "sourcePath": "src/pages/index.tsx"
+ },
+ {
+ "id": "P02",
+ "kind": "page",
+ "sourcePath": "src/pages/why-doris/compare/index.tsx"
+ },
+ {
+ "id": "P03",
+ "kind": "page",
+ "sourcePath": "src/pages/why-doris/benchmarks/index.tsx"
+ },
+ {
+ "id": "P04",
+ "kind": "page",
+ "sourcePath": "src/pages/why-doris/key-features/index.tsx"
+ },
+ {
+ "id": "P05",
+ "kind": "page",
+ "sourcePath": "src/pages/why-doris/users/index.tsx"
+ },
+ {
+ "id": "P06",
+ "kind": "page",
+ "sourcePath": "src/pages/use-cases/customer-facing-analytics/index.tsx"
+ },
+ {
+ "id": "P07",
+ "kind": "page",
+ "sourcePath": "src/pages/use-cases/data-warehousing/index.tsx"
+ },
+ {
+ "id": "P08",
+ "kind": "page",
+ "sourcePath": "src/pages/use-cases/observability/index.tsx"
+ },
+ {
+ "id": "P09",
+ "kind": "page",
+ "sourcePath": "src/pages/use-cases/ai/index.tsx"
+ },
+ {
+ "id": "P10",
+ "kind": "page",
+ "sourcePath": "src/pages/download/index.tsx"
+ },
+ {
+ "id": "P11",
+ "kind": "page",
+ "sourcePath": "src/pages/brand-assets/index.tsx"
+ },
+ {
+ "id": "P12",
+ "kind": "page",
+ "sourcePath": "src/pages/events/index.tsx"
+ },
+ {
+ "id": "P13",
+ "kind": "page",
+ "sourcePath": "src/pages/community/roadmap/index.tsx"
+ },
+ {
+ "id": "P14",
+ "kind": "page",
+ "sourcePath": "src/pages/community-report/index.tsx"
+ },
+ {
+ "id": "P15",
+ "kind": "page",
+ "sourcePath": "src/pages/course/index.tsx"
+ },
+ {
+ "id": "P16",
+ "kind": "page",
+ "sourcePath": "src/pages/course/hackathon/a1-hybrid-search.tsx"
+ },
+ {
+ "id": "P17",
+ "kind": "page",
+ "sourcePath": "src/pages/course/hackathon/a2-mcp.tsx"
+ },
+ {
+ "id": "P18",
+ "kind": "page",
+ "sourcePath": "src/pages/course/hackathon/a3-log-search.tsx"
+ },
+ {
+ "id": "P19",
+ "kind": "page",
+ "sourcePath": "src/pages/course/hackathon/a4-agent-traces.tsx"
+ },
+ {
+ "id": "P20",
+ "kind": "page",
+ "sourcePath": "src/pages/course/hackathon/track-b.tsx"
+ },
+ {
+ "id": "P21",
+ "kind": "page",
+ "sourcePath": "src/pages/profile-analysis/index.tsx"
+ },
+ {
+ "id": "slack",
+ "kind": "utility",
+ "sourcePath": "src/pages/slack.tsx"
+ }
+ ]
+}
diff --git a/translations/pages/state.json b/translations/pages/state.json
new file mode 100644
index 000000000000..f2350ea88165
--- /dev/null
+++ b/translations/pages/state.json
@@ -0,0 +1,4 @@
+{
+ "schemaVersion": 1,
+ "pages": []
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]