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 e142b9d4dc4a [i18n](docs) Maintain only future release notes (#4527)
e142b9d4dc4a is described below
commit e142b9d4dc4a94fab0a8dc4d7a3494ce0eab8755
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Tue Oct 27 02:21:42 2026 +0000
[i18n](docs) Maintain only future release notes (#4527)
Scheduled release-note maintenance currently lacks a fixed historical
boundary. Enable RELEASES for Japanese, Korean and Russian only for
genuinely new native sources after commit `b207a93551a3`, while
preserving the 83 historical notes, existing targets and their
publication/fallback behavior.
The shared policy follows Git lineage, excludes historical identities
and paths from required/pending work, and sends ambiguous delete/add
histories to manual reconciliation. Inventory, audit, planning and the
generator enforce the same policy; forced historical tasks fail before
model calls. The existing ownership checks, active-PR reservations,
whole-run lock, batch limits and trusted-master checks remain in place.
Audit workflows fetch history for cutoff and lineage checks.
Add a localized Release Notes search context for current real
translations. Add a small five-language native fixture covering
historical/current/missing notes, policy selection, reserved PRs, mock
generation, fallback SEO and actual search workers. CI keeps
representative compilation and validates the fixed policy offline.
Validation so far:
- i18n tests: 320 passed, one skipped.
- Workflow security tests: 22 passed.
- Real release-note fixture: 15 pages across five languages, three
managed-language search workers, correct fallback exclusion and
mock-only generation.
- Configuration validation passes for ja/ko/ru; `git diff --check`
passes.
- Three-language full native audits are running and will be recorded
before this draft is made ready.
No historical release-note prose, translated target, translation state,
blog body or Chinese automation is changed. No real future release note
was manufactured and no real model call was needed for this
implementation. The first real future note's
generation/merge/publication remains an explicit maintenance
observation.
---------
Co-authored-by: morningman <[email protected]>
---
.github/workflows/build-check.yml | 6 +-
.github/workflows/japanese-coverage-audit.yml | 5 +-
.github/workflows/korean-coverage-audit.yml | 5 +-
.github/workflows/russian-coverage-audit.yml | 4 +-
docusaurus.config.js | 4 +-
scripts/i18n/__tests__/expanded-scope.test.cjs | 9 +-
scripts/i18n/__tests__/multi-locale.test.cjs | 2 +-
scripts/i18n/__tests__/release-policy.test.cjs | 154 ++++++
scripts/i18n/__tests__/search-contexts.test.cjs | 2 +-
.../i18n/__tests__/search-verification.test.cjs | 6 +-
scripts/i18n/audit-coverage.cjs | 50 +-
scripts/i18n/build-scope.cjs | 2 +-
scripts/i18n/check-configuration.cjs | 17 +-
scripts/i18n/ci-plan.cjs | 2 +-
scripts/i18n/inventory.cjs | 19 +-
scripts/i18n/master-drift.cjs | 2 +-
scripts/i18n/plan-translations.cjs | 9 +-
scripts/i18n/release-policy.cjs | 131 +++++
scripts/i18n/search-plugin.cjs | 7 +-
scripts/i18n/translation-engine.cjs | 9 +
scripts/i18n/verify-release-policy-fixture.cjs | 160 ++++++
scripts/i18n/workflow-scan.cjs | 3 +-
translations/README.md | 64 ++-
translations/config.json | 9 +-
translations/release-notes-baseline.json | 594 +++++++++++++++++++++
translations/scope/ja.json | 9 +-
translations/scope/ko.json | 9 +-
translations/scope/ru.json | 9 +-
28 files changed, 1248 insertions(+), 54 deletions(-)
diff --git a/.github/workflows/build-check.yml
b/.github/workflows/build-check.yml
index bd85273539a6..67caefd37d92 100644
--- a/.github/workflows/build-check.yml
+++ b/.github/workflows/build-check.yml
@@ -116,7 +116,7 @@ jobs:
- name: Validate manual content and shared messages
if: steps.detect.outputs.validation_mode != 'skip'
run: |
- node scripts/i18n/check-configuration.cjs --all
+ node scripts/i18n/check-configuration.cjs --all
--release-history
node scripts/i18n/manual-report.cjs
node scripts/i18n/check-blog-translations.cjs
@@ -140,6 +140,7 @@ jobs:
run: |
node scripts/i18n/verify-content-fixture.cjs
node scripts/i18n/verify-blog-fixture.cjs
+ node scripts/i18n/verify-release-policy-fixture.cjs
- name: Workflow security boundary tests
run: PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s
.github/scripts -p 'test_*.py' -v
@@ -185,6 +186,9 @@ jobs:
- name: Verify global language fixture in Chromium
if: steps.detect.outputs.content_fixture == 'true'
run: node scripts/i18n/verify-global-language.cjs --fixture
--site .i18n-build/content-fixture/build --manifests
.i18n-build/content-fixture/.i18n-build/content-manifests --output
.i18n-build/browser-global-language-fixture.json
+ - name: Verify release-note policy fixture in Chromium
+ if: steps.detect.outputs.content_fixture == 'true'
+ run: node scripts/i18n/verify-release-policy-fixture.cjs
--verify-existing --browser
- name: Build
env:
diff --git a/.github/workflows/japanese-coverage-audit.yml
b/.github/workflows/japanese-coverage-audit.yml
index 64285b6b23ae..5418591c2e68 100644
--- a/.github/workflows/japanese-coverage-audit.yml
+++ b/.github/workflows/japanese-coverage-audit.yml
@@ -22,8 +22,9 @@ jobs:
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
- # The audit only reads HEAD; its git comparisons never need history.
- fetch-depth: 1
+ # Release-note eligibility follows the fixed cutoff and Git lineage.
+ fetch-depth: 0
+ filter: blob:none
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
diff --git a/.github/workflows/korean-coverage-audit.yml
b/.github/workflows/korean-coverage-audit.yml
index 6ffd588aa558..7c0e8621df86 100644
--- a/.github/workflows/korean-coverage-audit.yml
+++ b/.github/workflows/korean-coverage-audit.yml
@@ -22,8 +22,9 @@ jobs:
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
- # The audit only reads HEAD; its git comparisons never need history.
- fetch-depth: 1
+ # Release-note eligibility follows the fixed cutoff and Git lineage.
+ fetch-depth: 0
+ filter: blob:none
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
diff --git a/.github/workflows/russian-coverage-audit.yml
b/.github/workflows/russian-coverage-audit.yml
index 80332a295c27..add255077f76 100644
--- a/.github/workflows/russian-coverage-audit.yml
+++ b/.github/workflows/russian-coverage-audit.yml
@@ -22,7 +22,9 @@ jobs:
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
- fetch-depth: 1
+ # Release-note eligibility follows the fixed cutoff and Git lineage.
+ fetch-depth: 0
+ filter: blob:none
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
diff --git a/docusaurus.config.js b/docusaurus.config.js
index 9fc1ed4e1d7a..f47a974508f9 100644
--- a/docusaurus.config.js
+++ b/docusaurus.config.js
@@ -556,13 +556,13 @@ const config = {
highlightSearchTermsOnTargetPage: true,
// indexPages: true,
indexDocs: true,
- docsRouteBasePath: ['docs', 'course', 'zh-CN/docs',
'zh-CN/course'],
+ docsRouteBasePath: ['docs', 'course', 'releases',
'zh-CN/docs', 'zh-CN/course', 'zh-CN/releases'],
indexBlog: false,
explicitSearchResultPath: true,
searchBarShortcut: true,
searchBarShortcutHint: true,
searchResultLimits: 100,
- searchContextByPaths: ['docs', 'course'],
+ searchContextByPaths: ['docs', 'course', { path: 'releases',
label: { en: 'Release Notes', 'zh-CN': '发行说明' } }],
useAllContextsWithNoSearchContext: false,
ignoreFiles: [/^docs\/(?:[^/]+\/)?key-features\//],
},
diff --git a/scripts/i18n/__tests__/expanded-scope.test.cjs
b/scripts/i18n/__tests__/expanded-scope.test.cjs
index 41dcfd127237..b5b4a968ba88 100644
--- a/scripts/i18n/__tests__/expanded-scope.test.cjs
+++ b/scripts/i18n/__tests__/expanded-scope.test.cjs
@@ -90,6 +90,13 @@ function fixture(t) {
const root = fs.mkdtempSync(path.join(os.tmpdir(),
'expanded-translation-'));
t.after(() => fs.rmSync(root, {recursive: true, force: true}));
const write = (file, text) => {fs.mkdirSync(path.dirname(path.join(root,
file)), {recursive: true}); fs.writeFileSync(path.join(root, file), text);};
+ const git = (...args) => execFileSync('git', args, {cwd: root, encoding:
'utf8', stdio: ['ignore', 'pipe', 'pipe']}).trim();
+ git('init', '-q'); git('config', 'user.name', 'Fixture'); git('config',
'user.email', '[email protected]');
+ write('fixture-baseline.txt', 'Before release-note registration.\n');
+ git('add', '.'); git('commit', '-qm', 'Release-note cutoff');
+ const cutoff = git('rev-parse', 'HEAD');
+ write('translations/release-notes-baseline.json',
JSON.stringify({schemaVersion:1, sourceCommit:cutoff,
+ pluginId:'releases', version:'current', sourceRoot:'releasenotes',
documents:[], sourceFiles:[], historicalPaths:[]}));
fs.symlinkSync(path.join(repo, 'node_modules'), path.join(root,
'node_modules'));
write('translations/config.json', read('translations/config.json'));
for (const locale of ['ja', 'ko', 'ru']) {
@@ -116,8 +123,6 @@ plugins:[require(${JSON.stringify(path.join(repo,
'scripts/i18n/content-plugin.c
[require(${JSON.stringify(path.join(repo,
'scripts/i18n/docs-plugin.cjs'))}),{id:'course',path:'course',routeBasePath:'course',sidebarPath:false}],
[require(${JSON.stringify(path.join(repo,
'scripts/i18n/docs-plugin.cjs'))}),{id:'releases',path:'releasenotes',routeBasePath:'releases',sidebarPath:false}]],
presets:[['classic',{docs:false,blog:false}]]};\n`);
- const git = (...args) => execFileSync('git', args, {cwd: root, encoding:
'utf8', stdio: ['ignore', 'pipe', 'pipe']}).trim();
- git('init', '-q'); git('config', 'user.name', 'Fixture'); git('config',
'user.email', '[email protected]');
const commit = () => {git('add', '.'); git('commit', '-qm', 'fixture');
return git('rev-parse', 'HEAD');};
return {root, write, git, commit};
}
diff --git a/scripts/i18n/__tests__/multi-locale.test.cjs
b/scripts/i18n/__tests__/multi-locale.test.cjs
index bfa77a3a7d80..7c0e1ea8d6c4 100644
--- a/scripts/i18n/__tests__/multi-locale.test.cjs
+++ b/scripts/i18n/__tests__/multi-locale.test.cjs
@@ -211,7 +211,7 @@ test('inventory CLI snapshots committed page records for
the selected locale', a
{id: 'COMMUNITY', pluginId: 'community', version: 'current',
sourceRoot: 'community'},
], retainEnglish: []}));
// Exercise the real CLI boundary without adding a production root
override.
- for (const file of ['inventory.cjs', 'state.cjs', 'source-groups.cjs',
'code-prose.cjs', 'docs-language-policy.cjs'])
+ for (const file of ['inventory.cjs', 'state.cjs', 'source-groups.cjs',
'code-prose.cjs', 'docs-language-policy.cjs', 'release-policy.cjs'])
write(`scripts/i18n/${file}`, fs.readFileSync(path.join(repoRoot,
'scripts/i18n', file), 'utf8'));
const git = (...args) => execFileSync('git', args, {cwd: root, encoding:
'utf8', stdio: ['ignore', 'pipe', 'pipe']});
git('init', '-q');
diff --git a/scripts/i18n/__tests__/release-policy.test.cjs
b/scripts/i18n/__tests__/release-policy.test.cjs
new file mode 100644
index 000000000000..5c63aa4f1693
--- /dev/null
+++ b/scripts/i18n/__tests__/release-policy.test.cjs
@@ -0,0 +1,154 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const os = require('node:os');
+const path = require('node:path');
+const {execFileSync} = require('node:child_process');
+const {hash, identity} = require('../state.cjs');
+const {releasePolicy, loadBaseline} = require('../release-policy.cjs');
+const {checkedTask} = require('../translation-engine.cjs');
+const {prepare} = require('../translate-docs.cjs');
+const {plan} = require('../plan-translations.cjs');
+const {batchWide} = require('../master-drift.cjs');
+const {policy: ciPolicy} = require('../ci-plan.cjs');
+const {sidebarAudit, hasPending} = require('../audit-coverage.cjs');
+
+function fixture(t) {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(),
'doris-release-policy-'));
+ t.after(() => fs.rmSync(root, {recursive:true,force:true}));
+ const git = (...args) => execFileSync('git', args,
{cwd:root,encoding:'utf8',stdio:['ignore','pipe','pipe']}).trim();
+ const write = (name, text) =>
{fs.mkdirSync(path.dirname(path.join(root,name)),{recursive:true});fs.writeFileSync(path.join(root,name),text);};
+ const commit = () => {git('add','.');git('commit','-qm','Fixture
change');return git('rev-parse','HEAD');};
+
git('init','-q');git('config','user.name','Fixture');git('config','user.email','[email protected]');
+ write('releasenotes/legacy.md','---\ntitle: Release 1.0\n---\n\nHistorical
release details.\n');
+ const earlier = commit();
+ git('mv','releasenotes/legacy.md','releasenotes/history.md');
+ write('releasenotes/unregistered.md','An existing unregistered release
note.\n');
+ const cutoff = commit();
+ const group =
{id:'RELEASES',pluginId:'releases',version:'current',sourceRoot:'releasenotes',
+
sourcePolicy:{mode:'new-only',baseline:'translations/release-notes-baseline.json'}};
+ const baseline =
{schemaVersion:1,sourceCommit:cutoff,pluginId:'releases',version:'current',sourceRoot:'releasenotes',
+
documents:[{docId:'history',sourcePath:'releasenotes/history.md',sourceHash:hash(fs.readFileSync(path.join(root,'releasenotes/history.md'),'utf8'))}],
+ sourceFiles:['releasenotes/history.md','releasenotes/unregistered.md'],
+
historicalPaths:['releasenotes/history.md','releasenotes/legacy.md','releasenotes/unregistered.md']};
+ const saveBaseline = () =>
write(group.sourcePolicy.baseline,JSON.stringify(baseline));
+ saveBaseline();commit();
+ const scope =
{schemaVersion:1,locale:'ja',maxBatchPages:20,automaticGroups:['RELEASES'],groups:[group],retainEnglish:[]};
+ const page = (file, docId = path.basename(file,'.md')) =>
({pluginId:'releases',version:'current',locale:'ja',docId,
+
sourcePath:file,sourceHash:hash(fs.readFileSync(path.join(root,file),'utf8')),
+
targetPath:'i18n/ja/docusaurus-plugin-content-docs-releases/current/'+file.slice('releasenotes/'.length)});
+ const selectedPlan = task =>
({schemaVersion:1,locale:'ja',sourceCommit:git('rev-parse','HEAD'),selected:['selected'],
+ tasks:[{...task,id:'selected',kind:'translate_new'}]});
+ return
{root,git,write,commit,cutoff,earlier,group,scope,baseline,saveBaseline,page,selectedPlan};
+}
+
+test('the cutoff excludes registered, unregistered and previously renamed
historical sources', t => {
+ const f=fixture(t),p=releasePolicy(f.root,f.group);
+
assert.equal(p.classify(f.page('releasenotes/history.md')).kind,'historical_excluded');
+
assert.equal(p.classify(f.page('releasenotes/unregistered.md')).kind,'historical_excluded');
+ f.write('releasenotes/legacy.md','A historical path registered again with
different text.\n');f.commit();
+
assert.equal(releasePolicy(f.root,f.group).classify(f.page('releasenotes/legacy.md')).kind,'historical_excluded');
+});
+
+test('a genuinely new note and its later updates remain eligible', t => {
+ const f=fixture(t);f.write('releasenotes/release-2.md','---\ntitle:
Release 2.0\n---\n\nA genuinely new release.\n');f.commit();
+ const task=f.page('releasenotes/release-2.md');
+ assert.equal(releasePolicy(f.root,f.group).classify(task).kind,'eligible');
+
assert.equal(checkedTask(f.selectedPlan(task),'selected',f.scope,f.root).source.includes('new
release'),true);
+ f.write('releasenotes/release-2.md','---\ntitle: Release
2.0\n---\n\nUpdated new release.\n');f.commit();
+
assert.equal(releasePolicy(f.root,f.group).classify(f.page('releasenotes/release-2.md')).kind,'eligible');
+});
+
+test('historical moves, later edits and re-registered IDs do not become new
notes', t => {
+ const
f=fixture(t);f.git('mv','releasenotes/history.md','releasenotes/moved.md');f.commit();
+ f.write('releasenotes/moved.md','---\ntitle: Historical
update\n---\n\nChanged after moving.\n');f.commit();
+
assert.equal(releasePolicy(f.root,f.group).classify(f.page('releasenotes/moved.md')).kind,'historical_excluded');
+ f.write('releasenotes/registered-again.md','An old identity under a new
source path.\n');f.commit();
+
assert.equal(releasePolicy(f.root,f.group).classify(f.page('releasenotes/registered-again.md','history')).kind,'historical_excluded');
+});
+
+test('exact historical copies and ambiguous delete/add changes cannot trigger
generation', t => {
+ const
f=fixture(t);f.write('releasenotes/copy.md',fs.readFileSync(path.join(f.root,'releasenotes/history.md'),'utf8'));f.commit();
+
assert.notEqual(releasePolicy(f.root,f.group).classify(f.page('releasenotes/copy.md')).kind,'eligible');
+
f.git('rm','releasenotes/history.md');f.write('releasenotes/uncertain.md','Completely
rewritten source with an unidentified historical deletion.\n');f.commit();
+
assert.equal(releasePolicy(f.root,f.group).classify(f.page('releasenotes/uncertain.md')).kind,'policy_review');
+});
+
+test('a forced historical task is rejected before any adapter/model call',
async t => {
+ const f=fixture(t),task=f.page('releasenotes/history.md');let calls=0;
+ await
assert.rejects(prepare({root:f.root,plan:f.selectedPlan(task),taskId:'selected',scope:f.scope,
+ state:{data:{pages:[]}},adapter:{translate(){calls++;throw Error('must
not run')}},glossary:{},target:{locale:'ja'}}),
+ /Release Notes task excluded/);
+ assert.equal(calls,0);
+});
+
+test('generation rejects an uncommitted policy change and an absent policy', t
=> {
+ const f=fixture(t);f.write('releasenotes/new.md','A new
release.\n');f.commit();
+ const task=f.page('releasenotes/new.md'),p=f.selectedPlan(task);
+ f.write(f.group.sourcePolicy.baseline,JSON.stringify(f.baseline)+'\n');
+ assert.throws(()=>checkedTask(p,'selected',f.scope,f.root),/policy changed
since planning/);
+ const scope={...f.scope,groups:[{...f.group,sourcePolicy:undefined}]};
+ assert.throws(()=>checkedTask(p,'selected',scope,f.root),/requires a
new-only source policy/);
+});
+
+test('incomplete, changed or non-ancestor cutoff evidence fails closed', t => {
+ const f=fixture(t);
+ assert.throws(()=>releasePolicy(f.root,f.group,f.earlier),/baseline must
be an ancestor/);
+
f.baseline.historicalPaths=f.baseline.historicalPaths.filter(file=>!file.endsWith('/legacy.md'));f.saveBaseline();
+ assert.throws(()=>releasePolicy(f.root,f.group),/omits historical path
identities/);
+
f.baseline.historicalPaths.push('releasenotes/legacy.md');f.baseline.documents[0].sourceHash='0'.repeat(64);f.saveBaseline();
+ assert.throws(()=>releasePolicy(f.root,f.group),/hash mismatch/);
+
assert.throws(()=>loadBaseline(f.root,{...f.group,sourcePolicy:{mode:'new-only',baseline:'../outside.json'}}),/Invalid
release-note source policy/);
+});
+
+test('historical targets stay out of pending, deletion tasks and duplicate
generation', async t => {
+ const f=fixture(t);f.write('releasenotes/new.md','New release
details.\n');const sourceCommit=f.commit();
+ const
old=f.page('releasenotes/history.md'),fresh=f.page('releasenotes/new.md'),eligibility=releasePolicy(f.root,f.group).classify(old);
+ const
report={schemaVersion:1,sourceCommit,baselineCommit:f.cutoff,reports:[],
+ policyExclusions:[{...old,...eligibility}],
+
groups:[{...f.group,unregistered:[],entries:[{...old,state:'historical_excluded'},{...fresh,state:'untranslated'}]}]};
+ const args={report,scope:f.scope,records:new
Map([[identity(old),old]]),baseline:[old],
+
readCurrent:file=>fs.readFileSync(path.join(f.root,file),'utf8'),readAt:()=>null};
+ const result=await plan(args);
+
assert.equal(result.counts.required,1);assert.equal(result.automaticCounts.pending,1);
+
assert.equal(result.tasks.length,1);assert.equal(result.tasks[0].sourcePath,fresh.sourcePath);
+ assert.equal(result.selected.length,1);
+ const reserved=await
plan({...args,active:{schemaVersion:1,candidates:[{...fresh,pr:'https://github.com/apache/doris-website/pull/1'}]}});
+
assert.equal(reserved.selected.length,0);assert.equal(reserved.counts.activeCandidates,1);
+ const empty=await
plan({...args,report:{...report,groups:[{...f.group,unregistered:[],entries:[{...old,state:'historical_excluded'}]}]},
+ readCurrent:()=>{throw Error('An excluded historical source must not
be read for translation')}});
+
assert.equal(empty.automaticCounts.required,0);assert.equal(empty.selected.length,0);assert.deepEqual(empty.tasks,[]);
+});
+
+test('cutoff changes invalidate a batch and receive representative CI with
configuration validation', () => {
+ assert(batchWide('ja')('translations/release-notes-baseline.json'));
+ const p=ciPolicy(['translations/release-notes-baseline.json']);
+
assert.equal(p.mode,'sample');assert.equal(p.runTranslationTests,true);assert.equal(p.publicationAllowed,false);
+});
+
+test('baseline symlinks cannot read files outside the reviewed source tree', t
=> {
+ const
f=fixture(t),baseline=path.join(f.root,f.group.sourcePolicy.baseline);
+ fs.renameSync(baseline,baseline+'.saved');
+ fs.symlinkSync(baseline+'.saved',baseline);
+ assert.throws(()=>loadBaseline(f.root,f.group),/Symlink in Release Notes
baseline path/);
+});
+
+test('historical navigation stays in the census without becoming an automatic
label backlog', t => {
+ const f=fixture(t);f.write('releasenotes/new.md','A genuinely new
release.\n');f.commit();
+ const policy=releasePolicy(f.root,f.group);
+ const
native={pages:[f.page('releasenotes/history.md'),f.page('releasenotes/new.md')],
+ labels:[{pluginId:'releases',files:[{path:'current',content:{
+ 'sidebar.releases.doc.history':{message:'Release 1.0'},
+ 'sidebar.releases.doc.new':{message:'Release 2.0'},
+ 'sidebar.releases.category.Core':{message:'Doris Core'},
+ }}]}]};
+ const rows=sidebarAudit(native,f.root,f.scope,new
Map([['RELEASES',policy]]));
+
assert.equal(rows.find(row=>row.key.endsWith('.history')).maintenance,'historical_excluded');
+
assert.equal(rows.find(row=>row.key.endsWith('.new')).maintenance,'eligible');
+
assert.equal(rows.find(row=>row.key.endsWith('.Core')).maintenance,'retained_navigation');
+ const
report={groups:[{counts:{pending:83}}],labels:{missing:83,untranslated:10},
+
automaticCounts:{pending:0},automaticLabels:{missing:0,untranslated:0}};
+ assert.equal(hasPending(report),false);
+ assert.equal(hasPending({...report,automaticCounts:{pending:1}}),true);
+ assert.equal(hasPending({...report,automaticLabels:{missing:1}}),true);
+});
diff --git a/scripts/i18n/__tests__/search-contexts.test.cjs
b/scripts/i18n/__tests__/search-contexts.test.cjs
index f241fe45e4e0..a8716e86550a 100644
--- a/scripts/i18n/__tests__/search-contexts.test.cjs
+++ b/scripts/i18n/__tests__/search-contexts.test.cjs
@@ -95,7 +95,7 @@ test('the localized root index holds only documents outside
every context', () =
const all = [[doc('/ja/docs/dev/a/', 1), doc('/ja/docs/4.x/a/', 2),
doc('/ja/community/a/', 3)], []];
const selected = new Map([['/ja/docs/dev/a', 'docs/dev'],
['/ja/docs/4.x/a', 'docs/4.x'], ['/ja/community/a', 'community']]);
const groups = indexGroups(all, selected);
- assert.deepEqual([...groups.keys()], ['docs/dev', 'docs/4.x', 'community',
'course', '']);
+ assert.deepEqual([...groups.keys()], ['docs/dev', 'docs/4.x', 'community',
'course', 'releases', '']);
assert.deepEqual(groups.get('docs/4.x'), [[doc('/ja/docs/4.x/a/', 2)],
[]]);
assert.deepEqual(groups.get(''), [[], []]);
assert.deepEqual(indexGroups([[doc('/ja/other/', 4)]], selected).get(''),
[[doc('/ja/other/', 4)]]);
diff --git a/scripts/i18n/__tests__/search-verification.test.cjs
b/scripts/i18n/__tests__/search-verification.test.cjs
index fb2085ecb076..79b88cb55c8b 100644
--- a/scripts/i18n/__tests__/search-verification.test.cjs
+++ b/scripts/i18n/__tests__/search-verification.test.cjs
@@ -18,20 +18,22 @@ function fixture(t) {
{pluginId:'default',version:'4.x',permalink:'/ko/docs/4.x/current/'},
{pluginId:'community',version:'current',permalink:'/ko/community/current/'},
{pluginId:'course',version:'current',permalink:'/ko/course/101/lesson/'},
+
{pluginId:'releases',version:'current',permalink:'/ko/releases/v5.0/release-5.0.0/'},
].map(page =>
({...page,locale:'ko',status:'current',indexable:true,useTarget:true}));
const index = url => [{documents:[{u:url}]}];
- write('ko-search-summary.json',
{locale:'ko',indexedPages:4,contexts:{'docs/dev':1,'docs/4.x':1,community:1,course:1}});
+ write('ko-search-summary.json',
{locale:'ko',indexedPages:5,contexts:{'docs/dev':1,'docs/4.x':1,community:1,course:1,releases:1}});
write('search-index.json', [{documents:[]}]);
write('search-index-docs-dev.json', index(pages[0].permalink));
write('search-index-docs-4.x.json', index(pages[1].permalink));
write('search-index-community.json', index(pages[2].permalink));
write('search-index-course.json', index(pages[3].permalink));
+ write('search-index-releases.json', index(pages[4].permalink));
return {site,write,pages,index};
}
test('Korean index verification agrees with current native targets and an
empty root union', t => {
const {site,pages} = fixture(t);
-
assert.equal(checkIndexes({site,locale:'ko',manifest:{pages}}).summary.indexedPages,
4);
+
assert.equal(checkIndexes({site,locale:'ko',manifest:{pages}}).summary.indexedPages,
5);
});
test('fallback, stale, unlisted and unverified targets cannot enter the Korean
index', t => {
diff --git a/scripts/i18n/audit-coverage.cjs b/scripts/i18n/audit-coverage.cjs
index b19bab45d1b5..b5410c7c8233 100644
--- a/scripts/i18n/audit-coverage.cjs
+++ b/scripts/i18n/audit-coverage.cjs
@@ -6,6 +6,7 @@ const path = require('node:path');
const {execFileSync, spawnSync} = require('node:child_process');
const {hash, identity, inventoryBaseline, loadRecordsAt, pipelineLocale} =
require('./state.cjs');
const {collect, validateScope} = require('./inventory.cjs');
+const {policiesFor, docKey} = require('./release-policy.cjs');
const keyFor = locale => page => identity({...page,locale});
// One `git show` per page is several thousand processes for a full-scope
@@ -42,7 +43,7 @@ function baselineEntries(root) {
return meta.entryFiles.flatMap(shard =>
JSON.parse(fs.readFileSync(path.join(root,shard),'utf8')).entries);
});
}
-function sidebarAudit(native,root,scope) {
+function sidebarAudit(native,root,scope,policies = new Map()) {
const result = [];
for (const group of scope.groups) {
if (group.kind) continue; // Source modules/partials have no native
sidebar.
@@ -62,8 +63,16 @@ function sidebarAudit(native,root,scope) {
throw new Error(`Invalid retained sidebar label ${name}`);
const state = !translated[name]?.message ? 'missing' :
translated[name].message === value.message
? exception ? 'retained_technical' : 'untranslated' :
'translated';
+ const policy = policies.get(group.id);
+ const docId = name.match(/\.doc\.(.+)$/)?.[1];
+ const page = policy && native.pages.find(page => page.pluginId ===
group.pluginId
+ && page.version === group.version && page.docId === docId);
+ // Existing release categories/version labels remain manual native
+ // navigation; they do not become a historical translation backlog.
+ const maintenance = policy ? page ? policy.classify(page).kind :
'retained_navigation' : undefined;
result.push({group:group.id,
pluginId:group.pluginId,version:group.version,key:name,
- source:value.message,translation:translated[name]?.message ||
null,state});
+ source:value.message,translation:translated[name]?.message ||
null,state,
+ ...(maintenance && {maintenance})});
}
for (const exception of retained.filter(entry => entry.pluginId ===
group.pluginId && entry.version === group.version))
assert(sourceKeys.some(([name]) => name === exception.key),`Unused
sidebar exception ${exception.key}`);
@@ -82,6 +91,7 @@ async function audit({root,sourceRef,baseline,native}) {
const scope =
JSON.parse(fs.readFileSync(path.join(root,`translations/scope/${locale}.json`),'utf8'));
validateScope(scope, locale);
const committedRecords = loadRecordsAt(root, locale, commit);
+ const policies = policiesFor(root, scope, commit);
native ||= await collect(root);
const old = new Map(baselineEntries(baseline).map(page =>
[key(page),page]));
const current = new Set(), groups = [];
@@ -105,7 +115,9 @@ async function audit({root,sourceRef,baseline,native}) {
assert(!seen.has(page.englishPath),`Duplicate native source
${page.englishPath}`);
seen.add(page.englishPath); current.add(key(page));
const previous = old.get(key(page));
- const disposition = page.draft ? 'draft' :
scope.retainEnglish.some(entry => key(entry) === key(page)) ? 'retain_english'
: 'translate';
+ const eligibility = policies.get(group.id)?.classify(page);
+ const disposition = page.draft ? 'draft' :
scope.retainEnglish.some(entry => key(entry) === key(page)) ? 'retain_english'
+ : eligibility && eligibility.kind !== 'eligible' ?
eligibility.kind : 'translate';
const mergedCurrent = page.status === 'current' &&
page.reviewStatus !== 'unverified' && page.useTarget
&& targetHashes.get(page.targetPath) !== null
&& targetHashes.get(page.targetPath) === page.targetHash
@@ -115,30 +127,38 @@ async function audit({root,sourceRef,baseline,native}) {
entries.push({pluginId:page.pluginId,version:page.version,docId:page.docId,sourcePath:page.sourcePath,
targetPath:page.targetPath,sourceHash:page.sourceHash,targetHash:page.targetHash,
sourceCommit:commit,permalink:page.permalink,reviewStatus:page.reviewStatus,state,
+ ...(eligibility && {eligibility, contentState: page.status}),
change:!previous ? 'new' : previous.sourcePath !==
page.sourcePath ? 'moved'
: previous.sourceHash !== page.sourceHash ?
'source_changed' : 'unchanged'});
}
const unregistered = sourceFiles.filter(file => !seen.has(file));
- const required = entries.filter(entry =>
!['draft','retain_english'].includes(entry.state));
+ const required = entries.filter(entry =>
!['draft','retain_english','historical_excluded','policy_review'].includes(entry.state));
groups.push({id:group.id,pluginId:group.pluginId,version:group.version,
counts:{registered:entries.length,required:required.length,current:required.filter(entry
=> entry.state === 'current').length,
pending:required.filter(entry => entry.state !==
'current').length,new:entries.filter(entry => entry.change === 'new').length,
changed:entries.filter(entry => entry.change ===
'source_changed').length,moved:entries.filter(entry => entry.change ===
'moved').length,
- drafts:entries.filter(entry => entry.state ===
'draft').length,unregistered:unregistered.length},
+ drafts:entries.filter(entry => entry.state === 'draft').length,
+ historicalExcluded:entries.filter(entry => entry.state ===
'historical_excluded').length,
+ policyReview:entries.filter(entry => entry.state ===
'policy_review').length,unregistered:unregistered.length},
unregistered,entries});
}
const removed = [...old.values()].filter(page => !current.has(key(page)))
.map(page =>
({pluginId:page.pluginId,version:page.version,docId:page.docId,sourcePath:page.sourcePath,
likelyMoveTo: groups.flatMap(group => group.entries).find(entry =>
entry.sourceHash === page.sourceHash
&& entry.pluginId === page.pluginId && entry.version ===
page.version)?.sourcePath || null}));
- const sidebar = sidebarAudit(native,root,scope);
+ const sidebar = sidebarAudit(native,root,scope,policies);
const
automaticGroups=groups.filter(group=>!scope.automaticGroups||scope.automaticGroups.includes(group.id));
const
automaticCounts=Object.fromEntries(['required','current','pending'].map(name=>[name,
automaticGroups.reduce((sum,group)=>sum+group.counts[name],0)]));
const labels =
Object.fromEntries(['translated','untranslated','missing','retained_technical']
.map(state => [state,sidebar.filter(entry => entry.state ===
state).length]));
+ const automaticLabels =
Object.fromEntries(['translated','untranslated','missing','retained_technical']
+ .map(state => [state,sidebar.filter(entry => entry.state === state
+ && (!scope.automaticGroups ||
scope.automaticGroups.includes(entry.group))
+ && (!entry.maintenance || entry.maintenance ===
'eligible')).length]));
const routingInputs =
['docusaurus.config.js','versions.json','sidebars.ts','sidebarsCommunity.json','versioned_sidebars/version-4.x-sidebars.json',
'translations/config.json',`translations/scope/${locale}.json`];
+ routingInputs.push(...[...policies.values()].map(policy =>
policy.baselinePath));
if (scope.groups.some(group => group.pluginId === 'course'))
routingInputs.push('sidebarsCourse.ts');
if (scope.groups.some(group => group.pluginId === 'releases'))
routingInputs.push('sidebarsReleases.json');
routingInputs.push(`translations/state/${locale}/docs.json`,
`translations/state/${locale}/resolved-deletions.json`);
@@ -151,11 +171,23 @@ async function audit({root,sourceRef,baseline,native}) {
`Routing input differs from source ref: ${file}`);
return
{schemaVersion:1,sourceCommit:commit,baselineCommit:JSON.parse(fs.readFileSync(path.join(baseline,'summary.json'),'utf8')).sourceCommit,
routingInputHashes:Object.fromEntries(routingInputs.map(file =>
[file,routingHashes.get(file)])),
- groups,removed,labels,sidebar,reports:native.reports,automaticCounts,
+
groups,removed,labels,automaticLabels,sidebar,reports:native.reports,automaticCounts,
+ policyExclusions: [...new Map([...old.values(),
...committedRecords.values(), ...native.pages]
+ .flatMap(page => {
+ const group = scope.groups.find(group => group.pluginId ===
page.pluginId && group.version === page.version);
+ const eligibility = policies.get(group?.id)?.classify(page);
+ return eligibility && eligibility.kind !== 'eligible'
+ ? [[docKey(page),
{pluginId:page.pluginId,version:page.version,docId:page.docId,sourcePath:page.sourcePath,
...eligibility}]] : [];
+ })).values()],
deferredAutomaticGroups:groups.filter(group=>!automaticGroups.includes(group)).map(group=>group.id),
fallbackOnlyVersions:
require('./docs-language-policy.cjs').fallbackOnlyVersions.map(version =>
({pluginId:'default',version,
proseLocales:['en','zh-CN'],otherLocaleContent:'en',requiredTranslation:false}))};
}
+function hasPending(report) {
+ const pending = report.automaticCounts?.pending ??
report.groups.some(group => group.counts.pending);
+ const labels = report.automaticLabels ?? report.labels;
+ return !!(pending || labels.missing || labels.untranslated);
+}
if (require.main === module) {
const args = process.argv.slice(2), option = name =>
args[args.indexOf(name)+1];
const root = path.resolve(__dirname,'../..');
@@ -164,7 +196,7 @@ if (require.main === module) {
if(args.includes('--output'))
fs.writeFileSync(option('--output'),output); else process.stdout.write(output);
console.log(JSON.stringify({sourceCommit:result.sourceCommit,groups:result.groups.map(({id,counts})
=> ({id,counts})),
labels:result.labels,removed:result.removed.length}));
- if(args.includes('--fail-on-pending') && (result.groups.some(group
=> group.counts.pending) || result.labels.missing ||
result.labels.untranslated)) process.exitCode=1;
+ if(args.includes('--fail-on-pending') && hasPending(result))
process.exitCode=1;
}).catch(error => {console.error(error);process.exitCode=1;});
}
-module.exports = {audit,sidebarAudit};
+module.exports = {audit,sidebarAudit,hasPending};
diff --git a/scripts/i18n/build-scope.cjs b/scripts/i18n/build-scope.cjs
index f5a46cab6a94..d3d7f0136daf 100644
--- a/scripts/i18n/build-scope.cjs
+++ b/scripts/i18n/build-scope.cjs
@@ -71,7 +71,7 @@ function buildScope(files, versions, managed = [],
dependencies = null) {
json.add(file); // Offline configuration checks cover these
without a site build.
}
// Translation configuration applies to every managed locale at once.
- else if (file === 'translations/config.json') {managed.forEach(locale
=> languages.add(locale)); build();}
+ else if (['translations/config.json',
'translations/release-notes-baseline.json'].includes(file))
{managed.forEach(locale => languages.add(locale)); json.add(file); build();}
else if ((match =
file.match(/^translations\/state\/([^/]+)\/[^/]+\.json$/))) {
if (!locales.includes(match[1])) throw new Error(`Unconfigured
locale: ${match[1]}`);
languages.add(match[1]); build();
diff --git a/scripts/i18n/check-configuration.cjs
b/scripts/i18n/check-configuration.cjs
index 9bd0f2b377a3..32793fd0232a 100644
--- a/scripts/i18n/check-configuration.cjs
+++ b/scripts/i18n/check-configuration.cjs
@@ -5,11 +5,20 @@ const {loadConfig, pipelineLocale} = require('./state.cjs');
const {validateScope} = require('./inventory.cjs');
// Validate inputs without loading Docusaurus, an inventory, credentials or a
model.
-function checkConfiguration(root, locale) {
+function checkConfiguration(root, locale, {releaseHistory = false} = {}) {
assert(loadConfig(root).locales[locale], `Unconfigured translation locale:
${locale}`);
const read = kind => JSON.parse(fs.readFileSync(path.join(root,
`translations/${kind}/${locale}.json`), 'utf8'));
const scope = read('scope');
validateScope(scope, locale);
+ for (const group of scope.groups.filter(group => group.pluginId ===
'releases')) {
+ if (!scope.automaticGroups || scope.automaticGroups.includes(group.id))
+ assert(group.sourcePolicy, `${locale}: automatic Release Notes
need a new-only source policy`);
+ if (group.sourcePolicy) {
+ const policy = require('./release-policy.cjs');
+ policy.loadBaseline(root, group);
+ if (releaseHistory) policy.releasePolicy(root, group, 'HEAD');
+ }
+ }
const retained = scope.retainSidebarLabels ?? [];
assert(Array.isArray(retained), `${locale}: retainSidebarLabels must be an
array`);
const labels = new Set();
@@ -47,9 +56,11 @@ if (require.main === module) {
try {
const root = path.resolve(__dirname, '../..');
const args = process.argv.slice(2);
- assert(args.every(arg => arg === '--all'), 'Usage:
check-configuration.cjs [--all], or set I18N_LOCALE');
+ assert(args.every(arg => ['--all','--release-history'].includes(arg)),
+ 'Usage: check-configuration.cjs [--all] [--release-history], or
set I18N_LOCALE');
const locales = args.includes('--all') ?
Object.keys(loadConfig(root).locales) : [pipelineLocale(root)];
- console.log(JSON.stringify({checked: locales.map(locale =>
checkConfiguration(root, locale))}, null, 2));
+ console.log(JSON.stringify({checked: locales.map(locale =>
checkConfiguration(root, locale,
+ {releaseHistory:args.includes('--release-history')})),
releaseHistory:args.includes('--release-history')}, null, 2));
} catch (error) {console.error(error.message); process.exitCode = 1;}
}
module.exports = {checkConfiguration};
diff --git a/scripts/i18n/ci-plan.cjs b/scripts/i18n/ci-plan.cjs
index b14aa90c00cc..0615ba127796 100644
--- a/scripts/i18n/ci-plan.cjs
+++ b/scripts/i18n/ci-plan.cjs
@@ -21,7 +21,7 @@ const maintenanceNotes = /^translations\/[^/]+\.md$/;
// through native metadata plus representative builds, like other known shared
// language inputs. Unknown translation configuration still requires full CI.
const maintenanceConfig = file => {
- if (file === 'translations/config.json') return true;
+ if (['translations/config.json',
'translations/release-notes-baseline.json'].includes(file)) return true;
const
match=file.match(/^translations\/(?:scope|glossary)\/([^/]+)\.json$/);
return !!match &&
localeRegistry.some(locale=>locale.optional&&locale.code===match[1]);
};
diff --git a/scripts/i18n/inventory.cjs b/scripts/i18n/inventory.cjs
index 9716d373ba24..553809099b49 100644
--- a/scripts/i18n/inventory.cjs
+++ b/scripts/i18n/inventory.cjs
@@ -5,6 +5,7 @@ const fs = require('node:fs');
const path = require('node:path');
const {execFileSync} = require('node:child_process');
const {hash, identity, decide, loadConfig, languageName, loadRecordsAt,
pipelineLocale} = require('./state.cjs');
+const {validatePolicy, policiesFor} = require('./release-policy.cjs');
const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
// The scope's own locale must be the one this run manages, so a scope file for
@@ -18,6 +19,7 @@ function validateScope(scope, locale) {
const identities = new Set();
assert(Array.isArray(scope.groups) && scope.groups.length &&
Array.isArray(scope.retainEnglish), 'Scope needs groups and explicit
exceptions');
for (const group of scope.groups) {
+ validatePolicy(group);
assert(!require('./docs-language-policy.cjs').fallbackOnly(group.pluginId,
group.version),
`${group.version} supports English/Chinese only; other locales use
English fallback, not automatic translation`);
assert(typeof group.pluginId === 'string' && group.pluginId && typeof
group.version === 'string' && group.version, 'Group needs plugin/version');
@@ -65,7 +67,7 @@ function mergedCurrent(page, committedHash) {
&& committedHash(page.targetPath) === page.targetHash;
}
-function buildGroup({group, pages, files, order, scope, committedHash,
committedRecords}) {
+function buildGroup({group, pages, files, order, scope, committedHash,
committedRecords, policy}) {
const key = keyFor(scope.locale);
const relevant = pages.filter(page => page.pluginId === group.pluginId &&
page.version === group.version && page.locale === scope.locale);
assert(relevant.length, `Missing native plugin/version: ${group.id}`);
@@ -79,19 +81,22 @@ function buildGroup({group, pages, files, order, scope,
committedHash, committed
assert(page.englishPath.startsWith(group.sourceRoot + '/'), `Source
root mismatch: ${page.englishPath}`);
identities.add(key(page)); routes.add(page.permalink);
paths.add(page.englishPath);
const allowed = scope.retainEnglish.find(entry => key(entry) ===
key(page));
- const disposition = page.draft ? 'draft' : allowed ? 'retain_english'
: 'translate';
+ const eligibility = policy?.classify(page);
+ const disposition = page.draft ? 'draft' : allowed ? 'retain_english'
+ : eligibility && eligibility.kind !== 'eligible' ?
eligibility.kind : 'translate';
const record = committedRecords.get(key(page));
const complete = disposition === 'translate' && mergedCurrent(page,
committedHash)
&& record?.sourceHash === page.sourceHash && record?.targetHash
=== page.targetHash
&& record?.sourcePath === page.sourcePath && record?.targetPath
=== page.targetPath
&& record?.sourceLocale === page.sourceLocale &&
record?.reviewStatus === page.reviewStatus;
if (disposition !== 'translate') exceptions.push({kind: disposition,
sourcePath: page.englishPath,
- docId: page.docId, reason: page.draft ? 'Native front matter marks
this document as draft; no production page.' : allowed.reason});
+ docId: page.docId, reason: page.draft ? 'Native front matter marks
this document as draft; no production page.'
+ : allowed?.reason ?? eligibility.reason});
return {pluginId: page.pluginId, version: page.version, docId:
page.docId, locale: scope.locale,
sourceLocale: page.sourceLocale, sourcePath: page.sourcePath,
englishPath: page.englishPath,
targetPath: page.targetPath, permalink: page.permalink,
englishPermalink: page.englishPermalink,
sourceHash: page.sourceHash, targetHash: page.targetHash, status:
page.status,
- reviewStatus: page.reviewStatus, unlisted: page.unlisted,
disposition,
+ reviewStatus: page.reviewStatus, unlisted: page.unlisted,
disposition, ...(eligibility && {eligibility}),
mergedCurrent: complete, batchId: null};
}).sort((a, b) => (order.get(a.docId) ?? Infinity) - (order.get(b.docId)
?? Infinity)
|| compare(a.sourcePath, b.sourcePath));
@@ -122,6 +127,8 @@ function buildGroup({group, pages, files, order, scope,
committedHash, committed
needsUpdate: required.filter(page => !page.mergedCurrent &&
page.status !== 'untranslated').length,
drafts: entries.filter(page => page.disposition === 'draft').length,
retainEnglish: entries.filter(page => page.disposition ===
'retain_english').length,
+ historicalExcluded: entries.filter(page => page.disposition ===
'historical_excluded').length,
+ policyReview: entries.filter(page => page.disposition ===
'policy_review').length,
unregistered: exceptions.filter(entry => entry.kind ===
'unregistered_file').length,
pending: pending.length, batches: batches.length};
assert.equal(counts.required, counts.mergedCurrent + counts.untranslated +
counts.needsUpdate);
@@ -251,6 +258,7 @@ async function main() {
const englishHash = hash(fs.readFileSync(path.join(root,
page.englishPath), 'utf8'));
assert.equal(committedHash(page.englishPath), englishHash, `English
source differs from pinned commit: ${page.englishPath}`);
}
+ const policies = policiesFor(root, scope, sourceCommit);
const groups = scope.groups.map(group => {
const files = group.kind ?
require('./source-groups.cjs').sourceFiles(root, group)
.filter(file => require('./source-groups.cjs').hasProse(root,
group, file)) : markdownFiles(root, group.sourceRoot);
@@ -258,12 +266,13 @@ async function main() {
.split('\n').filter(file => group.kind ?
require('./source-groups.cjs').matchesSource(group, file)
&& require('./source-groups.cjs').hasProse(root, group, file)
: /\.(md|mdx)$/.test(file)).sort(compare);
assert.deepEqual(files, committedFiles, `Source census differs from
pinned commit: ${group.sourceRoot}`);
- return buildGroup({group, pages: native.pages, files, scope,
committedHash, committedRecords,
+ return buildGroup({group, pages: native.pages, files, scope,
committedHash, committedRecords, policy: policies.get(group.id),
order: native.versions.find(version => version.pluginId ===
group.pluginId && version.version === group.version)?.order ?? new Map()});
});
const reports = native.reports.map(report => ({...report, inScope:
scope.groups.some(group => group.pluginId === report.pluginId && group.version
=== report.version)}))
.sort((a, b) => compare(JSON.stringify(a), JSON.stringify(b)));
const routingInputs = ['docusaurus.config.js', 'versions.json',
'sidebars.ts', 'sidebarsCommunity.json',
'versioned_sidebars/version-4.x-sidebars.json', 'translations/config.json',
`translations/scope/${locale}.json`];
+ routingInputs.push(...[...policies.values()].map(policy =>
policy.baselinePath));
const snapshot = {schemaVersion: 1, locale, language: languageName(root,
locale), sourceCommit,
routingInputs: Object.fromEntries(routingInputs.map(file => [file,
hash(fs.readFileSync(path.join(root, file), 'utf8'))])),
nativeVersions: native.versions.map(({order, ...version}) => version),
groups, reports};
diff --git a/scripts/i18n/master-drift.cjs b/scripts/i18n/master-drift.cjs
index 692b965a83bc..39f4b979b9ff 100644
--- a/scripts/i18n/master-drift.cjs
+++ b/scripts/i18n/master-drift.cjs
@@ -14,7 +14,7 @@ function repositoryUrl(repository =
process.env.GITHUB_REPOSITORY || 'apache/dor
}
function batchWide(locale) {
- const files = new Set(['translations/config.json', 'config/i18n.js',
'docusaurus.config.js', 'versions.json',
+ const files = new Set(['translations/config.json',
'translations/release-notes-baseline.json', 'config/i18n.js',
'docusaurus.config.js', 'versions.json',
'sidebarsCourse.ts', 'sidebarsReleases.json',
`translations/scope/${locale}.json`,
`translations/glossary/${locale}.json`,
`translations/protection-exceptions/${locale}.json`]);
diff --git a/scripts/i18n/plan-translations.cjs
b/scripts/i18n/plan-translations.cjs
index 0c1631010485..a128b13bf2a4 100644
--- a/scripts/i18n/plan-translations.cjs
+++ b/scripts/i18n/plan-translations.cjs
@@ -9,6 +9,7 @@ const {audit} = require('./audit-coverage.cjs');
const {baselineEntries} = require('./plan-translations-baseline.cjs');
const {hash, identity, loadConfig, languageName, loadRecords, pipelineLocale}
= require('./state.cjs');
const {structure} = require('./structure.cjs');
+const {docKey} = require('./release-policy.cjs');
const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
const keyFor = locale => page => identity({...page, locale});
@@ -116,10 +117,12 @@ async function plan({report, scope, records, baseline =
[], active, readCurrent,
const resolvedDeletions = validateResolved(resolved);
const activeByKey = new Map(candidates.map(candidate => [key(candidate),
candidate]));
const baselineByKey = new Map(baseline.filter(page => managed(scope,
page)).map(page => [key(page), page]));
- const old = new Map(baselineByKey);
+ const excluded = new Set((report.policyExclusions ?? []).map(docKey));
+ const old = new Map([...baselineByKey].filter(([, page]) =>
!excluded.has(docKey(page))));
for (const record of records.values()) if (managed(scope, record))
old.set(key(record), {...old.get(key(record)), ...record});
+ for (const [id, page] of old) if (excluded.has(docKey(page)))
old.delete(id);
const pages = report.groups.filter(group => managed(scope,
group)).flatMap(group =>
- group.entries.filter(page => page.state !== 'draft' && page.state !==
'retain_english')
+ group.entries.filter(page =>
!['draft','retain_english','historical_excluded','policy_review'].includes(page.state))
.map(page => ({...page, groupId: group.id})));
const sourcePaths = new Set(pages.map(page => page.sourcePath));
for (const file of filters.paths ?? []) assert(sourcePaths.has(file), `Not
a registered translatable source file: ${file}`);
@@ -131,6 +134,8 @@ async function plan({report, scope, records, baseline = [],
active, readCurrent,
const tasks = [];
const exceptions = report.groups.flatMap(group =>
group.unregistered.map(sourcePath => ({kind: 'unregistered_file',
groupId: group.id, sourcePath}))).concat(report.reports.filter(item =>
managed(scope, item)));
+ exceptions.push(...(report.policyExclusions ?? []).map(page => ({...page,
+ groupId:managed(scope, page)?.id})));
const changedChinese = new Set(chineseChangedPaths);
for (const page of pages) {
const previous = old.get(key(page));
diff --git a/scripts/i18n/release-policy.cjs b/scripts/i18n/release-policy.cjs
new file mode 100644
index 000000000000..27712af2d3e3
--- /dev/null
+++ b/scripts/i18n/release-policy.cjs
@@ -0,0 +1,131 @@
+// The release-note cutoff controls automation only. Native publication and
+// existing translated files/records keep their ordinary fallback semantics.
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const {execFileSync} = require('node:child_process');
+const {hash} = require('./state.cjs');
+
+const cache = new Map();
+const sourcePath = file => typeof file === 'string' && file &&
!path.isAbsolute(file)
+ && !/[\\\x00-\x1f\x7f]/.test(file) && file.split('/').every(part => part
&& part !== '.' && part !== '..');
+const docKey = page => JSON.stringify([page.pluginId, page.version,
page.docId]);
+
+function validatePolicy(group) {
+ const policy = group.sourcePolicy;
+ if (policy === undefined) return;
+ assert(group.pluginId === 'releases' && group.version === 'current' &&
!group.kind
+ && policy && policy.mode === 'new-only' && policy.baseline ===
'translations/release-notes-baseline.json',
+ 'Invalid release-note source policy');
+}
+
+function loadBaseline(root, group) {
+ validatePolicy(group);
+ assert(group.sourcePolicy, 'Automatic Release Notes generation requires a
new-only source policy');
+ const file = group.sourcePolicy.baseline;
+ let cursor = root;
+ for (const part of file.split('/')) {
+ cursor = path.join(cursor, part);
+ assert(!fs.lstatSync(cursor).isSymbolicLink(), 'Symlink in Release
Notes baseline path');
+ }
+ assert(fs.lstatSync(cursor).isFile(), 'Release Notes baseline must be a
regular file');
+ const source = fs.readFileSync(path.join(root, file), 'utf8');
+ const data = JSON.parse(source);
+ assert(data.schemaVersion === 1 && /^[a-f0-9]{40}$/.test(data.sourceCommit)
+ && data.pluginId === group.pluginId && data.version === group.version
+ && data.sourceRoot === group.sourceRoot, 'Invalid Release Notes
baseline identity');
+ for (const name of ['documents', 'sourceFiles', 'historicalPaths'])
assert(Array.isArray(data[name]), `Missing baseline ${name}`);
+ const ids = new Set(), files = new Set();
+ for (const doc of data.documents) {
+ assert(typeof doc.docId === 'string' && doc.docId &&
!ids.has(doc.docId)
+ && sourcePath(doc.sourcePath) &&
doc.sourcePath.startsWith(group.sourceRoot + '/')
+ && !files.has(doc.sourcePath) &&
/^[a-f0-9]{64}$/.test(doc.sourceHash), 'Invalid/duplicate historical release
note');
+ ids.add(doc.docId); files.add(doc.sourcePath);
+ }
+ for (const name of ['sourceFiles', 'historicalPaths']) {
+ assert(new Set(data[name]).size === data[name].length &&
data[name].every(file => sourcePath(file)
+ && file.startsWith(group.sourceRoot + '/') &&
/\.mdx?$/.test(file)), `Invalid baseline ${name}`);
+ }
+ assert(data.sourceFiles.every(file => data.historicalPaths.includes(file))
+ && [...files].every(file => data.sourceFiles.includes(file)),
'Incomplete historical source census');
+ return {file, source, data};
+}
+
+function releasePolicy(root, group, sourceRef = 'HEAD') {
+ if (!group.sourcePolicy) return null;
+ const baseline = loadBaseline(root, group);
+ const git = args => execFileSync('git', args, {cwd: root, encoding:
'utf8', maxBuffer: 64 * 1024 * 1024,
+ stdio: ['ignore', 'pipe', 'pipe']});
+ const ref = git(['rev-parse', `${sourceRef}^{commit}`]).trim();
+ const key = JSON.stringify([root, group, ref, hash(baseline.source)]);
+ if (cache.has(key)) return cache.get(key);
+ const data = baseline.data;
+ try {git(['merge-base', '--is-ancestor', data.sourceCommit, ref]);}
+ catch {throw new Error('Release Notes baseline must be an ancestor of the
audited source commit');}
+ const census = git(['ls-tree', '-r', '--name-only', data.sourceCommit,
'--', group.sourceRoot])
+ .split('\n').filter(file => /\.mdx?$/.test(file)).sort();
+ assert.deepEqual([...data.sourceFiles].sort(), census, 'Release Notes
baseline omits historical source files');
+ const historicalCensus = [...new Set(git(['log', '--format=',
'--name-only', data.sourceCommit, '--', group.sourceRoot])
+ .split('\n').filter(file => file.startsWith(group.sourceRoot + '/') &&
/\.mdx?$/.test(file)))].sort();
+ assert.deepEqual([...data.historicalPaths].sort(), historicalCensus,
'Release Notes baseline omits historical path identities');
+ for (const doc of data.documents) assert.equal(hash(git(['show',
`${data.sourceCommit}:${doc.sourcePath}`])),
+ doc.sourceHash, `Historical release-note hash mismatch:
${doc.sourcePath}`);
+ const historical = new Set(data.historicalPaths), ids = new
Set(data.documents.map(doc => doc.docId));
+ const hashes = new Set(data.documents.map(doc => doc.sourceHash));
+ const origins = new Map([...historical].map(file => [file, {kind:
'historical', origin: file}]));
+ const inGroup = file => file?.startsWith(group.sourceRoot + '/') &&
/\.mdx?$/.test(file);
+ // First-parent diffs describe the actual committed tree at each mainline
+ // step. Branch commits must not be replayed a second time at their merge.
+ const history = git(['log', '--first-parent',
'--diff-merges=first-parent', '--reverse',
+ '--format=%x1e%H', '--name-status', '-z', '-M', '-C100%',
'--find-copies-harder', `${data.sourceCommit}..${ref}`]);
+ for (const chunk of history.split('\x1e').filter(Boolean)) {
+ const tokens = chunk.split('\0'), commit = tokens.shift().trim();
+ assert(/^[a-f0-9]{40}$/.test(commit), 'Invalid release-note Git
history');
+ const events = [];
+ for (let index = 0; index < tokens.length;) {
+ const status = tokens[index++].trim();
+ if (!status) continue;
+ assert(/^[ACDMRTUXB][0-9]*$/.test(status), 'Invalid release-note
Git change');
+ const from = tokens[index++];
+ const to = /^[RC]/.test(status) ? tokens[index++] : from;
+ events.push({status, from, to});
+ }
+ const ambiguous = events.some(event => event.status === 'D' &&
inGroup(event.from)
+ && origins.get(event.from)?.kind !== 'new');
+ for (const event of events) {
+ const prior = origins.get(event.from);
+ if (event.status[0] === 'R' || event.status[0] === 'C') {
+ if (prior || inGroup(event.to)) origins.set(event.to, prior ??
{kind: 'review', origin: event.from, commit});
+ } else if (event.status === 'A' && inGroup(event.to) &&
!origins.has(event.to)) {
+ origins.set(event.to, {kind: ambiguous ? 'review' : 'new',
origin: event.to, commit});
+ }
+ // Retain deleted path identities, so deleting and later adding a
+ // historical note never turns it into a newly eligible document.
+ }
+ }
+ function classify(page) {
+ const file = page.englishPath ?? page.sourcePath;
+ const origin = origins.get(file);
+ let kind, reason;
+ if (ids.has(page.docId) || historical.has(file) || origin?.kind ===
'historical') {
+ kind = 'historical_excluded'; reason = 'Registered or present
before the fixed release-note cutoff, including moves and re-registrations.';
+ } else if (origin?.kind !== 'new' || hashes.has(page.sourceHash)) {
+ kind = 'policy_review'; reason = 'Cannot establish a genuinely new
release note after the cutoff; reconcile its source identity manually.';
+ } else {
+ kind = 'eligible'; reason = 'A new source identity committed after
the fixed release-note cutoff.';
+ }
+ return {kind, reason, baselineCommit: data.sourceCommit, originPath:
origin?.origin ?? file,
+ introducedCommit: origin?.commit ?? null};
+ }
+ const result = {classify, baselinePath: baseline.file, baselineCommit:
data.sourceCommit,
+ baselineHash: hash(baseline.source), sourceCommit: ref};
+ cache.set(key, result);
+ return result;
+}
+
+function policiesFor(root, scope, ref = 'HEAD') {
+ return new Map(scope.groups.filter(group => group.sourcePolicy)
+ .map(group => [group.id, releasePolicy(root, group, ref)]));
+}
+
+module.exports = {docKey, validatePolicy, loadBaseline, releasePolicy,
policiesFor};
diff --git a/scripts/i18n/search-plugin.cjs b/scripts/i18n/search-plugin.cjs
index 744344a81ba4..7fb3c2949b3d 100644
--- a/scripts/i18n/search-plugin.cjs
+++ b/scripts/i18n/search-plugin.cjs
@@ -15,18 +15,19 @@ const {buildIndex: buildNativeIndex} =
require('@yang1666204/docusaurus-search-l
const {loadConfig, managedLocales} = require('./state.cjs');
const {normalizeRussianSearch} =
require('../../src/utils/searchNormalization.js');
-const contexts = ['docs/dev', 'docs/4.x', 'community', 'course'];
+const contexts = ['docs/dev', 'docs/4.x', 'community', 'course', 'releases'];
const normalize = url => url.replace(/\/+$/, '');
function groupFor(page) {
if (page.pluginId === 'default' && page.version === 'current') return
'docs/dev';
if (page.pluginId === 'default' && page.version === '4.x') return
'docs/4.x';
if (page.pluginId === 'community' && page.version === 'current') return
'community';
if (page.pluginId === 'course' && page.version === 'current') return
'course';
+ if (page.pluginId === 'releases' && page.version === 'current') return
'releases';
return null;
}
function localizedOptions(options, locale, labels) {
return {...options, language:locale === 'ru' ? ['en', 'ru'] :
[locale.split('-')[0]],
-
docsRouteBasePath:['docs','community','course',`${locale}/docs`,`${locale}/community`,`${locale}/course`],
+
docsRouteBasePath:['docs','community','course','releases',`${locale}/docs`,`${locale}/community`,`${locale}/course`,`${locale}/releases`],
// A locale without configured labels falls back to the context path.
searchContextByPaths:contexts.map(value =>
({path:value,label:{[locale]:labels?.[value] ?? value}})),
// Outside every context, search all of them. The browser queries each
@@ -61,7 +62,7 @@ function targetFingerprint(root, locale) {
}
scan(path.join(root,'i18n',locale));
scan(path.join(root,'translations/state',locale));
- for (const dir of
['docs','versioned_docs/version-4.x','community','course'])
scan(path.join(root,dir));
+ for (const dir of
['docs','versioned_docs/version-4.x','community','course','releasenotes'])
scan(path.join(root,dir));
return digester.digest('hex').slice(0,16);
}
function indexFilename(context) {
diff --git a/scripts/i18n/translation-engine.cjs
b/scripts/i18n/translation-engine.cjs
index d01ceb6b573a..a000db0ed58f 100644
--- a/scripts/i18n/translation-engine.cjs
+++ b/scripts/i18n/translation-engine.cjs
@@ -407,6 +407,15 @@ function checkedTask(plan, taskId, scope, root) {
managedPath(scope, task);
const group = scope.groups.find(group => group.pluginId === task.pluginId
&& group.version === task.version);
assert(!scope.automaticGroups || scope.automaticGroups.includes(group.id),
'Automatic generation is not enabled for this scope group');
+ if (group.pluginId === 'releases') {
+ const {releasePolicy} = require('./release-policy.cjs');
+ assert(group.sourcePolicy, 'Automatic Release Notes generation
requires a new-only source policy');
+ const policy = releasePolicy(root, group, plan.sourceCommit);
+ assert.equal(execFileSync('git', ['show',
`${plan.sourceCommit}:${policy.baselinePath}`], {cwd:root,encoding:'utf8'}),
+ fs.readFileSync(safeFile(root, policy.baselinePath), 'utf8'),
'Release Notes policy changed since planning');
+ const eligibility = policy.classify(task);
+ assert.equal(eligibility.kind, 'eligible', `Release Notes task
excluded by source policy: ${eligibility.kind}`);
+ }
const source = fs.readFileSync(safeFile(root, task.sourcePath), 'utf8');
assert(sha(task.sourceHash) && hash(source) === task.sourceHash, 'Source
changed since planning');
assert.equal(execFileSync('git', ['show', `HEAD:${task.sourcePath}`],
{cwd: root, encoding: 'utf8'}), source,
diff --git a/scripts/i18n/verify-release-policy-fixture.cjs
b/scripts/i18n/verify-release-policy-fixture.cjs
new file mode 100644
index 000000000000..8c1e63fb9bca
--- /dev/null
+++ b/scripts/i18n/verify-release-policy-fixture.cjs
@@ -0,0 +1,160 @@
+// Small real native-docs fixture: historical/current/missing release notes,
+// automatic eligibility, model-call guards, publication, search and SEO.
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const {execFileSync, spawnSync} = require('node:child_process');
+const cheerio = require('cheerio');
+const {createFixture} = require('./__tests__/fixtures/multi-locale.cjs');
+const {hash, identity, loadRecords} = require('./state.cjs');
+const {structure} = require('./structure.cjs');
+const {audit} = require('./audit-coverage.cjs');
+const {plan} = require('./plan-translations.cjs');
+const {prepare, readState, targetFor} = require('./translate-docs.cjs');
+const {checkIndexes} = require('./verify-search.cjs');
+const {createServer} = require('../publish/serve.cjs');
+
+const repo = path.resolve(__dirname, '../..');
+const root = path.join(repo, '.i18n-build/release-policy-fixture');
+const locales = ['en', 'zh-CN', 'ja', 'ko', 'ru'];
+const managed = ['ja', 'ko', 'ru'];
+const words = {ja:'日本語',ko:'한국어',ru:'Русский','zh-CN':'中文'};
+const git = (...args) => execFileSync('git', args,
{cwd:root,encoding:'utf8',stdio:['ignore','pipe','pipe']}).trim();
+function write(file, data) {const
target=path.join(root,file);fs.mkdirSync(path.dirname(target),{recursive:true});fs.writeFileSync(target,data);}
+function commit(message)
{git('add','.');git('-c','user.name=Fixture','-c','[email protected]','commit','-qm',message);return
git('rev-parse','HEAD');}
+const sourceFor = name => `---\ntitle: Release ${name}\nlast_update:\n date:
2026-01-01\n---\n\n## Usage\n\nReleaseFixtureSQL documents
${name}.\n\n\`\`\`sql\nSELECT 1;\n\`\`\`\n`;
+
+async function addTarget(locale, name, sourceCommit) {
+ const
sourcePath=`releasenotes/${name}.md`,source=fs.readFileSync(path.join(root,sourcePath),'utf8');
+ const
targetPath=`i18n/${locale}/docusaurus-plugin-content-docs-releases/current/${name}.md`;
+ const target=source.replace('title: Release',`title:
${words[locale]}`).replace('ReleaseFixtureSQL documents',`${words[locale]}
ReleaseFixtureSQL`);
+ write(targetPath,target);
+ if (!managed.includes(locale)) return;
+ const
file=`translations/state/${locale}/docs.json`,state=JSON.parse(fs.readFileSync(path.join(root,file)));
+
state.pages.push({pluginId:'releases',version:'current',docId:name,locale,sourceLocale:'en',sourcePath,targetPath,sourceCommit,
+
sourceHash:hash(source),targetHash:hash(target),sourceTechnicalHash:(await
structure(source,'.md')).technicalHash,
+
reviewStatus:'machine',ownership:'machine',translatedAt:'2026-10-27T00:00:00Z',glossaryVersion:'fixture'});
+ write(file,JSON.stringify(state));
+}
+
+async function create() {
+ fs.rmSync(root,{recursive:true,force:true});
+ await createFixture(root);
+ write('releasenotes/historical.md',sourceFor('historical'));
+ let source=commit('Historical source');
+ for (const locale of ['zh-CN',...managed]) await
addTarget(locale,'historical',source);
+ const cutoff=commit('Historical translations');
+
write('translations/release-notes-baseline.json',JSON.stringify({schemaVersion:1,sourceCommit:cutoff,
+
pluginId:'releases',version:'current',sourceRoot:'releasenotes',sourceFiles:['releasenotes/historical.md'],
+
historicalPaths:['releasenotes/historical.md'],documents:[{docId:'historical',sourcePath:'releasenotes/historical.md',sourceHash:hash(sourceFor('historical'))}]}));
+ const
group={id:'RELEASES',pluginId:'releases',version:'current',sourceRoot:'releasenotes',
+
sourcePolicy:{mode:'new-only',baseline:'translations/release-notes-baseline.json'}};
+ for (const locale of managed) {
+
write(`translations/scope/${locale}.json`,JSON.stringify({schemaVersion:1,locale,maxBatchPages:20,
+
groups:[group],automaticGroups:['RELEASES'],retainEnglish:[],retainSidebarLabels:[]}));
+
write(`translations/state/${locale}/resolved-deletions.json`,'{"schemaVersion":1,"deletions":[]}');
+ }
+ for (const name of ['new-current','new-missing'])
write(`releasenotes/${name}.md`,sourceFor(name));
+ source=commit('New release-note sources');
+ for (const locale of ['zh-CN',...managed]) await
addTarget(locale,'new-current',source);
+
fs.renameSync(path.join(root,'docusaurus.config.js'),path.join(root,'fixture.base.config.cjs'));
+ write('docusaurus.config.js',`const
base=require('./fixture.base.config.cjs');module.exports=()=>{const
config=base();
+config.plugins.splice(2,0,[require(${JSON.stringify(path.join(repo,'scripts/i18n/docs-plugin.cjs'))}),{id:'releases',path:'releasenotes',routeBasePath:'releases',sidebarPath:false}]);
+config.themes=[[require(${JSON.stringify(path.join(repo,'scripts/i18n/search-plugin.cjs'))}),{hashed:false,language:['en'],docsRouteBasePath:['docs','community','releases'],searchContextByPaths:['docs','community','releases']}]];
+return config;};\n`);
+ for (const file of
['sidebars.ts','sidebarsCommunity.json','sidebarsCourse.ts','sidebarsReleases.json'])
write(file,'{}');
+ // Exercise the actual site's index-root and cross-context worker adapters,
+ // not the upstream theme's different locale URL assumptions.
+ const {bodyDependencies}=require('./page-body.cjs');
+ for (const entry of
['src/theme/SearchPage/index.js','src/theme/SearchBar/index.js']) {
+ for (const file of bodyDependencies(repo,entry,{shared:true}).files)
+ write(file,fs.readFileSync(path.join(repo,file)));
+ }
+
write('static/images/search-icon.svg',fs.readFileSync(path.join(repo,'static/images/search-icon.svg')));
+ const final=commit('Release-note policy fixture');
+ const baseline=path.join(root,'audit-baseline');
+ write('audit-baseline/summary.json',JSON.stringify({sourceCommit:cutoff}));
+ for (const group of ['dev','4x','community'])
write(`audit-baseline/${group}.json`,'{"entryFiles":[]}');
+ const results=[];
+ for (const locale of managed) {
+ process.env.I18N_LOCALE=locale;
+ const report=await audit({root,sourceRef:final,baseline});
+ const releases=report.groups.find(group=>group.id==='RELEASES');
+ assert.equal(releases.counts.historicalExcluded,1);
+
assert.deepEqual(report.automaticCounts,{required:2,current:1,pending:1});
+ const
scope=JSON.parse(fs.readFileSync(path.join(root,`translations/scope/${locale}.json`)));
+ const planned=await
plan({report,scope,records:loadRecords(root,locale),baseline:[],
+
readCurrent:file=>fs.readFileSync(path.join(root,file),'utf8'),readAt:()=>null});
+ assert.equal(planned.selected.length,1);
+ const task=planned.tasks.find(task=>task.id===planned.selected[0]);
+ assert.equal(task.docId,'new-missing');
+ let calls=0;
+ const candidate=await
prepare({root,plan:planned,taskId:task.id,scope,state:readState(root,locale),target:targetFor(root,locale),
+
glossary:{schemaVersion:1,version:'fixture',keep:[],terms:[]},adapter:{model:'fixture',async
translate({units}){
+ calls++;return
{translations:units.map(unit=>({id:unit.id,text:`${words[locale]}
${unit.text}`}))};}}});
+ assert(calls>0&&candidate.target.includes(words[locale]));
+ assert(candidate.target.includes('SELECT 1;'));
+ assert(!fs.existsSync(path.join(root,task.targetPath)),'The mock
candidate must not replace the missing-target publication case');
+ const
active={schemaVersion:1,candidates:[{...task,pr:'https://github.com/apache/doris-website/pull/1'}]};
+ const reserved=await
plan({report,scope,records:loadRecords(root,locale),active,readCurrent:file=>fs.readFileSync(path.join(root,file),'utf8'),readAt:()=>null});
+ assert.equal(reserved.selected.length,0);
+
results.push({locale,counts:releases.counts,mockRequests:calls,selected:task.docId,reservedCount:reserved.selected.length});
+ }
+
fs.writeFileSync(path.join(root,'eligibility.json'),JSON.stringify(results,null,2)+'\n');
+ return results;
+}
+
+function checkSite() {
+ const site=path.join(root,'build'),pages=[],search=[];
+ for (const locale of locales) for (const name of
['historical','new-current','new-missing']) {
+ const
prefix=locale==='en'?'':`/${locale}`,route=`${prefix}/releases/${name}/`;
+ const
$=cheerio.load(fs.readFileSync(path.join(site,route,'index.html'),'utf8'));
+ const fallback=locale!=='en'&&name==='new-missing';
+
assert.equal($('.theme-doc-markdown').attr('lang'),fallback?'en':locale);
+
assert.equal($('link[rel=canonical]').attr('href'),`https://doris.apache.org${fallback?'':prefix}/releases/${name}/`);
+
assert.equal(/noindex/.test($('meta[name=robots]').attr('content')??''),fallback);
+
if(fallback)assert.equal($(`link[rel=alternate][hreflang="${locale}"]`).length,0);
+ pages.push({locale,name,route,fallback});
+ }
+ for(const locale of managed){
+ const
manifest=JSON.parse(fs.readFileSync(path.join(root,`.i18n-build/manifests/${locale}.json`)));
+ const checked=checkIndexes({site,locale,manifest});
+ assert.equal(checked.summary.contexts.releases,2);
+ search.push({locale,indexedReleases:2});
+ }
+ return {pages,search};
+}
+
+async function checkBrowser() {
+ const
{chromium}=require('playwright'),server=createServer(path.join(root,'build'));
+ await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));
+ const origin=`http://127.0.0.1:${server.address().port}`,browser=await
chromium.launch({headless:true}),errors=[],results=[];
+ try{const page=await
browser.newPage();page.on('pageerror',error=>errors.push(error.message));
+ for(const locale of managed){
+ await page.goto(`${origin}/${locale}/releases/new-current/`);
+ assert.equal(await
page.locator('.theme-doc-markdown').getAttribute('lang'),locale);
+ await
page.goto(`${origin}/${locale}/search/?q=ReleaseFixtureSQL&ctx=releases`);
+ await page.locator('section article
a').first().waitFor({timeout:30000});
+ const links=await page.locator('section article
a').evaluateAll(nodes=>nodes.map(node=>node.getAttribute('href')));
+ assert(links.some(link=>link.includes('/releases/new-current/')));
+
assert(links.every(link=>link.startsWith(`/${locale}/releases/`)&&!link.includes('new-missing')));
+ results.push({locale,workerSearch:true,noFallbackIndex:true});
+ }assert.deepEqual(errors,[]);return {results,errors};
+ }finally{await browser.close();await new
Promise(resolve=>server.close(resolve));}
+}
+
+async function main(){
+ if(!process.argv.includes('--verify-existing')){
+ await create();
+ const
built=spawnSync(process.execPath,[path.join(repo,'scripts/build-locales.cjs'),'--site-dir',root],
+
{stdio:'inherit',env:{...process.env,DORIS_ASSET_ORIGIN:'',DORIS_OPTIMIZE_PUBLISH:'',NODE_OPTIONS:'--max-old-space-size=4096'}});
+ assert.equal(built.status,0,'Release-note fixture build failed');
+ }
+ const result={scope:'synthetic three-note, five-language fixture; no real
model calls or production notes',
+
sourceCommit:git('rev-parse','HEAD'),eligibility:JSON.parse(fs.readFileSync(path.join(root,'eligibility.json'))),...checkSite()};
+ if(process.argv.includes('--browser'))result.browser=await checkBrowser();
+
fs.writeFileSync(path.join(repo,'.i18n-build/release-policy-fixture-verification.json'),JSON.stringify(result,null,2)+'\n');
+
console.log(JSON.stringify({releasePages:result.pages.length,search:result.search,browser:result.browser}));
+}
+if(require.main===module)main().catch(error=>{console.error(error);process.exitCode=1});
+module.exports={create,checkSite,checkBrowser};
diff --git a/scripts/i18n/workflow-scan.cjs b/scripts/i18n/workflow-scan.cjs
index 1e026ee83757..38ba72b5a2bf 100644
--- a/scripts/i18n/workflow-scan.cjs
+++ b/scripts/i18n/workflow-scan.cjs
@@ -9,7 +9,7 @@ const {hash, languageName, pipelineLocale} =
require('./state.cjs');
const root = path.resolve(__dirname, '../..');
const configInputs = ['docusaurus.config.js', 'config/i18n.js', 'sidebars.ts',
'sidebarsCommunity.json',
'sidebarsCourse.ts', 'sidebarsReleases.json',
- 'versioned_sidebars/version-4.x-sidebars.json',
'translations/config.json'];
+ 'versioned_sidebars/version-4.x-sidebars.json',
'translations/config.json', 'translations/release-notes-baseline.json'];
// A push to this locale's own scope or glossary changes what may be generated.
const rulesFor = locale => new Set([...configInputs,
`translations/scope/${locale}.json`,
`translations/glossary/${locale}.json`]);
@@ -59,6 +59,7 @@ function summarize(plan, reconciliation, eligible, mode,
language, reused = fals
`- Open PRs inspected: ${reconciliation.openPrCount}; relevant:
${reconciliation.relevantPrs.length}; active candidates:
${plan.counts.activeCandidates}`,
`- Selected for a possible next batch: ${plan.selected.length};
blocked targets: ${reconciliation.blockedTargets.length}`,
`- Outside enabled automatic groups: ${plan.deferredAutomationTasks ??
0}; reported separately, not sent to a model`,
+ `- Release-note policy: historical
excluded=${plan.exceptions.filter(item=>item.kind==='historical_excluded').length};
manual
reconciliation=${plan.exceptions.filter(item=>item.kind==='policy_review').length}`,
`- Robot batch PRs already open: ${reconciliation.botPrs.join(', ') ||
'none'}`,
`- Exceptions: ${[...exceptionKinds].map(([kind, count]) =>
`${kind}=${count}`).join(', ') || 'none'}`,
'',
diff --git a/translations/README.md b/translations/README.md
index 656858fce1d7..621d1844b002 100644
--- a/translations/README.md
+++ b/translations/README.md
@@ -49,7 +49,7 @@ workflows explicitly enable all three; default local builds
still serve English
and Chinese. Ordinary Russian pages retain Russian URLs and navigation;
missing manual prose falls back to English.
Russian translation runs twice daily and on manual dispatch, as described
below.
-The ja/ko/ru scopes explicitly enable DEV, 4X, COMMUNITY and COURSE in
+The ja/ko/ru scopes explicitly enable DEV, 4X, COMMUNITY, COURSE and RELEASES
in
`automaticGroups`. Course bodies use the same source hashes, per-page state,
manual-edit protection, open-PR reservations and incremental maintenance as the
other documentation plugins. Existing valid targets are reused. Chinese course
@@ -57,10 +57,9 @@ bodies remain manually maintained; their page records retain
machine review
status and human ownership, without enabling a Chinese translation workflow.
Course search uses its own localized context and indexes current targets only.
-Native audits still report releases and supplemental page groups so
-their existing content remains visible. These groups are not sent to a model.
-Release Notes expansion must pin a historical boundary before admitting future
-new entries; historical files and existing translations stay unchanged. React
landing pages
+RELEASES uses the new-only policy below; supplemental page groups remain
outside
+automatic generation. Historical release-note files and existing translations
+stay unchanged. React landing pages
and weekly reports use their manual page/entry state. Existing candidates from
earlier expanded runs remain separate pending reviews, not merged coverage.
The audit's `automaticCounts` and discovery summary report the effective
@@ -68,6 +67,61 @@ required/current/pending totals. Additional registered
groups remain in the
raw census and `deferredAutomaticGroups`; their old-channel missing files do
not become required translation backlog or override manual page/report state.
+### New-only Release Notes maintenance
+
+`translations/release-notes-baseline.json` freezes the English native
identities,
+source hashes and historical paths at commit
+`b207a93551a3705652bc261639e3eec7fd738c7a`: 83 registered notes and 83 source
+files, including the existing release index. Each managed RELEASES group names
+this file through `sourcePolicy: {mode: "new-only", baseline:
+"translations/release-notes-baseline.json"}`. Chinese remains manually
maintained.
+
+The inventory and audit classify historical identities as
`historical_excluded`.
+They remain in the census, with their ordinary native content/fallback state,
+but do not enter required/pending translation totals, update/deletion tasks or
+automatic generation. Existing targets and records are preserved; native stale
+checks and English fallback continue to apply. Publication does not depend on
+the automation policy.
+
+Raw sidebar label status remains visible. `automaticLabels` and
+`--fail-on-pending` use only enabled automatic groups and eligible note labels;
+historical release categories and version navigation remain manual native
+labels. They do not create an automatic missing-label backlog.
+
+Newly registered notes must have a genuinely new source identity committed
after
+the cutoff. Their later source updates use the existing incremental workflow,
+open-PR reservations, ownership protection, batch limits, shared lock and
normal
+PR/CI process. Current real translated notes have a `releases` search context;
+untranslated or stale English-fallback pages do not enter a managed locale's
+search index.
+
+Git lineage prevents a historical rename, move or re-registration from becoming
+a new translation task. Exact copies and ambiguous delete/add histories are
+excluded or reported as `policy_review` for manual identity reconciliation.
+These cases are not sent to a model. The generator independently checks the
+committed policy and eligibility before reading a task for translation, so a
+forced task or stale plan cannot bypass the cutoff.
+
+Do not regenerate the baseline or move its source commit forward to hide
+pending work. Configuration validation checks the policy format; CI also runs
+`check-configuration.cjs --all --release-history` to verify its Git census,
+hashes and ancestry without a site build. Inventory and generation
independently
+check the same evidence. A baseline
+or pipeline change during generation invalidates the batch.
+
+For a read-only check, use the existing locale workflow's `dry-run` mode or:
+
+```bash
+I18N_LOCALE=ru node scripts/i18n/plan-translations.cjs --source-ref HEAD \
+ --plugin releases --output /tmp/release-plan.json --audit-output
/tmp/release-audit.json
+```
+
+If no new eligible note exists, selected tasks stay empty and the workflow does
+not call a model or open a translation PR. Do not manufacture a note or source
+drift for acceptance. Record the first real future note's generation, merge and
+public result when it occurs. To pause RELEASES, remove it from the affected
+locale's `automaticGroups`; retain the fixed policy, existing content and URLs.
+
The [initial Russian inventory](inventory/ru/2026-10-20/batches.md) records
1,674 Dev,
1,619 4.x and 51 community documents, excluding two drafts. Four launch samples
have independent state shards and machine review status. They have not received
diff --git a/translations/config.json b/translations/config.json
index e5d64d527999..a4d8eb67cafc 100644
--- a/translations/config.json
+++ b/translations/config.json
@@ -10,7 +10,8 @@
"docs/dev": "Dev ドキュメント",
"docs/4.x": "4.x ドキュメント",
"community": "コミュニティ",
- "course": "コース"
+ "course": "コース",
+ "releases": "リリースノート"
}
},
"ko": {
@@ -22,7 +23,8 @@
"docs/dev": "Dev 문서",
"docs/4.x": "4.x 문서",
"community": "커뮤니티",
- "course": "강좌"
+ "course": "강좌",
+ "releases": "릴리스 노트"
}
},
"ru": {
@@ -34,7 +36,8 @@
"docs/dev": "Документация Dev",
"docs/4.x": "Документация 4.x",
"community": "Сообщество",
- "course": "Курсы"
+ "course": "Курсы",
+ "releases": "Примечания к выпуску"
}
}
}
diff --git a/translations/release-notes-baseline.json
b/translations/release-notes-baseline.json
new file mode 100644
index 000000000000..277755f58729
--- /dev/null
+++ b/translations/release-notes-baseline.json
@@ -0,0 +1,594 @@
+{
+ "schemaVersion": 1,
+ "sourceCommit": "b207a93551a3705652bc261639e3eec7fd738c7a",
+ "pluginId": "releases",
+ "version": "current",
+ "sourceRoot": "releasenotes",
+ "documents": [
+ {
+ "docId": "all-release",
+ "sourcePath": "releasenotes/all-release.md",
+ "sourceHash":
"392f3af162a087eca7416af910e10a045896765271d5bbe443fc20d33d3410dc"
+ },
+ {
+ "docId": "core",
+ "sourcePath": "releasenotes/core.md",
+ "sourceHash":
"d744120104a6512d9f24a74268135a15c9c116079581001142537f5fc6cb10cf"
+ },
+ {
+ "docId": "ecosystem/doris-cli",
+ "sourcePath": "releasenotes/ecosystem/doris-cli.md",
+ "sourceHash":
"282dc69d1690e92a24b0930eefb808c77bd6e9f60a6116cce58c6996cc88aa26"
+ },
+ {
+ "docId": "ecosystem/doris-flink-connector",
+ "sourcePath": "releasenotes/ecosystem/doris-flink-connector.md",
+ "sourceHash":
"ab235970b433a4f1e0ef05761020c1515aa6c36eab134b7e1231490c9b18c913"
+ },
+ {
+ "docId": "ecosystem/doris-kafka-connector",
+ "sourcePath": "releasenotes/ecosystem/doris-kafka-connector.md",
+ "sourceHash":
"4ef58f5761af434718b93275b8785320eeaf20e86fa79e2785b590ff65ec894f"
+ },
+ {
+ "docId": "ecosystem/doris-mcp-server",
+ "sourcePath": "releasenotes/ecosystem/doris-mcp-server.md",
+ "sourceHash":
"26de2dd77c7b09d00aed7d7f525cf253c47393c14aaf446fecc710970784ceb1"
+ },
+ {
+ "docId": "ecosystem/doris-operator",
+ "sourcePath": "releasenotes/ecosystem/doris-operator.md",
+ "sourceHash":
"c5038d3c275542480cefa8dcb535afb6c61072071b255b09adef78266aaf7b90"
+ },
+ {
+ "docId": "ecosystem/doris-skills",
+ "sourcePath": "releasenotes/ecosystem/doris-skills.md",
+ "sourceHash":
"0e7d43ffc650a706c774ef8129b2035b571dc86dad621f287efc57e48a106839"
+ },
+ {
+ "docId": "ecosystem/doris-spark-connector",
+ "sourcePath": "releasenotes/ecosystem/doris-spark-connector.md",
+ "sourceHash":
"89aeb845646272d599b9b9ca5f4fa469d06591a0e7227d10d8da48da0f62ff67"
+ },
+ {
+ "docId": "ecosystem/doris-streamloader",
+ "sourcePath": "releasenotes/ecosystem/doris-streamloader.md",
+ "sourceHash":
"dd5ac0749629e2b676a658181aa07410df80f8bc8f6024c632f04a3d4ba8bda4"
+ },
+ {
+ "docId": "v1.1/release-1.1.0",
+ "sourcePath": "releasenotes/v1.1/release-1.1.0.md",
+ "sourceHash":
"258d418c0d7e93e2c7326e584c2cca370c873e37cae5bf993fc56778873df9bc"
+ },
+ {
+ "docId": "v1.1/release-1.1.1",
+ "sourcePath": "releasenotes/v1.1/release-1.1.1.md",
+ "sourceHash":
"0609e6b8a674c67cfda7ff819183144348b4a8626cf7b29a89af8b77ab59a142"
+ },
+ {
+ "docId": "v1.1/release-1.1.2",
+ "sourcePath": "releasenotes/v1.1/release-1.1.2.md",
+ "sourceHash":
"8e82ee174e086d7d0eb645581209041aa25934f2c0d47f06a8e9fdd4d931ff77"
+ },
+ {
+ "docId": "v1.1/release-1.1.3",
+ "sourcePath": "releasenotes/v1.1/release-1.1.3.md",
+ "sourceHash":
"f5a4510f8a4c659de1ed06b38f2d51af77b62d4214a5f92ebba07a448ab786af"
+ },
+ {
+ "docId": "v1.1/release-1.1.4",
+ "sourcePath": "releasenotes/v1.1/release-1.1.4.md",
+ "sourceHash":
"669fa10d9852af0f2e23e114633edb74163df8f1f5c8ba9eeeb8d5df9f40d94b"
+ },
+ {
+ "docId": "v1.1/release-1.1.5",
+ "sourcePath": "releasenotes/v1.1/release-1.1.5.md",
+ "sourceHash":
"c62c676fede523ffeed0d928a24bfe1ed02c3bf831c27ff96580b51de656e40a"
+ },
+ {
+ "docId": "v1.2/release-1.2.0",
+ "sourcePath": "releasenotes/v1.2/release-1.2.0.md",
+ "sourceHash":
"7753839430aaaf7f0333df91fde807cbd729a9caf83de2938352cad9a4418427"
+ },
+ {
+ "docId": "v1.2/release-1.2.1",
+ "sourcePath": "releasenotes/v1.2/release-1.2.1.md",
+ "sourceHash":
"08376473f8c1a45ad07a8da04be70eb1874faede274b1dd074fbb482596ec749"
+ },
+ {
+ "docId": "v1.2/release-1.2.2",
+ "sourcePath": "releasenotes/v1.2/release-1.2.2.md",
+ "sourceHash":
"733723b493a62f8684f9f6f0c1dff75b2788b9e0b69a78eb56db2c8f86b5118b"
+ },
+ {
+ "docId": "v1.2/release-1.2.3",
+ "sourcePath": "releasenotes/v1.2/release-1.2.3.md",
+ "sourceHash":
"f8066c4e30bcf1d77ac184b9ddffe4bd0ed1465fab5e118bce6610d65bca4d38"
+ },
+ {
+ "docId": "v1.2/release-1.2.4",
+ "sourcePath": "releasenotes/v1.2/release-1.2.4.md",
+ "sourceHash":
"ccd81744c6cc257a2b99a34da362c8e88e7d58b05c4a0381ae63c5975a810225"
+ },
+ {
+ "docId": "v1.2/release-1.2.5",
+ "sourcePath": "releasenotes/v1.2/release-1.2.5.md",
+ "sourceHash":
"bf9487b04132700d6db0ed1be42047cf3bbee06efd62220f653379e44be29b09"
+ },
+ {
+ "docId": "v1.2/release-1.2.6",
+ "sourcePath": "releasenotes/v1.2/release-1.2.6.md",
+ "sourceHash":
"8ac327d956f0e56c025d795abe48878b7fe917e5e95ccb754493923757915d68"
+ },
+ {
+ "docId": "v1.2/release-1.2.7",
+ "sourcePath": "releasenotes/v1.2/release-1.2.7.md",
+ "sourceHash":
"1df6eaf88b34e7fc4ced08d18859b6891a52f4b02c8a0ab7da8d04333d822d1e"
+ },
+ {
+ "docId": "v1.2/release-1.2.8",
+ "sourcePath": "releasenotes/v1.2/release-1.2.8.md",
+ "sourceHash":
"be475325e33ae1e27884b58fb983eca076f8bf268ed09562dbd9e47e4f5cea1b"
+ },
+ {
+ "docId": "v2.0/release-2.0.0",
+ "sourcePath": "releasenotes/v2.0/release-2.0.0.md",
+ "sourceHash":
"5714e0bb902451809eb0c8a8150be7e423db1a95a9b7b7ce8376e020556caa28"
+ },
+ {
+ "docId": "v2.0/release-2.0.1",
+ "sourcePath": "releasenotes/v2.0/release-2.0.1.md",
+ "sourceHash":
"9643e05d30d2fa86844882efbe65e6747118db4dcdac175dd9042ddfc6c14570"
+ },
+ {
+ "docId": "v2.0/release-2.0.10",
+ "sourcePath": "releasenotes/v2.0/release-2.0.10.md",
+ "sourceHash":
"f94b574439f8ca7b85d3b6bf3c8fbea12d211fa918e60c0233d1c3f35e998c18"
+ },
+ {
+ "docId": "v2.0/release-2.0.11",
+ "sourcePath": "releasenotes/v2.0/release-2.0.11.md",
+ "sourceHash":
"bb0d97ebcb34d440aeedf06b1316f37cd6e4d21cea84e6fac578275d35648e85"
+ },
+ {
+ "docId": "v2.0/release-2.0.12",
+ "sourcePath": "releasenotes/v2.0/release-2.0.12.md",
+ "sourceHash":
"4855df1592d495b233d3e61bf9d9071ad564fe85a25275c38ff1752a10f16b1e"
+ },
+ {
+ "docId": "v2.0/release-2.0.13",
+ "sourcePath": "releasenotes/v2.0/release-2.0.13.md",
+ "sourceHash":
"4748d3af8414fec9bafefc1ecc9349165d72f382d1b0e9888ec91cdc1560f5fe"
+ },
+ {
+ "docId": "v2.0/release-2.0.14",
+ "sourcePath": "releasenotes/v2.0/release-2.0.14.md",
+ "sourceHash":
"d4f83609d816fd0d0dc867b13f766c7adb2bc5452634c2b1dfc5e1d8e0addd14"
+ },
+ {
+ "docId": "v2.0/release-2.0.15",
+ "sourcePath": "releasenotes/v2.0/release-2.0.15.md",
+ "sourceHash":
"8d824f2faf0cd199a8eb5e682b550d18e097390de05a04b97ab60994ff3e346a"
+ },
+ {
+ "docId": "v2.0/release-2.0.2",
+ "sourcePath": "releasenotes/v2.0/release-2.0.2.md",
+ "sourceHash":
"a6ef57e3ecbb9b519d785138113e6bd4440fd92e5ff14ef5e29db19116494b0c"
+ },
+ {
+ "docId": "v2.0/release-2.0.3",
+ "sourcePath": "releasenotes/v2.0/release-2.0.3.md",
+ "sourceHash":
"39f2c50cc80c5e5fbc0780ede2cc0dfd6d19670dd5a1ff12c82590cf3f6083b8"
+ },
+ {
+ "docId": "v2.0/release-2.0.4",
+ "sourcePath": "releasenotes/v2.0/release-2.0.4.md",
+ "sourceHash":
"bfbc06bbf43e14fa06f2b66871cb1741b925030972c95f99600c25ba92dbd4ab"
+ },
+ {
+ "docId": "v2.0/release-2.0.5",
+ "sourcePath": "releasenotes/v2.0/release-2.0.5.md",
+ "sourceHash":
"323ebef54b8aa7c59595f8e251f9efa35c082e90e22c864f4bdfb2e83b402c5e"
+ },
+ {
+ "docId": "v2.0/release-2.0.6",
+ "sourcePath": "releasenotes/v2.0/release-2.0.6.md",
+ "sourceHash":
"f26798750b09dab6c3e4eae510c58045e6bff2ca806cb557e383063ae5186075"
+ },
+ {
+ "docId": "v2.0/release-2.0.7",
+ "sourcePath": "releasenotes/v2.0/release-2.0.7.md",
+ "sourceHash":
"09ec0b145fe543754bf1a63ae35ab2a0757e51fea2387b257540a0c92b1e5039"
+ },
+ {
+ "docId": "v2.0/release-2.0.8",
+ "sourcePath": "releasenotes/v2.0/release-2.0.8.md",
+ "sourceHash":
"e1951e12a66efc185c3122c5d337c6787478c58ba11b6273188f9a9cbab4e4b2"
+ },
+ {
+ "docId": "v2.0/release-2.0.9",
+ "sourcePath": "releasenotes/v2.0/release-2.0.9.md",
+ "sourceHash":
"a4644fb253b6ea4bd497c08d4da7f3cae84f6737bb5f50a9b6561d019cb1ed93"
+ },
+ {
+ "docId": "v2.1/release-2.1.0",
+ "sourcePath": "releasenotes/v2.1/release-2.1.0.md",
+ "sourceHash":
"9510af3cc26eba327ad96e1a4ca619ca0bf2b1a748f91cc844f7f5c07aff987c"
+ },
+ {
+ "docId": "v2.1/release-2.1.1",
+ "sourcePath": "releasenotes/v2.1/release-2.1.1.md",
+ "sourceHash":
"55d7946e87ce4c5522ff7f786a9f17ec3d00369657c3bd988158b4af356aff6a"
+ },
+ {
+ "docId": "v2.1/release-2.1.10",
+ "sourcePath": "releasenotes/v2.1/release-2.1.10.md",
+ "sourceHash":
"5adffc245c59f5378ae2c65a7486c7b67c2a2f456f4172601148ee2b9775f178"
+ },
+ {
+ "docId": "v2.1/release-2.1.11",
+ "sourcePath": "releasenotes/v2.1/release-2.1.11.md",
+ "sourceHash":
"01a26efab9251e3471432d24210ce824496dd2073f4a1d2a79c2bf5f5ad33457"
+ },
+ {
+ "docId": "v2.1/release-2.1.2",
+ "sourcePath": "releasenotes/v2.1/release-2.1.2.md",
+ "sourceHash":
"f4d4fb5f1d3eb59d4df2569ba5ea6496d0825c7b91d96ec9b88d448b9c918961"
+ },
+ {
+ "docId": "v2.1/release-2.1.3",
+ "sourcePath": "releasenotes/v2.1/release-2.1.3.md",
+ "sourceHash":
"d54d3252d42244047993b90cd20fe3098052a5653922b6d96fcb521a2e281314"
+ },
+ {
+ "docId": "v2.1/release-2.1.4",
+ "sourcePath": "releasenotes/v2.1/release-2.1.4.md",
+ "sourceHash":
"ce21a04e3dd6cfd51bec00d815b83c98f6a6f39983e1b529414fb570f4bff2d2"
+ },
+ {
+ "docId": "v2.1/release-2.1.5",
+ "sourcePath": "releasenotes/v2.1/release-2.1.5.md",
+ "sourceHash":
"02a22e10fcb4bb236a1e2711af5eb6e7903bd9d2e905f59914121eb592e43e9a"
+ },
+ {
+ "docId": "v2.1/release-2.1.6",
+ "sourcePath": "releasenotes/v2.1/release-2.1.6.md",
+ "sourceHash":
"36fd06bf7bac7c67128778d05dfbfb1405bc80b3bc80e541b560b3ec599c82a8"
+ },
+ {
+ "docId": "v2.1/release-2.1.7",
+ "sourcePath": "releasenotes/v2.1/release-2.1.7.md",
+ "sourceHash":
"81d9da477b72199c956134ef29ddaecc5c39bf4edcd460a16ab59163bd4ac8d8"
+ },
+ {
+ "docId": "v2.1/release-2.1.8",
+ "sourcePath": "releasenotes/v2.1/release-2.1.8.md",
+ "sourceHash":
"dda4268a8c304eaa01477ef9ff34f5f46298a02d1bad547529f646645d404ead"
+ },
+ {
+ "docId": "v2.1/release-2.1.9",
+ "sourcePath": "releasenotes/v2.1/release-2.1.9.md",
+ "sourceHash":
"9a073bfaf6d5789efb8fbf42b52c8ca28f531ffe15179b6d140bf93fbd847ddb"
+ },
+ {
+ "docId": "v3.0/release-3.0.0",
+ "sourcePath": "releasenotes/v3.0/release-3.0.0.md",
+ "sourceHash":
"346191397c7e8bba4cb5e41cce17863e2545ab6106e973dd99d3b0399b905269"
+ },
+ {
+ "docId": "v3.0/release-3.0.1",
+ "sourcePath": "releasenotes/v3.0/release-3.0.1.md",
+ "sourceHash":
"086c5693948c48cc547392c4c9f63ecfbb66c917f3ab1af39076c9643647697e"
+ },
+ {
+ "docId": "v3.0/release-3.0.2",
+ "sourcePath": "releasenotes/v3.0/release-3.0.2.md",
+ "sourceHash":
"18abdab8852acf2c751a46d5e0fbcdff355cfeaf22bfd167dfb9da286d1b30c4"
+ },
+ {
+ "docId": "v3.0/release-3.0.3",
+ "sourcePath": "releasenotes/v3.0/release-3.0.3.md",
+ "sourceHash":
"e399289eaab25a1bd406b8c76783b74e0d5b359c91a7d2d25e0b8b94a82a1da7"
+ },
+ {
+ "docId": "v3.0/release-3.0.4",
+ "sourcePath": "releasenotes/v3.0/release-3.0.4.md",
+ "sourceHash":
"e08e7b3aa3f9331b77e4043ff0b2ab27b96a5317b6cd1a8e4c499a193cfb3886"
+ },
+ {
+ "docId": "v3.0/release-3.0.5",
+ "sourcePath": "releasenotes/v3.0/release-3.0.5.md",
+ "sourceHash":
"6bd2333765efb92cd6aa14d2dfe3505e0b12c3aab8addedce1f3c001ce575768"
+ },
+ {
+ "docId": "v3.0/release-3.0.6",
+ "sourcePath": "releasenotes/v3.0/release-3.0.6.md",
+ "sourceHash":
"ed4d69ddade3800aeba77746aabfe35cbf9bf9492f4fcef7b334307348822bb8"
+ },
+ {
+ "docId": "v3.0/release-3.0.7",
+ "sourcePath": "releasenotes/v3.0/release-3.0.7.md",
+ "sourceHash":
"8f42f8eabd9659cbb1e994df13ee15ec57ad1fb2c0893ab850d2b5242812e598"
+ },
+ {
+ "docId": "v3.0/release-3.0.8",
+ "sourcePath": "releasenotes/v3.0/release-3.0.8.md",
+ "sourceHash":
"1513ddf8bc9c2283568eab5ca08f4a14bad896db1458a5b0b72c4fc6d0fe72d4"
+ },
+ {
+ "docId": "v3.1/release-3.1.0",
+ "sourcePath": "releasenotes/v3.1/release-3.1.0.md",
+ "sourceHash":
"54fc225e329e341f1b28566d7ce74b420a7ecb1fcdd5f57779d17a1bc7646847"
+ },
+ {
+ "docId": "v3.1/release-3.1.1",
+ "sourcePath": "releasenotes/v3.1/release-3.1.1.md",
+ "sourceHash":
"e5809a67ec4977a6203d5db6384d6ea6383261727fd579721c47bad9c99a8e47"
+ },
+ {
+ "docId": "v3.1/release-3.1.2",
+ "sourcePath": "releasenotes/v3.1/release-3.1.2.md",
+ "sourceHash":
"cc93666eeb50039d2fb563940a1fb6e434ccc7fbe741f714453066f0a3b6e020"
+ },
+ {
+ "docId": "v3.1/release-3.1.3",
+ "sourcePath": "releasenotes/v3.1/release-3.1.3.md",
+ "sourceHash":
"3fbf7fdb1bf5c3264bb5304be8db79edc1691d0fc5b984bf74bb9844595bb6ce"
+ },
+ {
+ "docId": "v3.1/release-3.1.4",
+ "sourcePath": "releasenotes/v3.1/release-3.1.4.md",
+ "sourceHash":
"1153ef38791c75651067427cb63115bb6b7ee86a9206935d3746501bd21c2326"
+ },
+ {
+ "docId": "v4.0/release-4.0.0",
+ "sourcePath": "releasenotes/v4.0/release-4.0.0.md",
+ "sourceHash":
"a0c6bd68abc36e2ff8f59dedd8574de502a86d5d9ea73d5c07fbcffc3756c17d"
+ },
+ {
+ "docId": "v4.0/release-4.0.1",
+ "sourcePath": "releasenotes/v4.0/release-4.0.1.md",
+ "sourceHash":
"2b6cf112245ac12e131065e44829e6ebe8acf7785b65a82cead70cbd8dbfc2f6"
+ },
+ {
+ "docId": "v4.0/release-4.0.2",
+ "sourcePath": "releasenotes/v4.0/release-4.0.2.md",
+ "sourceHash":
"6c681f7ce1c71a08bb792f50ef11cf34a68e18692d8e8f202f71d9620ef192c4"
+ },
+ {
+ "docId": "v4.0/release-4.0.3",
+ "sourcePath": "releasenotes/v4.0/release-4.0.3.md",
+ "sourceHash":
"1acaf4e8f8b0b4e9aed9fa3f550f91aa4d7dff7b8f50e47342f1102e90e0880b"
+ },
+ {
+ "docId": "v4.0/release-4.0.4",
+ "sourcePath": "releasenotes/v4.0/release-4.0.4.md",
+ "sourceHash":
"c8a725af3fd77a73d37efc370501506b71619debe58676f68311ae7aa34fcd30"
+ },
+ {
+ "docId": "v4.0/release-4.0.5",
+ "sourcePath": "releasenotes/v4.0/release-4.0.5.md",
+ "sourceHash":
"c4000459fb999fdc33c8efad7d7de2cd1f6093b5b0f38bc0727feac678613cbd"
+ },
+ {
+ "docId": "v4.0/release-4.0.6",
+ "sourcePath": "releasenotes/v4.0/release-4.0.6.md",
+ "sourceHash":
"643fa678a51dadf09bdd9af190e13d2568e1277d25334e51bb2f523794892515"
+ },
+ {
+ "docId": "v4.0/release-4.0.7",
+ "sourcePath": "releasenotes/v4.0/release-4.0.7.md",
+ "sourceHash":
"096052ed0e7d93f03a6c73270470c42f6e680bdb75814e9b0578fbe3d04a39b4"
+ },
+ {
+ "docId": "v4.0/release-4.0.8",
+ "sourcePath": "releasenotes/v4.0/release-4.0.8.md",
+ "sourceHash":
"794469ab16e546baa6627ab1c978579e6ffa44a8e3b93cfeb332c39753efffbb"
+ },
+ {
+ "docId": "v4.1/release-4.1.0",
+ "sourcePath": "releasenotes/v4.1/release-4.1.0.md",
+ "sourceHash":
"82bd7d1d958c98220464d45a7ed43c25ca59f90f82bdacae5f2133affa760292"
+ },
+ {
+ "docId": "v4.1/release-4.1.1",
+ "sourcePath": "releasenotes/v4.1/release-4.1.1.md",
+ "sourceHash":
"5546a2af12f0d1f8d12813dbb0a2092c61c5dca04a4aedf25053cf092fe4b718"
+ },
+ {
+ "docId": "v4.1/release-4.1.2",
+ "sourcePath": "releasenotes/v4.1/release-4.1.2.md",
+ "sourceHash":
"817c126dd9b3546cf4cad63edf20a93ebe7b56394fcc986d20b888fb7b16ffca"
+ },
+ {
+ "docId": "v4.1/release-4.1.3",
+ "sourcePath": "releasenotes/v4.1/release-4.1.3.md",
+ "sourceHash":
"1b26109c975a9e5cbbbd09b27fad032bc74e520ddf291ae87f057d9d1d541b05"
+ },
+ {
+ "docId": "v4.1/release-4.1.4",
+ "sourcePath": "releasenotes/v4.1/release-4.1.4.md",
+ "sourceHash":
"5beeb0e25d86cd57f1ca39e70f4fbef352b65d4869d2135555ed27d0d7573105"
+ },
+ {
+ "docId": "v4.1/release-4.1.4.1",
+ "sourcePath": "releasenotes/v4.1/release-4.1.4.1.md",
+ "sourceHash":
"fe55d5c11b933ae46798ef59ba3f4ce8a13cb819c1becbea15a3395c755df244"
+ },
+ {
+ "docId": "v4.1/release-4.1.5",
+ "sourcePath": "releasenotes/v4.1/release-4.1.5.md",
+ "sourceHash":
"908effe8089816fb8ca499104daa5394507d2b7bbfa5423c330ad92d3aeb1044"
+ }
+ ],
+ "sourceFiles": [
+ "releasenotes/all-release.md",
+ "releasenotes/core.md",
+ "releasenotes/ecosystem/doris-cli.md",
+ "releasenotes/ecosystem/doris-flink-connector.md",
+ "releasenotes/ecosystem/doris-kafka-connector.md",
+ "releasenotes/ecosystem/doris-mcp-server.md",
+ "releasenotes/ecosystem/doris-operator.md",
+ "releasenotes/ecosystem/doris-skills.md",
+ "releasenotes/ecosystem/doris-spark-connector.md",
+ "releasenotes/ecosystem/doris-streamloader.md",
+ "releasenotes/v1.1/release-1.1.0.md",
+ "releasenotes/v1.1/release-1.1.1.md",
+ "releasenotes/v1.1/release-1.1.2.md",
+ "releasenotes/v1.1/release-1.1.3.md",
+ "releasenotes/v1.1/release-1.1.4.md",
+ "releasenotes/v1.1/release-1.1.5.md",
+ "releasenotes/v1.2/release-1.2.0.md",
+ "releasenotes/v1.2/release-1.2.1.md",
+ "releasenotes/v1.2/release-1.2.2.md",
+ "releasenotes/v1.2/release-1.2.3.md",
+ "releasenotes/v1.2/release-1.2.4.md",
+ "releasenotes/v1.2/release-1.2.5.md",
+ "releasenotes/v1.2/release-1.2.6.md",
+ "releasenotes/v1.2/release-1.2.7.md",
+ "releasenotes/v1.2/release-1.2.8.md",
+ "releasenotes/v2.0/release-2.0.0.md",
+ "releasenotes/v2.0/release-2.0.1.md",
+ "releasenotes/v2.0/release-2.0.10.md",
+ "releasenotes/v2.0/release-2.0.11.md",
+ "releasenotes/v2.0/release-2.0.12.md",
+ "releasenotes/v2.0/release-2.0.13.md",
+ "releasenotes/v2.0/release-2.0.14.md",
+ "releasenotes/v2.0/release-2.0.15.md",
+ "releasenotes/v2.0/release-2.0.2.md",
+ "releasenotes/v2.0/release-2.0.3.md",
+ "releasenotes/v2.0/release-2.0.4.md",
+ "releasenotes/v2.0/release-2.0.5.md",
+ "releasenotes/v2.0/release-2.0.6.md",
+ "releasenotes/v2.0/release-2.0.7.md",
+ "releasenotes/v2.0/release-2.0.8.md",
+ "releasenotes/v2.0/release-2.0.9.md",
+ "releasenotes/v2.1/release-2.1.0.md",
+ "releasenotes/v2.1/release-2.1.1.md",
+ "releasenotes/v2.1/release-2.1.10.md",
+ "releasenotes/v2.1/release-2.1.11.md",
+ "releasenotes/v2.1/release-2.1.2.md",
+ "releasenotes/v2.1/release-2.1.3.md",
+ "releasenotes/v2.1/release-2.1.4.md",
+ "releasenotes/v2.1/release-2.1.5.md",
+ "releasenotes/v2.1/release-2.1.6.md",
+ "releasenotes/v2.1/release-2.1.7.md",
+ "releasenotes/v2.1/release-2.1.8.md",
+ "releasenotes/v2.1/release-2.1.9.md",
+ "releasenotes/v3.0/release-3.0.0.md",
+ "releasenotes/v3.0/release-3.0.1.md",
+ "releasenotes/v3.0/release-3.0.2.md",
+ "releasenotes/v3.0/release-3.0.3.md",
+ "releasenotes/v3.0/release-3.0.4.md",
+ "releasenotes/v3.0/release-3.0.5.md",
+ "releasenotes/v3.0/release-3.0.6.md",
+ "releasenotes/v3.0/release-3.0.7.md",
+ "releasenotes/v3.0/release-3.0.8.md",
+ "releasenotes/v3.1/release-3.1.0.md",
+ "releasenotes/v3.1/release-3.1.1.md",
+ "releasenotes/v3.1/release-3.1.2.md",
+ "releasenotes/v3.1/release-3.1.3.md",
+ "releasenotes/v3.1/release-3.1.4.md",
+ "releasenotes/v4.0/release-4.0.0.md",
+ "releasenotes/v4.0/release-4.0.1.md",
+ "releasenotes/v4.0/release-4.0.2.md",
+ "releasenotes/v4.0/release-4.0.3.md",
+ "releasenotes/v4.0/release-4.0.4.md",
+ "releasenotes/v4.0/release-4.0.5.md",
+ "releasenotes/v4.0/release-4.0.6.md",
+ "releasenotes/v4.0/release-4.0.7.md",
+ "releasenotes/v4.0/release-4.0.8.md",
+ "releasenotes/v4.1/release-4.1.0.md",
+ "releasenotes/v4.1/release-4.1.1.md",
+ "releasenotes/v4.1/release-4.1.2.md",
+ "releasenotes/v4.1/release-4.1.3.md",
+ "releasenotes/v4.1/release-4.1.4.1.md",
+ "releasenotes/v4.1/release-4.1.4.md",
+ "releasenotes/v4.1/release-4.1.5.md"
+ ],
+ "historicalPaths": [
+ "releasenotes/all-release.md",
+ "releasenotes/core.md",
+ "releasenotes/ecosystem/doris-cli.md",
+ "releasenotes/ecosystem/doris-flink-connector.md",
+ "releasenotes/ecosystem/doris-kafka-connector.md",
+ "releasenotes/ecosystem/doris-mcp-server.md",
+ "releasenotes/ecosystem/doris-operator.md",
+ "releasenotes/ecosystem/doris-skills.md",
+ "releasenotes/ecosystem/doris-spark-connector.md",
+ "releasenotes/ecosystem/doris-streamloader.md",
+ "releasenotes/v1.1/release-1.1.0.md",
+ "releasenotes/v1.1/release-1.1.1.md",
+ "releasenotes/v1.1/release-1.1.2.md",
+ "releasenotes/v1.1/release-1.1.3.md",
+ "releasenotes/v1.1/release-1.1.4.md",
+ "releasenotes/v1.1/release-1.1.5.md",
+ "releasenotes/v1.2/release-1.2.0.md",
+ "releasenotes/v1.2/release-1.2.1.md",
+ "releasenotes/v1.2/release-1.2.2.md",
+ "releasenotes/v1.2/release-1.2.3.md",
+ "releasenotes/v1.2/release-1.2.4.md",
+ "releasenotes/v1.2/release-1.2.5.md",
+ "releasenotes/v1.2/release-1.2.6.md",
+ "releasenotes/v1.2/release-1.2.7.md",
+ "releasenotes/v1.2/release-1.2.8.md",
+ "releasenotes/v2.0/release-2.0.0.md",
+ "releasenotes/v2.0/release-2.0.1.md",
+ "releasenotes/v2.0/release-2.0.10.md",
+ "releasenotes/v2.0/release-2.0.11.md",
+ "releasenotes/v2.0/release-2.0.12.md",
+ "releasenotes/v2.0/release-2.0.13.md",
+ "releasenotes/v2.0/release-2.0.14.md",
+ "releasenotes/v2.0/release-2.0.15.md",
+ "releasenotes/v2.0/release-2.0.2.md",
+ "releasenotes/v2.0/release-2.0.3.md",
+ "releasenotes/v2.0/release-2.0.4.md",
+ "releasenotes/v2.0/release-2.0.5.md",
+ "releasenotes/v2.0/release-2.0.6.md",
+ "releasenotes/v2.0/release-2.0.7.md",
+ "releasenotes/v2.0/release-2.0.8.md",
+ "releasenotes/v2.0/release-2.0.9.md",
+ "releasenotes/v2.1/release-2.1.0.md",
+ "releasenotes/v2.1/release-2.1.1.md",
+ "releasenotes/v2.1/release-2.1.10.md",
+ "releasenotes/v2.1/release-2.1.11.md",
+ "releasenotes/v2.1/release-2.1.2.md",
+ "releasenotes/v2.1/release-2.1.3.md",
+ "releasenotes/v2.1/release-2.1.4.md",
+ "releasenotes/v2.1/release-2.1.5.md",
+ "releasenotes/v2.1/release-2.1.6.md",
+ "releasenotes/v2.1/release-2.1.7.md",
+ "releasenotes/v2.1/release-2.1.8.md",
+ "releasenotes/v2.1/release-2.1.9.md",
+ "releasenotes/v3.0/release-3.0.0.md",
+ "releasenotes/v3.0/release-3.0.1.md",
+ "releasenotes/v3.0/release-3.0.2.md",
+ "releasenotes/v3.0/release-3.0.3.md",
+ "releasenotes/v3.0/release-3.0.4.md",
+ "releasenotes/v3.0/release-3.0.5.md",
+ "releasenotes/v3.0/release-3.0.6.md",
+ "releasenotes/v3.0/release-3.0.7.md",
+ "releasenotes/v3.0/release-3.0.8.md",
+ "releasenotes/v3.1/release-3.1.0.md",
+ "releasenotes/v3.1/release-3.1.1.md",
+ "releasenotes/v3.1/release-3.1.2.md",
+ "releasenotes/v3.1/release-3.1.3.md",
+ "releasenotes/v3.1/release-3.1.4.md",
+ "releasenotes/v4.0/release-4.0.0.md",
+ "releasenotes/v4.0/release-4.0.1.md",
+ "releasenotes/v4.0/release-4.0.2.md",
+ "releasenotes/v4.0/release-4.0.3.md",
+ "releasenotes/v4.0/release-4.0.4.md",
+ "releasenotes/v4.0/release-4.0.5.md",
+ "releasenotes/v4.0/release-4.0.6.md",
+ "releasenotes/v4.0/release-4.0.7.md",
+ "releasenotes/v4.0/release-4.0.8.md",
+ "releasenotes/v4.1/release-4.1.0.md",
+ "releasenotes/v4.1/release-4.1.1.md",
+ "releasenotes/v4.1/release-4.1.2.md",
+ "releasenotes/v4.1/release-4.1.3.md",
+ "releasenotes/v4.1/release-4.1.4.1.md",
+ "releasenotes/v4.1/release-4.1.4.md",
+ "releasenotes/v4.1/release-4.1.5.md"
+ ]
+}
diff --git a/translations/scope/ja.json b/translations/scope/ja.json
index 6480136e7207..40ac9da14a4b 100644
--- a/translations/scope/ja.json
+++ b/translations/scope/ja.json
@@ -6,7 +6,8 @@
"DEV",
"4X",
"COMMUNITY",
- "COURSE"
+ "COURSE",
+ "RELEASES"
],
"groups": [
{
@@ -37,7 +38,11 @@
"id": "RELEASES",
"pluginId": "releases",
"version": "current",
- "sourceRoot": "releasenotes"
+ "sourceRoot": "releasenotes",
+ "sourcePolicy": {
+ "mode": "new-only",
+ "baseline": "translations/release-notes-baseline.json"
+ }
},
{
"id": "COURSEUI",
diff --git a/translations/scope/ko.json b/translations/scope/ko.json
index acee050bd895..0e1afdbd9051 100644
--- a/translations/scope/ko.json
+++ b/translations/scope/ko.json
@@ -6,7 +6,8 @@
"DEV",
"4X",
"COMMUNITY",
- "COURSE"
+ "COURSE",
+ "RELEASES"
],
"groups": [
{
@@ -37,7 +38,11 @@
"id": "RELEASES",
"pluginId": "releases",
"version": "current",
- "sourceRoot": "releasenotes"
+ "sourceRoot": "releasenotes",
+ "sourcePolicy": {
+ "mode": "new-only",
+ "baseline": "translations/release-notes-baseline.json"
+ }
},
{
"id": "COURSEUI",
diff --git a/translations/scope/ru.json b/translations/scope/ru.json
index 73208a9683cd..f18eef027392 100644
--- a/translations/scope/ru.json
+++ b/translations/scope/ru.json
@@ -6,7 +6,8 @@
"DEV",
"4X",
"COMMUNITY",
- "COURSE"
+ "COURSE",
+ "RELEASES"
],
"groups": [
{
@@ -37,7 +38,11 @@
"id": "RELEASES",
"pluginId": "releases",
"version": "current",
- "sourceRoot": "releasenotes"
+ "sourceRoot": "releasenotes",
+ "sourcePolicy": {
+ "mode": "new-only",
+ "baseline": "translations/release-notes-baseline.json"
+ }
},
{
"id": "COURSEUI",
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]