fix(i18n,notice): make locale formatting idempotent and guard early notice translations
This commit is contained in:
@@ -945,10 +945,15 @@ function loadLocales() {
|
|||||||
function ensureBackup(localePath) {
|
function ensureBackup(localePath) {
|
||||||
const backupPath = `${localePath}.bak`;
|
const backupPath = `${localePath}.bak`;
|
||||||
if (fs.existsSync(backupPath)) {
|
if (fs.existsSync(backupPath)) {
|
||||||
throw new Error(
|
try {
|
||||||
`Backup file already exists for ${path.basename(localePath)}; ` +
|
fs.rmSync(backupPath);
|
||||||
"either remove it manually or rerun with --no-backup",
|
} catch (error) {
|
||||||
);
|
throw new Error(
|
||||||
|
`Failed to recycle existing backup for ${path.basename(
|
||||||
|
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,46 +1038,55 @@ export default resources;
|
|||||||
}
|
}
|
||||||
|
|
||||||
function writeLocale(locale, data, options) {
|
function writeLocale(locale, data, options) {
|
||||||
const backups = new Set();
|
const backups = new Map();
|
||||||
|
let success = false;
|
||||||
|
|
||||||
if (locale.format === "single-file") {
|
try {
|
||||||
const target = locale.files[0].path;
|
if (locale.format === "single-file") {
|
||||||
backupIfNeeded(target, backups, options);
|
const target = locale.files[0].path;
|
||||||
const serialized = JSON.stringify(data, null, 2);
|
backupIfNeeded(target, backups, options);
|
||||||
fs.writeFileSync(target, `${serialized}\n`, "utf8");
|
const serialized = JSON.stringify(data, null, 2);
|
||||||
return;
|
fs.writeFileSync(target, `${serialized}\n`, "utf8");
|
||||||
}
|
success = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const entries = Object.entries(data);
|
const entries = Object.entries(data);
|
||||||
const orderedNamespaces = entries.map(([namespace]) => namespace);
|
const orderedNamespaces = entries.map(([namespace]) => namespace);
|
||||||
const existingFiles = new Map(
|
const existingFiles = new Map(
|
||||||
locale.files.map((file) => [file.namespace, file.path]),
|
locale.files.map((file) => [file.namespace, file.path]),
|
||||||
);
|
);
|
||||||
const visited = new Set();
|
const visited = new Set();
|
||||||
|
|
||||||
for (const [namespace, value] of entries) {
|
for (const [namespace, value] of entries) {
|
||||||
const target =
|
const target =
|
||||||
existingFiles.get(namespace) ??
|
existingFiles.get(namespace) ??
|
||||||
path.join(locale.dir, `${namespace}.json`);
|
path.join(locale.dir, `${namespace}.json`);
|
||||||
backupIfNeeded(target, backups, options);
|
backupIfNeeded(target, backups, options);
|
||||||
const serialized = JSON.stringify(value ?? {}, null, 2);
|
const serialized = JSON.stringify(value ?? {}, null, 2);
|
||||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||||
fs.writeFileSync(target, `${serialized}\n`, "utf8");
|
fs.writeFileSync(target, `${serialized}\n`, "utf8");
|
||||||
visited.add(namespace);
|
visited.add(namespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [namespace, filePath] of existingFiles.entries()) {
|
for (const [namespace, filePath] of existingFiles.entries()) {
|
||||||
if (!visited.has(namespace) && fs.existsSync(filePath)) {
|
if (!visited.has(namespace) && fs.existsSync(filePath)) {
|
||||||
backupIfNeeded(filePath, backups, options);
|
backupIfNeeded(filePath, backups, options);
|
||||||
fs.rmSync(filePath);
|
fs.rmSync(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
regenerateLocaleIndex(locale.dir, orderedNamespaces);
|
||||||
|
locale.files = orderedNamespaces.map((namespace) => ({
|
||||||
|
namespace,
|
||||||
|
path: path.join(locale.dir, `${namespace}.json`),
|
||||||
|
}));
|
||||||
|
success = true;
|
||||||
|
} finally {
|
||||||
|
if (success) {
|
||||||
|
cleanupBackups(backups);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
regenerateLocaleIndex(locale.dir, orderedNamespaces);
|
|
||||||
locale.files = orderedNamespaces.map((namespace) => ({
|
|
||||||
namespace,
|
|
||||||
path: path.join(locale.dir, `${namespace}.json`),
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function processLocale(
|
function processLocale(
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user