snapshot of https://github.com/makeplane/plane.git @ d868349a55de79979eb8b98701bf1702c67ad634 (2026-09-15)
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
This commit is contained in:
245
packages/i18n/scripts/sync-check.ts
Normal file
245
packages/i18n/scripts/sync-check.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
// Usage:
|
||||
// tsx packages/i18n/scripts/sync-check.ts # Report only
|
||||
// tsx packages/i18n/scripts/sync-check.ts --ci # Exit 1 if issues found
|
||||
|
||||
import type { LocaleData } from "./lib/locale-io.js";
|
||||
import { LOCALES_DIR, listLocales, loadLocale } from "./lib/locale-io.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Format a number with commas (e.g. 7712 -> "7,712"). */
|
||||
function fmt(n: number): string {
|
||||
return n.toLocaleString("en-US");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Checks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface CollisionEntry {
|
||||
key: string;
|
||||
files: string[];
|
||||
}
|
||||
|
||||
/** Cross-namespace collision check: same flattened key in multiple namespace files. */
|
||||
function findCollisions(localeData: LocaleData): CollisionEntry[] {
|
||||
const keyToFiles = new Map<string, string[]>();
|
||||
for (const ns of localeData.namespaces) {
|
||||
for (const key of ns.keys) {
|
||||
const existing = keyToFiles.get(key);
|
||||
if (existing) {
|
||||
existing.push(`${ns.name}.json`);
|
||||
} else {
|
||||
keyToFiles.set(key, [`${ns.name}.json`]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const collisions: CollisionEntry[] = [];
|
||||
for (const [key, files] of keyToFiles) {
|
||||
if (files.length > 1) {
|
||||
collisions.push({ key, files });
|
||||
}
|
||||
}
|
||||
return collisions.toSorted((a, b) => a.key.localeCompare(b.key));
|
||||
}
|
||||
|
||||
interface PathConflict {
|
||||
leaf: string;
|
||||
branch: string;
|
||||
}
|
||||
|
||||
/** Path conflict check: a key is both a leaf AND a prefix of another key. */
|
||||
function findPathConflicts(localeData: LocaleData): PathConflict[] {
|
||||
const allKeysArray = [...localeData.allKeys].toSorted();
|
||||
const conflicts: PathConflict[] = [];
|
||||
|
||||
// Build a set of all prefixes used in the keys
|
||||
const prefixes = new Set<string>();
|
||||
for (const key of allKeysArray) {
|
||||
const parts = key.split(".");
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
prefixes.add(parts.slice(0, i).join("."));
|
||||
}
|
||||
}
|
||||
|
||||
// A conflict exists when a leaf key is also a prefix
|
||||
for (const key of allKeysArray) {
|
||||
if (prefixes.has(key)) {
|
||||
// Find one example of a key that extends this prefix
|
||||
const extending = allKeysArray.find((k) => k.startsWith(key + "."));
|
||||
if (extending) {
|
||||
conflicts.push({ leaf: key, branch: extending });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return conflicts;
|
||||
}
|
||||
|
||||
interface LocaleComparison {
|
||||
locale: string;
|
||||
totalKeys: number;
|
||||
missingKeys: string[];
|
||||
staleKeys: string[];
|
||||
coverage: number; // 0-100
|
||||
}
|
||||
|
||||
function compareToEnglish(enKeys: Set<string>, other: LocaleData): LocaleComparison {
|
||||
const missingKeys: string[] = [];
|
||||
const staleKeys: string[] = [];
|
||||
|
||||
for (const key of enKeys) {
|
||||
if (!other.allKeys.has(key)) {
|
||||
missingKeys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of other.allKeys) {
|
||||
if (!enKeys.has(key)) {
|
||||
staleKeys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
const coverage = enKeys.size > 0 ? ((enKeys.size - missingKeys.length) / enKeys.size) * 100 : 100;
|
||||
|
||||
return {
|
||||
locale: other.locale,
|
||||
totalKeys: other.allKeys.size,
|
||||
missingKeys: missingKeys.toSorted(),
|
||||
staleKeys: staleKeys.toSorted(),
|
||||
coverage,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function main() {
|
||||
const ciMode = process.argv.includes("--ci");
|
||||
|
||||
// Discover all locale directories
|
||||
const localeDirs = listLocales();
|
||||
|
||||
if (!localeDirs.includes("en")) {
|
||||
console.error("ERROR: English locale (en) not found in", LOCALES_DIR);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Load all locales
|
||||
const localeDataMap = new Map<string, LocaleData>();
|
||||
for (const locale of localeDirs) {
|
||||
localeDataMap.set(locale, loadLocale(locale));
|
||||
}
|
||||
|
||||
const enData = localeDataMap.get("en")!;
|
||||
|
||||
// Run checks
|
||||
const collisions = findCollisions(enData);
|
||||
const pathConflicts = findPathConflicts(enData);
|
||||
|
||||
const comparisons: LocaleComparison[] = [];
|
||||
for (const locale of localeDirs) {
|
||||
if (locale === "en") continue;
|
||||
comparisons.push(compareToEnglish(enData.allKeys, localeDataMap.get(locale)!));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Print report
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
let hasFailure = false;
|
||||
|
||||
console.log("\n=== Sync Check Results ===\n");
|
||||
console.log(` en: ${fmt(enData.allKeys.size)} keys (source)\n`);
|
||||
|
||||
for (const comp of comparisons) {
|
||||
const status = comp.missingKeys.length === 0 ? "✓" : "✗";
|
||||
const missingStr = comp.missingKeys.length > 0 ? ` — ${fmt(comp.missingKeys.length)} missing` : "";
|
||||
const staleStr = comp.staleKeys.length > 0 ? `, ${fmt(comp.staleKeys.length)} stale` : "";
|
||||
console.log(
|
||||
` ${status} ${comp.locale.padEnd(10)} ${fmt(comp.totalKeys)} keys (${comp.coverage.toFixed(1)}%)${missingStr}${staleStr}`
|
||||
);
|
||||
if (comp.missingKeys.length > 0) {
|
||||
hasFailure = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Cross-namespace collisions
|
||||
if (collisions.length > 0) {
|
||||
hasFailure = true;
|
||||
console.log("\nCROSS-NAMESPACE COLLISIONS:");
|
||||
for (const c of collisions) {
|
||||
console.log(` ✗ "${c.key}" exists in: ${c.files.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Path conflicts
|
||||
if (pathConflicts.length > 0) {
|
||||
hasFailure = true;
|
||||
console.log("\nPATH CONFLICTS:");
|
||||
for (const pc of pathConflicts) {
|
||||
console.log(` ✗ "${pc.leaf}" is a leaf but "${pc.branch}" extends it`);
|
||||
}
|
||||
}
|
||||
|
||||
// Missing keys detail
|
||||
const withMissing = comparisons.filter((c) => c.missingKeys.length > 0);
|
||||
if (withMissing.length > 0) {
|
||||
console.log("\n--- Missing Keys Detail ---\n");
|
||||
for (const comp of withMissing) {
|
||||
console.log(`${comp.locale} (${fmt(comp.missingKeys.length)} missing):`);
|
||||
const show = comp.missingKeys.slice(0, 20);
|
||||
for (const key of show) {
|
||||
console.log(` - ${key}`);
|
||||
}
|
||||
if (comp.missingKeys.length > 20) {
|
||||
console.log(` ... and ${fmt(comp.missingKeys.length - 20)} more`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
// Stale keys detail
|
||||
const withStale = comparisons.filter((c) => c.staleKeys.length > 0);
|
||||
if (withStale.length > 0) {
|
||||
console.log("--- Stale Keys Detail ---\n");
|
||||
for (const comp of withStale) {
|
||||
console.log(`${comp.locale} (${fmt(comp.staleKeys.length)} stale):`);
|
||||
const show = comp.staleKeys.slice(0, 20);
|
||||
for (const key of show) {
|
||||
console.log(` - ${key}`);
|
||||
}
|
||||
if (comp.staleKeys.length > 20) {
|
||||
console.log(` ... and ${fmt(comp.staleKeys.length - 20)} more`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
// CI exit code
|
||||
if (ciMode && hasFailure) {
|
||||
console.log("CI mode: exiting with code 1 due to missing keys, collisions, or path conflicts.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!hasFailure) {
|
||||
console.log("\nAll locales are in sync with English. No issues found.");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (err) {
|
||||
console.error("Sync check failed:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user