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]

Reply via email to