+ }`
+ )
+
+ expect(reports.map((report) => report.text)).toEqual(['Retry after reconnect', 'Reconnect now'])
+ })
})
describe('localization coverage file skipping', () => {
diff --git a/config/scripts/localization-extraction-change-scope.mjs b/config/scripts/localization-extraction-change-scope.mjs
index 21fccd98d9bd..66667ef710bd 100644
--- a/config/scripts/localization-extraction-change-scope.mjs
+++ b/config/scripts/localization-extraction-change-scope.mjs
@@ -11,12 +11,25 @@ const TOOLCHAIN_FILES = new Set([
'.github/workflows/pr.yml'
])
+function isIgnoredTestSource(path) {
+ if (!/\.(?:js|jsx|ts|tsx|mts|cts)$/.test(path)) {
+ return false
+ }
+ const segments = path.split('/')
+ const filename = segments.at(-1) ?? ''
+ return (
+ filename.includes('.test.') ||
+ filename.includes('.spec.') ||
+ segments.slice(0, -1).some((segment) => segment === '__tests__' || segment === '__snapshots__')
+ )
+}
+
export function affectsLocalizationExtraction(paths) {
return paths.some(
(path) =>
TOOLCHAIN_FILES.has(path) ||
- // Include all source paths so catalog moves and future extractor inputs stay covered.
- path.startsWith('src/') ||
+ // Keep catalogs and future source inputs; skip only configured test exclusions.
+ (path.startsWith('src/') && !isIgnoredTestSource(path)) ||
path.startsWith('config/i18next.') ||
path.startsWith('config/tsconfig') ||
path.startsWith('config/patches/') ||
diff --git a/config/scripts/localization-extraction-change-scope.test.mjs b/config/scripts/localization-extraction-change-scope.test.mjs
index b1c142b8cd0b..f8a19e61a8bb 100644
--- a/config/scripts/localization-extraction-change-scope.test.mjs
+++ b/config/scripts/localization-extraction-change-scope.test.mjs
@@ -1,8 +1,20 @@
import { readFileSync } from 'node:fs'
import { expect, it } from 'vitest'
import { parse } from 'yaml'
+import localizationConfig from '../i18next.config'
import { affectsLocalizationExtraction } from './localization-extraction-change-scope.mjs'
+it('pins the extractor inputs and exclusions used by the routing decision', () => {
+ expect(localizationConfig.extract.input).toEqual(['src/**/*.{js,jsx,ts,tsx,mts,cts}'])
+ expect(localizationConfig.extract.ignore).toEqual([
+ '**/*.test.*',
+ '**/*.spec.*',
+ '**/__tests__/**',
+ '**/__snapshots__/**',
+ '**/assets/**'
+ ])
+})
+
it.each([
'src/renderer/src/components/Example.tsx',
'src/main/notifications.ts',
@@ -24,6 +36,52 @@ it.each([
expect(affectsLocalizationExtraction([path])).toBe(true)
})
+it.each([
+ 'src/main/notifications.test.ts',
+ 'src/main/notifications.spec.ts',
+ 'src/renderer/src/components/Example.test.tsx',
+ 'src/main/example.test.js',
+ 'src/main/example.spec.jsx',
+ 'src/main/example.test.mts',
+ 'src/main/example.spec.cts',
+ 'src/.test.ts',
+ 'src/.spec.ts',
+ 'src/__tests__/notifications.ts',
+ 'src/main/__tests__/nested/notifications.ts',
+ 'src/main/__snapshots__/notifications.ts'
+])('avoids extraction for an explicitly ignored test source: %s', (path) => {
+ expect(affectsLocalizationExtraction([path])).toBe(false)
+})
+
+it.each([
+ 'src/main/notifications-test.ts',
+ 'src/main/notifications.test-support.ts',
+ 'src/main/notifications.specification.ts',
+ 'src/main/notifications.TEST.ts',
+ 'src/main/.test./notifications.ts',
+ 'src/main/notifications.test/notifications.ts',
+ 'src/main/__tests__-support/notifications.ts',
+ 'src/main/__snapshots__-support/notifications.ts',
+ 'src/main/__tests__.ts',
+ 'src/main/assets/notifications.ts',
+ 'src/main/__tests__/en.json',
+ 'src/renderer/src/i18n/locales/en.test.json',
+ 'src/main/notifications.test.unknown'
+])('retains extraction for catalogs, assets and near-match source paths: %s', (path) => {
+ expect(affectsLocalizationExtraction([path])).toBe(true)
+})
+
+it('retains mixed changes and both sides of source-to-test renames', () => {
+ const ignored = 'src/main/notifications.test.ts'
+ expect(affectsLocalizationExtraction([ignored, 'src/main/notifications.ts'])).toBe(true)
+ expect(affectsLocalizationExtraction(['src/main/notifications.ts', ignored])).toBe(true)
+ expect(affectsLocalizationExtraction([ignored, 'config/i18next.config.ts'])).toBe(true)
+ expect(affectsLocalizationExtraction([ignored, 'src/renderer/src/i18n/locales/en.json'])).toBe(
+ true
+ )
+ expect(affectsLocalizationExtraction([ignored, 'src/main/notifications.spec.ts'])).toBe(false)
+})
+
it('avoids extraction for unrelated CI, documentation, and native changes', () => {
expect(
affectsLocalizationExtraction([
diff --git a/docs/reference/ci-runner-efficiency.md b/docs/reference/ci-runner-efficiency.md
index 611ef54b8cb6..aa8146649bb9 100644
--- a/docs/reference/ci-runner-efficiency.md
+++ b/docs/reference/ci-runner-efficiency.md
@@ -2583,3 +2583,47 @@ The single hosted trial occupied 134 runner-seconds including all six samples,
shared setup and upload. That is trial cost, not a production saving. These
focused process measurements do not establish full-shard savings, PR runtime
percentiles, queue relief or a change in the organization's runner allowance.
+
+## October 5 localization gate work
+
+The localization coverage audit now classifies only AST nodes that can emit string
+parts, and reuses the class-property exclusion set. It still visits every child and
+handles JSX text separately. The extraction scope skips only source extensions and
+test paths explicitly excluded by the real extractor configuration. Catalogs,
+assets, unknown extensions, configuration changes and mixed production changes
+still select extraction; both sides of a rename remain covered.
+
+The [hosted comparison](https://github.com/stablyai/orca/actions/runs/37360932616)
+used fixed source `761d63a4e52ff7915d4432c004df13182db63010`, Linux ARM,
+Node 24.21.0 and pnpm 12.8.1. Dependency setup was shared outside the samples.
+Each timed check was a fresh process with Node compile caching disabled.
+
+| Complete coverage audit | Original | Candidate | Paired saving |
+| ----------------------- | -------- | --------- | ------------- |
+| Pair 1 | 9.182s | 4.924s | 4.258s |
+| Pair 2, reversed order | 9.133s | 4.924s | 4.209s |
+| Pair 3 | 8.930s | 4.924s | 4.007s |
+
+The median paired saving is 4.209 seconds, or
+46.1% of this audit. All three pairs improved; the original range was
+0.252 seconds. Complete JSON finding inventories match byte-for-byte,
+including all 13 findings and their source locations. All 53 candidate tests pass.
+Four real audit mutations fail their intended assertions. An ignored test fixture
+passes the real extraction gate; renaming it into production source selects the
+gate and fails on the deliberately missing translation key.
+
+For an existing ignored test path, three conditional comparisons avoid a median
+27.628 seconds of extraction work. Original arms really run the full gate
+and pass; candidate extraction arms are explicitly `not_selected`. The routing
+tests pin the actual extractor inputs and exclusions, so configuration drift fails
+the tests. This saving applies only when every changed source path is ignored.
+
+Relay integration also omits desktop native preparation and the Electron archive
+cache. Its existing two files use Node's SQLite, HTTP and WebSocket paths; general
+unit shards retain their desktop setup. The existing 16 relay cases pass locally
+on Node 24.20.0 and 26.7.0. Removed setup work is not a measured timing saving.
+
+These comparisons exclude queues and other checks. Extraction already overlaps
+other preflight work, so conditional avoidance does not translate directly into
+preflight wall time. No change to full-PR percentiles or the concurrency allowance
+is established.