feat(i18n): add mobile localization release gates
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import Database from "better-sqlite3";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { applySqlitePragmas, ensureDataLayout } from "../lib/data-dir.mjs";
|
||||
|
||||
const registry = [
|
||||
{ entityType: "project", table: "projects", fields: { name: "name", description: "description", coverImageAlt: "cover_image_alt" } },
|
||||
{ entityType: "planning_section", table: "project_planning_sections", fields: { title: "title", content: "content" } },
|
||||
{ entityType: "task", table: "tasks", fields: { title: "title", description: "description" } },
|
||||
{ entityType: "branding", table: "instance_branding", fields: { portalWelcome: "portal_welcome_text", portalFooter: "portal_footer_text" } },
|
||||
{ entityType: "calendar_event", table: "calendar_events", fields: { title: "title", description: "description" } },
|
||||
{ entityType: "client", table: "clients", fields: { notes: "notes" } },
|
||||
{ entityType: "client_activity", table: "client_activities", fields: { title: "title", content: "content" } },
|
||||
{ entityType: "finance_transaction", table: "finance_transactions", fields: { category: "category", description: "description" } },
|
||||
{ entityType: "journal_entry", table: "journal_entries", fields: { moodLabel: "mood_label", note: "note" } },
|
||||
{ entityType: "chat_session", table: "chat_sessions", fields: { title: "title" } },
|
||||
{ entityType: "proposal", table: "proposals", fields: { title: "title", description: "description" } },
|
||||
{ entityType: "subscription", table: "subscriptions", fields: { name: "name", category: "category" } },
|
||||
];
|
||||
|
||||
export function runBackfill(argv = process.argv.slice(2)) {
|
||||
const write = argv.includes("--write");
|
||||
const startedAt = Date.now();
|
||||
const config = ensureDataLayout();
|
||||
const sqlite = new Database(config.databasePath);
|
||||
|
||||
try {
|
||||
applySqlitePragmas(sqlite);
|
||||
assertTable(sqlite, "content_translations");
|
||||
const defaultLocale = getDefaultLocale(sqlite);
|
||||
const insert = sqlite.prepare(`
|
||||
insert into content_translations (entity_type, entity_id, field, locale, value, created_at, updated_at)
|
||||
values (@entityType, @entityId, @field, @locale, @value, @now, @now)
|
||||
on conflict(entity_type, entity_id, field, locale) do nothing
|
||||
`);
|
||||
const existing = sqlite.prepare(`
|
||||
select 1
|
||||
from content_translations
|
||||
where entity_type = ? and entity_id = ? and field = ? and locale = ?
|
||||
limit 1
|
||||
`);
|
||||
const summary = {
|
||||
dryRun: !write,
|
||||
databasePath: config.databasePath,
|
||||
defaultLocale,
|
||||
planned: 0,
|
||||
inserted: 0,
|
||||
skippedTables: [],
|
||||
byEntity: {},
|
||||
durationMs: 0,
|
||||
};
|
||||
|
||||
const apply = sqlite.transaction(() => {
|
||||
for (const item of registry) {
|
||||
if (!tableExists(sqlite, item.table)) {
|
||||
summary.skippedTables.push(item.table);
|
||||
continue;
|
||||
}
|
||||
const columns = Object.values(item.fields);
|
||||
const rows = sqlite
|
||||
.prepare(`select id, ${columns.map((column) => `"${column}"`).join(", ")} from "${item.table}"`)
|
||||
.all();
|
||||
|
||||
for (const row of rows) {
|
||||
for (const [field, column] of Object.entries(item.fields)) {
|
||||
const value = normalizeText(row[column]);
|
||||
if (!value) continue;
|
||||
if (existing.get(item.entityType, String(row.id), field, defaultLocale)) continue;
|
||||
|
||||
summary.planned += 1;
|
||||
summary.byEntity[item.entityType] = (summary.byEntity[item.entityType] ?? 0) + 1;
|
||||
if (write) {
|
||||
const result = insert.run({
|
||||
entityType: item.entityType,
|
||||
entityId: String(row.id),
|
||||
field,
|
||||
locale: defaultLocale,
|
||||
value,
|
||||
now: Date.now(),
|
||||
});
|
||||
summary.inserted += result.changes;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
apply();
|
||||
summary.durationMs = Date.now() - startedAt;
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
return summary;
|
||||
} finally {
|
||||
sqlite.close();
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultLocale(sqlite) {
|
||||
if (!tableExists(sqlite, "instance_i18n_settings")) return "tr";
|
||||
return sqlite
|
||||
.prepare("select default_locale from instance_i18n_settings where key = 'default'")
|
||||
.get()
|
||||
?.default_locale ?? "tr";
|
||||
}
|
||||
|
||||
function normalizeText(value) {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
||||
}
|
||||
|
||||
function assertTable(sqlite, table) {
|
||||
if (!tableExists(sqlite, table)) {
|
||||
throw new Error(`${table} tablosu bulunamadı. Önce pnpm db:migrate çalıştır.`);
|
||||
}
|
||||
}
|
||||
|
||||
function tableExists(sqlite, table) {
|
||||
return Boolean(sqlite.prepare("select 1 from sqlite_master where type = 'table' and name = ?").get(table));
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
runBackfill();
|
||||
}
|
||||
Reference in New Issue
Block a user