fix(i18n,notice): make locale formatting idempotent and guard early notice translations

This commit is contained in:
Slinetrac
2025-11-06 19:09:07 +08:00
Unverified
parent 9e3078e017
commit 30750df724
2 changed files with 77 additions and 39 deletions

View File

@@ -945,11 +945,16 @@ function loadLocales() {
function ensureBackup(localePath) { function ensureBackup(localePath) {
const backupPath = `${localePath}.bak`; const backupPath = `${localePath}.bak`;
if (fs.existsSync(backupPath)) { if (fs.existsSync(backupPath)) {
try {
fs.rmSync(backupPath);
} catch (error) {
throw new Error( throw new Error(
`Backup file already exists for ${path.basename(localePath)}; ` + `Failed to recycle existing backup for ${path.basename(
"either remove it manually or rerun with --no-backup", localePath,
)}: ${error.message}`,
); );
} }
}
fs.copyFileSync(localePath, backupPath); fs.copyFileSync(localePath, backupPath);
return backupPath; return backupPath;
} }
@@ -959,10 +964,27 @@ function backupIfNeeded(filePath, backups, options) {
if (!fs.existsSync(filePath)) return; if (!fs.existsSync(filePath)) return;
if (backups.has(filePath)) return; if (backups.has(filePath)) return;
const backupPath = ensureBackup(filePath); const backupPath = ensureBackup(filePath);
backups.add(filePath); backups.set(filePath, backupPath);
return backupPath; return backupPath;
} }
function cleanupBackups(backups) {
for (const backupPath of backups.values()) {
try {
if (fs.existsSync(backupPath)) {
fs.rmSync(backupPath);
}
} catch (error) {
console.warn(
`Warning: failed to remove backup ${path.basename(
backupPath,
)}: ${error.message}`,
);
}
}
backups.clear();
}
function toModuleIdentifier(namespace, seen) { function toModuleIdentifier(namespace, seen) {
const RESERVED = new Set([ const RESERVED = new Set([
"default", "default",
@@ -1016,13 +1038,16 @@ export default resources;
} }
function writeLocale(locale, data, options) { function writeLocale(locale, data, options) {
const backups = new Set(); const backups = new Map();
let success = false;
try {
if (locale.format === "single-file") { if (locale.format === "single-file") {
const target = locale.files[0].path; const target = locale.files[0].path;
backupIfNeeded(target, backups, options); backupIfNeeded(target, backups, options);
const serialized = JSON.stringify(data, null, 2); const serialized = JSON.stringify(data, null, 2);
fs.writeFileSync(target, `${serialized}\n`, "utf8"); fs.writeFileSync(target, `${serialized}\n`, "utf8");
success = true;
return; return;
} }
@@ -1056,6 +1081,12 @@ function writeLocale(locale, data, options) {
namespace, namespace,
path: path.join(locale.dir, `${namespace}.json`), path: path.join(locale.dir, `${namespace}.json`),
})); }));
success = true;
} finally {
if (success) {
cleanupBackups(backups);
}
}
} }
function processLocale( function processLocale(

View File

@@ -44,6 +44,8 @@ const DEFAULT_DURATIONS: Readonly<Record<NoticeType, number>> = {
error: 8000, error: 8000,
}; };
const TRANSLATION_KEY_PATTERN = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+$/;
let nextId = 0; let nextId = 0;
let notices: NoticeItem[] = []; let notices: NoticeItem[] = [];
const subscribers: Set<NoticeSubscriber> = new Set(); const subscribers: Set<NoticeSubscriber> = new Set();
@@ -157,11 +159,16 @@ function createRawDescriptor(message: string): NoticeTranslationDescriptor {
}; };
} }
function isLikelyTranslationKey(key: string) {
return TRANSLATION_KEY_PATTERN.test(key);
}
function shouldUseTranslationKey( function shouldUseTranslationKey(
key: string, key: string,
params?: Record<string, unknown>, params?: Record<string, unknown>,
) { ) {
if (params && Object.keys(params).length > 0) return true; if (params && Object.keys(params).length > 0) return true;
if (isLikelyTranslationKey(key)) return true;
if (i18n.isInitialized) { if (i18n.isInitialized) {
return i18n.exists(key); return i18n.exists(key);
} }