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

This commit is contained in:
upstream-mirror
2026-09-15 15:14:58 +05:00
commit 23059a1de3
5252 changed files with 573834 additions and 0 deletions

View File

@@ -0,0 +1,170 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
// Usage: npx tsx packages/i18n/scripts/generate-types.ts
// Reads: src/locales/en/*.json
// Writes: src/types/keys.generated.ts
import fs from "node:fs";
import path from "node:path";
const COPYRIGHT_HEADER = `/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/`;
type NestedObject = { [key: string]: string | NestedObject };
/**
* Recursively flatten a nested object into dot-notated keys.
* Returns an array of flattened key strings.
*/
function flattenKeys(obj: NestedObject, prefix = ""): string[] {
const keys: string[] = [];
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === "string") {
keys.push(fullKey);
} else if (typeof value === "object" && value !== null) {
keys.push(...flattenKeys(value as NestedObject, fullKey));
}
}
return keys;
}
/**
* Detect path conflicts where a key is both a leaf (string value) and a prefix
* of another key. For example, "workspace" as a leaf and "workspace.settings" as
* another key would be a conflict.
*/
function detectPathConflicts(keys: string[]): string[] {
// Build a set of all prefixes used across keys
const prefixes = new Set<string>();
for (const key of keys) {
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
const conflicts: string[] = [];
for (const key of keys) {
if (prefixes.has(key)) {
const extending = keys.find((k) => k.startsWith(key + "."));
if (extending) {
conflicts.push(`Path conflict: "${key}" is both a leaf key and a prefix of "${extending}"`);
}
}
}
return conflicts;
}
function main(): void {
const rootDir = import.meta.dirname;
const localesDir = path.resolve(rootDir, "..", "src", "locales", "en");
const outputDir = path.resolve(rootDir, "..", "src", "types");
const outputFile = path.join(outputDir, "keys.generated.ts");
// Read all JSON files from the English locales directory
if (!fs.existsSync(localesDir)) {
console.error(`Error: Locales directory not found: ${localesDir}`);
process.exit(1);
}
const jsonFiles = fs
.readdirSync(localesDir)
.filter((file) => file.endsWith(".json"))
.sort();
if (jsonFiles.length === 0) {
console.error(`Error: No JSON files found in ${localesDir}`);
process.exit(1);
}
// Track keys per namespace file for collision detection
const keysByFile = new Map<string, string[]>();
const allKeys = new Set<string>();
const collisions: string[] = [];
for (const file of jsonFiles) {
const filePath = path.join(localesDir, file);
const content = fs.readFileSync(filePath, "utf-8");
const parsed = (() => {
try {
return JSON.parse(content) as NestedObject;
} catch {
console.error(`Error: Failed to parse JSON in ${file}`);
process.exit(1);
}
})();
const fileKeys = flattenKeys(parsed);
keysByFile.set(file, fileKeys);
// Detect cross-namespace collisions
for (const key of fileKeys) {
if (allKeys.has(key)) {
// Find which file already had this key
for (const [otherFile, otherKeys] of keysByFile.entries()) {
if (otherFile !== file && otherKeys.includes(key)) {
collisions.push(`Cross-namespace collision: key "${key}" exists in both "${otherFile}" and "${file}"`);
}
}
}
allKeys.add(key);
}
}
if (collisions.length > 0) {
console.error("Error: Cross-namespace key collisions detected:");
for (const collision of collisions) {
console.error(` ${collision}`);
}
process.exit(1);
}
// Detect path conflicts
const sortedKeys = [...allKeys].sort();
const pathConflicts = detectPathConflicts(sortedKeys);
if (pathConflicts.length > 0) {
console.error("Error: Path conflicts detected:");
for (const conflict of pathConflicts) {
console.error(` ${conflict}`);
}
process.exit(1);
}
// Ensure output directory exists
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Generate the output file
const keyLines = sortedKeys.map((key) => ` | "${key}"`).join("\n");
const output = `${COPYRIGHT_HEADER}
// AUTO-GENERATED — DO NOT EDIT
// Generated from ${jsonFiles.length} English namespace files (${sortedKeys.length} keys)
// Run: pnpm run generate:types
export type TTranslationKeys =
${keyLines}
;
`;
fs.writeFileSync(outputFile, output, "utf-8");
console.log(`Generated ${sortedKeys.length} keys from ${jsonFiles.length} namespace files`);
}
main();

View File

@@ -0,0 +1,77 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import fs from "node:fs";
import path from "node:path";
export const LOCALES_DIR = path.resolve(import.meta.dirname, "../../src/locales");
/** Recursively flatten an object into dot-notation keys. */
export function flattenKeys(obj: Record<string, unknown>, prefix = ""): string[] {
const keys: string[] = [];
for (const [k, v] of Object.entries(obj)) {
const full = prefix ? `${prefix}.${k}` : k;
if (v !== null && typeof v === "object" && !Array.isArray(v)) {
keys.push(...flattenKeys(v as Record<string, unknown>, full));
} else {
keys.push(full);
}
}
return keys;
}
/** Parse JSON from a file, including the file path in any error message. */
export function readJsonFile(filePath: string): Record<string, unknown> {
const raw = fs.readFileSync(filePath, "utf-8");
try {
return JSON.parse(raw) as Record<string, unknown>;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(`Failed to parse JSON at ${filePath}: ${message}`, { cause: err });
}
}
export interface NamespaceData {
name: string; // file stem, e.g. "common"
keys: Set<string>;
data: Record<string, unknown>; // original parsed object
}
export interface LocaleData {
locale: string;
namespaces: NamespaceData[];
allKeys: Set<string>;
}
export function listLocales(): string[] {
const entries = fs.readdirSync(LOCALES_DIR, { withFileTypes: true });
return entries
.filter((e) => e.isDirectory())
.map((e) => e.name)
.toSorted();
}
export function loadLocale(locale: string): LocaleData {
const localeDir = path.join(LOCALES_DIR, locale);
const files = fs.readdirSync(localeDir).filter((f) => f.endsWith(".json"));
const namespaces: NamespaceData[] = [];
const allKeys = new Set<string>();
for (const file of files) {
const filePath = path.join(localeDir, file);
const data = readJsonFile(filePath);
const name = path.basename(file, ".json");
const keys = flattenKeys(data);
const keySet = new Set(keys);
namespaces.push({ name, keys: keySet, data });
for (const key of keys) {
allKeys.add(key);
}
}
return { locale, namespaces, allKeys };
}

View 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);
}

View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"types": ["node"]
},
"include": ["."]
}