Files
orca/config/scripts/audit-localization-coverage.mjs
T
OrcaWinandOrcaWin 7d6603aa17 [comment audit] Complete repository-wide comment cleanup (#33249)
* [comment audit] Align legacy failure-read counts with cached display

* [comment audit] Condense shared provider lifecycle and browser recovery comments [skip ci]

* [comment audit] Condense shared generation, path and preview comments [skip ci]

* [comment audit] Condense host, file and Git contract rationale [skip ci]

* [comment audit] Tighten Git lifecycle and provider contract comments [skip ci]

* [comment audit] Clarify shared host authority and launch isolation [skip ci]

* [comment audit] simplify local access mobile and native chat contracts [skip ci]

* [comment audit] simplify native chat presentation and state contracts [skip ci]

* [comment audit] simplify native runtime and provider contracts [skip ci]

* [comment audit] simplify migration and orchestration contracts [skip ci]

* [comment audit] simplify plugin trust and process capture rationale [skip ci]

* [comment audit] simplify terminal delivery and remote compatibility rationale [skip ci]

* [comment audit] clarify remote recovery and launch wire rationale [skip ci]

* [comment audit] shorten shared RPC compatibility and runtime evidence comments [skip ci]

* [comment audit] simplify secure storage shell startup and sidebar rationale [skip ci]

* [comment audit] clarify sidebar, skill and SSH invariants [skip ci]

* [comment audit] condense structured chat delivery and timing rationale [skip ci]

* [comment audit] condense terminal replay and telemetry invariants [skip ci]

* [comment audit] clarify launch permission and terminal delivery rationale [skip ci]

* [comment audit] clarify workspace ownership and layout contracts [skip ci]

* [comment audit] wait for fitted and painted SSH terminal links

* [comment audit] clarify layout persistence and session ownership [skip ci]

* [comment audit] clarify host routing and workspace lifecycle contracts [skip ci]

* [comment audit] clarify startup persistence and app lifetime comments [skip ci]

* [comment audit] clarify recovery and activity boundaries [skip ci]

* [comment audit] condense activity and artifact invariants [skip ci]

* [comment audit] condense automation authority contracts [skip ci]

* [comment audit] simplify automation scheduling and history contracts [skip ci]

* [comment audit] clarify browser chrome and annotation lifecycles [skip ci]

* [comment audit] clarify browser routing and retained guest ownership [skip ci]

* [comment audit] clarify browser recovery and document ownership comments [skip ci]

* [comment audit] restore hosted runners required by native IME fixtures

* [comment audit] shorten preview, palette and dashboard invariants [skip ci]

* [comment audit] clarify terminal preview, dashboard and retention comments [skip ci]

* [comment audit] clarify diff-note recovery and editor lifecycle comments [skip ci]

* [comment audit] shorten rich-text and combined-diff ownership comments [skip ci]

* [comment audit] shorten autosave, draft recovery and model ownership comments [skip ci]

* [comment audit] shorten save-queue, notebook and Markdown constraints [skip ci]

* [comment audit] shorten Monaco, PDF and rich Markdown safeguards [skip ci]

* [comment audit] clarify rich Markdown source and selection safeguards [skip ci]

* [comment audit] clarify editor loading and emulator stream lifetimes [skip ci]

* [comment audit] trim emulator and feature tour narration [skip ci]

* [comment audit] clarify floating workspace and GitHub cache ownership [skip ci]

* [comment audit] trim provider controls and Linear filter explanations [skip ci]

* [comment audit] clarify pairing and chat resume lifetimes [skip ci]

* [comment audit] clarify chat delivery and transcript lifetimes [skip ci]

* [comment audit] clarify session options and host-owned status [skip ci]

* [comment audit] clarify draft durability and live-status precedence [skip ci]

* [comment audit] Trim chat send and model ownership explanations [skip ci]

* [comment audit] Shorten transcript ownership and send recovery comments [skip ci]

* [comment audit] Simplify native chat composer and transcript hook comments [skip ci]

* [comment audit] Shorten native chat scroll and structured session explanations [skip ci]

* [comment audit] Trim workspace picker and onboarding commentary [skip ci]

* [comment audit] Simplify pet rendering, review cache and quick open comments [skip ci]

* [comment audit] Trim sidebar and session history explanations [skip ci]

* [comment audit] Condense session resume and review checks comments [skip ci]

* [comment audit] Shorten checks state and file explorer refresh comments [skip ci]

* [comment audit] Trim plugin bridge and source-control action rationale [skip ci]

* [comment audit] Trim source-control listing and review rationale [skip ci]

* [comment audit] Shorten sidebar lifecycle and explorer safeguards [skip ci]

* [comment audit] Condense Settings account and appearance rationale [skip ci]

* [comment audit] Trim pairing and repository Settings explanations [skip ci]

* [comment audit] Condense Settings search SSH and terminal safeguards [skip ci]

* [comment audit] Shorten Settings voice and provider ownership comments [skip ci]

* [comment audit] Condense Settings draft persistence and host-state rationale [skip ci]

* [comment audit] Trim Settings hooks and sidebar setup rationale [skip ci]

* [comment audit] Condense sidebar deletion and board-filter safeguards [skip ci]

* [comment audit] Condense worktree cards, host ownership and Markdown contracts [skip ci]

* [comment audit] Condense sidebar drag, folder and filtering contracts [skip ci]

* [comment audit] Condense sidebar parity, sleep and host ownership contracts [skip ci]

* [comment audit] Prune focus-read baseline after comment cleanup

* [comment audit] Clarify sidebar visibility, board drops and agent evidence [skip ci]

* [comment audit] Tighten sidebar listing, drag and reveal contracts [skip ci]

* [comment audit] Clarify viewport geometry and skill update safeguards [skip ci]

* [comment audit] Tighten skill inventory, usage and host status comments [skip ci]

* [comment audit] Clarify usage ownership and resource kill safeguards [skip ci]

* [comment audit] Tighten tab selection, rename and search contracts [skip ci]

* [comment audit] Tighten tab-strip geometry and drag ownership comments [skip ci]

* [comment audit] Tighten Tasks recovery, pagination and mutation comments [skip ci]

* [comment audit] Tighten provider loading and terminal recovery comments [skip ci]

* [comment audit] Tighten completion, cadence and PTY ownership comments [skip ci]

* [comment audit] Clarify terminal restore and parked-pane boundaries [skip ci]

* [comment audit] Tighten PTY startup and replay explanations [skip ci]

* [comment audit] Clarify PTY identity and delivery ownership [skip ci]

* [comment audit] Shorten remote reconnect and input replay rationale [skip ci]

* [comment audit] Condense terminal parking clipboard and file link comments [skip ci]

* [comment audit] Clarify IME ownership and trim trace history [skip ci]

* [comment audit] Shorten pane recovery and parked watcher explanations [skip ci]

* [comment audit] Condense snapshot replay and visibility recovery rationale [skip ci]

* [comment audit] Shorten terminal parking and keyboard ownership rationale [skip ci]

* [comment audit] Trim renderer lifecycle and menu rationale [skip ci]

* [comment audit] Condense workspace cleanup ownership and settlement notes [skip ci]

* [comment audit] Clarify renderer hook evidence and source ownership [skip ci]

* [comment audit] Tighten IPC ownership and reconnect explanations [skip ci]

* [comment audit] Trim renderer focus, paste and attention comments [skip ci]

* [comment audit] Shorten scroll, locale and agent lifecycle comments [skip ci]

* [comment audit] Trim agent launch, resume and ownership commentary [skip ci]

* [comment audit] Shorten account, browser and editor comments [skip ci]

* [comment audit] Clarify host-mirror waits, previews and input ownership [skip ci]

* [comment audit] Condense launch recovery and editor tokenization comments [skip ci]

* [comment audit] Condense workspace selection and terminal pane rationale [skip ci]

* [comment audit] Condense terminal fitting and rendering recovery comments [skip ci]

* [comment audit] Condense terminal input and host routing explanations [skip ci]

* [comment audit] Condense ownership and sleeping-session recovery rationale [skip ci]

* [comment audit] Condense structured launch and terminal link explanations [skip ci]

* [comment audit] Condense terminal ownership and workspace file routing rationale [skip ci]

* [comment audit] Condense session persistence and workspace activation rationale [skip ci]

* [comment audit] Condense workspace routing and browser ownership rationale [skip ci]

* [comment audit] Condense mirror recovery and layout authority rationale [skip ci]

* [comment audit] Condense runtime transport and file safety rationale [skip ci]

* [comment audit] Condense runtime graph and structured session rationale [skip ci]

* [comment audit] Condense browser creation and remote tab recovery rationale [skip ci]

* [comment audit] Condense snapshot authority and publisher recovery rationale [skip ci]

* [comment audit] Condense frame, startup and provider cache rationale [skip ci]

* [comment audit] Condense host twin and agent lifecycle rationale [skip ci]

* [comment audit] Condense browser authority and editor cleanup rationale [skip ci]

* [comment audit] Condense editor buffer identity and review cache rationale [skip ci]

* [comment audit] Condense provider lifetime and repository ownership rationale [skip ci]

* [comment audit] Condense reconnect, hydration and terminal retirement rationale [skip ci]

* [comment audit] Condense recovery, reconciliation and persistence rationale [skip ci]

* [comment audit] Condense cleanup ownership and listing admission rationale [skip ci]

* [comment audit] Condense teardown ordering and terminal ownership rationale [skip ci]

* [comment audit] Condense recovery ledger and web host-boundary rationale [skip ci]

* [comment audit] Condense web reconnect and credential lifecycle rationale [skip ci]

* [comment audit] Condense cloud rollout and assignment safety rationale [skip ci]

* [comment audit] Condense relay session and database recovery rationale [skip ci]

* [comment audit] Condense relay placement and protocol compatibility rationale [skip ci]

* [comment audit] Condense build and packaging constraints [skip ci]

* [comment audit] Condense CI gates and validation isolation rationale [skip ci]

* [comment audit] Condense catalog identity and browser validation rationale [skip ci]

* [comment audit] Condense mobile browser and frame-budget rationale [skip ci]

* [comment audit] Shorten mobile route and browser test explanations [skip ci]

* [comment audit] Condense editor and session seam test rationale [skip ci]

* [comment audit] Trim session and terminal closure history [skip ci]

* [comment audit] Shorten terminal browser and census explanations [skip ci]

* [comment audit] Preserve root-only E2E diagnostic baseline

* [comment audit] Condense mobile seam and HTML preview test rationale [skip ci]

* [comment audit] Condense preview instrumentation and render harness rationale [skip ci]

* [comment audit] Condense bundle contracts and native-build constraints [skip ci]

* [comment audit] Shorten native packaging and CI routing rationale [skip ci]

* [comment audit] Condense release tooling and benchmark constraints [skip ci]

* [comment audit] Condense packaging and platform gate rationale [skip ci]

* [comment audit] Shorten mobile route and history rationale [skip ci]

* [comment audit] Condense mobile browser and component rationale [skip ci]

* [comment audit] Shorten drawer and workspace-form rationale [skip ci]

* [comment audit] Trim mobile review and editor-document explanations [skip ci]

* [comment audit] Condense editor lifecycle and Markdown explanations [skip ci]

* [comment audit] Trim mobile file, diagnostics and dictation commentary [skip ci]

* [comment audit] Simplify mobile host-list and workspace-view explanations [skip ci]

* [comment audit] Clarify mobile shell lifetime and bridge delivery constraints [skip ci]

* [comment audit] Condense bridge stream, native request and audio rationale [skip ci]

* [comment audit] Reduce bridge schema and page-client commentary [skip ci]

* [comment audit] Tighten mobile page bridge contracts [skip ci]

* [comment audit] Clarify mobile cache and session constraints [skip ci]

* [comment audit] Condense mobile route and lifecycle rationale [skip ci]

* [comment audit] Tighten mobile shell hook timing rationale [skip ci]

* [comment audit] Clarify route handoff and notification compatibility [skip ci]

* [comment audit] Clarify push registration and dictation cleanup constraints [skip ci]

* [comment audit] Condense keyboard geometry and native media ownership rationale [skip ci]

* [comment audit] Tighten mobile chat scroll and composer concurrency rationale [skip ci]

* [comment audit] Condense mobile reply compatibility and upload ownership rationale [skip ci]

* [comment audit] Condense optimistic echo and scroll ownership rationale [skip ci]

* [comment audit] Tighten stream replay and queued session ownership rationale [skip ci]

* [comment audit] Condense mobile composer and reply-schema constraints [skip ci]

* [comment audit] Clarify mobile transcript recovery and session ownership [skip ci]

* [comment audit] Clarify mobile queue and source-control compatibility [skip ci]

* [comment audit] Condense storage and task launch constraints [skip ci]

* [comment audit] Clarify task schemas and terminal document ownership [skip ci]

* [comment audit] Tighten terminal input and viewport rationale [skip ci]

* [comment audit] Shorten terminal and RPC recorder contracts [skip ci]

* [comment audit] Condense recorder engine and connection contracts [skip ci]

* [comment audit] Shorten host credential and relay recovery comments [skip ci]

* [comment audit] Tighten pairing recovery and bundle transport rationale [skip ci]

* [comment audit] Clarify RPC delivery and workspace catalog boundaries [skip ci]

* [comment audit] Condense catalog and relay entry contracts [skip ci]

* [comment audit] Shorten shutdown and SSH relay delivery contracts [skip ci]

* [comment audit] Condense WSL relay and E2E regression rationale [skip ci]

* [comment audit] Shorten cross-version harness and renderer fixture explanations [skip ci]

* [comment audit] Condense paired-host and E2E lifecycle rationale [skip ci]

* [comment audit] Shorten layout oracle and regression explanations [skip ci]

* [comment audit] Condense paired recovery and ownership regression contracts [skip ci]

* [comment audit] Shorten restart, sidebar and SSH lifecycle contracts [skip ci]

* [comment audit] Condense terminal rendering and IME regression rationale [skip ci]

* [comment audit] Shorten terminal recovery and benchmark contracts [skip ci]

* [comment audit] Condense tooling and Windows lifecycle proof rationale [skip ci]

* [comment audit] Condense workflow routing and release rationale [skip ci]

* [comment audit] Condense mobile security and installer contracts [skip ci]

* [comment audit] Shorten native rationale and stylesheet workarounds [skip ci]

* [comment audit] Shorten Windows visibility probe rationale [skip ci]

* [comment audit] Condense embedded release and signing rationale [skip ci]

* [comment audit] Retain current main test behavior in final reconciliation [skip ci]

* [comment audit] Complete final comment review across current source inventory

* [comment audit] Handle consolidated audit reports before final CI

* [comment audit] Preserve credential storage census detail [skip ci]

* [comment audit] Scale broad changed E2E selections with verified timing shards

* [comment audit] Align E2E assertions with browser close and settled session metadata

* [comment audit] Match existing E2Es to current resource and windowing contracts

* [comment audit] Observe the shared server outage notice in reconnect coverage

* [comment audit] Make existing renderer probes measure settled tab and PTY work

* [comment audit] Align palette E2E selector with task search

* [comment audit] Refresh remaining palette search selectors

* [comment audit] Keep selector correction within selected audit specs

* [comment audit] Await emoji fixture settings inside renderer evaluation

* [comment audit] Cancel fixture fit retries between terminal tests

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-25 04:05:46 -07:00

641 lines
17 KiB
JavaScript

import fs from 'node:fs/promises'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
import process from 'node:process'
// TypeScript 7 is a native CLI; AST consumers still need the legacy JavaScript API.
import ts from 'typescript-api'
import { isTestOnlySourcePath } from './test-only-source-path.mjs'
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts'])
const SKIP_PATH_PARTS = new Set(['.git', 'dist', 'node_modules', 'out', '__snapshots__', 'assets'])
const LOCALIZATION_CALL_NAMES = new Set(['t', 'translate'])
const CLASS_PROPERTY_NAMES = new Set(['className', 'classNames'])
// Why: the children of these elements are CSS or code, never copy a translator should see.
const NON_COPY_JSX_ELEMENTS = new Set(['style', 'script'])
const USER_VISIBLE_JSX_ATTRIBUTES = new Set([
'ariaLabel',
'aria-label',
'aria-description',
'alt',
'description',
'emptyText',
'helperText',
'keywords',
'label',
'message',
'placeholder',
'subtitle',
'text',
'title',
'toggleDescription',
'tooltip'
])
const USER_VISIBLE_OBJECT_KEYS = new Set([
'ariaLabel',
'badge',
'description',
'emptyText',
'error',
'helperText',
'keywords',
'label',
'message',
'placeholder',
'subtitle',
'title',
'toggleDescription',
'tooltip'
])
const USER_VISIBLE_FUNCTION_NAMES = new Set([
'alert',
'confirm',
'prompt',
'showError',
'showToast'
])
const USER_VISIBLE_OBJECT_METHODS = new Set([
'error',
'info',
'loading',
'message',
'promise',
'success',
'warning'
])
const USER_VISIBLE_OBJECT_NAMES = new Set(['toast'])
// Only comparison operands are non-copy; rejecting all non-+ operators hides guarded JSX and ?? fallback strings.
const COPY_PRESERVING_BINARY_OPERATORS = new Set([
ts.SyntaxKind.PlusToken,
ts.SyntaxKind.QuestionQuestionToken,
ts.SyntaxKind.BarBarToken,
ts.SyntaxKind.AmpersandAmpersandToken
])
function normalizePath(root, filePath) {
return path.relative(root, filePath).split(path.sep).join('/')
}
export function isSkippedFile(root, filePath) {
const relative = normalizePath(root, filePath)
if (relative.endsWith('.d.ts') || isTestOnlySourcePath(relative)) {
return true
}
return relative.split('/').some((part) => SKIP_PATH_PARTS.has(part))
}
async function collectSourceFiles(root, dir) {
const entries = await fs.readdir(dir, { withFileTypes: true })
const files = []
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
if (!SKIP_PATH_PARTS.has(entry.name)) {
files.push(...(await collectSourceFiles(root, fullPath)))
}
continue
}
if (
entry.isFile() &&
SOURCE_EXTENSIONS.has(path.extname(entry.name)) &&
!isSkippedFile(root, fullPath)
) {
files.push(fullPath)
}
}
return files
}
function hasHumanLanguageText(text) {
const trimmed = text.replace(/\s+/g, ' ').trim()
if (trimmed.length < 2) {
return false
}
if (/^[\d\s!-/:-@[-`{-~]+$/.test(trimmed)) {
return false
}
return /[A-Za-z\u00C0-\u024F\u3040-\u30ff\u3400-\u9fff\uac00-\ud7af]/.test(trimmed)
}
function compactText(text) {
return text.replace(/\s+/g, ' ').trim()
}
function lineAndColumn(sourceFile, node) {
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
return { line: position.line + 1, column: position.character + 1 }
}
function propertyNameText(name) {
if (ts.isIdentifier(name) || ts.isStringLiteralLike(name) || ts.isNumericLiteral(name)) {
return name.text
}
if (ts.isComputedPropertyName(name) && ts.isStringLiteralLike(name.expression)) {
return name.expression.text
}
return undefined
}
function expressionNameText(node) {
if (ts.isIdentifier(node)) {
return node.text
}
if (ts.isPropertyAccessExpression(node)) {
return `${expressionNameText(node.expression) ?? ''}.${node.name.text}`.replace(/^\./, '')
}
return undefined
}
function stringParts(node) {
if (ts.isStringLiteralLike(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
return [{ text: node.text, dynamic: false }]
}
if (!ts.isTemplateExpression(node)) {
return []
}
return [
{ text: node.head.text, dynamic: true },
...node.templateSpans.map((span) => ({ text: span.literal.text, dynamic: true }))
]
}
function isInsideLocalizationCall(node) {
let current = node.parent
while (current) {
if (ts.isCallExpression(current)) {
const name = expressionNameText(current.expression)
if (name && LOCALIZATION_CALL_NAMES.has(name.split('.').at(-1) ?? name)) {
return true
}
}
current = current.parent
}
return false
}
function isJsxAttributeValue(node) {
const parent = node.parent
if (!parent) {
return undefined
}
if (ts.isJsxAttribute(parent)) {
return propertyNameText(parent.name)
}
if (parent && ts.isJsxExpression(parent) && parent.parent && ts.isJsxAttribute(parent.parent)) {
return propertyNameText(parent.parent.name)
}
return undefined
}
function ancestorJsxAttributeName(node) {
let current = node.parent
while (current) {
if (ts.isJsxAttribute(current)) {
return propertyNameText(current.name)
}
if (
ts.isJsxExpression(current) ||
ts.isConditionalExpression(current) ||
ts.isParenthesizedExpression(current) ||
ts.isBinaryExpression(current)
) {
current = current.parent
continue
}
return undefined
}
return undefined
}
function isRenderedJsxExpression(node) {
let current = node.parent
while (current) {
if (ts.isJsxExpression(current)) {
return (
ts.isJsxElement(current.parent) ||
ts.isJsxFragment(current.parent) ||
ts.isJsxSelfClosingElement(current.parent)
)
}
if (
ts.isConditionalExpression(current) ||
ts.isParenthesizedExpression(current) ||
ts.isTemplateExpression(current) ||
ts.isNoSubstitutionTemplateLiteral(current)
) {
if (ts.isConditionalExpression(current) && current.condition === node) {
return false
}
current = current.parent
continue
}
if (ts.isBinaryExpression(current)) {
if (!COPY_PRESERVING_BINARY_OPERATORS.has(current.operatorToken.kind)) {
return false
}
current = current.parent
continue
}
return false
}
return false
}
function nearestObjectPropertyName(node) {
let current = node.parent
while (current) {
if (ts.isPropertyAssignment(current) || ts.isShorthandPropertyAssignment(current)) {
return propertyNameText(current.name)
}
if (ts.isObjectLiteralExpression(current) || ts.isArrayLiteralExpression(current)) {
current = current.parent
continue
}
return undefined
}
return undefined
}
function hasAncestorObjectPropertyName(node, names) {
let current = node.parent
while (current) {
if (
(ts.isPropertyAssignment(current) || ts.isShorthandPropertyAssignment(current)) &&
names.has(propertyNameText(current.name) ?? '')
) {
return true
}
current = current.parent
}
return false
}
function nearestAncestorObjectPropertyName(node) {
let current = node.parent
while (current) {
if (ts.isPropertyAssignment(current) || ts.isShorthandPropertyAssignment(current)) {
return propertyNameText(current.name)
}
current = current.parent
}
return undefined
}
function findAncestor(node, predicate) {
let current = node.parent
while (current) {
if (predicate(current)) {
return current
}
current = current.parent
}
return undefined
}
function isUserVisibleCallArgument(node) {
const call = findAncestor(node, ts.isCallExpression)
if (!call) {
return false
}
const expressionName = expressionNameText(call.expression)
if (!expressionName) {
return false
}
const parts = expressionName.split('.')
const methodName = parts.at(-1)
const objectName = parts.at(-2)
return (
USER_VISIBLE_FUNCTION_NAMES.has(expressionName) ||
USER_VISIBLE_FUNCTION_NAMES.has(methodName ?? '') ||
(objectName !== undefined &&
USER_VISIBLE_OBJECT_NAMES.has(objectName) &&
USER_VISIBLE_OBJECT_METHODS.has(methodName ?? ''))
)
}
function isInsideNonCopyJsxElement(node) {
const element = findAncestor(node, (ancestor) => ts.isJsxElement(ancestor))
return Boolean(element && NON_COPY_JSX_ELEMENTS.has(element.openingElement.tagName.getText()))
}
function classifyStringNode(node) {
if (
hasAncestorObjectPropertyName(node, CLASS_PROPERTY_NAMES) ||
isInsideNonCopyJsxElement(node)
) {
return undefined
}
if (
findAncestor(
node,
(ancestor) =>
ts.isBinaryExpression(ancestor) &&
!COPY_PRESERVING_BINARY_OPERATORS.has(ancestor.operatorToken.kind)
)
) {
return undefined
}
const jsxAttributeName = isJsxAttributeValue(node)
if (jsxAttributeName) {
return USER_VISIBLE_JSX_ATTRIBUTES.has(jsxAttributeName)
? `jsx-attribute:${jsxAttributeName}`
: undefined
}
const ancestorAttributeName = ancestorJsxAttributeName(node)
if (ancestorAttributeName) {
return USER_VISIBLE_JSX_ATTRIBUTES.has(ancestorAttributeName)
? `jsx-attribute:${ancestorAttributeName}`
: undefined
}
if (ts.isJsxText(node)) {
return 'jsx-text'
}
const objectPropertyName = nearestObjectPropertyName(node)
if (objectPropertyName && !USER_VISIBLE_OBJECT_KEYS.has(objectPropertyName)) {
return undefined
}
const ancestorObjectPropertyName = nearestAncestorObjectPropertyName(node)
if (ancestorObjectPropertyName && !USER_VISIBLE_OBJECT_KEYS.has(ancestorObjectPropertyName)) {
return undefined
}
if (isRenderedJsxExpression(node)) {
return 'jsx-expression'
}
if (isUserVisibleCallArgument(node)) {
return 'user-visible-call'
}
if (objectPropertyName) {
return `object-property:${objectPropertyName}`
}
return undefined
}
function areaForFile(relativePath) {
const rendererPrefix = 'src/renderer/src/'
if (!relativePath.startsWith(rendererPrefix)) {
return relativePath.split('/').slice(0, 2).join('/')
}
const withoutPrefix = relativePath.slice(rendererPrefix.length)
const parts = withoutPrefix.split('/')
if (parts[0] === 'components' && parts[1]) {
return `renderer/${parts[1]}`
}
return `renderer/${parts[0] ?? 'root'}`
}
export function collectLocalizationCandidates(filePath, sourceText, root = process.cwd()) {
const sourceKind =
filePath.endsWith('.tsx') || filePath.endsWith('.jsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS
const sourceFile = ts.createSourceFile(
filePath,
sourceText,
ts.ScriptTarget.Latest,
true,
sourceKind
)
const reports = []
const relativePath = normalizePath(root, filePath)
function pushReport(node, kind, text, dynamic = false) {
const value = compactText(text)
if (!hasHumanLanguageText(value) || isInsideLocalizationCall(node)) {
return
}
const position = lineAndColumn(sourceFile, node)
reports.push({
area: areaForFile(relativePath),
filePath: relativePath,
start: node.getStart(sourceFile),
end: node.getEnd(),
line: position.line,
column: position.column,
kind,
text: value,
dynamic
})
}
function visit(node) {
if (ts.isJsxText(node)) {
pushReport(node, 'jsx-text', node.text)
return
}
if (
ts.isStringLiteralLike(node) ||
ts.isNoSubstitutionTemplateLiteral(node) ||
ts.isTemplateExpression(node)
) {
const kind = classifyStringNode(node)
if (kind) {
for (const part of stringParts(node)) {
pushReport(node, kind, part.text, part.dynamic)
}
}
}
ts.forEachChild(node, visit)
}
visit(sourceFile)
return reports
}
function groupByArea(reports) {
const groups = new Map()
for (const report of reports) {
const group = groups.get(report.area) ?? { area: report.area, count: 0, files: new Map() }
group.count += 1
group.files.set(report.filePath, (group.files.get(report.filePath) ?? 0) + 1)
groups.set(report.area, group)
}
return [...groups.values()].sort((left, right) => right.count - left.count)
}
function formatReports(_root, reports) {
return reports
.map(
(report) =>
`${report.filePath}:${report.line}:${report.column} ${report.kind}: ${JSON.stringify(report.text)}`
)
.join('\n')
}
function formatMarkdownReport(reports) {
const groups = groupByArea(reports)
const lines = [
'# Localization Candidate Inventory',
'',
`Generated: ${new Date().toISOString()}`,
'',
`Total candidates: ${reports.length}`,
'',
'## Area Summary',
''
]
for (const group of groups) {
lines.push(`- ${group.area}: ${group.count} candidates across ${group.files.size} files`)
}
lines.push('', '## Candidates', '')
for (const group of groups) {
lines.push(`### ${group.area}`, '')
for (const report of reports.filter((entry) => entry.area === group.area)) {
lines.push(
`- \`${report.filePath}:${report.line}:${report.column}\` ${report.kind}: ${JSON.stringify(report.text)}`
)
}
lines.push('')
}
return lines.join('\n')
}
function parseArgs(argv) {
const options = {
allowlistPath: path.join('config', 'localization-coverage-allowlist.json'),
check: false,
format: 'summary',
outputPath: null,
sourceRoot: path.join('src', 'renderer', 'src')
}
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index]
if (arg === '--json') {
options.format = 'json'
} else if (arg === '--markdown') {
options.format = 'markdown'
} else if (arg === '--check') {
options.check = true
} else if (arg === '--allowlist') {
options.allowlistPath = argv[index + 1] ?? options.allowlistPath
index += 1
} else if (arg === '--output') {
options.outputPath = argv[index + 1] ?? null
index += 1
} else if (arg === '--source-root') {
options.sourceRoot = argv[index + 1] ?? options.sourceRoot
index += 1
}
}
return options
}
function candidateSignature(candidate) {
return JSON.stringify({
filePath: candidate.filePath,
kind: candidate.kind,
text: candidate.text,
dynamic: candidate.dynamic
})
}
function countBySignature(reports) {
const counts = new Map()
for (const report of reports) {
const signature = candidateSignature(report)
counts.set(signature, (counts.get(signature) ?? 0) + 1)
}
return counts
}
async function readAllowlist(root, allowlistPath) {
const absolutePath = path.resolve(root, allowlistPath)
const raw = await fs.readFile(absolutePath, 'utf8')
return JSON.parse(raw)
}
function findNewCandidates(reports, allowlist) {
const allowedCounts = new Map(
allowlist.map((entry) => [
JSON.stringify({
filePath: entry.filePath,
kind: entry.kind,
text: entry.text,
dynamic: entry.dynamic
}),
entry.count
])
)
const seenCounts = countBySignature(reports)
const newCandidates = []
for (const report of reports) {
const signature = candidateSignature(report)
const seenCount = seenCounts.get(signature) ?? 0
const allowedCount = allowedCounts.get(signature) ?? 0
if (seenCount > allowedCount) {
newCandidates.push(report)
seenCounts.set(signature, seenCount - 1)
}
}
return newCandidates
}
export async function main(root = process.cwd(), argv = process.argv.slice(2)) {
const options = parseArgs(argv)
const absoluteSourceRoot = path.resolve(root, options.sourceRoot)
const files = await collectSourceFiles(root, absoluteSourceRoot)
const reports = []
for (const filePath of files) {
const sourceText = await fs.readFile(filePath, 'utf8')
reports.push(...collectLocalizationCandidates(filePath, sourceText, root))
}
if (options.check) {
const allowlist = await readAllowlist(root, options.allowlistPath)
const newCandidates = findNewCandidates(reports, allowlist)
if (newCandidates.length > 0) {
console.error('New unlocalized renderer strings were found.')
console.error('Localize them or add a reviewed exclusion to the localization allowlist.')
console.error('')
console.error(formatReports(root, newCandidates))
return 1
}
console.log(`Localization coverage check passed with ${reports.length} allowlisted candidates.`)
return 0
}
const output =
options.format === 'json'
? `${JSON.stringify(reports, null, 2)}\n`
: options.format === 'markdown'
? `${formatMarkdownReport(reports)}\n`
: `${reports.length} localization candidates in ${files.length} files.\n${groupByArea(
reports
)
.map((group) => `${group.area}: ${group.count}`)
.join('\n')}\n`
if (options.outputPath) {
await fs.mkdir(path.dirname(path.resolve(root, options.outputPath)), { recursive: true })
await fs.writeFile(path.resolve(root, options.outputPath), output)
} else {
process.stdout.write(output)
}
return 0
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
process.exit(await main())
}