Server-i18n katalog + backfill for befintliga anvandare utan hushall

This commit is contained in:
Sven (AAMOS AI)
2026-08-06 22:33:00 +07:00
parent 3696aadf98
commit 3d1d823583
3 changed files with 107 additions and 26 deletions
+58
View File
@@ -0,0 +1,58 @@
/**
* One-off backfill: create a default household for every user that does not
* have a household_members row yet. Used after the auto-household onboarding
* change to avoid leaving existing staging/test users stranded.
*/
import { config as loadDotenv } from "dotenv";
import path from "node:path";
import { existsSync } from "node:fs";
for (const candidate of [".env", "../.env", "../../.env"]) {
const p = path.resolve(process.cwd(), candidate);
if (existsSync(p)) {
loadDotenv({ path: p });
break;
}
}
import { createDatabase, closeDatabase, schema } from "@app/database";
import { eq, isNull } from "drizzle-orm";
import { createHouseholdWithDefaults } from "../src/lib/helpers.js";
import { t } from "../src/lib/i18n.js";
async function main() {
const dbUrl = process.env.DATABASE_URL;
if (!dbUrl) {
console.error("Sätt DATABASE_URL innan backfill körs.");
process.exit(1);
}
const { db, pool } = createDatabase(dbUrl);
const usersWithoutHousehold = await db
.select({ id: schema.users.id, locale: schema.users.locale })
.from(schema.users)
.leftJoin(
schema.householdMembers,
eq(schema.householdMembers.userId, schema.users.id),
)
.where(isNull(schema.householdMembers.userId));
console.log(`[backfill] Hittade ${usersWithoutHousehold.length} användare utan hushåll.`);
let created = 0;
for (const user of usersWithoutHousehold) {
const name = t("onboarding.householdDefaultName", user.locale ?? "sv-SE");
await createHouseholdWithDefaults(db, { userId: user.id, name, size: 1 });
created++;
}
console.log(`[backfill] Skapade ${created} hushåll.`);
await closeDatabase();
await pool.end();
}
main().catch((err) => {
console.error("[backfill] MISSLYCKADES:", err);
process.exit(1);
});
+27 -26
View File
@@ -1,31 +1,32 @@
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const cache: Record<string, Record<string, string>> = {};
/** /**
* Minimal server-side i18n lookup into the mobile app's common.json files. * Minimal server-side i18n catalog. API code must never read files from other
* Falls back to the requested key if the translation is missing. * apps' source trees (e.g. apps/mobile). Values are copied once from the mobile
* common.json files at implementation time.
*/ */
const HOUSEHOLD_DEFAULT_NAMES: Record<string, string> = {
da: "Hjemme",
de: "Zuhause",
en: "Home",
es: "Casa",
fi: "Koti",
fr: "Maison",
it: "Casa",
nb: "Hjemme",
nl: "Thuis",
pl: "Dom",
pt: "Casa",
sv: "Hemma",
};
export function t(key: string, languageTag: string): string { export function t(key: string, languageTag: string): string {
const lang = (languageTag.split("-")[0] ?? languageTag).toLowerCase(); if (key !== "onboarding.householdDefaultName") {
let dict = cache[lang]; // No other server-side keys are supported yet; fall back to a safe default.
if (!dict) { return HOUSEHOLD_DEFAULT_NAMES["sv"]!;
try {
const file = path.resolve(
__dirname,
"../../../../apps/mobile/src/locales",
lang,
"common.json",
);
dict = JSON.parse(readFileSync(file, "utf8")) as Record<string, string>;
} catch {
dict = {};
}
cache[lang] = dict;
} }
return dict[key] ?? key;
const lang = (languageTag.split("-")[0] ?? "sv").toLowerCase();
return (
HOUSEHOLD_DEFAULT_NAMES[lang] ?? HOUSEHOLD_DEFAULT_NAMES["en"] ?? HOUSEHOLD_DEFAULT_NAMES["sv"]!
);
} }
+22
View File
@@ -0,0 +1,22 @@
import "./setup-env.js";
import { describe, expect, it } from "vitest";
import { t } from "../src/lib/i18n.js";
describe("server i18n", () => {
it("returns locale-specific household default name", () => {
expect(t("onboarding.householdDefaultName", "sv-SE")).toBe("Hemma");
expect(t("onboarding.householdDefaultName", "en-US")).toBe("Home");
expect(t("onboarding.householdDefaultName", "pl-PL")).toBe("Dom");
});
it("falls back to English for unsupported languages", () => {
expect(t("onboarding.householdDefaultName", "xx-XX")).toBe("Home");
expect(t("onboarding.householdDefaultName", "jp")).toBe("Home");
});
it("never returns the key string as value", () => {
const result = t("onboarding.householdDefaultName", "zz");
expect(result).not.toBe("onboarding.householdDefaultName");
expect(result.length).toBeGreaterThan(0);
});
});