From 3d1d82358318a06ad51204f2be08d58905ba57e3 Mon Sep 17 00:00:00 2001 From: "Sven (AAMOS AI)" Date: Thu, 6 Aug 2026 22:33:00 +0700 Subject: [PATCH] Server-i18n katalog + backfill for befintliga anvandare utan hushall --- apps/api/scripts/backfill-households.ts | 58 +++++++++++++++++++++++++ apps/api/src/lib/i18n.ts | 53 +++++++++++----------- apps/api/test/i18n.test.ts | 22 ++++++++++ 3 files changed, 107 insertions(+), 26 deletions(-) create mode 100644 apps/api/scripts/backfill-households.ts create mode 100644 apps/api/test/i18n.test.ts diff --git a/apps/api/scripts/backfill-households.ts b/apps/api/scripts/backfill-households.ts new file mode 100644 index 0000000..350abf0 --- /dev/null +++ b/apps/api/scripts/backfill-households.ts @@ -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); +}); diff --git a/apps/api/src/lib/i18n.ts b/apps/api/src/lib/i18n.ts index 9e2d7f8..be46943 100644 --- a/apps/api/src/lib/i18n.ts +++ b/apps/api/src/lib/i18n.ts @@ -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> = {}; - /** - * Minimal server-side i18n lookup into the mobile app's common.json files. - * Falls back to the requested key if the translation is missing. + * Minimal server-side i18n catalog. API code must never read files from other + * 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 = { + 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 { - const lang = (languageTag.split("-")[0] ?? languageTag).toLowerCase(); - let dict = cache[lang]; - if (!dict) { - try { - const file = path.resolve( - __dirname, - "../../../../apps/mobile/src/locales", - lang, - "common.json", - ); - dict = JSON.parse(readFileSync(file, "utf8")) as Record; - } catch { - dict = {}; - } - cache[lang] = dict; + if (key !== "onboarding.householdDefaultName") { + // No other server-side keys are supported yet; fall back to a safe default. + return HOUSEHOLD_DEFAULT_NAMES["sv"]!; } - return dict[key] ?? key; + + const lang = (languageTag.split("-")[0] ?? "sv").toLowerCase(); + return ( + HOUSEHOLD_DEFAULT_NAMES[lang] ?? HOUSEHOLD_DEFAULT_NAMES["en"] ?? HOUSEHOLD_DEFAULT_NAMES["sv"]! + ); } diff --git a/apps/api/test/i18n.test.ts b/apps/api/test/i18n.test.ts new file mode 100644 index 0000000..9780108 --- /dev/null +++ b/apps/api/test/i18n.test.ts @@ -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); + }); +});