Files
bookorbit/client/scripts/locale-source-keys.mjs
neon 3b9bae7525 ci(i18n): fix the two guards that keep the crowdin sync failing
Two independent guards were failing the workflow on healthy input.

Retention read "exported value equals the English source" as a lost translation, but exports are
built with skipUntranslatedStrings, so Crowdin omits a key entirely once it stops carrying a
translation. Anything present in the export is by definition translated, and the equality check only
ever fired on correct work: Spanish "Error", "Feb" and "Cookie", French "Description", Portuguese
"Readwise". Retention now compares presence alone, which is what absence already meant. The same
inference lived in a legacyComplete branch that would have silently stopped protecting the 2,318
keys whose translation legitimately equals the source once any locale reached full translation.

Scheduled runs had no way out of that. The only escape hatch is allowed_translation_losses, a
workflow_dispatch input that is never persisted, so every schedule rediscovered the same losses and
failed until someone dispatched the run by hand.

The slot count rule then only runs for the keys it is given, and the sync passed none, so its
rejection filter was weaker than the locale validation running on its output one step later. An
invalid ja message survived the filter, reached the catalog, and failed the next step instead of
being dropped and reported. Move the source scan into locale-source-keys.mjs so the sync and
validate-locales.mjs derive the same keys.
2026-08-21 15:50:03 +00:00

43 lines
1.5 KiB
JavaScript

import { readdir, readFile } from 'node:fs/promises'
import path from 'node:path'
const scriptDirectory = import.meta.dirname ?? path.join(process.cwd(), 'scripts')
const clientRoot = path.resolve(scriptDirectory, '..')
async function sourceFiles(directory) {
const entries = await readdir(directory, { withFileTypes: true })
const files = await Promise.all(
entries.map(async (entry) => {
const entryPath = path.join(directory, entry.name)
if (entry.isDirectory()) {
if (entry.name === '__tests__') return []
return sourceFiles(entryPath)
}
return entry.isFile() && /\.(ts|vue)$/.test(entry.name) && !/\.(spec|test)\.ts$/.test(entry.name) ? [entryPath] : []
}),
)
return files.flat()
}
export async function collectSourceMessageKeys(sourceDirectory = path.join(clientRoot, 'src')) {
const files = await sourceFiles(sourceDirectory)
const keys = new Set()
const slotCountKeys = new Set()
for (const file of files) {
const source = await readFile(file, 'utf8')
for (const match of source.matchAll(/(?<![\w$])(?:\$?t|\$?tc|\$?te)\(\s*(['"])([A-Za-z0-9_.-]+)\1/g)) {
keys.add(match[2])
}
for (const match of source.matchAll(/<i18n-t\b[^>]*\bkeypath\s*=\s*(['"])([A-Za-z0-9_.-]+)\1/g)) {
keys.add(match[2])
}
for (const match of source.matchAll(/<IcuCountText\b[^>]*\bkeypath\s*=\s*(['"])([A-Za-z0-9_.-]+)\1/g)) {
keys.add(match[2])
slotCountKeys.add(match[2])
}
}
return { keys, slotCountKeys }
}