# Del 20 – Internationalisering: audit, målarkitektur & migreringsplan Styrande spec: den uppladdade i18n-specifikationen (`*_Internationalisering_Claude.md`). Detta dokument är den audit + plan som i18n-spec §34 kräver före implementation, samt kvitto på vad som redan är genomfört. Status: ✅ klart · 🔨 genomfört i denna leverans · 📋 planerat (fas angiven). ## 1. Nulägesrapport (audit av kodbasen) ### Redan korrekt (byggt språkneutralt från start) | Område | i18n-spec | Läge | | ------------------------------------------------------------------------------------------------ | --------- | ---------------------------------------------------------------------------------------------------------------------------------- | | Interna enums (måltid, kök, diet, plats, svårighet, notistyp, plan, creator-nivå …) | §4, §16 | ✅ Engelska maskinidentifierare överallt (`dinner`, `fridge`, `vegetarian`) | | Canonical ingredient-IDs | §12 | ✅ Engelska slugs (`chicken_breast`); `aliases[]` finns; nameSv/nameEn-kolumner | | Ingrediensdensitet | §10 | ✅ `density_g_per_ml` + `grams_per_piece` per ingrediens; generell volym↔massa-konvertering vägrar utan densitet (returnerar null) | | Nutrition canonical per 100 g + provenance | §19 | ✅ (kJ + natrium/salt-profiler 📋 fas i7) | | Allergener som canonical IDs, deterministiska | §18 | ✅ (marknadsregler/visningsprofiler 📋 fas i7) | | Högtider datadrivna med market-kod + datumregler | §17 | ✅ `season_events(market, date_rule, lead_days …)` – uppfyller market_events-syftet | | Datum: timestamps UTC (`timestamptz`), bäst före som lokalt kalenderdatum (`date`) | §21 | ✅ | | AI: canonical separerat från displaytext i kontrakten (`canonicalIngredientId` + `detectedName`) | §22, §30 | ✅ | | Minne: strukturerad `value` + key, inte fritextsanning | §23 | ✅ delvis (summarySv finns som cache – renderas om vid språkbyte 📋 fas i6) | | Recept: struktur (ingrediens-IDs, mängder, tider, temperaturer, DNA) skild från text | §13 | ✅ struktur; översättningstabeller 📋 fas i5 | | Varumärket som konfiguration | §28 | 🔨 `brand.config.json` – enda platsen; kod svep-verifierad | | Store-produkt-IDs språkneutrala | §25 | 🔨 `household_monthly` m.fl., härledda ur brand-config | ### Avvikelser som åtgärdas i DENNA leverans (🔨) 1. **Enhetskoder var svenska** (`msk`, `tsk`, `krm`, `dl`, `st`, `portion`) – brott mot §11. Åtgärd: språkneutrala koder `GRAM, KILOGRAM, MILLILITER, DECILITER, LITER, TEASPOON, TABLESPOON, CUP_US, FLUID_OUNCE_US, OUNCE, POUND, COUNT, PORTION, PINCH, SLICE, CLOVE, CAN, PACKAGE` i enum/DB/motorer/seed/API/app. Dokumenterad standard (§10): TEASPOON=5 ml, TABLESPOON=15 ml (metrisk), CUP_US=236,59 ml, FL_OZ_US=29,57 ml, OUNCE=28,35 g, POUND=453,59 g, PINCH≈0,5 ml. `krm` i svenska recept = 1 ml → lagras som MILLILITER; svensk visning ("krm", "msk") sker via översättningslagret. 2. **UserLocalePreferences saknades** (§6): ny tabell `user_locale_preferences` (languageTag BCP 47, regionCode ISO 3166-1, timeZone IANA, measurementSystem METRIC/US_CUSTOMARY/MIXED, temperatureUnit, currencyCode ISO 4217, firstDayOfWeek, use24HourTime) + `GET/PATCH /v1/me/locale-preferences`; språk ≠ region ≠ enheter. 3. **AAMOS-anrop saknade full lokaliseringskontext** (§22): kuvertets metadata har nu `localeContext` (languageTag, regionCode, timeZone, measurementSystem, temperatureUnit, currencyCode) och skickas från API/worker. 4. **Hårdkodade UI-texter i mobilappen utanför i18n-modulen** (§7): options-/etikett- kartor (onboarding, samtycken, måltidstyper, butiksavdelningar, enhetsetiketter, Alert-rubriker) flyttade till översättningsnycklar; `t()` injicerar `{brand}`. 5. **Notiser sparade endast färdig text** (§26): `notifications` har nu `template_key`, `variables`, `locale`; renderad text behålls som cache och renderas om vid utskick. 6. **Namnet i koden** (§28): all kod avvarumärkad – paketscope `@app/*`, UI via `{brand}`, tjänste-/könamn ur brand-slug, neutrala standardvärden i config. ### Status M1–M10 | Fas | Status | Verifiering | | --- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | M1 | ✅ Genomförd | Alla pengakolumner `*_minor` (integer) + `households.currency_code`; E2E: budget i minor + valuta, receptfilter på minor-kostnad | | M2 | ✅ Genomförd | `ingredient_translations` (101 en-rader seedade ur nameEn) + `unit_translations` (36 rader sv+en) + `/v1/i18n/units`; en-sök träffar översättningar | | M3 | ✅ Genomförd | `recipe_translations` + `recipe_step_translations`; TRANSLATE_RECIPE-jobb → deterministisk verifiering (stegantal, bevarade tal, konfidens) → admin publicerar; E2E: en-användare får översatt titel/steg/ingrediensnamn med sv-fallback | | M4 | ✅ Genomförd | i18next med Intl-plural (`_one`/`_other`), locales/{sv,en}/common.json (175 nycklar, paritetstestad), språkbyte utan omstart (versionsbaserad re-mount), språkval i profilen synkat mot locale-preferenser | | M5 | ✅ Genomförd | `displayQuantity` i shared-types (METRIC/US_CUSTOMARY/MIXED, kvartsavrundning för cups/tsp, ≈-märkning); mobilens formatQuantity konverterar per användarens måttsystem; 15 tester | | M6 | ✅ Genomförd | `nutrition_display_profiles` + `allergen_market_rules` seedade (EU/SE/GB 14, US 9, CA 12) + `/v1/i18n/nutrition-profile` med EU-fallback; kJ- och natrium↔salt-hjälpare med tester | | M7 | ✅ Genomförd | unaccent + pg_trgm + trigramindex (skapas idempotent i migrate, master-varianten i create-database.sql); sök: "creme"→Crème fraiche, "jordgubar"→Jordgubbar | | M8 | 📋 Kvar (ops) | Store-metadata, skärmbilder och prissättning per marknad görs i butikskonsolerna inför lansering – produkt-ID:n redan varumärkes- och språkneutrala | | M9 | 📋 Kvar (beslut D-024) | Adminpanelen förblir svensk tills internationell ops finns | | M10 | ✅ Genomförd | `buildMemoryOverview(items, languageTag)`: rubriker per språk, deterministisk summary-rendering ur strukturerad value med sv-fallback; nya minnen får lokaliserad summary via localeContext | ### Ursprunglig plan (behålls som referens) | # | Åtgärd | i18n-spec | Fas | Kommentar | | --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | M1 | Pengar i minor units + valuta: alla `*_sek`-kolumner → `*_minor int` + `currency_code` (hushållsnivå), `Money`-typ i API | §20 | i5 | Typ + hjälpare levererade i `shared-types/money.ts`; kolumnbyte görs som nästa migration MEDAN databasen är produktionstom – ingen datamigrering behövs | | M2 | `ingredient_translations` + flytt av nameSv/nameEn → rader; `unit_translations` i DB (visning finns nu i app-lagret) | §11–12 | i3 | Canonical-IDs är redan rätt → ren additiv migration | | M3 | `recipe_translations` + `recipe_step_translations`; AI-utkast med status DRAFT_AI→…→PUBLISHED + automatiska verifieringar (samma IDs/mängder/tider/allergener) | §13–14 | i5 | Receptstrukturen är redan språkoberoende | | M4 | i18next + ICU-plural i mobilen (ersätter interims-t()), namespaces `/locales/{lng}/{ns}.json`, expo-localization för förslag, språkbyte utan omstart, CI-vakt mot hårdkodad text | §5, §7–8, §31 | i4 | Nyckelstrukturen är redan namespace-formad (`home.*`, `scan.*` …) → mekanisk flytt. Motivering i beslutslogg D-023 | | M5 | Måttvisningstjänst: canonical→display per measurementSystem (ml→cup/tsp, g→oz/lb, °C→°F) med förlustfri lagring (canonical + original + källa + confidence) | §9–10, §30 | i5 | Konverteringstabellen finns; visningsvalet styrs av locale-prefs | | M6 | `nutrition_display_profiles`, `allergen_market_rules`, kJ + natrium | §18–19 | i7 | | | M7 | Sök: unaccent + pg_trgm + alias/synonymtabeller per locale | §24 | i5 | `ingredient_aliases`-fältet finns redan | | M8 | Lokaliserad store-metadata, screenshots, prissättning per marknad | §25 | i8 | Produkt-IDs redan neutrala | | M9 | Adminpanelen till nycklar | §7 | i8 | Beslut: internt verktyg får vara svenskt tills internationell ops finns (D-024) | | M10 | AAMOS-minne: rendera summaries ur strukturerad value vid läsning i valt språk | §23 | i6 | value är redan strukturerad | ## 2. Berörda filer (denna leverans) `packages/shared-types` (enums/units/brand/money/locale) · `packages/validation` (unit-schema, locale-prefs-schema) · `packages/database` (unit-enum, user_locale_preferences, notifications-kolumner, seed) · motorerna (enhetsberäkning via kodtabellen) · `packages/ai-contracts` (localeContext) · `apps/api` (locale-prefs-routes, kontext till AAMOS, neutrala defaults) · `apps/worker` (notismallar, kontext) · `apps/mobile` (unit-etiketter, nya nycklar, brand-injektion) · alla package.json (scope `@app/*`). ## 3. Migreringsplan & rollback Pre-launch utan produktionsdata ⇒ enum-/kolumnbyten görs i **basmigrationen** (0000 regenererad; loggat i Del 7). Efter lansering gäller i stället spec §29-mönstret: nya kolumner → mapping → dubbelskrivning → verifiering → borttag efter kontroll; varje migration med backup (deploy.sh vägrar utan dump) och nedskriven rollback. Rollback för denna leverans: git-tag före sveper + regenererbar databas. ## 4. Testplan (§31) 🔨 Nu: enhetstester för alla nya enhetskoder inkl. cup/oz/lb-konvertering och "vägra-utan-densitet"; locale-prefs-API; oförändrade motorresultat (61 befintliga tester gröna efter kodbytet). 📋 i4–i8: pseudolokal-test (nycklar aldrig synliga), plural-ICU, formatteringstester (decimal/tusental/datum/valuta) för sv-SE, en-US, en-GB, de-DE, fi-FI, snapshot av receptöversättningsverifieringen, CI-grep-vakt mot hårdkodade strängar. ## 5. Exit criteria-läge (§33) Uppfyllda redan: språkneutrala enums, canonical ingredienser, datadrivna högtider, UTC/lokaldatum, AAMOS-kontext, enhetsbyte utan dataförlust (canonical lagring), nytt språk = nya rader (ingen schemaändring), nytt land = data (market-rader). Återstår före internationell lansering: M1–M8 ovan – uppskattat 2–3 sprintar, kan ske parallellt med svensk beta eftersom allt är additivt.