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 cf4e0128fafb [i18n] Translate courses, release notes and community 
reports (#4482)
cf4e0128fafb is described below

commit cf4e0128fafb44c8aaaeb83670f94b8ad7a7e2e1
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Fri Oct 23 21:05:24 2026 +0100

    [i18n] Translate courses, release notes and community reports (#4482)
    
    Course lessons, release notes and community reports are outside
    automated translation discovery, so their additions and edits do not
    produce translation tasks. Extend the Japanese, Korean and Russian
    scopes to cover these content types, including the course landing page,
    Hackathon tasks and weekly report interface.
    
    Courses and releases reuse native Docusaurus document identities. Course
    props, TSX prose and weekly report exports use text-unit extraction and
    local assembly, with code, links, IDs and numbers protected. The build
    loader applies current reviewed translations at the original resource
    path. Ordinary pages become available as translated pages after their
    required interface/content groups are current.
    
    Japanese, Korean and Russian discovery all use the shared pipeline on
    twice-daily schedules and manual dispatch. Russian runs at 05:03 and
    17:03 UTC (01:03 and 13:03 Asia/Shanghai), twenty minutes after Korean
    discovery, under the same docs-translation concurrency lock. Scheduled
    runs and default manual runs use generate; dry-run remains selectable.
    Translation candidates will be generated by these workflows after merge.
    
    ## Validation
    
    - All locale configurations pass `node
    scripts/i18n/check-configuration.cjs --all`.
    - `node --test scripts/i18n/__tests__/*.test.cjs`: 233 passed; the
    optional production-build test was run separately.
    - English and Japanese Docusaurus production fixture builds pass,
    rendering translated TSX and MDX report exports while preserving English
    output.
    - Every existing Doris 101 module, course/Hackathon source module and
    weekly report passes a mock translation round trip with
    protected-content checks.
    - A real read-only inventory/planner run discovers 121 additional
    sources: 6 lessons, 83 release documents, 15 course/Hackathon modules,
    16 reports and 1 report interface module. All five new groups have zero
    unregistered sources.
    - A schedule-mode discovery check using that inventory selects
    representative lesson, release, course UI, report body and report UI
    tasks and reports `can_generate=true`. No live model requests or
    translation workflow dispatches were made.
    - Russian scheduling follow-up: 55 workflow, generation and
    Russian-toolchain tests pass, including all three locale schedules,
    generation defaults, the shared lock and master-only admission.
    - Build Check fix: the Russian browser check required an English
    fallback linked from the Dev "What is Apache Doris" page. #4475
    completed the Russian Dev translations, so this PR's full build found
    none. The check now accepts a linked fallback from any docs version: the
    version menu links the 3.x and 2.1 pages, which no translation scope
    covers. Dev-only builds skip the Russian requirement. Replaying the
    click, canonical/noindex/alternate and reload steps on doris.apache.org
    passes for ru, ko and ja with no hydration errors; Build Check reruns
    the full browser verification.
    - `git diff --check` passes.
    
    ## Review checkpoints
    
    1. Goal: discovery, planning, generation, open-PR reservations,
    admission and rendering accept the added content formats. Tests cover
    new source files, updates to an existing report, and scheduled
    generation for all three locales.
    2. Scope: the additional source groups and text-unit adapter are needed
    because report exports and course UI are executable MDX/TSX rather than
    ordinary Markdown documents.
    3. Information architecture: existing document plugins, sidebars,
    versions and localized directory conventions remain authoritative.
    Supplemental modules create translation identities without adding public
    routes.
    4. Links and routing: public paths and weekly report hashes remain
    stable. Course landing links now use Docusaurus Link so lesson
    navigation retains the selected locale.
    5. Frontend: the existing responsive DOM, styles and semantic theme
    tokens are retained. SSR and client compilation pass in the production
    fixtures.
    6. Validation: automated tests, production fixtures and the real content
    inventory cover the changed pipeline. Generated articles will still
    receive normal translation PR checks and proofreading.
    7. Other issues: source freshness, ownership, escaping and
    executable-content checks are enforced before translated modules
    compile. The existing master-only checkout, permissions and explicit
    secrets remain covered by workflow tests. Self-review found no
    additional correctness, accessibility, SEO or maintainability findings.
    
    ## Versions
    
    - [x] dev
    - [x] 4.x
    - [ ] 3.x
    - [ ] 2.1 or older (not covered by version/language sync gate)
    
    This is shared translation infrastructure plus unversioned course,
    release and report content scope; it does not edit versioned article
    text.
    
    ## Languages
    
    - [ ] Chinese
    - [x] English
    - [x] Japanese
    - [x] Korean
    - [x] Russian
    
    ## Docs Checklist
    
    - [x] Checked by AI
    - [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
    
    All three managed locale scopes are updated together. The English
    developer guide describes the shared workflow; no article translations
    are included in this infrastructure change.
    
    ---------
    
    Co-authored-by: morningman <[email protected]>
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .github/workflows/japanese-translation.yml         |   4 +-
 .github/workflows/korean-translation.yml           |   4 +-
 .github/workflows/russian-translation.yml          |  13 +-
 .github/workflows/translation-run.yml              |   4 +-
 developer_docs/translation-content-scope.md        |  57 ++++++
 docusaurus.config.js                               |   7 +-
 scripts/i18n/__tests__/doc-navigation.test.cjs     |   5 +-
 scripts/i18n/__tests__/expanded-scope.test.cjs     | 215 +++++++++++++++++++++
 scripts/i18n/__tests__/multi-locale.test.cjs       |   2 +-
 .../i18n/__tests__/translation-workflows.test.cjs  |  16 +-
 scripts/i18n/audit-coverage.cjs                    |   6 +-
 scripts/i18n/build-scope.cjs                       |   2 +-
 scripts/i18n/check-translations.cjs                |   2 +-
 scripts/i18n/code-prose.cjs                        | 105 ++++++++++
 scripts/i18n/content-loader.cjs                    |  33 ++++
 scripts/i18n/content-plugin.cjs                    |  20 ++
 scripts/i18n/document-checks.cjs                   |   1 +
 scripts/i18n/inventory.cjs                         |  13 +-
 scripts/i18n/master-drift.cjs                      |   1 +
 scripts/i18n/paths.cjs                             |   4 +-
 scripts/i18n/plan-translations.cjs                 |   5 +-
 scripts/i18n/pr-preflight.cjs                      |   4 +-
 scripts/i18n/protection.cjs                        |  24 ++-
 scripts/i18n/source-groups.cjs                     |  86 +++++++++
 scripts/i18n/status-plugin.cjs                     |   2 +-
 scripts/i18n/structure.cjs                         |   7 +
 scripts/i18n/translate-docs.cjs                    |   7 +-
 scripts/i18n/translation-engine.cjs                |  29 ++-
 scripts/i18n/verify-doc-navigation.cjs             |  13 +-
 scripts/i18n/verify-search.cjs                     |   6 +-
 scripts/i18n/verify-static.cjs                     |   3 +-
 scripts/i18n/workflow-generate.cjs                 |   2 +-
 scripts/i18n/workflow-prs.cjs                      |   2 +-
 scripts/i18n/workflow-scan.cjs                     |   5 +-
 src/components/course/CourseLanding.tsx            |  11 +-
 translations/README.md                             |  23 ++-
 translations/scope/ja.json                         |  44 +++++
 translations/scope/ko.json                         |  44 +++++
 translations/scope/ru.json                         |  44 +++++
 39 files changed, 805 insertions(+), 70 deletions(-)

diff --git a/.github/workflows/japanese-translation.yml 
b/.github/workflows/japanese-translation.yml
index 6ff024927529..83f240211af3 100644
--- a/.github/workflows/japanese-translation.yml
+++ b/.github/workflows/japanese-translation.yml
@@ -10,7 +10,7 @@ on:
         default: generate
         required: true
       plugin:
-        description: Optional plugin filter (default or community)
+        description: Optional plugin filter (default, community, course, 
releases, course-ui, community-report, community-report-ui)
         type: string
       version:
         description: Optional version filter (current or 4.x)
@@ -19,7 +19,7 @@ on:
         description: Optional repository-relative source path prefix
         type: string
       paths:
-        description: Optional JSON array of exact English .md/.mdx files 
(exclusive with path)
+        description: Optional JSON array of exact English .md/.mdx/.ts/.tsx 
files (exclusive with path)
         type: string
   schedule:
     - cron: '23 4,16 * * *'
diff --git a/.github/workflows/korean-translation.yml 
b/.github/workflows/korean-translation.yml
index 2b764f575482..cee0e6c85df3 100644
--- a/.github/workflows/korean-translation.yml
+++ b/.github/workflows/korean-translation.yml
@@ -10,7 +10,7 @@ on:
         default: generate
         required: true
       plugin:
-        description: Optional plugin filter (default or community)
+        description: Optional plugin filter (default, community, course, 
releases, course-ui, community-report, community-report-ui)
         type: string
       version:
         description: Optional version filter (current or 4.x)
@@ -19,7 +19,7 @@ on:
         description: Optional repository-relative source path prefix
         type: string
       paths:
-        description: Optional JSON array of exact English .md/.mdx files 
(exclusive with path)
+        description: Optional JSON array of exact English .md/.mdx/.ts/.tsx 
files (exclusive with path)
         type: string
   # Twenty minutes after the Japanese schedule; the shared lock below holds
   # this run while a Japanese run is still in progress.
diff --git a/.github/workflows/russian-translation.yml 
b/.github/workflows/russian-translation.yml
index 508545b7a3e6..1fb04c1e0118 100644
--- a/.github/workflows/russian-translation.yml
+++ b/.github/workflows/russian-translation.yml
@@ -7,10 +7,10 @@ on:
         description: Read-only dry run, one-page live validation, or batch 
generation
         type: choice
         options: [dry-run, validate, generate]
-        default: dry-run
+        default: generate
         required: true
       plugin:
-        description: Optional plugin filter (default or community)
+        description: Optional plugin filter (default, community, course, 
releases, course-ui, community-report, community-report-ui)
         type: string
       version:
         description: Optional version filter (current or 4.x)
@@ -19,8 +19,12 @@ on:
         description: Optional repository-relative source path prefix
         type: string
       paths:
-        description: Optional JSON array of exact English .md/.mdx files 
(exclusive with path)
+        description: Optional JSON array of exact English .md/.mdx/.ts/.tsx 
files (exclusive with path)
         type: string
+  # Twenty minutes after the Korean schedule; the shared lock queues this
+  # run while another language's discovery or generation is still in progress.
+  schedule:
+    - cron: '3 5,17 * * *'
 
 permissions:
   contents: read
@@ -38,7 +42,8 @@ jobs:
     with:
       locale: ru
       workspace: russian-translation
-      mode: ${{ inputs.mode || 'dry-run' }}
+      # Scheduled runs have no inputs and generate.
+      mode: ${{ inputs.mode || 'generate' }}
       plugin: ${{ inputs.plugin }}
       version: ${{ inputs.version }}
       path: ${{ inputs.path }}
diff --git a/.github/workflows/translation-run.yml 
b/.github/workflows/translation-run.yml
index 0d12d99ccd0a..cc88587d32b7 100644
--- a/.github/workflows/translation-run.yml
+++ b/.github/workflows/translation-run.yml
@@ -19,7 +19,7 @@ on:
         type: string
         required: true
       plugin:
-        description: Optional plugin filter (default or community)
+        description: Optional plugin filter (default, community, course, 
releases, course-ui, community-report, community-report-ui)
         type: string
         default: ''
       version:
@@ -31,7 +31,7 @@ on:
         type: string
         default: ''
       paths:
-        description: Optional JSON array of exact English .md/.mdx files 
(exclusive with path)
+        description: Optional JSON array of exact English .md/.mdx/.ts/.tsx 
files (exclusive with path)
         type: string
         default: ''
     secrets:
diff --git a/developer_docs/translation-content-scope.md 
b/developer_docs/translation-content-scope.md
new file mode 100644
index 000000000000..e008492116c4
--- /dev/null
+++ b/developer_docs/translation-content-scope.md
@@ -0,0 +1,57 @@
+# Automated translation content scope
+
+The Japanese, Korean and Russian pipelines use 
`translations/scope/<locale>.json`
+to discover source additions, edits and removals. All three languages run twice
+daily and support manual dispatch. Japanese runs at 04:23 and 16:23 UTC, Korean
+at 04:43 and 16:43 UTC, and Russian at 05:03 and 17:03 UTC. The shared
+`docs-translation` lock queues discovery and generation across all three 
languages.
+Scheduled runs and default manual runs use `generate`; select `dry-run` 
explicitly
+to inspect pending work without model calls.
+
+In addition to Dev, 4.x and community documentation, each scope includes:
+
+| Content | Source | Translation group |
+| --- | --- | --- |
+| Doris 101 lessons | `course/` | `COURSE` |
+| Release notes | `releasenotes/` | `RELEASES` |
+| Course landing page, lesson controls and Hackathon tasks | 
`src/components/course/`, `src/pages/course/` | `COURSEUI` |
+| Weekly report bodies and exported metadata | 
`src/pages/community-report/_reports/` | `REPORT` |
+| Weekly report interface | `src/components/community-report-next/`, 
`src/pages/community-report/` | `REPORTUI` |
+
+`COURSE` and `RELEASES` use native Docusaurus document identities and sidebars.
+The other groups describe source modules or MDX partials, rather than new 
public
+routes. Templates, tests and declaration files are excluded. TypeScript modules
+without visible prose do not create translation tasks.
+
+Course props, JSX text and the static `label`, `week`, `stats` and `report` 
exports
+contain prose inside executable source. These groups use extracted text units.
+The model receives text values; the pipeline escapes and inserts translations
+into the original source. Code, property names, URLs, IDs, numbers and code 
samples
+remain protected. Unchanged text units reuse the previous translation.
+
+Targets use the existing 
`i18n/<locale>/docusaurus-plugin-content-docs-<plugin>/current/`
+layout and page state records. Open-PR reservations, source hashes, ownership 
and
+publication checks apply to these targets too. At build time, the content 
loader
+uses only a current, reviewed target and checks protected content again before
+compilation. Imports resolve relative to the original source module. Missing or
+stale targets use the current English source.
+
+Course and Hackathon ordinary pages are advertised as translated after the
+course interface group is current. The weekly report page requires both its
+interface and report groups to be current. Until then, the existing English
+fallback routing and SEO behavior continue to apply. Individual translated
+lessons use the native document translation status independently.
+
+To inspect a specific source without generating translations, choose `dry-run`
+in a language's discovery workflow and set `path`, or supply an exact JSON list
+in `paths`. These filters accept Markdown, MDX, TypeScript and TSX source 
files;
+TypeScript files must belong to a configured `code` group. Scheduled runs scan
+the entire current scope, including newly added source files.
+
+Validation:
+
+```sh
+node scripts/i18n/check-configuration.cjs --all
+node --test scripts/i18n/__tests__/*.test.cjs
+I18N_VERIFY_CONTENT_BUILD=true node --test --test-name-pattern='production 
builds render' scripts/i18n/__tests__/expanded-scope.test.cjs
+```
diff --git a/docusaurus.config.js b/docusaurus.config.js
index 7158f0889244..6589a9d04fa0 100644
--- a/docusaurus.config.js
+++ b/docusaurus.config.js
@@ -10,6 +10,8 @@ const REDIRECTS_4X = require('./config/redirects-4.x.json');
 const path = require('path');
 const translationDocsPlugin = require('./scripts/i18n/docs-plugin.cjs');
 const translationStatusPlugin = require('./scripts/i18n/status-plugin.cjs');
+const contentTranslationPlugin = require('./scripts/i18n/content-plugin.cjs');
+const {translatedContentPages} = require('./scripts/i18n/source-groups.cjs');
 const searchPlugin = require('./scripts/i18n/search-plugin.cjs');
 const {enabledLocales, enabledLocaleRecords, hasBlogTranslations} = 
require('./config/i18n');
 const {managedLocales} = require('./scripts/i18n/state.cjs');
@@ -370,14 +372,15 @@ const config = {
         defaultLocalePageLocales: enabledLocaleRecords()
             .filter(locale => locale.ordinaryPages === false).map(locale => 
locale.code),
         translatedPagePaths: Object.fromEntries(enabledLocaleRecords()
-            .filter(locale => locale.translatedPages?.length)
-            .map(locale => [locale.code, locale.translatedPages])),
+            .map(locale => [locale.code, [...(locale.translatedPages ?? []), 
...translatedContentPages(__dirname, locale.code)]])
+            .filter(([, pages]) => pages.length)),
         defaultLocaleBlogLocales: DEFAULT_LOCALE_BLOG_LOCALES,
     },
     future: {
         experimental_faster: true,
     },
     plugins: [
+        contentTranslationPlugin,
         SKIP_DOCS ? null : [translationDocsPlugin, MAIN_DOCS_OPTIONS],
         MANAGED_LOCALES.length ? translationStatusPlugin : null,
         'docusaurus-plugin-sass',
diff --git a/scripts/i18n/__tests__/doc-navigation.test.cjs 
b/scripts/i18n/__tests__/doc-navigation.test.cjs
index 0e5e4da028e9..1e7404b4a1f8 100644
--- a/scripts/i18n/__tests__/doc-navigation.test.cjs
+++ b/scripts/i18n/__tests__/doc-navigation.test.cjs
@@ -40,10 +40,11 @@ test('navigation samples a native sidebar member when a 
current standalone doc s
     write('site/ru/docs/index.html', 
createRedirect({toUrl:pages[2].permalink}));
     write('site/ru/community/index.html', 
createRedirect({toUrl:pages[3].permalink}));
     write('site/.publish/redirects.json', JSON.stringify({redirects:[]}));
-    const 
result=verify({site:path.join(root,'site'),locale:'ru',snapshot:path.join(root,'snapshot'),manifest:path.join(root,'manifest.json')});
+    const scope = {groups: 
require('../../../translations/scope/ru.json').groups.filter(group => 
['DEV','4X','COMMUNITY'].includes(group.id))};
+    const 
result=verify({site:path.join(root,'site'),locale:'ru',snapshot:path.join(root,'snapshot'),manifest:path.join(root,'manifest.json'),scope});
     assert.equal(result.routes,4);
     assert.equal(result.checks.find(check=>check.group === 
'DEV').sample,pages[1].permalink);
-    const 
options={site:path.join(root,'site'),locale:'ru',snapshot:path.join(root,'snapshot'),manifest:path.join(root,'manifest.json'),devOnly:true};
+    const 
options={site:path.join(root,'site'),locale:'ru',snapshot:path.join(root,'snapshot'),manifest:path.join(root,'manifest.json'),devOnly:true,scope};
     fs.unlinkSync(path.join(root,'site/ru/community/index.html'));
     assert.throws(()=>verify(options), /Missing compacted entry: 
\/ru\/community\//);
     write('site/.publish/redirects.json', 
JSON.stringify({redirects:[['/ru/community/',pages[3].permalink,true,301]]}));
diff --git a/scripts/i18n/__tests__/expanded-scope.test.cjs 
b/scripts/i18n/__tests__/expanded-scope.test.cjs
new file mode 100644
index 000000000000..921501d0816a
--- /dev/null
+++ b/scripts/i18n/__tests__/expanded-scope.test.cjs
@@ -0,0 +1,215 @@
+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, stateFileFor} = require('../state.cjs');
+const {structure} = require('../structure.cjs');
+const {protectionSignature} = require('../protection.cjs');
+const {extractUnits, translateDocument, managedPath} = 
require('../translation-engine.cjs');
+const {contentGroups, sourceFiles, hasProse, pageFor, collectSources, 
translatedContentPages} = require('../source-groups.cjs');
+const {localizedSource} = require('../content-loader.cjs');
+const {relevantPush} = require('../workflow-scan.cjs');
+const {collect} = require('../inventory.cjs');
+const {audit} = require('../audit-coverage.cjs');
+const {plan} = require('../plan-translations.cjs');
+const {prepare, readState, applyCandidates, targetFor} = 
require('../translate-docs.cjs');
+const {managedTarget} = require('../workflow-prs.cjs');
+
+const repo = path.resolve(__dirname, '../../..');
+const read = file => fs.readFileSync(path.join(repo, file), 'utf8');
+const glossary = {schemaVersion: 1, version: 'fixture', keep: [], terms: []};
+const adapter = {model: 'fixture', async translate({units}) {
+    return {translations: units.map(unit => ({id: unit.id, text: `日本語 
${unit.text}`}))};
+}};
+const translate = (source, extension) => translateDocument({source, extension, 
adapter, glossary,
+    targetLocale: 'ja', targetScriptPattern: '[日]', maxDocumentChars: 300000});
+
+test('all existing course modules, course/Hackathon UI and weekly reports 
translate without changing code', async () => {
+    const groups = contentGroups(repo, 'ja');
+    const files = groups.flatMap(group => sourceFiles(repo, group).filter(file 
=> hasProse(repo, group, file)))
+        .concat(fs.readdirSync(path.join(repo, 'course/101')).map(name => 
`course/101/${name}`));
+    assert(files.some(file => file.endsWith('CourseLanding.tsx')));
+    assert(files.some(file => file.endsWith('a1HybridSearch.tsx')));
+    assert(!files.some(file => file.includes('_TEMPLATE') || 
/\.test\./.test(file)));
+    for (const file of files) {
+        const source = read(file), extension = path.extname(file);
+        const result = await translate(source, extension);
+        assert(result.targetChanged, file);
+        assert.equal(protectionSignature(await structure(result.target, 
extension)),
+            protectionSignature(await structure(source, extension)), file);
+    }
+});
+
+test('JSX text is escaped, object keys/URLs/SQL/logic are immutable, and 
updates reuse unchanged units', async () => {
+    const source = `import React from 'react';
+const data = {"title": "A title", href: '/course/101/start', sql: 'SELECT 1;', 
count: 3};
+export default function Page(){ return <section className={'fixed'} 
aria-label="A label"><h1>Start here</h1><code>SELECT 
1;</code><p>{data.title}</p></section>; }
+`;
+    const units = await extractUnits(source, '.tsx');
+    assert.deepEqual(units.map(unit => unit.text), ['A title', 'A label', 
'Start here']);
+    const result = await translateDocument({source, extension: '.tsx', 
glossary, targetLocale: 'ja', targetScriptPattern: '[日]',
+        adapter: {async translate({units}) {return {translations: 
units.map(unit => ({id: unit.id, text: `日本語 <>&{}" ${unit.text}`}))};}}});
+    assert(result.target.includes('&lt;&gt;&amp;&#123;&#125;&quot;'));
+    assert(result.target.includes('"title":'));
+    for (const change of [text => text.replace('SELECT 1;', 'SELECT 2;'), text 
=> text.replace("'/course/101/start'", "'/evil'"),
+        text => text.replace("className={'fixed'}", "className={'changed'}"), 
text => text.replace('count: 3', 'count: 4')]) {
+        assert.notEqual(protectionSignature(await 
structure(change(result.target), '.tsx')), protectionSignature(await 
structure(source, '.tsx')));
+    }
+    const updated = source.replace('Start here', 'Continue here');
+    let requested;
+    const next = await translateDocument({source: updated, extension: '.tsx', 
previousSource: source, previousTarget: result.target,
+        glossary, targetLocale: 'ja', targetScriptPattern: '[日]', adapter: 
{async translate({units}) {
+            requested = units; return {translations: units.map(unit => ({id: 
unit.id, text: `日本語 ${unit.text}`}))};
+        }}});
+    assert.deepEqual(requested.map(unit => unit.text), ['Continue here']);
+    assert.equal(next.reusedUnits, 2);
+});
+
+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);};
+    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']) {
+        const scope = JSON.parse(read(`translations/scope/${locale}.json`));
+        scope.groups = scope.groups.filter(group => !['DEV', '4X', 
'COMMUNITY'].includes(group.id));
+        scope.retainSidebarLabels = [];
+        write(`translations/scope/${locale}.json`, JSON.stringify(scope));
+        write(`translations/state/${locale}/docs.json`, 
'{"schemaVersion":1,"pages":[]}');
+        write(`translations/state/${locale}/resolved-deletions.json`, 
'{"schemaVersion":1,"deletions":[]}');
+    }
+    write('course/lesson.mdx', '---\ntitle: Course\n---\n\nLearn Doris.\n');
+    write('releasenotes/release.md', '---\ntitle: Release\n---\n\nRelease 
details.\n');
+    write('src/components/course/CourseLanding.tsx', "import React from 
'react'; export default function Course(){return <h1>Learn Doris</h1>;}\n");
+    write('src/components/community-report-next/Report.tsx', "import React 
from 'react'; export default function Report(){return <h1>Weekly 
report</h1>;}\n");
+    write('src/pages/community-report/_reports/2026-10-12.mdx', 'export const 
label = "Week 42";\n\nexport const report = {summary:{lead:"This 
week"},refs:[{num:123,url:"https://github.com/apache/doris/pull/123",title:"A 
feature"}]};\n');
+    write('src/pages/community-report/_reports/_TEMPLATE.mdx', 'Not 
published.\n');
+    write('src/pages/index.tsx', "import React from 'react'; import Course 
from '../components/course/CourseLanding'; export default function 
Home(){return <Course/>;}\n");
+    write('versions.json', '[]');
+    for (const file of ['sidebars.ts', 'sidebarsCommunity.json', 
'sidebarsCourse.ts', 'sidebarsReleases.json', 
'versioned_sidebars/version-4.x-sidebars.json']) write(file, '{}');
+    write('docusaurus.config.js', 
`module.exports={title:'Fixture',url:'https://doris.apache.org',baseUrl:'/',trailingSlash:true,
+i18n:{defaultLocale:'en',locales:['en','zh-CN','ja','ko','ru']},
+plugins:[require(${JSON.stringify(path.join(repo, 
'scripts/i18n/content-plugin.cjs'))}),
+[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};
+}
+
+test('native discovery, scheduling, planning and generation recognize all 
three content classes and source updates', async t => {
+    const {root, write, git, commit} = fixture(t);
+    const previousLocale = process.env.I18N_LOCALE;
+    process.env.I18N_LOCALE = 'ja';
+    t.after(() => {if (previousLocale === undefined) delete 
process.env.I18N_LOCALE; else process.env.I18N_LOCALE = previousLocale;});
+    const sourceCommit = commit();
+    const baseline = path.join(root, 'baseline');
+    write('baseline/summary.json', JSON.stringify({sourceCommit}));
+    for (const name of ['dev', '4x', 'community']) 
write(`baseline/${name}.json`, '{"entryFiles":[]}');
+    const scope = JSON.parse(fs.readFileSync(path.join(root, 
'translations/scope/ja.json'), 'utf8'));
+    const native = await collect(root);
+    assert.deepEqual(new Set(native.pages.map(page => page.pluginId)), new 
Set(scope.groups.map(group => group.pluginId)));
+    let report = await audit({root, sourceRef: sourceCommit, baseline, 
native});
+    const options = {scope, records: new Map(), active: {schemaVersion: 1, 
candidates: []},
+        readCurrent: file => fs.existsSync(path.join(root, file)) ? 
fs.readFileSync(path.join(root, file), 'utf8') : null,
+        readAt: (ref, file) => {try {return git('show', `${ref}:${file}`) + 
'\n';} catch {return null;}}};
+    let planned = await plan({...options, report});
+    assert.equal(planned.selected.length, 5);
+    for (const task of planned.tasks) {
+        assert(relevantPush([task.sourcePath], report, scope), 
task.sourcePath);
+        assert(managedTarget(scope, task.targetPath), task.targetPath);
+        assert.equal(managedPath(scope, task), task.targetPath);
+    }
+    const task = planned.tasks.find(task => task.pluginId === 
'community-report');
+    const state = readState(root, 'ja');
+    const candidate = await prepare({root, plan: planned, taskId: task.id, 
scope, state, adapter,
+        glossary, target: targetFor(root, 'ja')});
+    applyCandidates(state, [candidate]);
+    assert(candidate.target.includes('日本語 This week'));
+    assert(candidate.target.includes('num:123'));
+    
assert(candidate.target.includes('https://github.com/apache/doris/pull/123'));
+    commit();
+    const source = fs.readFileSync(path.join(root, task.sourcePath), 'utf8');
+    write(task.sourcePath, source.replace('This week', 'A new development this 
week'));
+    const changedCommit = commit();
+    report = await audit({root, sourceRef: changedCommit, baseline, native: 
await collect(root)});
+    planned = await plan({...options, report, records: new 
Map([[identity(candidate.record), candidate.record]])});
+    assert.equal(planned.tasks.find(item => item.sourcePath === 
task.sourcePath).kind, 'update');
+    assert(relevantPush([task.sourcePath], report, scope));
+});
+
+test('the render loader uses only current reviewed translations, watches 
missing targets and rejects executable changes', async t => {
+    const {root, write} = fixture(t);
+    const group = contentGroups(root, 'ja').find(group => group.pluginId === 
'course-ui');
+    const sourcePath = 'src/components/course/CourseLanding.tsx';
+    const source = fs.readFileSync(path.join(root, sourcePath), 'utf8');
+    const page = pageFor(group, sourcePath, 'ja');
+    const input = {root, locale: 'ja', file: path.join(root, sourcePath), 
source};
+    const dependencies = [];
+    assert.equal(await localizedSource({...input, dependency: file => 
dependencies.push(file)}), source);
+    assert(dependencies.includes(path.join(root, page.targetPath)));
+    const target = (await translate(source, '.tsx')).target;
+    const record = {...page, sourceHash: hash(source), targetHash: 
hash(target), sourceCommit: 'a'.repeat(40),
+        sourceTechnicalHash: (await structure(source, '.tsx')).technicalHash, 
reviewStatus: 'machine', ownership: 'machine',
+        translatedAt: '2026-10-23T00:00:00Z', glossaryVersion: 'fixture'};
+    write(page.targetPath, target);
+    const save = () => write(stateFileFor(page), 
JSON.stringify({schemaVersion: 1, page: record}));
+    save();
+    assert.equal(await localizedSource(input), target);
+    assert(translatedContentPages(root, 'ja').includes('/course'));
+    assert.equal(await localizedSource({...input, source: 
source.replace('Learn Doris', 'Learn more')}), source.replace('Learn Doris', 
'Learn more'));
+    assert.equal(await localizedSource({...input, locale: 'en'}), source);
+    const tampered = target.replace('<h1>', '<h1 onClick={() => alert(1)}>');
+    write(page.targetPath, tampered); record.targetHash = hash(tampered); 
save();
+    await assert.rejects(() => localizedSource(input), /Protected executable 
content/);
+});
+
+test('a later scan discovers new report dates and course route modules while 
keeping other site pages outside the scope', async t => {
+    const {root, write} = fixture(t);
+    const before = await collectSources(root, 'ja');
+    write('src/pages/course/new-task.tsx', "export default function 
Task(){return <h1>A new task</h1>;}\n");
+    write('src/pages/community-report/_reports/2026-10-19.mdx', 'export const 
label = "Week 43";\n\nexport const report = {summary:{lead:"Next week"}};\n');
+    write('src/pages/unrelated.tsx', "export default function Page(){return 
<h1>Outside the scope</h1>;}\n");
+    const after = await collectSources(root, 'ja');
+    const added = after.pages.filter(page => !before.pages.some(previous => 
identity(previous) === identity(page)));
+    assert.deepEqual(new Set(added.map(page => page.sourcePath)), new Set([
+        'src/pages/course/new-task.tsx', 
'src/pages/community-report/_reports/2026-10-19.mdx']));
+    assert(added.every(page => page.status === 'untranslated'));
+    const scope = JSON.parse(fs.readFileSync(path.join(root, 
'translations/scope/ja.json'), 'utf8'));
+    assert.throws(() => managedPath(scope, {...added[0], sourcePath: 
'src/pages/unrelated.tsx'}), /managed include/);
+});
+
+test('production builds render translated course TSX and report exports 
without changing the English build',
+    {skip: process.env.I18N_VERIFY_CONTENT_BUILD !== 'true'}, async t => {
+        const {root, write} = fixture(t);
+        write('src/pages/index.tsx', `import React from 'react';
+import Course from '../components/course/CourseLanding';
+import Report from '../components/community-report-next/Report';
+import {report} from './community-report/_reports/2026-10-12.mdx';
+export default function Home(){return 
<main><Course/><Report/><p>{report.summary.lead}</p></main>;}\n`);
+        for (const group of contentGroups(root, 'ja')) for (const file of 
sourceFiles(root, group)) {
+            const source = fs.readFileSync(path.join(root, file), 'utf8');
+            const page = pageFor(group, file, 'ja'), target = (await 
translate(source, path.extname(file))).target;
+            write(page.targetPath, target);
+            const record = {...page, sourceHash: hash(source), targetHash: 
hash(target), sourceCommit: 'a'.repeat(40),
+                sourceTechnicalHash: (await structure(source, 
path.extname(file))).technicalHash,
+                reviewStatus: 'machine', ownership: 'machine', translatedAt: 
'2026-10-23T00:00:00Z', glossaryVersion: 'fixture'};
+            write(stateFileFor(page), JSON.stringify({schemaVersion: 1, page: 
record}));
+        }
+        for (const locale of ['ja', 'en']) {
+            const output = path.join(root, `build-${locale}`);
+            const result = 
require('node:child_process').spawnSync(process.execPath,
+                [path.join(repo, 
'node_modules/@docusaurus/core/bin/docusaurus.mjs'), 'build', root, '--locale', 
locale, '--out-dir', output],
+                {cwd: root, encoding: 'utf8', env: {...process.env, NODE_ENV: 
'production'}, maxBuffer: 16 * 1024 * 1024});
+            assert.equal(result.status, 0, (result.stdout + 
result.stderr).slice(-12000));
+            const html = fs.readFileSync(path.join(output, 'index.html'), 
'utf8');
+            for (const message of ['Learn Doris', 'Weekly report', 'This 
week'])
+                assert(html.includes(locale === 'ja' ? `日本語 ${message}` : 
message), `${locale}: ${message}`);
+            if (locale === 'en') assert(!html.includes('日本語'));
+        }
+    });
diff --git a/scripts/i18n/__tests__/multi-locale.test.cjs 
b/scripts/i18n/__tests__/multi-locale.test.cjs
index 58db92a2cbf2..1c0fac66e63e 100644
--- a/scripts/i18n/__tests__/multi-locale.test.cjs
+++ b/scripts/i18n/__tests__/multi-locale.test.cjs
@@ -205,7 +205,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'])
+    for (const file of ['inventory.cjs', 'state.cjs', 'source-groups.cjs', 
'code-prose.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__/translation-workflows.test.cjs 
b/scripts/i18n/__tests__/translation-workflows.test.cjs
index 06ef5e1d8666..f760f81c5215 100644
--- a/scripts/i18n/__tests__/translation-workflows.test.cjs
+++ b/scripts/i18n/__tests__/translation-workflows.test.cjs
@@ -9,23 +9,23 @@ const {allowGeneration} = require('../workflow-generate.cjs');
 const workflows = path.resolve(__dirname, '../../../.github/workflows');
 const load = name => yaml.load(fs.readFileSync(path.join(workflows, name), 
'utf8'));
 const shared = load('translation-run.yml');
-const callers = {ja: load('japanese-translation.yml'), ko: 
load('korean-translation.yml')};
-const russian = load('russian-translation.yml');
+const callers = {ja: load('japanese-translation.yml'), ko: 
load('korean-translation.yml'), ru: load('russian-translation.yml')};
+const russian = callers.ru;
 
-test('Russian discovery is manual only and uses the shared run, lock and 
explicit secrets', () => {
-    assert.deepEqual(Object.keys(russian.on), ['workflow_dispatch']);
+test('Russian scheduled and manual generation use the shared run, lock and 
explicit secrets', () => {
+    assert.deepEqual(Object.keys(russian.on), ['workflow_dispatch', 
'schedule']);
     const inputs = russian.on.workflow_dispatch.inputs;
     const koreanInputs = callers.ko.on.workflow_dispatch.inputs;
-    assert.deepEqual(inputs, {...koreanInputs, mode: {...koreanInputs.mode, 
default: 'dry-run'}});
+    assert.deepEqual(inputs, koreanInputs);
     assert.deepEqual(russian.permissions, shared.permissions);
     for (const workflow of Object.values(callers)) 
assert.deepEqual(russian.concurrency, workflow.concurrency);
     assert.deepEqual(Object.keys(russian.jobs), ['translation']);
     const job = russian.jobs.translation;
     assert.equal(job.uses, './.github/workflows/translation-run.yml');
     assert.deepEqual(job.with, {...callers.ko.jobs.translation.with, locale: 
'ru', workspace: 'russian-translation',
-        mode: "${{ inputs.mode || 'dry-run' }}"});
+        mode: "${{ inputs.mode || 'generate' }}"});
     assert.deepEqual(job.secrets, callers.ko.jobs.translation.secrets);
-    assert.equal(allowGeneration({I18N_MODE: inputs.mode.default, 
GITHUB_EVENT_NAME: 'workflow_dispatch'}), false);
+    assert.equal(allowGeneration({I18N_MODE: inputs.mode.default, 
GITHUB_EVENT_NAME: 'workflow_dispatch'}), true);
     assert.equal(allowGeneration({I18N_MODE: 'validate', GITHUB_EVENT_NAME: 
'workflow_dispatch', I18N_MAX_PAGES: '1',
         I18N_PATH: 'community/how-to-contribute/how-to-share-blogs.md'}), 
true);
     assert.equal(allowGeneration({I18N_MODE: 'generate', GITHUB_EVENT_NAME: 
'workflow_dispatch', I18N_MAX_PAGES: '10'}), true);
@@ -48,6 +48,8 @@ test('each language workflow passes one fixed locale to the 
shared run under one
         ja: {workspace: 'japanese-translation', cron: '23 4,16 * * *'},
         // Twenty minutes later; the shared lock queues it behind a Japanese 
run.
         ko: {workspace: 'korean-translation', cron: '43 4,16 * * *'},
+        // Another twenty minutes later, including the hour rollover.
+        ru: {workspace: 'russian-translation', cron: '3 5,17 * * *'},
     };
     for (const [locale, workflow] of Object.entries(callers)) {
         assert.deepEqual(Object.keys(workflow.on), ['workflow_dispatch', 
'schedule']);
diff --git a/scripts/i18n/audit-coverage.cjs b/scripts/i18n/audit-coverage.cjs
index 8dfeeec3a932..bf52c81cf81f 100644
--- a/scripts/i18n/audit-coverage.cjs
+++ b/scripts/i18n/audit-coverage.cjs
@@ -45,6 +45,7 @@ function baselineEntries(root) {
 function sidebarAudit(native,root,scope) {
     const result = [];
     for (const group of scope.groups) {
+        if (group.kind) continue; // Source modules/partials have no native 
sidebar.
         const plugin = native.labels.find(item => item.pluginId === 
group.pluginId);
         assert(plugin, `Missing native sidebar plugin ${group.pluginId}`);
         const versionFile = plugin.files.find(file => file.path === 
(group.version === 'current' ? 'current' : `version-${group.version}`));
@@ -85,7 +86,8 @@ async function audit({root,sourceRef,baseline,native}) {
     const old = new Map(baselineEntries(baseline).map(page => 
[key(page),page]));
     const current = new Set(), groups = [];
     const files = group => 
execFileSync('git',['ls-tree','-r','--name-only',commit,'--',group.sourceRoot],{cwd:root,encoding:'utf8'})
-        .split('\n').filter(file => /\.(md|mdx)$/.test(file));
+        .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));
     const scoped = scope.groups.map(group => ({group,
         pages:native.pages.filter(page => page.pluginId === group.pluginId && 
page.version === group.version)}));
     const sourceHashes = refHashes(root, commit, scoped.flatMap(({pages}) => 
pages.map(page => page.englishPath)));
@@ -134,6 +136,8 @@ async function audit({root,sourceRef,baseline,native}) {
         .map(state => [state,sidebar.filter(entry => entry.state === 
state).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`];
+    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`);
     const schemaPath = `translations/state/${locale}/shards.schema.json`;
     if (refHashes(root, commit, [schemaPath]).get(schemaPath) !== null) 
routingInputs.push(schemaPath);
diff --git a/scripts/i18n/build-scope.cjs b/scripts/i18n/build-scope.cjs
index c1b70a00da6f..12d47e7c51bf 100644
--- a/scripts/i18n/build-scope.cjs
+++ b/scripts/i18n/build-scope.cjs
@@ -40,7 +40,7 @@ function buildScope(files, versions, managed = []) {
         else if (/^sidebars(Community|Releases|Course)\./.test(file)) shared();
         else if (/^(src|config)\//.test(file) || 
['docusaurus.config.js','package.json','yarn.lock','tailwind.config.js','tsconfig.json'].includes(file)
             || /^scripts\/(key-features|last-update|publish)\//.test(file) || 
/^scripts\/(patch-search-tokenize.js|build-locales.cjs|build-locale.cjs)$/.test(file)
-            || 
/^scripts\/i18n\/(docs-plugin|status-plugin|state|structure)\.cjs$/.test(file)) 
shared();
+            || 
/^scripts\/i18n\/(docs-plugin|status-plugin|state|structure|content-plugin|content-loader|source-groups|code-prose|protection)\.cjs$/.test(file))
 shared();
         else if ((match = 
file.match(/^translations\/(scope|glossary)\/([^/]+)\.json$/))) {
             if (!locales.includes(match[2])) throw new Error(`Unconfigured 
locale: ${match[2]}`);
             json.add(file); // Offline configuration checks cover these 
without a site build.
diff --git a/scripts/i18n/check-translations.cjs 
b/scripts/i18n/check-translations.cjs
index 5ff70333cbd0..3a1290357193 100644
--- a/scripts/i18n/check-translations.cjs
+++ b/scripts/i18n/check-translations.cjs
@@ -74,7 +74,7 @@ async function checkTranslations({root, locale = 
pipelineLocale(root), pages, re
         } catch (error) {errors.push({path:page.targetPath, 
message:error.message});}
     }
     const known = new Set(pages.map(page => page.targetPath));
-    const targetPattern = new 
RegExp(`^i18n/${locale}/docusaurus-plugin-content-docs[^/]*/(current|version-[^/]+)/.+\\.(md|mdx)$`);
+    const targetPattern = new 
RegExp(`^i18n/${locale}/docusaurus-plugin-content-docs[^/]*/(current|version-[^/]+)/.+\\.(mdx?|tsx?)$`);
     for (const file of files.filter(file => targetPattern.test(file))) {
         if (fs.existsSync(path.join(root,file)) && !known.has(file)) 
errors.push({path:file, message:'Target has no registered source in the same 
plugin/version'});
     }
diff --git a/scripts/i18n/code-prose.cjs b/scripts/i18n/code-prose.cjs
new file mode 100644
index 000000000000..7b6098de87d4
--- /dev/null
+++ b/scripts/i18n/code-prose.cjs
@@ -0,0 +1,105 @@
+// Extract visible text without asking a model to rewrite executable code.
+const assert = require('node:assert/strict');
+const ts = require('typescript');
+const cheerio = require('cheerio');
+
+const textFields = new Set(['title', 'shortTitle', 'name', 'description', 
'label', 'summary', 'lead',
+    'narrative', 'mergedNarrative', 'inProgressNarrative', 'caption', 
'ariaLabel', 'aria-label',
+    'alt', 'placeholder', 'readingTime', 'coachTitle', 'calloutTitle', 
'question',
+    'correctFeedback', 'incorrectFeedback', 'leftTitle', 'leftLead', 
'rightTitle', 'rightLead',
+    'note', 'membersIn', 'problem', 'fix', 'tab', 'term', 'text', 'pitch', 
'titleLead', 'titleTail',
+    'hook', 'short', 'body', 'checkpoint', 'tip', 'tests', 'data-label']);
+const containers = new Set(['items', 'options', 'check', 'coach', 'callout', 
'children', 'leftItems', 'rightItems']);
+const decode = value => cheerio.load(`<span>${value}</span>`, {decodeEntities: 
true}, false)('span').text();
+const escapeJsx = value => value.replace(/&/g, '&amp;').replace(/</g, 
'&lt;').replace(/>/g, '&gt;')
+    .replace(/"/g, '&quot;').replace(/\{/g, '&#123;').replace(/\}/g, '&#125;');
+
+function serialize(unit, value) {
+    if (unit.quote === 'js-string') return JSON.stringify(value);
+    if (unit.quote === 'jsx-attribute') return `"${escapeJsx(value)}"`;
+    if (unit.quote === 'jsx-text') return escapeJsx(value);
+    if (unit.quote === 'template-fragment') return value.replace(/\\/g, 
'\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
+    return value;
+}
+
+function parseScript(raw) {
+    const file = ts.createSourceFile('content.tsx', raw, 
ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
+    assert(!file.parseDiagnostics.length, 'Invalid TypeScript/JSX content');
+    return file;
+}
+
+function staticData(node) {
+    if (ts.isStringLiteral(node) || ts.isNumericLiteral(node)
+        || [ts.SyntaxKind.TrueKeyword, ts.SyntaxKind.FalseKeyword, 
ts.SyntaxKind.NullKeyword].includes(node.kind)) return true;
+    if (ts.isArrayLiteralExpression(node)) return 
node.elements.every(staticData);
+    return ts.isObjectLiteralExpression(node) && 
node.properties.every(property => ts.isPropertyAssignment(property)
+        && !ts.isComputedPropertyName(property.name) && 
staticData(property.initializer));
+}
+
+function scriptUnits(raw, {report = false, expression = false} = {}) {
+    const prefix = expression ? 'const __prose = (' : '';
+    const file = parseScript(prefix + raw + (expression ? ');' : ''));
+    const units = [];
+    const propertyName = node => node?.name && (ts.isIdentifier(node.name) || 
ts.isStringLiteral(node.name)) ? node.name.text : '';
+    function inCode(node) {
+        for (let parent = node.parent; parent; parent = parent.parent) {
+            if (ts.isJsxElement(parent) && 
/^(code|pre|CodeBlock)$/.test(parent.openingElement.tagName.getText(file))) 
return true;
+        }
+        return false;
+    }
+    function visible(node) {
+        if (inCode(node)) return false;
+        if (ts.isPropertyAssignment(node.parent) && node.parent.name === node) 
return false;
+        for (let parent = node.parent; parent; parent = parent.parent) {
+            if (ts.isPropertyAssignment(parent)) return 
textFields.has(propertyName(parent)) || containers.has(propertyName(parent))
+                || propertyName(parent) === 'file' && /\s/.test(node.text ?? 
'');
+            if (ts.isJsxAttribute(parent)) return 
textFields.has(parent.name.getText(file)) || 
containers.has(parent.name.getText(file));
+            if (ts.isJsxExpression(parent)) return 
!ts.isJsxAttribute(parent.parent)
+                || textFields.has(parent.parent.name.getText(file)) || 
containers.has(parent.parent.name.getText(file));
+            if (ts.isCallExpression(parent) || ts.isImportDeclaration(parent) 
|| ts.isElementAccessExpression(parent)) return false;
+            if (ts.isVariableDeclaration(parent)) return expression || report 
&& ['label', 'week'].includes(propertyName(parent));
+            if (ts.isBindingElement(parent)) return 
textFields.has(propertyName(parent));
+        }
+        return false;
+    }
+    function add(start, end, text, quote) {
+        start -= prefix.length; end -= prefix.length;
+        if (start < 0 || end > raw.length || 
!/[A-Za-z\u0080-\uffff]/.test(text)) return;
+        // URLs, inline code and numbers are protected separately by the 
engine.
+        units.push({start, end, text, quote, kind: 'script:text'});
+    }
+    function visit(node) {
+        if (report && ts.isVariableDeclaration(node)) {
+            if (!['label', 'week', 'stats', 
'report'].includes(propertyName(node)) || !node.initializer || 
!staticData(node.initializer)) return;
+        }
+        if (ts.isJsxText(node) && !inCode(node)) {
+            const value = raw.slice(node.pos - prefix.length, node.end - 
prefix.length);
+            const leading = /^\s*/.exec(value)[0].length, trailing = 
/\s*$/.exec(value)[0].length;
+            if (value.trim()) add(node.pos + leading, node.end - trailing, 
decode(value.trim()), 'jsx-text');
+        } else if ((ts.isStringLiteral(node) || 
ts.isNoSubstitutionTemplateLiteral(node)) && visible(node)) {
+            const jsx = ts.isJsxAttribute(node.parent);
+            add(node.getStart(file), node.end, jsx ? decode(node.text) : 
node.text, jsx ? 'jsx-attribute' : 'js-string');
+        } else if ((ts.isTemplateHead(node) || ts.isTemplateMiddle(node) || 
ts.isTemplateTail(node)) && visible(node)) {
+            add(node.getStart(file) + 1, node.end - (ts.isTemplateTail(node) ? 
1 : 2), node.text, 'template-fragment');
+        }
+        ts.forEachChild(node, visit);
+    }
+    visit(file);
+    return units.sort((a, b) => a.start - b.start);
+}
+
+function canonicalScript(raw, options) {
+    const units = scriptUnits(raw, options);
+    let masked = raw;
+    for (const unit of [...units].reverse()) masked = masked.slice(0, 
unit.start) + serialize(unit, 'DORIS_PROSE') + masked.slice(unit.end);
+    const prefix = options?.expression ? 'const __prose = (' : '';
+    const file = parseScript(prefix + masked + (options?.expression ? ');' : 
''));
+    return {code: ts.createPrinter({newLine: 
ts.NewLineKind.LineFeed}).printFile(file), prose: units.map(unit => unit.text),
+        protectedText: units.flatMap(unit => 
unit.text.match(/`[^`]+`|https?:\/\/[^\s<>"')]+/g) || [])};
+}
+
+function reportEsm(raw) {
+    return /^\s*export\s+const\s+(?:label|week|stats|report)\s*=/.test(raw);
+}
+
+module.exports = {textFields, containers, serialize, scriptUnits, 
canonicalScript, reportEsm, parseScript};
diff --git a/scripts/i18n/content-loader.cjs b/scripts/i18n/content-loader.cjs
new file mode 100644
index 000000000000..3aa616ae15eb
--- /dev/null
+++ b/scripts/i18n/content-loader.cjs
@@ -0,0 +1,33 @@
+// Substitute a reviewed, current translation at the original resource path.
+// Relative imports, source URLs and public routes therefore remain unchanged.
+const fs = require('node:fs');
+const path = require('node:path');
+const {decide, stateFileFor} = require('./state.cjs');
+const {structure} = require('./structure.cjs');
+const {protectionSignature} = require('./protection.cjs');
+const {matchesSource, pageFor, contentGroups, readRecord} = 
require('./source-groups.cjs');
+
+async function localizedSource({root, locale, file, source, dependency = () => 
{}}) {
+    const relative = path.relative(root, file).split(path.sep).join('/');
+    const group = contentGroups(root, locale).find(group => 
matchesSource(group, relative));
+    if (!group) return source;
+    const page = pageFor(group, relative, locale);
+    for (const name of [page.targetPath, stateFileFor(page), 
`translations/state/${locale}/docs.json`, `translations/scope/${locale}.json`])
+        dependency(path.join(root, name));
+    if (!fs.existsSync(path.join(root, page.targetPath))) return source;
+    const target = fs.readFileSync(path.join(root, page.targetPath), 'utf8');
+    const state = decide({page, source, target, record: readRecord(root, 
page)});
+    if (state.status !== 'current' || !state.indexable) return source;
+    // This loader can compile executable TSX/MDX. Even an admitted candidate
+    // carrying advisory warnings must never change executable source here.
+    const extension = path.extname(file);
+    if (protectionSignature(await structure(source, extension)) !== 
protectionSignature(await structure(target, extension)))
+        throw new Error(`Protected executable content differs from its source: 
${page.targetPath}`);
+    return target;
+}
+module.exports = function contentLoader(source) {
+    const {root, locale} = this.getOptions();
+    return localizedSource({root, locale, file: this.resourcePath, source,
+        dependency: file => fs.existsSync(file) ? this.addDependency(file) : 
this.addMissingDependency(file)});
+};
+module.exports.localizedSource = localizedSource;
diff --git a/scripts/i18n/content-plugin.cjs b/scripts/i18n/content-plugin.cjs
new file mode 100644
index 000000000000..5ff1699c0078
--- /dev/null
+++ b/scripts/i18n/content-plugin.cjs
@@ -0,0 +1,20 @@
+const path = require('node:path');
+const {contentGroups, matchesSource} = require('./source-groups.cjs');
+
+module.exports = function contentTranslations(context) {
+    const root = context.siteDir, locale = context.i18n.currentLocale;
+    const groups = contentGroups(root, locale);
+    return {name: 'doris-content-translations',
+        configureWebpack() {
+            return groups.length ? {module: {rules: [{enforce: 'pre',
+                test: file => groups.some(group => matchesSource(group, 
path.relative(root, file).split(path.sep).join('/'))),
+                use: [{loader: require.resolve('./content-loader.cjs'), 
options: {root, locale}}],
+            }]}} : {};
+        },
+        getPathsToWatch() {
+            return groups.flatMap(group => [`${root}/${group.sourceRoot}/**/*`,
+                
`${root}/i18n/${locale}/docusaurus-plugin-content-docs-${group.pluginId}/**/*`])
+                .concat(`${root}/translations/state/${locale}/**/*.json`, 
`${root}/translations/scope/${locale}.json`);
+        },
+    };
+};
diff --git a/scripts/i18n/document-checks.cjs b/scripts/i18n/document-checks.cjs
index 4a3f56900f78..d9a8e478dd34 100644
--- a/scripts/i18n/document-checks.cjs
+++ b/scripts/i18n/document-checks.cjs
@@ -64,6 +64,7 @@ function checkLinks({root, targetPath, target}) {
 
 async function checkSyntax(target, extension) {
     const parsed = await structure(target, extension);
+    if (parsed.format === 'code') return;
     if (parsed.format === 'mdx') {
         const {compile} = await import('@mdx-js/mdx');
         const {escapeMarkdownHeadingIds} = require('@docusaurus/utils');
diff --git a/scripts/i18n/inventory.cjs b/scripts/i18n/inventory.cjs
index da5b772bcf03..6175695fffed 100644
--- a/scripts/i18n/inventory.cjs
+++ b/scripts/i18n/inventory.cjs
@@ -22,6 +22,11 @@ function validateScope(scope, locale) {
         assert(/^[A-Z0-9]+$/.test(group.id) && !groups.has(group.id), 
'Invalid/duplicate group');
         assert(!identities.has(key({...group, docId: ''})), 'Duplicate 
plugin/version group');
         assert(group.sourceRoot && !path.isAbsolute(group.sourceRoot) && 
!group.sourceRoot.split('/').includes('..'), 'Invalid source root');
+        if (group.kind) assert(['partial', 'code'].includes(group.kind) && 
group.version === 'current'
+            && /^\/[a-z0-9/-]+$/.test(group.routeBasePath), 'Invalid 
supplemental content group');
+        if (group.include) assert(group.kind && Array.isArray(group.include) 
&& group.include.length > 0
+            && group.include.every(prefix => typeof prefix === 'string' && 
prefix && !path.isAbsolute(prefix)
+                && !prefix.split('/').includes('..')), 'Invalid supplemental 
source includes');
         groups.add(group.id);
         identities.add(key({...group, docId: ''}));
     }
@@ -163,6 +168,8 @@ async function collect(root) {
         }
         labels.push({pluginId: plugin.options.id, files: await 
plugin.getTranslationFiles({content})});
     }
+    const extra = await require('./source-groups.cjs').collectSources(root, 
locale);
+    pages.push(...extra.pages); reports.push(...extra.reports); 
versions.push(...extra.versions);
     return {pages, reports: [...new Map(reports.map(report => 
[JSON.stringify(report), report])).values()], versions, labels};
 }
 
@@ -240,9 +247,11 @@ async function main() {
         assert.equal(committedHash(page.englishPath), englishHash, `English 
source differs from pinned commit: ${page.englishPath}`);
     }
     const groups = scope.groups.map(group => {
-        const files = markdownFiles(root, group.sourceRoot);
+        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);
         const committedFiles = execFileSync('git', ['ls-tree', '-r', 
'--name-only', sourceCommit, '--', group.sourceRoot], {cwd: root, encoding: 
'utf8'})
-            .split('\n').filter(file => 
/\.(md|mdx)$/.test(file)).sort(compare);
+            .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,
             order: native.versions.find(version => version.pluginId === 
group.pluginId && version.version === group.version)?.order ?? new Map()});
diff --git a/scripts/i18n/master-drift.cjs b/scripts/i18n/master-drift.cjs
index cf5bdf442dde..692b965a83bc 100644
--- a/scripts/i18n/master-drift.cjs
+++ b/scripts/i18n/master-drift.cjs
@@ -15,6 +15,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',
+        'sidebarsCourse.ts', 'sidebarsReleases.json',
         `translations/scope/${locale}.json`, 
`translations/glossary/${locale}.json`,
         `translations/protection-exceptions/${locale}.json`]);
     // Top-level state files: the legacy ledger, resolved deletions and the 
shard schema.
diff --git a/scripts/i18n/paths.cjs b/scripts/i18n/paths.cjs
index 576ab7413d31..7261fcd2e5b2 100644
--- a/scripts/i18n/paths.cjs
+++ b/scripts/i18n/paths.cjs
@@ -2,7 +2,9 @@ function sourceForLocalized(file) {
     const match = 
file.match(/^i18n\/[^/]+\/docusaurus-plugin-content-docs(-[^/]+)?\/(current|version-([^/]+))\/(.*)$/);
     if (!match) return null;
     const plugin = match[1] || '', version = match[3], relative = match[4];
-    const roots = {'':'docs', '-community':'community', 
'-releases':'releasenotes', '-course':'course'};
+    const roots = {'':'docs', '-community':'community', 
'-releases':'releasenotes', '-course':'course',
+        '-course-ui':'src', 
'-community-report':'src/pages/community-report/_reports',
+        '-community-report-ui':'src'};
     if (!roots[plugin]) return null;
     if (version) return plugin === '' ? 
`versioned_docs/version-${version}/${relative}` : null;
     return `${roots[plugin]}/${relative}`;
diff --git a/scripts/i18n/plan-translations.cjs 
b/scripts/i18n/plan-translations.cjs
index 05e20f793076..6d80e8ace4a3 100644
--- a/scripts/i18n/plan-translations.cjs
+++ b/scripts/i18n/plan-translations.cjs
@@ -90,9 +90,10 @@ function validateSourcePaths(paths, scope) {
     assert(Array.isArray(paths) && paths.length > 0 && paths.length <= 20, 
'paths must contain 1 to 20 exact source files');
     assert.equal(new Set(paths).size, paths.length, 'Duplicate exact source 
path');
     for (const file of paths) {
-        assert(typeof file === 'string' && /\.mdx?$/.test(file) && 
!file.includes('\\') && !/[\x00-\x1f\x7f]/.test(file)
+        assert(typeof file === 'string' && /\.(mdx?|tsx?)$/.test(file) && 
!file.includes('\\') && !/[\x00-\x1f\x7f]/.test(file)
             && file.split('/').every(part => part && part !== '.' && part !== 
'..')
-            && scope.groups.some(group => 
file.startsWith(`${group.sourceRoot}/`)),
+            && scope.groups.some(group => 
file.startsWith(`${group.sourceRoot}/`)
+                && (group.kind ? 
require('./source-groups.cjs').matchesSource(group, file) : 
/\.mdx?$/.test(file))),
         `Exact source path is outside the managed scope: ${file}`);
     }
 }
diff --git a/scripts/i18n/pr-preflight.cjs b/scripts/i18n/pr-preflight.cjs
index 6f0d1ed6439d..732f22ba310f 100644
--- a/scripts/i18n/pr-preflight.cjs
+++ b/scripts/i18n/pr-preflight.cjs
@@ -11,11 +11,11 @@ async function preflight({root, base, locale = 
pipelineLocale(root)}) {
         {cwd: root, stdio: 'inherit', env: {...process.env, I18N_LOCALE: 
locale}});
     const files = execFileSync('git', ['diff', '--name-only', 
'--diff-filter=AM', '-z', base, 'HEAD'],
         {cwd: root, encoding: 'utf8'}).split('\0').filter(Boolean)
-        .filter(file => 
/^i18n\/[^/]+\/docusaurus-plugin-content-docs[^/]*\/.+\.mdx?$/.test(file));
+        .filter(file => 
/^i18n\/[^/]+\/docusaurus-plugin-content-docs[^/]*\/.+\.(mdx?|tsx?)$/.test(file));
     for (const targetPath of files) {
         const target = fs.readFileSync(path.join(root, targetPath), 'utf8');
         await checkSyntax(target, path.extname(targetPath));
-        checkLinks({root, targetPath, target});
+        if (/\.mdx?$/.test(targetPath)) checkLinks({root, targetPath, target});
     }
     process.stdout.write(`Shared PR admission checks passed: translation 
state, syntax and internal links (${files.length} documents).\n`);
     return {checked: files.length};
diff --git a/scripts/i18n/protection.cjs b/scripts/i18n/protection.cjs
index 03fb6c43dac8..01175506a43e 100644
--- a/scripts/i18n/protection.cjs
+++ b/scripts/i18n/protection.cjs
@@ -1,5 +1,6 @@
 const cheerio = require('cheerio');
 const {hash} = require('./state.cjs');
+const {textFields, containers, canonicalScript, reportEsm} = 
require('./code-prose.cjs');
 const translatable = new Set(['title', 'description', 'sidebar_label', 
'keywords', 'language']);
 const visibleAttribute = name => ['alt', 'title', 'aria-label', 'description', 
'label', 'caption'].includes(name);
 
@@ -14,6 +15,12 @@ function protectedContent(parsed, {normalizeProseNumbers = 
false} = {}) {
     const nodes = [], numbers = [];
     for (const node of parsed.protectedNodes) {
         if (node[0] === 'numbers') {if (!normalizeProseNumbers) 
numbers.push(...node[1]); continue;}
+        if (node[0] === 'script' || node[0] === 'mdxjsEsm' && 
reportEsm(node[3])) {
+            const canonical = canonicalScript(node[3], {report: node[0] === 
'mdxjsEsm'});
+            numbers.push(...canonical.prose.flatMap(normalizedNumbers));
+            nodes.push([...node.slice(0, 3), canonical.code, 
canonical.protectedText]);
+            continue;
+        }
         if (node[0] === 'html' && !node[3].trim().startsWith('<!--')) {
             const $ = cheerio.load(node[3], {decodeEntities:false}, false);
             $('*').contents().each((i, child) => {
@@ -28,8 +35,21 @@ function protectedContent(parsed, {normalizeProseNumbers = 
false} = {}) {
             }));
             nodes.push(['html', $.html()]);
         } else if (node[0].startsWith('mdxJsx')) {
-            nodes.push([...node.slice(0,2), node[2].map(attribute => 
visibleAttribute(attribute.name)
-                && typeof attribute.value === 'string' ? {...attribute, 
value:''} : attribute)]);
+            const course = node[1]?.startsWith('Course');
+            nodes.push([...node.slice(0,2), node[2].map(attribute => {
+                const visible = visibleAttribute(attribute.name) || course && 
(textFields.has(attribute.name) || containers.has(attribute.name));
+                if (!visible) return attribute;
+                if (typeof attribute.value === 'string') {
+                    if (course) 
numbers.push(...normalizedNumbers(attribute.value));
+                    return {...attribute, value: ''};
+                }
+                if (attribute.value?.type === 
'mdxJsxAttributeValueExpression') {
+                    const canonical = canonicalScript(attribute.value.value, 
{expression: true});
+                    
numbers.push(...canonical.prose.flatMap(normalizedNumbers));
+                    return {...attribute, value: {type: attribute.value.type, 
value: canonical.code, protectedText: canonical.protectedText}};
+                }
+                return attribute;
+            })]);
         } else nodes.push(node);
     }
     return 
[Object.fromEntries(Object.entries(parsed.technicalMetadata).filter(([key]) => 
!translatable.has(key))),
diff --git a/scripts/i18n/source-groups.cjs b/scripts/i18n/source-groups.cjs
new file mode 100644
index 000000000000..f6fe82de08d0
--- /dev/null
+++ b/scripts/i18n/source-groups.cjs
@@ -0,0 +1,86 @@
+const fs = require('node:fs');
+const path = require('node:path');
+const {identity, loadRecords, decide, stateFileFor} = require('./state.cjs');
+const {scriptUnits} = require('./code-prose.cjs');
+
+function matchesSource(group, file) {
+    const suffix = file.startsWith(`${group.sourceRoot}/`) && 
file.slice(group.sourceRoot.length + 1);
+    return !!suffix && !suffix.split('/').some(part => part.startsWith('_') || 
part === '..')
+        && (!group.include || group.include.some(prefix => 
prefix.endsWith('/') ? suffix.startsWith(prefix) : suffix === prefix))
+        && (group.kind === 'code' ? /(?<!\.d)\.tsx?$/.test(suffix) && 
!/\.(test|logic)\./.test(suffix) : /\.mdx?$/.test(suffix));
+}
+function sourceFiles(root, group) {
+    const walk = dir => !fs.existsSync(path.join(root, dir)) ? [] : 
fs.readdirSync(path.join(root, dir), {withFileTypes: true})
+        .flatMap(entry => entry.isDirectory() ? entry.name.startsWith('_') ? 
[] : walk(`${dir}/${entry.name}`)
+            : entry.isFile() && matchesSource(group, `${dir}/${entry.name}`) ? 
[`${dir}/${entry.name}`] : []);
+    return walk(group.sourceRoot).sort();
+}
+function pageFor(group, sourcePath, locale) {
+    const suffix = sourcePath.slice(group.sourceRoot.length + 1);
+    const docId = suffix.replace(/\.(mdx?|tsx?)$/, '');
+    return {pluginId: group.pluginId, version: group.version, docId, locale, 
sourceLocale: 'en', sourcePath,
+        englishPath: sourcePath, targetPath: 
`i18n/${locale}/docusaurus-plugin-content-docs-${group.pluginId}/current/${suffix}`,
+        kind: group.kind, draft: false, unlisted: true,
+        permalink: 
`/${locale}${group.routeBasePath}/#${encodeURIComponent(docId)}`,
+        englishPermalink: 
`${group.routeBasePath}/#${encodeURIComponent(docId)}`,
+        sourcePermalink: 
`${group.routeBasePath}/#${encodeURIComponent(docId)}`};
+}
+function contentGroups(root, locale) {
+    const file = path.join(root, `translations/scope/${locale}.json`);
+    return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 
'utf8')).groups.filter(group => group.kind) : [];
+}
+function hasProse(root, group, file) {
+    return group.kind !== 'code' || 
scriptUnits(fs.readFileSync(path.join(root, file), 'utf8')).length > 0;
+}
+async function collectSources(root, locale) {
+    const groups = contentGroups(root, locale);
+    if (!groups.length) return {pages: [], reports: [], versions: []};
+    const {structure} = require('./structure.cjs');
+    const records = loadRecords(root, locale), pages = [], reports = [], 
versions = [];
+    for (const group of groups) {
+        const sources = new Set();
+        for (const file of sourceFiles(root, group)) {
+            if (!hasProse(root, group, file)) continue;
+            const page = pageFor(group, file, locale);
+            sources.add(identity(page));
+            const source = fs.readFileSync(path.join(root, file), 'utf8');
+            const target = fs.existsSync(path.join(root, page.targetPath)) ? 
fs.readFileSync(path.join(root, page.targetPath), 'utf8') : undefined;
+            const technicalHash = target === undefined ? undefined : (await 
structure(source, path.extname(file))).technicalHash;
+            pages.push({...decide({page, source, target, record: 
records.get(identity(page)), technicalHash}), indexable: false});
+        }
+        for (const record of records.values()) {
+            if (record.pluginId === group.pluginId && record.version === 
group.version && !sources.has(identity(record)))
+                reports.push({kind: 'source_deleted', ...record});
+        }
+        versions.push({pluginId: group.pluginId, version: group.version, 
order: new Map()});
+    }
+    return {pages, reports, versions};
+}
+
+// Load only this module's shard during Webpack compilation, rather than every
+// document record for each course component/report partial.
+function readRecord(root, page) {
+    const shard = stateFileFor(page);
+    if (fs.existsSync(path.join(root, shard))) return 
JSON.parse(fs.readFileSync(path.join(root, shard), 'utf8')).page;
+    const ledger = path.join(root, 
`translations/state/${page.locale}/docs.json`);
+    return fs.existsSync(ledger) ? JSON.parse(fs.readFileSync(ledger, 
'utf8')).pages.find(record => identity(record) === identity(page)) : undefined;
+}
+
+function translatedContentPages(root, locale) {
+    const groups = contentGroups(root, locale);
+    const complete = group => {
+        const files = sourceFiles(root, group).filter(file => hasProse(root, 
group, file));
+        return files.length > 0 && files.every(file => {
+            const page = pageFor(group, file, locale), record = 
readRecord(root, page);
+            if (!record || !fs.existsSync(path.join(root, page.targetPath))) 
return false;
+            return decide({page, source: fs.readFileSync(path.join(root, 
file), 'utf8'),
+                target: fs.readFileSync(path.join(root, page.targetPath), 
'utf8'), record}).indexable;
+        });
+    };
+    const ready = new Set(groups.filter(complete).map(group => 
group.pluginId));
+    return [...(ready.has('course-ui') ? ['/course', ...['a1-hybrid-search', 
'a2-mcp', 'a3-log-search', 'a4-agent-traces', 'track-b']
+        .map(task => `/course/hackathon/${task}`)] : []),
+    ...(ready.has('community-report') && ready.has('community-report-ui') ? 
['/community-report'] : [])];
+}
+
+module.exports = {matchesSource, sourceFiles, pageFor, contentGroups, 
hasProse, collectSources, readRecord, translatedContentPages};
diff --git a/scripts/i18n/status-plugin.cjs b/scripts/i18n/status-plugin.cjs
index 3303086b2f2b..c60d03713bf3 100644
--- a/scripts/i18n/status-plugin.cjs
+++ b/scripts/i18n/status-plugin.cjs
@@ -42,7 +42,7 @@ module.exports = function translationStatusPlugin(context) {
             const output = path.join(context.siteDir, '.i18n-build/manifests', 
`${locale}.json`);
             await fs.mkdir(path.dirname(output), {recursive: true});
             await fs.writeFile(output, JSON.stringify({schemaVersion: 1, 
locale,
-                pages, pageRoutes, reports}, null, 2) + '\n');
+                pages, pageRoutes, reports, translatedPagePaths: 
context.siteConfig?.customFields?.translatedPagePaths ?? {}}, null, 2) + '\n');
         },
     };
 };
diff --git a/scripts/i18n/structure.cjs b/scripts/i18n/structure.cjs
index 7714d9578ae8..b43aedbd7fcf 100644
--- a/scripts/i18n/structure.cjs
+++ b/scripts/i18n/structure.cjs
@@ -14,6 +14,13 @@ function withoutPositions(value) {
 }
 
 async function structure(raw, extension, anchorsMaintainCase = false) {
+    if (/^\.tsx?$/.test(extension)) {
+        const {parseScript} = require('./code-prose.cjs');
+        const file = parseScript(raw);
+        return {content: raw, headings: [], format: 'code', technicalMetadata: 
{}, proseNumbers: [],
+            protectedNodes: [['script', null, null, raw]], technicalHash: 
hash(raw), semanticHash: hash(raw),
+            blocks: file.statements.map(node => ({kind: 'script', start: 
node.getFullStart(), end: node.end}))};
+    }
     const [{unified}, {default: parse}, {default: gfm}, {default: mdx}, 
{visit}, {default: directive}] = await Promise.all([
         import('unified'), import('remark-parse'), import('remark-gfm'), 
import('remark-mdx'), import('unist-util-visit'),
         import('remark-directive'),
diff --git a/scripts/i18n/translate-docs.cjs b/scripts/i18n/translate-docs.cjs
index 026f8555d4de..527ff8332798 100644
--- a/scripts/i18n/translate-docs.cjs
+++ b/scripts/i18n/translate-docs.cjs
@@ -109,7 +109,10 @@ async function prepare({root, plan, taskId, scope, state, 
adapter, glossary, tar
         'Existing target or identity changed; manual review required');
     }
     const baseline = stagedBaseline(state, task, previousTarget);
-    const wholeDocument = typeof adapter.translateDocument === 'function';
+    // Course props and report exports contain prose inside code. Translate
+    // extracted text values and assemble the original code locally.
+    const group = scope.groups.find(group => group.pluginId === task.pluginId 
&& group.version === task.version);
+    const wholeDocument = typeof adapter.translateDocument === 'function' && 
!group.kind && task.pluginId !== 'course';
     let previousSource = null, baselineReason;
     if (existing) {
         try {previousSource = readCommitted(root, existing.sourceCommit, 
existing.sourcePath);}
@@ -131,7 +134,7 @@ async function prepare({root, plan, taskId, scope, state, 
adapter, glossary, tar
         previousSource: canReuse ? previousSource : null,
         previousTarget: canReuse ? previousTarget : null,
         targetLocale: target.locale, targetScriptPattern: target.scriptPattern,
-        maxDocumentChars, maxChunkChars});
+        maxDocumentChars: maxDocumentChars ?? (group.kind || task.pluginId === 
'course' ? 300000 : 60000), maxChunkChars});
     if (!wholeDocument) checkFrontMatter(source, generated.target, 
target.locale);
     if (!wholeDocument && adapter.model !== 'fixture') {
         assert(generated.targetChanged,
diff --git a/scripts/i18n/translation-engine.cjs 
b/scripts/i18n/translation-engine.cjs
index 6f39364381a4..6a1446eea7b4 100644
--- a/scripts/i18n/translation-engine.cjs
+++ b/scripts/i18n/translation-engine.cjs
@@ -6,6 +6,7 @@ const {execFileSync} = require('node:child_process');
 const {hash, identity} = require('./state.cjs');
 const {structure, alignHeadings} = require('./structure.cjs');
 const {protectionSignature} = require('./protection.cjs');
+const {scriptUnits, serialize, textFields, containers, reportEsm} = 
require('./code-prose.cjs');
 
 const visibleAttributes = new Set(['alt', 'title', 'aria-label', 
'description', 'label', 'caption']);
 const translatableFields = new Set(['title', 'description', 'sidebar_label']);
@@ -16,9 +17,10 @@ function managedPath(scope, task) {
     const group = scope.groups.find(item => item.pluginId === task.pluginId && 
item.version === task.version);
     assert(group && task.locale === scope.locale, 'Task is outside the managed 
locale/plugin/version');
     assert(typeof task.sourcePath === 'string' && 
task.sourcePath.startsWith(`${group.sourceRoot}/`), 'Source is outside its 
managed root');
+    if (group.kind) assert(require('./source-groups.cjs').matchesSource(group, 
task.sourcePath), 'Source is outside its managed include rules');
     const suffix = task.sourcePath.slice(group.sourceRoot.length + 1);
     assert(suffix && !suffix.split('/').some(part => !part || part === '.' || 
part === '..')
-        && /\.mdx?$/.test(suffix), 'Invalid managed source path');
+        && (group.kind === 'code' ? /\.tsx?$/.test(suffix) : 
/\.mdx?$/.test(suffix)), 'Invalid managed source path');
     const plugin = task.pluginId === 'default' ? '' : `-${task.pluginId}`;
     const version = task.version === 'current' ? 'current' : 
`version-${task.version}`;
     const expected = 
`i18n/${scope.locale}/docusaurus-plugin-content-docs${plugin}/${version}/${suffix}`;
@@ -39,6 +41,8 @@ function safeFile(root, relativePath) {
 }
 
 async function extractUnits(raw, extension) {
+    if (/^\.tsx?$/.test(extension)) return scriptUnits(raw).map((unit, index) 
=>
+        ({...unit, id: `u${String(index + 1).padStart(4, '0')}`, section: 0}));
     const [{unified}, {default: parse}, {default: gfm}, {default: mdx}, 
{visit}] = await Promise.all([
         import('unified'), import('remark-parse'), import('remark-gfm'), 
import('remark-mdx'), import('unist-util-visit'),
     ]);
@@ -136,13 +140,27 @@ async function extractUnits(raw, extension) {
                     add(at, at + match[3].length, `html:${match[1]}`);
                 }
             }
+        } else if (node.type === 'mdxjsEsm' && reportEsm(node.value)) {
+            for (const unit of scriptUnits(node.value, {report: true})) 
units.push({...unit,
+                start: prefix + node.position.start.offset + unit.start, end: 
prefix + node.position.start.offset + unit.end});
         } else if (node.type.startsWith('mdxJsx') && node.position) {
             for (const attribute of node.attributes ?? []) {
-                if (attribute.type !== 'mdxJsxAttribute' || 
!visibleAttributes.has(attribute.name)
-                    || typeof attribute.value !== 'string' || 
!attribute.position) continue;
+                const visible = visibleAttributes.has(attribute.name) || 
node.name?.startsWith('Course')
+                    && (textFields.has(attribute.name) || 
containers.has(attribute.name));
+                if (attribute.type !== 'mdxJsxAttribute' || !visible || 
!attribute.position) continue;
                 const start = prefix + attribute.position.start.offset;
                 const end = prefix + attribute.position.end.offset;
                 const fragment = raw.slice(start, end);
+                if (attribute.value?.type === 
'mdxJsxAttributeValueExpression') {
+                    const offset = fragment.indexOf('{') + 1;
+                    // remark-mdx strips container indentation from .value;
+                    // edit the actual source range, whose offsets Webpack 
sees.
+                    const code = fragment.slice(offset, -1);
+                    for (const unit of scriptUnits(code, {expression: true})) 
units.push({...unit,
+                        start: start + offset + unit.start, end: start + 
offset + unit.end});
+                    continue;
+                }
+                if (typeof attribute.value !== 'string') continue;
                 const match = /^[^=]+=[ \t]*(["'])([\s\S]*)\1$/.exec(fragment);
                 if (!match) throw new Error(`Unsupported visible MDX 
attribute: ${attribute.name}`);
                 add(start + fragment.indexOf(match[2]), start + 
fragment.indexOf(match[2]) + match[2].length,
@@ -162,7 +180,7 @@ async function extractUnits(raw, extension) {
 
 function protectText(value) {
     const tokens = [];
-    const masked = 
value.replace(/\\?\{#[^}]+\}|\$\{[^}]+\}|\{\{[^}]+\}\}|(?<![\w])[+-]?[vV]?\d+(?:\.\d+)*(?:\.x)?(?![\w])/g,
 match => {
+    const masked = 
value.replace(/`[^`]+`|https?:\/\/[^\s<>"')]+|\\?\{#[^}]+\}|\$\{[^}]+\}|\{\{[^}]+\}\}|(?<![\w])[+-]?[vV]?\d+(?:\.\d+)*(?:\.x)?(?![\w])/g,
 match => {
         const marker = `⟦DORIS_KEEP_${tokens.length}⟧`;
         tokens.push({marker, value: match});
         return marker;
@@ -346,7 +364,8 @@ async function translateDocument({source, extension, 
adapter, glossary, previous
     let target = source;
     for (const unit of [...units].reverse()) {
         let value = translated.get(unit.id);
-        if (unit.quote === 'json-"') value = JSON.stringify(value).slice(1, 
-1);
+        if (unit.kind === 'script:text') value = serialize(unit, value);
+        else if (unit.quote === 'json-"') value = 
JSON.stringify(value).slice(1, -1);
         else if (unit.quote === "json-'") value = value.replace(/\\/g, '\\\\');
         target = target.slice(0, unit.start) + value + target.slice(unit.end);
     }
diff --git a/scripts/i18n/verify-doc-navigation.cjs 
b/scripts/i18n/verify-doc-navigation.cjs
index 72a39e398093..b71c07ee72a0 100644
--- a/scripts/i18n/verify-doc-navigation.cjs
+++ b/scripts/i18n/verify-doc-navigation.cjs
@@ -21,12 +21,12 @@ function verifyRedirect(html, target) {
 }
 
 function verify({site, locale = 'ja', snapshot = inventoryBaseline(root, 
locale), devOnly = false,
-    manifest = path.join(root, '.i18n-build/manifests', `${locale}.json`)}) {
+    manifest = path.join(root, '.i18n-build/manifests', `${locale}.json`),
+    scope = JSON.parse(fs.readFileSync(path.join(root, 
`translations/scope/${locale}.json`), 'utf8'))}) {
     const summary = JSON.parse(fs.readFileSync(path.join(snapshot, 
'summary.json'), 'utf8'));
     assert.equal(summary.locale, locale, 'Inventory belongs to a different 
locale');
     const data = JSON.parse(fs.readFileSync(manifest, 'utf8'));
     assert.equal(data.locale, locale, 'Translation manifest belongs to a 
different locale');
-    const scope = JSON.parse(fs.readFileSync(path.join(root, 
`translations/scope/${locale}.json`), 'utf8'));
     const dictionary = JSON.parse(fs.readFileSync(path.join(root, 
`i18n/${locale}/code.json`), 'utf8'));
     const filename = route => path.join(site, decodeURIComponent(route), 
'index.html');
     const read = route => {
@@ -35,7 +35,7 @@ function verify({site, locale = 'ja', snapshot = 
inventoryBaseline(root, locale)
     };
     let routes = 0;
     const checks = [];
-    for (const group of scope.groups.filter(group => !devOnly || group.version 
!== '4.x')) {
+    for (const group of scope.groups.filter(group => !group.kind && (!devOnly 
|| group.version !== '4.x'))) {
         // Use the current build manifest, not a historical snapshot allowlist.
         const pages = data.pages.filter(page => page.pluginId === 
group.pluginId && page.version === group.version && !page.draft);
         assert(pages.length, `Missing built native group: ${group.id}`);
@@ -46,10 +46,11 @@ function verify({site, locale = 'ja', snapshot = 
inventoryBaseline(root, locale)
         // Native standalone docs can be current without belonging to a 
sidebar.
         // Cover their routes above, and inspect navigation on a sidebar 
member.
         const candidates = [...pages.filter(page => page.status === 'current' 
&& page.indexable), ...pages];
-        const representative = candidates.find(page => 
read(page.permalink)('.theme-doc-sidebar-container').length) ?? pages[0];
+        const representative = group.pluginId === 'course' ? pages[0]
+            : candidates.find(page => 
read(page.permalink)('.theme-doc-sidebar-container').length) ?? pages[0];
         const $ = read(representative.permalink);
-        assert.equal($('.theme-doc-markdown').attr('lang'), 
representative.contentLocale, representative.permalink);
-        const sidebar = $('.theme-doc-sidebar-container');
+        
assert.equal($('.theme-doc-markdown,.course-doc-content').attr('lang'), 
representative.contentLocale, representative.permalink);
+        const sidebar = $(group.pluginId === 'course' ? '.course-player__rail' 
: '.theme-doc-sidebar-container');
         assert(sidebar.length, `Missing document sidebar: 
${representative.permalink}`);
         const links = sidebar.find('a[href]').map((_, node) => 
$(node).attr('href')).get();
         for (const link of links.filter(link => 
link.startsWith(`/${locale}/`))) read(new URL(link, 
'https://doris.apache.org').pathname);
diff --git a/scripts/i18n/verify-search.cjs b/scripts/i18n/verify-search.cjs
index b8ae8b1af71b..39f605fe8f16 100644
--- a/scripts/i18n/verify-search.cjs
+++ b/scripts/i18n/verify-search.cjs
@@ -159,9 +159,11 @@ async function verify({site, locale = 'ja', devOnly = 
false, sourceLocales = [],
             current.englishPermalink + source.search + source.hash);
             assert.equal(await englishSwitch.getAttribute('href'), 
current.englishPermalink + source.search + source.hash);
             const linked = await 
navigation.locator('a[href]').evaluateAll(nodes => nodes.map(node => new 
URL(node.href).pathname));
-            const fallback = data.pages.find(page => page.pluginId === 
'default' && page.version === 'current'
+            // A fully translated Dev links no fallback, but the version menu 
still
+            // reaches versions outside the scope. Dev-only builds omit them.
+            const fallback = data.pages.find(page => page.pluginId === 
'default'
                 && !page.draft && !page.useTarget && linked.some(link => 
route(link) === route(page.permalink)));
-            if (locale === 'ru') assert(fallback, 'Russian SPA verification 
needs a linked English fallback');
+            if (locale === 'ru' && !devOnly) assert(fallback, 'Russian SPA 
verification needs a linked English fallback');
             if (fallback) {
                 // Use an actual native Link, including a collapsed sidebar 
entry.
                 await navigation.locator('a[href]').evaluateAll((nodes, 
target) => {
diff --git a/scripts/i18n/verify-static.cjs b/scripts/i18n/verify-static.cjs
index 6d91baafdabe..e240c12d8f29 100644
--- a/scripts/i18n/verify-static.cjs
+++ b/scripts/i18n/verify-static.cjs
@@ -99,7 +99,8 @@ function verify({site, manifest, partial = false}) {
         assert(fs.existsSync(filename), `Missing ordinary page: ${permalink}`);
         const $ = cheerio.load(fs.readFileSync(filename, 'utf8'));
         const source = permalink.startsWith(`/${locale}/`) ? 
permalink.slice(locale.length + 1) : permalink;
-        const fallback = record?.ordinaryPages === false && 
!record.translatedPages?.includes(source.replace(/\/+$/, ''));
+        const translatedPages = data.translatedPagePaths?.[locale] ?? 
record?.translatedPages;
+        const fallback = record?.ordinaryPages === false && 
!translatedPages?.includes(source.replace(/\/+$/, ''));
         if (fallback) {
             assert.equal($('html').attr('lang'), htmlLangFor('en'), `Fallback 
page language: ${permalink}`);
             assert.equal($('link[rel="canonical"]').length, 1, `Duplicate 
ordinary canonical: ${permalink}`);
diff --git a/scripts/i18n/workflow-generate.cjs 
b/scripts/i18n/workflow-generate.cjs
index 635726e3e874..66aa0adc9c7a 100644
--- a/scripts/i18n/workflow-generate.cjs
+++ b/scripts/i18n/workflow-generate.cjs
@@ -197,7 +197,7 @@ async function generate({plan, reconciliation, maxPages = 
10, target, adapter, g
 function allowGeneration(env) {
     if (env.I18N_MODE === 'validate') {
         return env.GITHUB_EVENT_NAME === 'workflow_dispatch'
-            && /\.mdx?$/.test(env.I18N_PATH || '') && env.I18N_MAX_PAGES === 
'1';
+            && /\.(mdx?|tsx?)$/.test(env.I18N_PATH || '') && 
env.I18N_MAX_PAGES === '1';
     }
     return env.I18N_MODE === 'generate';
 }
diff --git a/scripts/i18n/workflow-prs.cjs b/scripts/i18n/workflow-prs.cjs
index 8f90ceee1a73..65ce1318a749 100644
--- a/scripts/i18n/workflow-prs.cjs
+++ b/scripts/i18n/workflow-prs.cjs
@@ -17,7 +17,7 @@ function managedTarget(scope, target) {
         const suffix = group.pluginId === 'default' ? '' : 
`-${group.pluginId}`;
         const version = group.version === 'current' ? 'current' : 
`version-${group.version}`;
         return 
target.startsWith(`i18n/${scope.locale}/docusaurus-plugin-content-docs${suffix}/${version}/`)
-            && /\.mdx?$/.test(target);
+            && (group.kind === 'code' ? /\.tsx?$/.test(target) : 
/\.mdx?$/.test(target));
     });
 }
 
diff --git a/scripts/i18n/workflow-scan.cjs b/scripts/i18n/workflow-scan.cjs
index 687212311576..eb6fdaa356be 100644
--- a/scripts/i18n/workflow-scan.cjs
+++ b/scripts/i18n/workflow-scan.cjs
@@ -8,6 +8,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'];
 // A push to this locale's own scope or glossary changes what may be generated.
 const rulesFor = locale => new Set([...configInputs,
@@ -108,8 +109,8 @@ function main() {
     const locale = pipelineLocale(root);
     const scope = JSON.parse(fs.readFileSync(path.join(root, 
`translations/scope/${locale}.json`), 'utf8'));
     if (event === 'workflow_dispatch' && mode === 'validate') {
-        assert(/\.mdx?$/.test(process.env.I18N_PATH || ''),
-            'Validation mode needs an exact English .md or .mdx source path');
+        assert(/\.(mdx?|tsx?)$/.test(process.env.I18N_PATH || ''),
+            'Validation mode needs an exact English Markdown, MDX or 
TypeScript source path');
         assert(plan.selected.length === 1 && plan.tasks.find(task => task.id 
=== plan.selected[0])?.sourcePath === process.env.I18N_PATH,
             'Validation mode must select exactly one eligible source file');
     }
diff --git a/src/components/course/CourseLanding.tsx 
b/src/components/course/CourseLanding.tsx
index f4d3fde19b51..06eaf29012f5 100644
--- a/src/components/course/CourseLanding.tsx
+++ b/src/components/course/CourseLanding.tsx
@@ -1,4 +1,5 @@
 import React, { JSX, useRef } from 'react';
+import Link from '@docusaurus/Link';
 import { LayoutNext } from '@site/src/components/home-next/LayoutNext';
 import { COURSE_101_MODULES } from './courseModules';
 import { HACKATHON_COURSE_TRACK_ENABLED } from './hackathon/hackathonEvent';
@@ -20,14 +21,14 @@ function CoursePath(): JSX.Element {
         <ol className="course-landing__path-list">
             {COURSE_101_MODULES.map((module, index) => (
                 <li key={module.number}>
-                    <a
+                    <Link
                         className={`course-landing__path-step${index === 0 ? ' 
course-landing__path-step--current' : ''}`}
-                        href={module.href}
+                        to={module.href}
                     >
                         <span 
className="course-landing__path-number">{module.number}</span>
                         <span 
className="course-landing__path-name">{module.shortTitle}</span>
                         <span className="course-landing__path-arrow" 
aria-hidden="true">→</span>
-                    </a>
+                    </Link>
                 </li>
             ))}
             <li>
@@ -95,9 +96,9 @@ export function CourseLanding(): JSX.Element {
                                 The database knowledge you gain will also 
apply to other systems.
                             </p>
                             <div className="course-landing__actions">
-                                <a className="course-landing__button 
course-landing__button--primary" href={COURSE_101_MODULES[0].href}>
+                                <Link className="course-landing__button 
course-landing__button--primary" to={COURSE_101_MODULES[0].href}>
                                     Start with the overview →
-                                </a>
+                                </Link>
                             </div>
                         </div>
                         <CoursePath />
diff --git a/translations/README.md b/translations/README.md
index c92e45a575a1..832730d1849d 100644
--- a/translations/README.md
+++ b/translations/README.md
@@ -34,8 +34,7 @@ Russian is an optional, docs-first locale. Enable it with 
`DORIS_ENABLE_RU=true`
 or use `DORIS_ENABLE_LOCALES=ja,ko,ru` for all managed languages. The 
deployment
 workflows explicitly enable all three; default local builds still serve English
 and Chinese. Ordinary Russian pages link to English until individually 
registered.
-Russian generation has a manual-only workflow, described below. Product landing
-pages and a Russian generation schedule remain outside this launch.
+Russian translation runs twice daily and on manual dispatch, as described 
below.
 
 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
@@ -73,10 +72,11 @@ rebuild through the existing process; retain source 
translations and state.
 
 `.github/workflows/russian-translation.yml` calls the existing
 `translation-run.yml` with `locale: ru` and the `russian-translation` 
workspace.
-It only accepts manual dispatch and defaults to `dry-run`, which discovers
-pending work without calling a model. Japanese, Korean and Russian runs share
-the whole-run `docs-translation` lock, including discovery and validation.
-Japanese and Korean schedules keep their existing behavior.
+It runs at 05:03 and 17:03 UTC daily, twenty minutes after Korean discovery, 
and
+also accepts manual dispatch. Scheduled runs and default manual runs use
+`generate`. Select `dry-run` explicitly to discover pending work without 
calling
+a model. Japanese, Korean and Russian runs share the whole-run
+`docs-translation` lock, including discovery and validation.
 
 The shared workflow runs only on Apache `master` and checks out `master` again
 before generation. Merge the Russian configuration and workflow before running
@@ -557,10 +557,13 @@ review are recommended before running batch generation.
 and on manual dispatch; `.github/workflows/korean-translation.yml` runs twenty
 minutes later. Each sets only its triggers, manual defaults and the lock, then
 calls `.github/workflows/translation-run.yml` with its locale and artifact
-prefix. The manual-only Russian wrapper uses the same steps and lock. None has
-a push trigger. The description below uses Japanese; the other languages differ
-in locale, label, artifact names, triggers and manual default. Every run first 
recounts
-the current native Dev, 4.x and community documents, reads every open master
+prefix. Russian runs another twenty minutes later, at 05:03 and 17:03 UTC, 
using
+the same steps and lock. All three support manual dispatch and default to
+`generate`; select `dry-run` explicitly for a read-only scan. None has a push
+trigger. The description below uses Japanese; the other languages differ in
+locale, label, artifact names and scheduled times. Every run first recounts
+the current configured translation scope, including Dev, 4.x, community,
+courses, release notes and reports, reads every open master
 PR's changed files, and checks Japanese target files against that PR's state
 records. Each run uploads `plan.json`, `coverage.json`,
 `active-candidates.json` and `open-pr-reconciliation.json` and writes a short
diff --git a/translations/scope/ja.json b/translations/scope/ja.json
index 306374dc56cf..4004feb028e1 100644
--- a/translations/scope/ja.json
+++ b/translations/scope/ja.json
@@ -20,6 +20,50 @@
       "pluginId": "community",
       "version": "current",
       "sourceRoot": "community"
+    },
+    {
+      "id": "COURSE",
+      "pluginId": "course",
+      "version": "current",
+      "sourceRoot": "course"
+    },
+    {
+      "id": "RELEASES",
+      "pluginId": "releases",
+      "version": "current",
+      "sourceRoot": "releasenotes"
+    },
+    {
+      "id": "COURSEUI",
+      "pluginId": "course-ui",
+      "version": "current",
+      "sourceRoot": "src",
+      "kind": "code",
+      "routeBasePath": "/course",
+      "include": [
+        "components/course/",
+        "pages/course/"
+      ]
+    },
+    {
+      "id": "REPORT",
+      "pluginId": "community-report",
+      "version": "current",
+      "sourceRoot": "src/pages/community-report/_reports",
+      "kind": "partial",
+      "routeBasePath": "/community-report"
+    },
+    {
+      "id": "REPORTUI",
+      "pluginId": "community-report-ui",
+      "version": "current",
+      "sourceRoot": "src",
+      "kind": "code",
+      "routeBasePath": "/community-report",
+      "include": [
+        "components/community-report-next/",
+        "pages/community-report/"
+      ]
     }
   ],
   "retainEnglish": [],
diff --git a/translations/scope/ko.json b/translations/scope/ko.json
index cb6dd2d4391c..e38f3781f176 100644
--- a/translations/scope/ko.json
+++ b/translations/scope/ko.json
@@ -20,6 +20,50 @@
       "pluginId": "community",
       "version": "current",
       "sourceRoot": "community"
+    },
+    {
+      "id": "COURSE",
+      "pluginId": "course",
+      "version": "current",
+      "sourceRoot": "course"
+    },
+    {
+      "id": "RELEASES",
+      "pluginId": "releases",
+      "version": "current",
+      "sourceRoot": "releasenotes"
+    },
+    {
+      "id": "COURSEUI",
+      "pluginId": "course-ui",
+      "version": "current",
+      "sourceRoot": "src",
+      "kind": "code",
+      "routeBasePath": "/course",
+      "include": [
+        "components/course/",
+        "pages/course/"
+      ]
+    },
+    {
+      "id": "REPORT",
+      "pluginId": "community-report",
+      "version": "current",
+      "sourceRoot": "src/pages/community-report/_reports",
+      "kind": "partial",
+      "routeBasePath": "/community-report"
+    },
+    {
+      "id": "REPORTUI",
+      "pluginId": "community-report-ui",
+      "version": "current",
+      "sourceRoot": "src",
+      "kind": "code",
+      "routeBasePath": "/community-report",
+      "include": [
+        "components/community-report-next/",
+        "pages/community-report/"
+      ]
     }
   ],
   "retainEnglish": [],
diff --git a/translations/scope/ru.json b/translations/scope/ru.json
index c86a9483e317..9dbd3ffa8953 100644
--- a/translations/scope/ru.json
+++ b/translations/scope/ru.json
@@ -20,6 +20,50 @@
       "pluginId": "community",
       "version": "current",
       "sourceRoot": "community"
+    },
+    {
+      "id": "COURSE",
+      "pluginId": "course",
+      "version": "current",
+      "sourceRoot": "course"
+    },
+    {
+      "id": "RELEASES",
+      "pluginId": "releases",
+      "version": "current",
+      "sourceRoot": "releasenotes"
+    },
+    {
+      "id": "COURSEUI",
+      "pluginId": "course-ui",
+      "version": "current",
+      "sourceRoot": "src",
+      "kind": "code",
+      "routeBasePath": "/course",
+      "include": [
+        "components/course/",
+        "pages/course/"
+      ]
+    },
+    {
+      "id": "REPORT",
+      "pluginId": "community-report",
+      "version": "current",
+      "sourceRoot": "src/pages/community-report/_reports",
+      "kind": "partial",
+      "routeBasePath": "/community-report"
+    },
+    {
+      "id": "REPORTUI",
+      "pluginId": "community-report-ui",
+      "version": "current",
+      "sourceRoot": "src",
+      "kind": "code",
+      "routeBasePath": "/community-report",
+      "include": [
+        "components/community-report-next/",
+        "pages/community-report/"
+      ]
     }
   ],
   "retainEnglish": [],


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to