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 456f6b379116 [i18n](website) Verify product pages under the global
language contract (#4503)
456f6b379116 is described below
commit 456f6b379116d8a801a7b5d087b1d90475d4662c
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Sun Oct 25 03:42:19 2026 +0000
[i18n](website) Verify product pages under the global language contract
(#4503)
The seven product pages already use the global language contract. Add
explicit verification of their existing Japanese, Korean and Russian
bodies, native document-backed feature cards, Chinese English-body
fallback, navigation and public resources. Record machine review for 19
verified page units while preserving publication decisions and every
source/target digest.
The new verifier covers 35 language routes, 1440/390 layouts under eight
themes, comparison tabs/cases, benchmark interactions, all 47 feature
cards, search/clear states, use-case expansion and language switches
with query/hash. Its default completion check rejects unreviewed or
fallback feature documents. `--allow-document-fallback` is an explicit
baseline-audit mode that retains the gaps and reports `contentComplete:
false`.
Changelog / Table Stream still needs the renamed Japanese and Korean
documents. Korean candidates are already in #4501; Japanese current/4.x
candidates were generated through the formal workflow in [run
40153250578](https://github.com/apache/doris-website/actions/runs/40153250578)
and are in #4504. This draft remains pending those dependencies and the
resulting P04 refresh. It does not create duplicate document candidates
or metadata stubs.
Final head `b3cdac4bdbc965b0b2949eda50533931e909e7b1`: all five CI
checks pass, including [Build Check
40154834335](https://github.com/apache/doris-website/actions/runs/40154834335).
CI retains the existing five-language sample policy; its artifacts are
not publication inputs. Japanese dependency #4504 also has all four
checks green.
Validation:
- 19 focused marketing/manual-report tests pass; manual report has zero
drift and complete shared UI.
- Three locale admission checks pass with no body edits in this PR;
these do not claim a full documentation freshness scan.
- Existing production artifact from
[40140944703](https://github.com/apache/doris-website/actions/runs/40140944703):
local and public product baselines pass 560 layout/theme checks and 22
interaction groups; public HTML and 1,409 requested resources match that
artifact. All 2,652 reachable body messages retain their placeholders;
number and technical-name review preserves the existing meanings.
- The committed verifier’s default strict public check repeats all 560
layout checks and verifies 1,419 requested resources. It correctly
rejects completion for exactly the two Japanese/Korean Changelog
document dependencies; there are no other failures or browser errors.
- Chinese landing bodies follow the established intentional English
fallback. Source bodies, URLs, dictionaries and versioned documentation
remain shared with the existing release.
## Versions
Site-level landing pages and tooling apply across the active
documentation versions. This PR does not edit versioned document bodies.
- [ ] dev
- [ ] 4.x
- [ ] 3.x
- [ ] 2.1 or older (not covered by version/language sync gate)
## Languages
- [x] Chinese fallback behavior verified
- [x] English
- [x] Japanese
- [x] Korean
- [x] Russian
## Docs Checklist
- [x] Checked by AI (machine review; no human native-language
proofreading)
- [x] Test Cases Built
- [x] Updated required version and language counterparts, or explained
why not
- [x] If only one language changed, confirmed whether source/translation
counterparts need sync
---------
Co-authored-by: morningman <[email protected]>
---
scripts/i18n/verify-product-pages.cjs | 323 ++++++++++++++++++++++++++++++++++
translations/CONTENT_MANIFEST.md | 81 +++++++++
translations/pages/state.json | 114 ++++++++----
3 files changed, 480 insertions(+), 38 deletions(-)
diff --git a/scripts/i18n/verify-product-pages.cjs
b/scripts/i18n/verify-product-pages.cjs
new file mode 100644
index 000000000000..e1f1b743670e
--- /dev/null
+++ b/scripts/i18n/verify-product-pages.cjs
@@ -0,0 +1,323 @@
+// Verify the seven existing product pages against native content decisions.
+// Missing document translations remain explicit gaps even when the page works.
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const {createHash} = require('node:crypto');
+const {execFileSync} = require('node:child_process');
+const cheerio = require('cheerio');
+const matter = require('gray-matter');
+const {createServer} = require('../publish/serve.cjs');
+const {localeRegistry} = require('../../config/i18n');
+const {generate} = require('../key-features/generate');
+
+const root = path.resolve(__dirname, '../..');
+const productIds = ['P02', 'P03', 'P04', 'P06', 'P07', 'P08', 'P09'];
+const locales = ['en', 'zh-CN', 'ja', 'ko', 'ru'];
+const themes = ['doris', 'golden', 'blue', 'read', 'yellow-blue', 'purple',
'yellow-black', 'sky'];
+const hash = bytes => createHash('sha256').update(bytes).digest('hex');
+const normalized = pathname => pathname.replace(/\/+$/, '') || '/';
+const withLocale = (locale, route) => (locale === 'en' ? '' : '/' + locale) +
route;
+
+function assetResolver(site) {
+ site = path.resolve(site);
+ const file = path.join(site, '.publish/redirects.json');
+ const manifest = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file)) :
{redirects: [], assetPrefixes: []};
+ const redirects = new Map(manifest.redirects.map(([from, to]) =>
[decodeURIComponent(from), to]));
+ return pathname => {
+ let current = decodeURIComponent(pathname);
+ for (let count = 0; count < 20; count++) {
+ const file = path.join(site, current);
+ assert(file.startsWith(site + path.sep), 'Asset outside artifact');
+ if (fs.existsSync(file) && fs.statSync(file).isFile()) return file;
+ const target = redirects.get(current) ?? redirects.get(current +
'/');
+ if (target) {
+ const next = new URL(target, 'https://doris.apache.org');
+ assert.equal(next.origin, 'https://doris.apache.org');
+ current = decodeURIComponent(next.pathname); continue;
+ }
+ const prefix = manifest.assetPrefixes.find(prefix =>
current.startsWith(prefix));
+ if (!prefix) break;
+ current = '/assets/' + current.slice(prefix.length);
+ }
+ throw new Error('Unresolved artifact asset: ' + pathname);
+ };
+}
+
+function staticChecks(site, directory) {
+ const manifests = Object.fromEntries(locales.map(locale => [locale,
+ JSON.parse(fs.readFileSync(path.join(directory, locale + '.json')))]));
+ const sourceSha = manifests.en.sourceSha;
+ const cards = generate({rootDir: root});
+ const pages = [], documents = [];
+ for (const locale of locales) {
+ assert.equal(manifests[locale].sourceSha, sourceSha, 'Mixed native
sources');
+ const indexed = new Set(cheerio.load(fs.readFileSync(path.join(site,
locale === 'en' ? '' : locale, 'sitemap.xml')),
+ {xmlMode: true})('loc').map((_, node) => new
URL(node.children[0].data).pathname).get());
+ for (const id of productIds) {
+ const native = manifests[locale].pages.find(row => row.contentId
=== 'page:' + id);
+ assert(native, `Missing native product: ${id}/${locale}`);
+ const expectedLocale = locale === 'zh-CN' ? 'en' : locale;
+ assert.equal(native.contentLocale, expectedLocale);
+ assert.equal(native.fallback, locale === 'zh-CN');
+ const html = fs.readFileSync(path.join(site,
decodeURIComponent(native.route), 'index.html'));
+ const $ = cheerio.load(html);
+ assert.equal($('html').attr('lang'), localeRegistry.find(item =>
item.code === locale).htmlLang);
+ assert.equal($('[data-content-id="page:' + id +
'"]').attr('lang'), expectedLocale);
+ assert.equal($('.page-fallback-notice').length, native.fallback ?
1 : 0);
+ const canonical = 'https://doris.apache.org' + (native.fallback ?
native.englishRoute : native.route);
+ assert.equal($('link[rel="canonical"]').length, 1);
+ assert.equal($('link[rel="canonical"]').attr('href'), canonical);
+ assert.equal($('meta[property="og:url"]').attr('content'),
canonical);
+
assert.equal(/noindex/.test($('meta[name="robots"]').attr('content') ?? ''),
!native.indexable);
+ assert.equal(indexed.has(native.route), native.indexable);
+ assert.deepEqual([...native.availableTranslations].sort(), ['en',
'ja', 'ko', 'ru']);
+ assert.equal($('link[rel="alternate"][hreflang]').length, 5);
+ for (const other of ['en', 'ja', 'ko', 'ru']) {
+ const language = localeRegistry.find(item => item.code ===
other).htmlLang;
+
assert.equal($(`link[rel="alternate"][hreflang="${language}"]`).attr('href'),
+ 'https://doris.apache.org' +
native.navigationRoutes[other]);
+ }
+ assert($('title').text().trim() &&
$('meta[name="description"]').attr('content')?.trim());
+ if (id === 'P04') {
+ const tiles = $('.kf-next__tile'); assert.equal(tiles.length,
cards.length);
+ cards.forEach((card, index) => {
+ const route = withLocale(expectedLocale, card.href);
+ const doc = manifests[expectedLocale].pages.find(row =>
row.kind === 'docs'
+ && normalized(row.route) === normalized(route));
+ assert(doc, 'Missing feature document identity: ' + route);
+ const target = doc.contentLocale === 'en' ? card.sourcePath
+ :
`i18n/${doc.contentLocale}/docusaurus-plugin-content-docs/current/${card.sourcePath.replace(/^docs\//,
'')}`;
+ const {data, content} =
matter(fs.readFileSync(path.join(root, target), 'utf8'));
+ assert(content.trim().length > 100, 'Feature metadata
without a complete body: ' + target);
+ const tile = tiles.eq(index);
+ assert.equal(tile.find('.kf-next__tile-name').text(),
data.title);
+ if (tile.find('.kf-next__tile-desc').length)
+ assert.equal(tile.find('.kf-next__tile-desc').text(),
data.description);
+ assert.equal(normalized(tile.find('a').attr('href')),
normalized(withLocale(locale, card.href)));
+ documents.push({locale, id: card.id, route:
withLocale(locale, card.href), sourcePath: card.sourcePath,
+ metadataSource: target, contentLocale:
doc.contentLocale, state: doc.state,
+ fallback: doc.fallback, reviewStatus:
doc.reviewStatus});
+ });
+ }
+ pages.push({locale, id, route: native.route, englishRoute:
native.englishRoute,
+ contentLocale: native.contentLocale, fallback:
native.fallback, sha256: hash(html)});
+ }
+ }
+ return {sourceSha, cards, pages, documents, documentGaps:
documents.filter(row =>
+ ['ja', 'ko', 'ru'].includes(row.locale)
+ && (row.fallback || row.state !== 'current' || row.reviewStatus ===
'unverified'))};
+}
+
+function svgBounds() {
+ const issues = [];
+ for (const svg of document.querySelectorAll('main svg[viewBox]')) {
+ const view = svg.viewBox.baseVal;
+ for (const text of svg.querySelectorAll('text')) {
+ const box = text.getBBox(); if (!box.width) continue;
+ const matrix = svg.getCTM().inverse().multiply(text.getCTM());
+ const points = [[box.x, box.y], [box.x + box.width, box.y +
box.height]]
+ .map(([x, y]) => [matrix.a * x + matrix.c * y + matrix.e,
matrix.b * x + matrix.d * y + matrix.f]);
+ if (points.some(([x, y]) => x < view.x - 1 || x > view.x +
view.width + 1
+ || y < view.y - 1 || y > view.y + view.height + 1))
issues.push(text.textContent);
+ }
+ }
+ return issues;
+}
+
+async function verify({site, directory, output, origin: publicOrigin,
requireComplete = true}) {
+ site = path.resolve(site); directory = path.resolve(directory); output =
path.resolve(output);
+ const {chromium} = require('playwright');
+ const native = staticChecks(site, directory);
+ if (publicOrigin) {
+ assert(!fs.existsSync(path.join(site, '.ci-validation.json')), 'A
partial CI artifact cannot establish public acceptance');
+ assert(fs.existsSync(path.join(site, '.publish/redirects.json')),
'Public acceptance requires the optimized publication artifact');
+ for (const locale of locales) {
+ const manifest = JSON.parse(fs.readFileSync(path.join(directory,
locale + '.json')));
+ for (const version of ['current', '4.x', '3.x', '2.1'])
+ assert(manifest.pages.some(row => row.kind === 'docs' &&
row.pluginId === 'default' && row.version === version),
+ `Missing publication version: ${locale}/${version}`);
+ }
+ }
+ const report = {schemaVersion: 1, sourceSha: native.sourceSha,
+ verificationCodeSha: execFileSync('git', ['rev-parse', 'HEAD'], {cwd:
root, encoding: 'utf8'}).trim(),
+ public: !!publicOrigin, requireComplete, contentComplete:
native.documentGaps.length === 0,
+ ...native, layouts: [], interactions: [], resources: [], errors: [],
failures: [],
+ humanProofreading: false, startedAt: new Date().toISOString()};
+ fs.mkdirSync(path.dirname(output), {recursive: true});
+ let server, browser;
+ const check = (value, message, detail) => {if (!value)
report.failures.push({message, detail});};
+ try {
+ if (!publicOrigin) {server = createServer(site); await new
Promise(resolve => server.listen(0, '127.0.0.1', resolve));}
+ const origin = publicOrigin?.replace(/\/$/, '') ??
`http://127.0.0.1:${server.address().port}`;
+ browser = await chromium.launch();
+ const context = await browser.newContext({serviceWorkers: 'block',
reducedMotion: 'reduce'});
+ await context.route('**/*', async route => {
+ const url = new URL(route.request().url());
+ if (url.origin === origin) return route.continue();
+ if (['cdnd.selectdb.com',
'cdn.selectdb.com'].includes(url.hostname)) {
+ if (publicOrigin) return route.continue();
+ return route.fulfill({response: await
context.request.get(origin + url.pathname + url.search)});
+ }
+ return route.abort();
+ });
+ const page = await context.newPage(), assets = new Map();
+ page.on('pageerror', error => report.errors.push(error.message));
+ page.on('console', message => {if (/hydration|did not match|server
HTML/i.test(message.text())) report.errors.push(message.text());});
+ page.on('response', response => {
+ const url = new URL(response.url());
+ if ((url.origin === origin || ['cdnd.selectdb.com',
'cdn.selectdb.com'].includes(url.hostname))
+ && /\.(js|css|svg|png|jpe?g|webp|woff2?)$/.test(url.pathname))
assets.set(url.href, url.pathname);
+ });
+ const open = async route => {
+ const response = await page.goto(origin + route, {waitUntil:
'domcontentloaded'});
+ assert.equal(response.status(), 200, route);
+ await page.locator('html[data-has-hydrated="true"]').waitFor();
+ await page.evaluate(() => document.fonts.ready);
+ await page.evaluate(() =>
document.querySelectorAll('[data-reveal]').forEach(node =>
node.classList.add('is-visible')));
+ return response;
+ };
+ for (const width of [1440, 390]) {
+ await page.setViewportSize({width, height: 1000});
+ for (const product of native.pages) {
+ const response = await open(product.route + '?from=t10#t10');
+ if (publicOrigin) assert.equal(hash(await response.body()),
product.sha256, product.route);
+ assert.equal(await
page.locator(`[data-content-id="page:${product.id}"]`).getAttribute('lang'),
product.contentLocale);
+ const badLinks = await page.locator('[data-content-id]
a[href]').evaluateAll((links, expected) => links
+ .filter(link =>
!link.closest('.page-language-switcher,.language-menu'))
+ .map(link => new URL(link.href)).filter(url => url.origin
=== location.origin).map(url => url.pathname)
+ .filter(href =>
!/\.(pdf|zip|gz|png|svg|jpe?g)(?:[?#]|$)/.test(href))
+ .filter(href => expected === 'en' ?
/^\/(?:zh-CN|ja|ko|ru)(?:\/|$)/.test(href)
+ : !href.startsWith('/' + expected + '/')),
product.locale);
+ check(!badLinks.length, 'Product link lost site language',
{product, badLinks});
+ for (const theme of themes) {
+ // The shared selector itself is covered by first-release;
+ // inspect every product's rendering under each real token
set.
+ await page.evaluate(theme =>
{document.documentElement.dataset.brandTheme = theme;}, theme);
+ const layout = await page.evaluate(() => ({viewport:
innerWidth, scrollWidth: document.documentElement.scrollWidth,
+ primary:
getComputedStyle(document.documentElement).getPropertyValue('--brand-primary').trim(),
+ accent:
getComputedStyle(document.documentElement).getPropertyValue('--brand-accent').trim()}));
+ const svgIssues = await page.evaluate(svgBounds);
+ check(layout.scrollWidth <= width + 1 && layout.primary &&
layout.accent && !svgIssues.length,
+ 'Product layout/theme', {id: product.id, locale:
product.locale, width, theme, layout, svgIssues});
+ report.layouts.push({id: product.id, locale:
product.locale, width, theme, ...layout, svgIssues});
+ if ((width === 1440 && theme === 'doris') || (width ===
390 && theme === 'sky')) {
+ await page.evaluate(async () => {
+ await new Promise(requestAnimationFrame);
+ await Promise.allSettled(document.getAnimations()
+ .filter(animation => animation.playState ===
'running'
+ &&
Number.isFinite(animation.effect?.getComputedTiming().endTime))
+ .map(animation => animation.finished));
+ });
+ await page.screenshot({path:
path.join(path.dirname(output),
`${product.id}-${product.locale}-${width}-${theme}.png`)});
+ }
+ }
+ const inPanel = !await
page.locator('.language-menu:not(.language-menu--mobile)').isVisible();
+ if (inPanel) await
page.locator('.navbar-next__menu-button').click();
+ const menu = page.locator(inPanel ? '.language-menu--mobile' :
'.language-menu:not(.language-menu--mobile)');
+ await menu.locator('summary').focus(); await
page.keyboard.press('Enter');
+ assert.equal(await menu.getAttribute('open'), ''); await
page.keyboard.press('Escape');
+ assert.equal(await menu.getAttribute('open'), null);
+ assert(await menu.locator('summary').evaluate(node =>
document.activeElement === node));
+ if (inPanel) await
page.locator('.navbar-next__menu-button').click();
+ }
+ }
+ await page.setViewportSize({width: 1440, height: 1000});
+ const section = async (locale, name, run) => {
+ try {report.interactions.push({locale, name, ...await run()});}
+ catch (error) {report.failures.push({locale, name, message:
error.message});}
+ };
+ for (const locale of ['ja', 'ko', 'ru']) {
+ const route = id => native.pages.find(row => row.id === id &&
row.locale === locale).route;
+ await section(locale, 'compare tabs, cases and anchor', async ()
=> {
+ await open(route('P02') + '#trino');
+ await page.waitForFunction(() =>
document.querySelector('.cmp-next__tabs
[aria-selected=true]')?.textContent.includes('Trino'));
+ const tabs = page.locator('.cmp-next__tabs [role=tab]'); let
selected = 0, cases = 0;
+ for (const tab of await tabs.all()) {
+ if (await tab.locator('.cmp-next__tab-soon').count())
continue;
+ await tab.focus(); await page.keyboard.press('Enter');
assert.equal(await tab.getAttribute('aria-selected'), 'true'); selected++;
+ for (const [index, dot] of (await
page.locator('.cmp-cf__dot').all()).entries()) {
+ await dot.focus(); await page.keyboard.press('Enter');
+ assert.equal(await
page.locator('.cmp-cf__item--active').getAttribute('data-idx'), String(index));
cases++;
+ }
+ }
+ assert.equal(selected, 3); assert.equal(cases, 9);
+ return {tabs: selected, cases};
+ });
+ await section(locale, 'benchmark tabs, details and tooltip', async
() => {
+ await open(route('P03')); const tabs = page.locator('.b-tabs
[role=tab]'); assert.equal(await tabs.count(), 5);
+ for (const tab of await tabs.all()) {await tab.focus(); await
page.keyboard.press('Enter'); assert.equal(await
tab.getAttribute('aria-selected'), 'true');}
+ const card = page.locator('.b-card').first(); await
card.scrollIntoViewIfNeeded(); await card.focus(); await
page.keyboard.press('Enter');
+ await page.locator('.b-detail').first().waitFor();
+ const bar = page.locator('.b-cluster-bar').first(); if (await
bar.count()) {await bar.hover(); await
page.locator('.b-floating-tip').first().waitFor();}
+ return {tabs: await tabs.count(), details: await
page.locator('.b-detail').count()};
+ });
+ await section(locale, 'feature search, clear and native document',
async () => {
+ await open(route('P04')); assert.equal(await
page.locator('.kf-next__tile').count(), native.cards.length);
+ const visible = () =>
page.locator('.kf-next__tile:not(.is-hidden)').count();
+ const title = await
page.locator('.kf-next__tile-name').first().innerText();
+ for (const query of [title, 'MPP', 'lakehouse']) {await
page.locator('input[type=search]').fill(query); assert(await visible() > 0,
query);}
+ await page.locator('input[type=search]').fill('zzzzzzzzzzzz');
assert.equal(await visible(), 0);
+ await page.locator('.kf-next__empty-clear').focus(); await
page.keyboard.press('Enter'); assert.equal(await visible(),
native.cards.length);
+ await page.locator('input[type=search]').fill('MPP'); await
page.locator('.kf-next__search-clear').focus();
+ await page.keyboard.press('Enter'); assert.equal(await
visible(), native.cards.length);
+ await page.locator('.kf-next__tile a').first().click();
+ await page.waitForURL(url => url.pathname.startsWith('/' +
locale + '/docs/dev/'));
+ await page.locator('.theme-doc-markdown').waitFor();
assert.equal(await page.locator('html').getAttribute('lang'), locale);
+ return {cards: native.cards.length, title, document: new
URL(page.url()).pathname};
+ });
+ for (const id of ['P06', 'P07', 'P08', 'P09']) await
section(locale, 'requirement keyboard ' + id, async () => {
+ await open(route(id)); const toggles = page.locator('main
button[aria-expanded]');
+ assert.equal(await toggles.count(), {P06: 4, P07: 5, P08: 5,
P09: 5}[id]);
+ for (const toggle of await toggles.all()) {const before =
await toggle.getAttribute('aria-expanded');
+ await toggle.scrollIntoViewIfNeeded(); await
toggle.focus(); await page.keyboard.press('Enter');
+ assert.notEqual(await
toggle.getAttribute('aria-expanded'), before); assert(await
toggle.evaluate(node => document.activeElement === node));}
+ return {toggles: await toggles.count()};
+ });
+ }
+ await section('all', 'language switches retain product identity and
suffix', async () => {
+ const product = native.pages.find(row => row.id === 'P09' &&
row.locale === 'ja');
+ await open(product.route + '?from=t10#t10'); const routes = [];
+ for (const locale of ['ko', 'ru', 'zh-CN', 'en', 'ja']) {
+ const htmlLang = localeRegistry.find(row => row.code ===
locale).htmlLang;
+ await page.locator(`.page-language-switcher
a[lang="${htmlLang}"]`).click();
+ const expected = withLocale(locale, product.englishRoute);
+ await page.waitForURL(url => url.pathname === expected);
+ const url = new URL(page.url()); assert.equal(url.search,
'?from=t10'); assert.equal(url.hash, '#t10');
+ routes.push(url.pathname); await page.reload();
assert.equal(new URL(page.url()).pathname, expected);
+ }
+ return {routes};
+ });
+ if (publicOrigin) {
+ await page.waitForLoadState('networkidle');
+ const resolve = assetResolver(site), pending = [...assets]; let
cursor = 0;
+ await Promise.all(Array.from({length: 8}, async () => {
+ while (cursor < pending.length) {
+ const [url, pathname] = pending[cursor++], file =
resolve(pathname), response = await context.request.get(url);
+ assert(response.ok(), url); const digest = hash(await
response.body()); assert.equal(digest, hash(fs.readFileSync(file)), url);
+ report.resources.push({url, artifactPath:
path.relative(site, file), sha256: digest});
+ }
+ }));
+ assert.equal(assets.size, pending.length, 'Resource inventory
changed');
+ }
+ check(!report.errors.length, 'Browser/hydration errors',
report.errors);
+ if (requireComplete) check(!report.documentGaps.length, 'Feature
documents still use English fallback', report.documentGaps);
+ } catch (error) {report.failures.push({message: error.message, stack:
error.stack});}
+ finally {
+ await browser?.close(); if (server) await new Promise(resolve =>
server.close(resolve));
+ report.completedAt = new Date().toISOString(); report.passed =
!report.failures.length;
+ fs.writeFileSync(output, JSON.stringify(report, null, 2) + '\n');
+ }
+ return report;
+}
+
+if (require.main === module) {
+ const args = process.argv.slice(2), value = (name, fallback) =>
args.includes(name) ? args[args.indexOf(name) + 1] : fallback;
+ verify({site: value('--site', 'build'), directory: value('--manifests',
'.i18n-build/content-manifests'),
+ output: value('--output', '.i18n-build/product-pages.json'), origin:
value('--origin'), requireComplete:
!args.includes('--allow-document-fallback')})
+ .then(report => {console.log(JSON.stringify({passed: report.passed,
pages: report.pages.length, layouts: report.layouts.length,
+ interactions: report.interactions.length, contentComplete:
report.contentComplete, requireComplete: report.requireComplete,
+ documentGaps: report.documentGaps, failures: report.failures},
null, 2)); if (!report.passed) process.exitCode = 1;})
+ .catch(error => {console.error(error); process.exitCode = 1;});
+}
+module.exports = {verify, staticChecks, assetResolver};
diff --git a/translations/CONTENT_MANIFEST.md b/translations/CONTENT_MANIFEST.md
index 37a73775ebe1..0a2e008e28e5 100644
--- a/translations/CONTENT_MANIFEST.md
+++ b/translations/CONTENT_MANIFEST.md
@@ -230,3 +230,84 @@ Shared UI and homepage maintenance are documented in
[SHARED_UI.md](SHARED_UI.md
Run `node scripts/i18n/manual-report.cjs` for pre-build admission/manual drift
and
add `--coverage` after a native build for versioned coverage. The first-release
procedure and its public source/artifact checks are in
[FIRST_RELEASE.md](FIRST_RELEASE.md).
+
+## Product-page verification
+
+The seven product pages use the global content identity, navigation and
fallback
+contract. Their existing Japanese, Korean and Russian translations are reused;
+page components are shared across languages. Chinese keeps its language URL and
+navigation while displaying the complete English body, with English canonical
+and `noindex`, as declared by `pages/registry.json`.
+
+| Identity | English route |
+| --- | --- |
+| P02 | `/why-doris/compare/` |
+| P03 | `/why-doris/benchmarks/` |
+| P04 | `/why-doris/key-features/` |
+| P06 | `/use-cases/customer-facing-analytics/` |
+| P07 | `/use-cases/data-warehousing/` |
+| P08 | `/use-cases/observability/` |
+| P09 | `/use-cases/ai/` |
+
+`pages/state.json` records the published source and target dependencies. A
+`machine` review records machine review and checks; it does not assert human
+language proofreading. Preserve the previous publication digests when source
+changes need review. Run the manual drift report before publishing an update:
+
+```bash
+yarn key-features:generate
+node scripts/i18n/manual-report.cjs
+```
+
+The product-page verifier consumes the native manifests and a real build. It
+checks five language routes, SSR metadata, Chinese fallback, sitemap inclusion,
+page links, 1440/390 layouts under all eight brand themes, SVG text bounds,
+keyboard menus, language switching with query/hash, comparison tabs and cases,
+benchmark tabs/details, feature search and clearing, and use-case expansion.
+Key Features is enumerated from the actual generator inputs: the initial T10
+review has 47 cards. Each card's title and visible description are compared
with
+the document body selected by the native manifest, including its state and
+review identity. A front-matter-only file cannot satisfy this check.
+
+```bash
+NODE_PATH=/path/to/playwright/node_modules \
+node scripts/i18n/verify-product-pages.cjs \
+ --site /absolute/path/to/site \
+ --manifests /absolute/path/to/content-manifests \
+ --output .i18n-build/product-pages.json
+```
+
+The default check requires current, reviewed Japanese/Korean/Russian documents
+behind every card. `--allow-document-fallback` is an explicit baseline-audit
+option. Its report retains every document gap and sets `contentComplete:
false`;
+such a run cannot close the content task. It does not change page publication
or
+silently remove a card from the denominator.
+
+To verify a production release, download that run's complete site artifact and
+native manifests, confirm its source/asf-site identities, and add
+`--origin https://doris.apache.org`. Public mode compares every product HTML
+response and requested compiled/media resource with that artifact. Resource
+lookup follows the artifact's own optimized redirect and asset-prefix mappings.
+The five manifests must have the same source. A partial CI build cannot
establish
+public acceptance; the site and its publication identity must be verified
first.
+
+The October 25, 2026 baseline reuses production source
+`e083e9d28695878be58f6e69aeb8f8b6106bd448` from
+[run
40140944703](https://github.com/apache/doris-website/actions/runs/40140944703).
+Its seven page bodies, dictionaries, media and generated cards are unchanged at
+T10 baseline `2faa3720f5e2725b04113197b0120e598e25f529`. There are 884
reachable
+body messages per target language. The older 885-key marketing dictionaries
+also contain the legacy `marketing.pageLanguage` label, which the global
+language entry has replaced. Official names, technical terms, units and
+placeholder-only templates retain their intended spelling.
+
+Two document dependencies remain separate from the existing page translations:
+the renamed Changelog / Table Stream documents in Japanese and Korean. Korean
+current/4.x candidates already belong to
+[#4501](https://github.com/apache/doris-website/pull/4501); Japanese
current/4.x
+generation is restricted to those exact source files in
+[run
40153250578](https://github.com/apache/doris-website/actions/runs/40153250578).
+Use the normal complete-document translation and review flow. Do not duplicate
+active candidates or create metadata stubs. After those changes merge,
regenerate
+cards, review the resulting P04 source drift and repeat the default verifier
and
+production acceptance before declaring all 21 translated page units complete.
diff --git a/translations/pages/state.json b/translations/pages/state.json
index ce99ffbc8fda..5082bd2ce499 100644
--- a/translations/pages/state.json
+++ b/translations/pages/state.json
@@ -285,8 +285,10 @@
],
"sourceHash":
"337dfd3f9d86e98e05591d6fdf27f5d460fabb70298294ad7f770a74d74c9dff",
"targetHash":
"5cce8f76159f57206a2a25bff5f3967fa7f7a2335263008fda78f5d0c96f554b",
- "reviewStatus": "unverified",
- "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2"
+ "reviewStatus": "machine",
+ "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2",
+ "reviewedSourceCommit": "2faa3720f5e2725b04113197b0120e598e25f529",
+ "reviewedAt": "2026-10-25T02:17:50.928102+00:00"
},
{
"contentId": "page:P02",
@@ -313,8 +315,10 @@
],
"sourceHash":
"337dfd3f9d86e98e05591d6fdf27f5d460fabb70298294ad7f770a74d74c9dff",
"targetHash":
"43d9c2daa3014f2f97505cdd414d9dad12b2c1bdd95896c2be17b03efd43ba26",
- "reviewStatus": "unverified",
- "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2"
+ "reviewStatus": "machine",
+ "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2",
+ "reviewedSourceCommit": "2faa3720f5e2725b04113197b0120e598e25f529",
+ "reviewedAt": "2026-10-25T02:17:50.928204+00:00"
},
{
"contentId": "page:P02",
@@ -341,8 +345,10 @@
],
"sourceHash":
"337dfd3f9d86e98e05591d6fdf27f5d460fabb70298294ad7f770a74d74c9dff",
"targetHash":
"a2d220c6d05da0525ede6d820fd835f8d5b5af2c233ce790f5ef75f3fa1a88e3",
- "reviewStatus": "unverified",
- "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2"
+ "reviewStatus": "machine",
+ "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2",
+ "reviewedSourceCommit": "2faa3720f5e2725b04113197b0120e598e25f529",
+ "reviewedAt": "2026-10-25T02:17:50.928206+00:00"
},
{
"contentId": "page:P03",
@@ -376,8 +382,10 @@
],
"sourceHash":
"faa2dfb60b7b2f2f263f67eb4dd791feb6b1c763259ce1aacb0280c3f2d1184c",
"targetHash":
"5cce8f76159f57206a2a25bff5f3967fa7f7a2335263008fda78f5d0c96f554b",
- "reviewStatus": "unverified",
- "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2"
+ "reviewStatus": "machine",
+ "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2",
+ "reviewedSourceCommit": "2faa3720f5e2725b04113197b0120e598e25f529",
+ "reviewedAt": "2026-10-25T02:17:50.928207+00:00"
},
{
"contentId": "page:P03",
@@ -395,8 +403,10 @@
],
"sourceHash":
"faa2dfb60b7b2f2f263f67eb4dd791feb6b1c763259ce1aacb0280c3f2d1184c",
"targetHash":
"43d9c2daa3014f2f97505cdd414d9dad12b2c1bdd95896c2be17b03efd43ba26",
- "reviewStatus": "unverified",
- "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2"
+ "reviewStatus": "machine",
+ "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2",
+ "reviewedSourceCommit": "2faa3720f5e2725b04113197b0120e598e25f529",
+ "reviewedAt": "2026-10-25T02:17:50.928208+00:00"
},
{
"contentId": "page:P03",
@@ -414,8 +424,10 @@
],
"sourceHash":
"faa2dfb60b7b2f2f263f67eb4dd791feb6b1c763259ce1aacb0280c3f2d1184c",
"targetHash":
"a2d220c6d05da0525ede6d820fd835f8d5b5af2c233ce790f5ef75f3fa1a88e3",
- "reviewStatus": "unverified",
- "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2"
+ "reviewStatus": "machine",
+ "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2",
+ "reviewedSourceCommit": "2faa3720f5e2725b04113197b0120e598e25f529",
+ "reviewedAt": "2026-10-25T02:17:50.928209+00:00"
},
{
"contentId": "page:P04",
@@ -493,11 +505,13 @@
],
"sourceHash":
"f42c3ba8bac1e0226018673242f4ae3a696e2bc6b3ddc7ba49bbe3ccb2fa1ebe",
"targetHash":
"a2d220c6d05da0525ede6d820fd835f8d5b5af2c233ce790f5ef75f3fa1a88e3",
- "reviewStatus": "unverified",
+ "reviewStatus": "machine",
"compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2",
"derivedDependencies": [
"src/generated/key-features.ts"
- ]
+ ],
+ "reviewedSourceCommit": "2faa3720f5e2725b04113197b0120e598e25f529",
+ "reviewedAt": "2026-10-25T02:17:50.928211+00:00"
},
{
"contentId": "page:P05",
@@ -1151,8 +1165,10 @@
],
"sourceHash":
"572f5460f3e88a1d45232ffa837c236f06d2a8f18132fcd63cade5f19d831500",
"targetHash":
"5cce8f76159f57206a2a25bff5f3967fa7f7a2335263008fda78f5d0c96f554b",
- "reviewStatus": "unverified",
- "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2"
+ "reviewStatus": "machine",
+ "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2",
+ "reviewedSourceCommit": "2faa3720f5e2725b04113197b0120e598e25f529",
+ "reviewedAt": "2026-10-25T02:17:50.928212+00:00"
},
{
"contentId": "page:P06",
@@ -1175,8 +1191,10 @@
],
"sourceHash":
"572f5460f3e88a1d45232ffa837c236f06d2a8f18132fcd63cade5f19d831500",
"targetHash":
"43d9c2daa3014f2f97505cdd414d9dad12b2c1bdd95896c2be17b03efd43ba26",
- "reviewStatus": "unverified",
- "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2"
+ "reviewStatus": "machine",
+ "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2",
+ "reviewedSourceCommit": "2faa3720f5e2725b04113197b0120e598e25f529",
+ "reviewedAt": "2026-10-25T02:17:50.928213+00:00"
},
{
"contentId": "page:P06",
@@ -1199,8 +1217,10 @@
],
"sourceHash":
"572f5460f3e88a1d45232ffa837c236f06d2a8f18132fcd63cade5f19d831500",
"targetHash":
"a2d220c6d05da0525ede6d820fd835f8d5b5af2c233ce790f5ef75f3fa1a88e3",
- "reviewStatus": "unverified",
- "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2"
+ "reviewStatus": "machine",
+ "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2",
+ "reviewedSourceCommit": "2faa3720f5e2725b04113197b0120e598e25f529",
+ "reviewedAt": "2026-10-25T02:17:50.928215+00:00"
},
{
"contentId": "page:P07",
@@ -1244,8 +1264,10 @@
],
"sourceHash":
"82b97c655120ad6aafaa445bc35f4b6a48330ad70aba5e8128c998c7ca3dd92f",
"targetHash":
"5cce8f76159f57206a2a25bff5f3967fa7f7a2335263008fda78f5d0c96f554b",
- "reviewStatus": "unverified",
- "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2"
+ "reviewStatus": "machine",
+ "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2",
+ "reviewedSourceCommit": "2faa3720f5e2725b04113197b0120e598e25f529",
+ "reviewedAt": "2026-10-25T02:17:50.928216+00:00"
},
{
"contentId": "page:P07",
@@ -1268,8 +1290,10 @@
],
"sourceHash":
"82b97c655120ad6aafaa445bc35f4b6a48330ad70aba5e8128c998c7ca3dd92f",
"targetHash":
"43d9c2daa3014f2f97505cdd414d9dad12b2c1bdd95896c2be17b03efd43ba26",
- "reviewStatus": "unverified",
- "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2"
+ "reviewStatus": "machine",
+ "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2",
+ "reviewedSourceCommit": "2faa3720f5e2725b04113197b0120e598e25f529",
+ "reviewedAt": "2026-10-25T02:17:50.928217+00:00"
},
{
"contentId": "page:P07",
@@ -1292,8 +1316,10 @@
],
"sourceHash":
"82b97c655120ad6aafaa445bc35f4b6a48330ad70aba5e8128c998c7ca3dd92f",
"targetHash":
"a2d220c6d05da0525ede6d820fd835f8d5b5af2c233ce790f5ef75f3fa1a88e3",
- "reviewStatus": "unverified",
- "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2"
+ "reviewStatus": "machine",
+ "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2",
+ "reviewedSourceCommit": "2faa3720f5e2725b04113197b0120e598e25f529",
+ "reviewedAt": "2026-10-25T02:17:50.928218+00:00"
},
{
"contentId": "page:P08",
@@ -1337,8 +1363,10 @@
],
"sourceHash":
"41300ff30c8bfa4923eadc74667c4195cc246a20fb823c4edb4f3aaa97124aef",
"targetHash":
"5cce8f76159f57206a2a25bff5f3967fa7f7a2335263008fda78f5d0c96f554b",
- "reviewStatus": "unverified",
- "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2"
+ "reviewStatus": "machine",
+ "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2",
+ "reviewedSourceCommit": "2faa3720f5e2725b04113197b0120e598e25f529",
+ "reviewedAt": "2026-10-25T02:17:50.928219+00:00"
},
{
"contentId": "page:P08",
@@ -1361,8 +1389,10 @@
],
"sourceHash":
"41300ff30c8bfa4923eadc74667c4195cc246a20fb823c4edb4f3aaa97124aef",
"targetHash":
"43d9c2daa3014f2f97505cdd414d9dad12b2c1bdd95896c2be17b03efd43ba26",
- "reviewStatus": "unverified",
- "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2"
+ "reviewStatus": "machine",
+ "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2",
+ "reviewedSourceCommit": "2faa3720f5e2725b04113197b0120e598e25f529",
+ "reviewedAt": "2026-10-25T02:17:50.928220+00:00"
},
{
"contentId": "page:P08",
@@ -1385,8 +1415,10 @@
],
"sourceHash":
"41300ff30c8bfa4923eadc74667c4195cc246a20fb823c4edb4f3aaa97124aef",
"targetHash":
"a2d220c6d05da0525ede6d820fd835f8d5b5af2c233ce790f5ef75f3fa1a88e3",
- "reviewStatus": "unverified",
- "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2"
+ "reviewStatus": "machine",
+ "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2",
+ "reviewedSourceCommit": "2faa3720f5e2725b04113197b0120e598e25f529",
+ "reviewedAt": "2026-10-25T02:17:50.928221+00:00"
},
{
"contentId": "page:P09",
@@ -1424,8 +1456,10 @@
],
"sourceHash":
"15d1b62c09dd2e12754c4219df7a67b426d8088d7b3506837e12171349acec00",
"targetHash":
"5cce8f76159f57206a2a25bff5f3967fa7f7a2335263008fda78f5d0c96f554b",
- "reviewStatus": "unverified",
- "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2"
+ "reviewStatus": "machine",
+ "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2",
+ "reviewedSourceCommit": "2faa3720f5e2725b04113197b0120e598e25f529",
+ "reviewedAt": "2026-10-25T02:17:50.928221+00:00"
},
{
"contentId": "page:P09",
@@ -1445,8 +1479,10 @@
],
"sourceHash":
"15d1b62c09dd2e12754c4219df7a67b426d8088d7b3506837e12171349acec00",
"targetHash":
"43d9c2daa3014f2f97505cdd414d9dad12b2c1bdd95896c2be17b03efd43ba26",
- "reviewStatus": "unverified",
- "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2"
+ "reviewStatus": "machine",
+ "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2",
+ "reviewedSourceCommit": "2faa3720f5e2725b04113197b0120e598e25f529",
+ "reviewedAt": "2026-10-25T02:17:50.928222+00:00"
},
{
"contentId": "page:P09",
@@ -1466,8 +1502,10 @@
],
"sourceHash":
"15d1b62c09dd2e12754c4219df7a67b426d8088d7b3506837e12171349acec00",
"targetHash":
"a2d220c6d05da0525ede6d820fd835f8d5b5af2c233ce790f5ef75f3fa1a88e3",
- "reviewStatus": "unverified",
- "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2"
+ "reviewStatus": "machine",
+ "compatibilitySource": "Existing seven marketing pages at
bb7e86a7ff7b9232af24090928cfbc9a87d8a3b2",
+ "reviewedSourceCommit": "2faa3720f5e2725b04113197b0120e598e25f529",
+ "reviewedAt": "2026-10-25T02:17:50.928223+00:00"
},
{
"contentId": "page:P10",
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]