feat(i18n): server-genererad prosa -> nyckel+params (min dag, ombokning, scan-diff)
- Min-dag-noten: noteKey (estimateNote/computedNote) bredvid svensk note; klient t(noteKey).
- Ombokningsskäl (planning.ts): rescheduleReasonKey + params bredvid rescheduleReasonSv;
klient t(key, {title}). Workerns två andra ombokningstexter lämnade på svensk fallback.
- Scan-diff: reasonCode-enum (6 koder) bredvid reasonSv; klient t(scanDiff.reason.<code>).
- 8 nya nycklar i alla 12 språk. Ingen beslutslogik rörd; alla Sv-fält kvar som fallback.
- Kvar som egna spår: recept-säkerhetsmeddelanden + substitutions-data (översättningstabell).
- typecheck grönt (20/20), vakt grön, inventory-engine 53/53 tester.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -184,9 +184,12 @@ export async function mealRoutes(app: FastifyInstance) {
|
|||||||
title: m.recipeId ? (titleMap.get(m.recipeId) ?? m.titleSv) : m.titleSv,
|
title: m.recipeId ? (titleMap.get(m.recipeId) ?? m.titleSv) : m.titleSv,
|
||||||
})),
|
})),
|
||||||
summary,
|
summary,
|
||||||
|
// Svenska strängen behålls som fallback; noteKey låter klienten översätta
|
||||||
|
// (samma två fall som förr – noten visas alltid, exakt som tidigare).
|
||||||
note: hasEstimates
|
note: hasEstimates
|
||||||
? "Dagen innehåller uppskattade värden från foto – justera gärna vid behov."
|
? "Dagen innehåller uppskattade värden från foto – justera gärna vid behov."
|
||||||
: "Värdena är beräknade ur recept och livsmedelsdata och visas som uppskattningar.",
|
: "Värdena är beräknade ur recept och livsmedelsdata och visas som uppskattningar.",
|
||||||
|
noteKey: hasEstimates ? "myday.estimateNote" : "myday.computedNote",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ import { loadLocalePreferences } from "../lib/localeContext.js";
|
|||||||
* (GENERATE_WEEK_PLAN) som väger lager, utgångsdatum, matlådor, budget,
|
* (GENERATE_WEEK_PLAN) som väger lager, utgångsdatum, matlådor, budget,
|
||||||
* variation och mål – med deterministisk kärna och AAMOS som rådgivare.
|
* variation och mål – med deterministisk kärna och AAMOS som rådgivare.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Kanonisk svensk motivering vid dynamisk omplanering (spec §25). Samma funktion
|
||||||
|
* används både när planposten skrivs och för att härleda den översättbara i18n-
|
||||||
|
* nyckeln vid läsning (så att klienten kan visa den på användarens språk).
|
||||||
|
*/
|
||||||
|
export const rescheduleMovedReasonSv = (title: string): string =>
|
||||||
|
`${title} flyttades hit eftersom råvarorna bör användas först.`;
|
||||||
|
|
||||||
export async function planningRoutes(app: FastifyInstance) {
|
export async function planningRoutes(app: FastifyInstance) {
|
||||||
const auth = { preHandler: [app.authenticate] };
|
const auth = { preHandler: [app.authenticate] };
|
||||||
|
|
||||||
@@ -54,10 +62,20 @@ export async function planningRoutes(app: FastifyInstance) {
|
|||||||
);
|
);
|
||||||
const result = plansWithEntries.map(({ plan, entries }) => ({
|
const result = plansWithEntries.map(({ plan, entries }) => ({
|
||||||
...plan,
|
...plan,
|
||||||
entries: entries.map((e) => ({
|
entries: entries.map((e) => {
|
||||||
...e,
|
const title = e.recipeId ? (titleMap.get(e.recipeId) ?? e.titleSv) : e.titleSv;
|
||||||
title: e.recipeId ? (titleMap.get(e.recipeId) ?? e.titleSv) : e.titleSv,
|
// Behåll rescheduleReasonSv (fallback) men lägg till översättbar nyckel +
|
||||||
})),
|
// parametrar för just omplanerings-meningen (andra motiveringar, t.ex.
|
||||||
|
// matlåde-noteringar från workern, faller kvar på svenska).
|
||||||
|
const structured =
|
||||||
|
e.rescheduleReasonSv && e.rescheduleReasonSv === rescheduleMovedReasonSv(e.titleSv)
|
||||||
|
? {
|
||||||
|
rescheduleReasonKey: "plan.rescheduleReason",
|
||||||
|
rescheduleReasonParams: { title },
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
return { ...e, title, ...structured };
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
return { plans: result };
|
return { plans: result };
|
||||||
});
|
});
|
||||||
@@ -167,7 +185,7 @@ export async function planningRoutes(app: FastifyInstance) {
|
|||||||
recipeId: entry.recipeId,
|
recipeId: entry.recipeId,
|
||||||
titleSv: entry.titleSv,
|
titleSv: entry.titleSv,
|
||||||
status: "moved",
|
status: "moved",
|
||||||
rescheduleReasonSv: `${entry.titleSv} flyttades hit eftersom råvarorna bör användas först.`,
|
rescheduleReasonSv: rescheduleMovedReasonSv(entry.titleSv),
|
||||||
})
|
})
|
||||||
.where(eq(schema.weekPlanEntries.id, later[0].id));
|
.where(eq(schema.weekPlanEntries.id, later[0].id));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ interface DayResponse {
|
|||||||
saltWarning: boolean;
|
saltWarning: boolean;
|
||||||
};
|
};
|
||||||
note: string;
|
note: string;
|
||||||
|
/** i18n-nyckel för noten; klienten översätter, `note` är svensk fallback. */
|
||||||
|
noteKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Måltidstypernas etiketter bor i i18n-katalogen: myday.mealType.<typ> (12 språk). */
|
/** Måltidstypernas etiketter bor i i18n-katalogen: myday.mealType.<typ> (12 språk). */
|
||||||
@@ -82,7 +84,7 @@ export default function MyDayScreen() {
|
|||||||
if (query.isLoading) return <LoadingView />;
|
if (query.isLoading) return <LoadingView />;
|
||||||
if (query.isError || !query.data) return <ErrorView onRetry={() => void query.refetch()} />;
|
if (query.isError || !query.data) return <ErrorView onRetry={() => void query.refetch()} />;
|
||||||
|
|
||||||
const { summary, meals, note } = query.data;
|
const { summary, meals, note, noteKey } = query.data;
|
||||||
const remaining = summary.remaining.kcal;
|
const remaining = summary.remaining.kcal;
|
||||||
|
|
||||||
const macros = [
|
const macros = [
|
||||||
@@ -186,7 +188,7 @@ export default function MyDayScreen() {
|
|||||||
|
|
||||||
<Spacer size={spacing.sm} />
|
<Spacer size={spacing.sm} />
|
||||||
<Button label={t("myday.logMeal")} onPress={() => router.push("/log-meal")} />
|
<Button label={t("myday.logMeal")} onPress={() => router.push("/log-meal")} />
|
||||||
<Small>{note}</Small>
|
<Small>{noteKey ? t(noteKey) : note}</Small>
|
||||||
<Small>{t("myday.targetsNote")}</Small>
|
<Small>{t("myday.targetsNote")}</Small>
|
||||||
</Screen>
|
</Screen>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ interface WeekPlanEntry {
|
|||||||
portions: number;
|
portions: number;
|
||||||
status: string;
|
status: string;
|
||||||
rescheduleReasonSv: string | null;
|
rescheduleReasonSv: string | null;
|
||||||
|
/** i18n-nyckel + parametrar; klienten översätter, rescheduleReasonSv är fallback. */
|
||||||
|
rescheduleReasonKey?: string | null;
|
||||||
|
rescheduleReasonParams?: Record<string, string | number> | null;
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
}
|
}
|
||||||
interface WeekPlan {
|
interface WeekPlan {
|
||||||
@@ -351,7 +354,11 @@ export default function PlanScreen() {
|
|||||||
</Row>
|
</Row>
|
||||||
</Row>
|
</Row>
|
||||||
<Small>{t("plan.portions", { count: entry.portions })}</Small>
|
<Small>{t("plan.portions", { count: entry.portions })}</Small>
|
||||||
{entry.rescheduleReasonSv ? <Small>{entry.rescheduleReasonSv}</Small> : null}
|
{entry.rescheduleReasonKey ? (
|
||||||
|
<Small>{t(entry.rescheduleReasonKey, entry.rescheduleReasonParams ?? undefined)}</Small>
|
||||||
|
) : entry.rescheduleReasonSv ? (
|
||||||
|
<Small>{entry.rescheduleReasonSv}</Small>
|
||||||
|
) : null}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ type DiffRow = {
|
|||||||
newLocationId?: string;
|
newLocationId?: string;
|
||||||
confidence: number;
|
confidence: number;
|
||||||
reason: string;
|
reason: string;
|
||||||
|
/** Stabil kod; klienten översätter via scanDiff.reason.<code>, reason är fallback. */
|
||||||
|
reasonCode?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ScanDiffReviewScreen() {
|
export default function ScanDiffReviewScreen() {
|
||||||
@@ -85,7 +87,7 @@ export default function ScanDiffReviewScreen() {
|
|||||||
<Body>{row.displayName}</Body>
|
<Body>{row.displayName}</Body>
|
||||||
<Tag label={t(`scan.diff.${row.kind}`)} tone={toneForKind(row.kind)} />
|
<Tag label={t(`scan.diff.${row.kind}`)} tone={toneForKind(row.kind)} />
|
||||||
</Row>
|
</Row>
|
||||||
<Small>{row.reason}</Small>
|
<Small>{row.reasonCode ? t(`scanDiff.reason.${row.reasonCode}`) : row.reason}</Small>
|
||||||
{(row.kind === "quantity_changed" || row.kind === "moved") && (
|
{(row.kind === "quantity_changed" || row.kind === "moved") && (
|
||||||
<Small>
|
<Small>
|
||||||
{formatQty(row.previousQuantity)} → {formatQty(row.newQuantity)} {row.unit}
|
{formatQty(row.previousQuantity)} → {formatQty(row.newQuantity)} {row.unit}
|
||||||
|
|||||||
@@ -601,5 +601,13 @@
|
|||||||
"onboarding.allergen.mustard": "Sennep",
|
"onboarding.allergen.mustard": "Sennep",
|
||||||
"onboarding.allergen.sulphites": "Sulfitter",
|
"onboarding.allergen.sulphites": "Sulfitter",
|
||||||
"onboarding.allergen.lupin": "Lupin",
|
"onboarding.allergen.lupin": "Lupin",
|
||||||
"onboarding.allergen.molluscs": "Bløddyr"
|
"onboarding.allergen.molluscs": "Bløddyr",
|
||||||
|
"myday.computedNote": "Værdierne er beregnet ud fra opskrifter og fødevaredata og vises som skøn.",
|
||||||
|
"plan.rescheduleReason": "{title} blev flyttet hertil, fordi råvarerne bør bruges først.",
|
||||||
|
"scanDiff.reason.moved_and_qty": "Flyttet og ændret mængde",
|
||||||
|
"scanDiff.reason.moved": "Flyttet til et andet sted",
|
||||||
|
"scanDiff.reason.qty_changed": "Ændret mængde",
|
||||||
|
"scanDiff.reason.unchanged": "Uændret",
|
||||||
|
"scanDiff.reason.new_item": "Ny vare på billedet",
|
||||||
|
"scanDiff.reason.vanished": "Mangler på det nye billede — bekræft, at den er brugt op"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -601,5 +601,13 @@
|
|||||||
"onboarding.allergen.mustard": "Senf",
|
"onboarding.allergen.mustard": "Senf",
|
||||||
"onboarding.allergen.sulphites": "Sulfite",
|
"onboarding.allergen.sulphites": "Sulfite",
|
||||||
"onboarding.allergen.lupin": "Lupinen",
|
"onboarding.allergen.lupin": "Lupinen",
|
||||||
"onboarding.allergen.molluscs": "Weichtiere"
|
"onboarding.allergen.molluscs": "Weichtiere",
|
||||||
|
"myday.computedNote": "Die Werte werden aus Rezepten und Lebensmitteldaten berechnet und als Schätzwerte angezeigt.",
|
||||||
|
"plan.rescheduleReason": "{title} wurde hierher verschoben, weil die Zutaten zuerst verbraucht werden sollten.",
|
||||||
|
"scanDiff.reason.moved_and_qty": "Verschoben und Menge geändert",
|
||||||
|
"scanDiff.reason.moved": "An einen anderen Ort verschoben",
|
||||||
|
"scanDiff.reason.qty_changed": "Menge geändert",
|
||||||
|
"scanDiff.reason.unchanged": "Unverändert",
|
||||||
|
"scanDiff.reason.new_item": "Neuer Artikel auf dem Bild",
|
||||||
|
"scanDiff.reason.vanished": "Fehlt auf dem neuen Bild – bestätige, dass er aufgebraucht ist"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -601,5 +601,13 @@
|
|||||||
"onboarding.allergen.mustard": "Mustard",
|
"onboarding.allergen.mustard": "Mustard",
|
||||||
"onboarding.allergen.sulphites": "Sulphites",
|
"onboarding.allergen.sulphites": "Sulphites",
|
||||||
"onboarding.allergen.lupin": "Lupin",
|
"onboarding.allergen.lupin": "Lupin",
|
||||||
"onboarding.allergen.molluscs": "Molluscs"
|
"onboarding.allergen.molluscs": "Molluscs",
|
||||||
|
"myday.computedNote": "Values are calculated from recipes and food data and shown as estimates.",
|
||||||
|
"plan.rescheduleReason": "{title} was moved here because the ingredients should be used first.",
|
||||||
|
"scanDiff.reason.moved_and_qty": "Moved and quantity changed",
|
||||||
|
"scanDiff.reason.moved": "Moved to another location",
|
||||||
|
"scanDiff.reason.qty_changed": "Quantity changed",
|
||||||
|
"scanDiff.reason.unchanged": "Unchanged",
|
||||||
|
"scanDiff.reason.new_item": "New item in the photo",
|
||||||
|
"scanDiff.reason.vanished": "Missing from the new photo — confirm it's used up"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -601,5 +601,13 @@
|
|||||||
"onboarding.allergen.mustard": "Mostaza",
|
"onboarding.allergen.mustard": "Mostaza",
|
||||||
"onboarding.allergen.sulphites": "Sulfitos",
|
"onboarding.allergen.sulphites": "Sulfitos",
|
||||||
"onboarding.allergen.lupin": "Altramuces",
|
"onboarding.allergen.lupin": "Altramuces",
|
||||||
"onboarding.allergen.molluscs": "Moluscos"
|
"onboarding.allergen.molluscs": "Moluscos",
|
||||||
|
"myday.computedNote": "Los valores se calculan a partir de recetas y datos de alimentos y se muestran como estimaciones.",
|
||||||
|
"plan.rescheduleReason": "{title} se movió aquí porque los ingredientes deberían usarse primero.",
|
||||||
|
"scanDiff.reason.moved_and_qty": "Movido y cantidad cambiada",
|
||||||
|
"scanDiff.reason.moved": "Movido a otra ubicación",
|
||||||
|
"scanDiff.reason.qty_changed": "Cantidad cambiada",
|
||||||
|
"scanDiff.reason.unchanged": "Sin cambios",
|
||||||
|
"scanDiff.reason.new_item": "Artículo nuevo en la foto",
|
||||||
|
"scanDiff.reason.vanished": "No aparece en la nueva foto: confirma que se ha agotado"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -601,5 +601,13 @@
|
|||||||
"onboarding.allergen.mustard": "Sinappi",
|
"onboarding.allergen.mustard": "Sinappi",
|
||||||
"onboarding.allergen.sulphites": "Sulfiitit",
|
"onboarding.allergen.sulphites": "Sulfiitit",
|
||||||
"onboarding.allergen.lupin": "Lupiini",
|
"onboarding.allergen.lupin": "Lupiini",
|
||||||
"onboarding.allergen.molluscs": "Nilviäiset"
|
"onboarding.allergen.molluscs": "Nilviäiset",
|
||||||
|
"myday.computedNote": "Arvot on laskettu resepteistä ja elintarviketiedoista ja näytetään arvioina.",
|
||||||
|
"plan.rescheduleReason": "{title} siirrettiin tähän, koska raaka-aineet kannattaa käyttää ensin.",
|
||||||
|
"scanDiff.reason.moved_and_qty": "Siirretty ja määrä muuttunut",
|
||||||
|
"scanDiff.reason.moved": "Siirretty toiseen paikkaan",
|
||||||
|
"scanDiff.reason.qty_changed": "Määrä muuttunut",
|
||||||
|
"scanDiff.reason.unchanged": "Ennallaan",
|
||||||
|
"scanDiff.reason.new_item": "Uusi tuote kuvassa",
|
||||||
|
"scanDiff.reason.vanished": "Puuttuu uudesta kuvasta – vahvista, että se on loppu"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -601,5 +601,13 @@
|
|||||||
"onboarding.allergen.mustard": "Moutarde",
|
"onboarding.allergen.mustard": "Moutarde",
|
||||||
"onboarding.allergen.sulphites": "Sulfites",
|
"onboarding.allergen.sulphites": "Sulfites",
|
||||||
"onboarding.allergen.lupin": "Lupin",
|
"onboarding.allergen.lupin": "Lupin",
|
||||||
"onboarding.allergen.molluscs": "Mollusques"
|
"onboarding.allergen.molluscs": "Mollusques",
|
||||||
|
"myday.computedNote": "Les valeurs sont calculées à partir des recettes et des données alimentaires et affichées à titre d'estimation.",
|
||||||
|
"plan.rescheduleReason": "{title} a été déplacé ici, car les ingrédients devraient être utilisés en premier.",
|
||||||
|
"scanDiff.reason.moved_and_qty": "Déplacé et quantité modifiée",
|
||||||
|
"scanDiff.reason.moved": "Déplacé vers un autre emplacement",
|
||||||
|
"scanDiff.reason.qty_changed": "Quantité modifiée",
|
||||||
|
"scanDiff.reason.unchanged": "Inchangé",
|
||||||
|
"scanDiff.reason.new_item": "Nouvel article sur la photo",
|
||||||
|
"scanDiff.reason.vanished": "Absent de la nouvelle photo — confirmez qu'il est épuisé"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -601,5 +601,13 @@
|
|||||||
"onboarding.allergen.mustard": "Senape",
|
"onboarding.allergen.mustard": "Senape",
|
||||||
"onboarding.allergen.sulphites": "Solfiti",
|
"onboarding.allergen.sulphites": "Solfiti",
|
||||||
"onboarding.allergen.lupin": "Lupini",
|
"onboarding.allergen.lupin": "Lupini",
|
||||||
"onboarding.allergen.molluscs": "Molluschi"
|
"onboarding.allergen.molluscs": "Molluschi",
|
||||||
|
"myday.computedNote": "I valori sono calcolati da ricette e dati alimentari e mostrati come stime.",
|
||||||
|
"plan.rescheduleReason": "{title} è stato spostato qui perché gli ingredienti andrebbero usati prima.",
|
||||||
|
"scanDiff.reason.moved_and_qty": "Spostato e quantità modificata",
|
||||||
|
"scanDiff.reason.moved": "Spostato in un'altra posizione",
|
||||||
|
"scanDiff.reason.qty_changed": "Quantità modificata",
|
||||||
|
"scanDiff.reason.unchanged": "Invariato",
|
||||||
|
"scanDiff.reason.new_item": "Nuovo articolo nella foto",
|
||||||
|
"scanDiff.reason.vanished": "Assente nella nuova foto — conferma che è esaurito"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -601,5 +601,13 @@
|
|||||||
"onboarding.allergen.mustard": "Sennep",
|
"onboarding.allergen.mustard": "Sennep",
|
||||||
"onboarding.allergen.sulphites": "Sulfitter",
|
"onboarding.allergen.sulphites": "Sulfitter",
|
||||||
"onboarding.allergen.lupin": "Lupin",
|
"onboarding.allergen.lupin": "Lupin",
|
||||||
"onboarding.allergen.molluscs": "Bløtdyr"
|
"onboarding.allergen.molluscs": "Bløtdyr",
|
||||||
|
"myday.computedNote": "Verdiene er beregnet ut fra oppskrifter og matvaredata og vises som anslag.",
|
||||||
|
"plan.rescheduleReason": "{title} ble flyttet hit fordi råvarene bør brukes først.",
|
||||||
|
"scanDiff.reason.moved_and_qty": "Flyttet og endret mengde",
|
||||||
|
"scanDiff.reason.moved": "Flyttet til et annet sted",
|
||||||
|
"scanDiff.reason.qty_changed": "Endret mengde",
|
||||||
|
"scanDiff.reason.unchanged": "Uendret",
|
||||||
|
"scanDiff.reason.new_item": "Ny vare på bildet",
|
||||||
|
"scanDiff.reason.vanished": "Mangler på det nye bildet – bekreft at den er brukt opp"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -601,5 +601,13 @@
|
|||||||
"onboarding.allergen.mustard": "Mosterd",
|
"onboarding.allergen.mustard": "Mosterd",
|
||||||
"onboarding.allergen.sulphites": "Sulfieten",
|
"onboarding.allergen.sulphites": "Sulfieten",
|
||||||
"onboarding.allergen.lupin": "Lupine",
|
"onboarding.allergen.lupin": "Lupine",
|
||||||
"onboarding.allergen.molluscs": "Weekdieren"
|
"onboarding.allergen.molluscs": "Weekdieren",
|
||||||
|
"myday.computedNote": "De waarden zijn berekend op basis van recepten en voedingsdata en worden als schattingen getoond.",
|
||||||
|
"plan.rescheduleReason": "{title} is hierheen verplaatst omdat de ingrediënten eerst gebruikt moeten worden.",
|
||||||
|
"scanDiff.reason.moved_and_qty": "Verplaatst en hoeveelheid gewijzigd",
|
||||||
|
"scanDiff.reason.moved": "Verplaatst naar een andere locatie",
|
||||||
|
"scanDiff.reason.qty_changed": "Hoeveelheid gewijzigd",
|
||||||
|
"scanDiff.reason.unchanged": "Ongewijzigd",
|
||||||
|
"scanDiff.reason.new_item": "Nieuw item op de foto",
|
||||||
|
"scanDiff.reason.vanished": "Ontbreekt op de nieuwe foto — bevestig dat het op is"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -615,5 +615,13 @@
|
|||||||
"onboarding.allergen.mustard": "Gorczyca",
|
"onboarding.allergen.mustard": "Gorczyca",
|
||||||
"onboarding.allergen.sulphites": "Siarczyny",
|
"onboarding.allergen.sulphites": "Siarczyny",
|
||||||
"onboarding.allergen.lupin": "Łubin",
|
"onboarding.allergen.lupin": "Łubin",
|
||||||
"onboarding.allergen.molluscs": "Mięczaki"
|
"onboarding.allergen.molluscs": "Mięczaki",
|
||||||
|
"myday.computedNote": "Wartości są obliczane na podstawie przepisów i danych o żywności i pokazywane jako szacunki.",
|
||||||
|
"plan.rescheduleReason": "Przeniesiono tutaj „{title}”, ponieważ składniki należy zużyć najpierw.",
|
||||||
|
"scanDiff.reason.moved_and_qty": "Przeniesiono i zmieniono ilość",
|
||||||
|
"scanDiff.reason.moved": "Przeniesiono w inne miejsce",
|
||||||
|
"scanDiff.reason.qty_changed": "Zmieniono ilość",
|
||||||
|
"scanDiff.reason.unchanged": "Bez zmian",
|
||||||
|
"scanDiff.reason.new_item": "Nowy produkt na zdjęciu",
|
||||||
|
"scanDiff.reason.vanished": "Brak na nowym zdjęciu — potwierdź, że produkt się skończył"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -601,5 +601,13 @@
|
|||||||
"onboarding.allergen.mustard": "Mostarda",
|
"onboarding.allergen.mustard": "Mostarda",
|
||||||
"onboarding.allergen.sulphites": "Sulfitos",
|
"onboarding.allergen.sulphites": "Sulfitos",
|
||||||
"onboarding.allergen.lupin": "Tremoço",
|
"onboarding.allergen.lupin": "Tremoço",
|
||||||
"onboarding.allergen.molluscs": "Moluscos"
|
"onboarding.allergen.molluscs": "Moluscos",
|
||||||
|
"myday.computedNote": "Os valores são calculados a partir de receitas e dados alimentares e mostrados como estimativas.",
|
||||||
|
"plan.rescheduleReason": "{title} foi movido para aqui porque os ingredientes devem ser usados primeiro.",
|
||||||
|
"scanDiff.reason.moved_and_qty": "Movido e quantidade alterada",
|
||||||
|
"scanDiff.reason.moved": "Movido para outro local",
|
||||||
|
"scanDiff.reason.qty_changed": "Quantidade alterada",
|
||||||
|
"scanDiff.reason.unchanged": "Inalterado",
|
||||||
|
"scanDiff.reason.new_item": "Novo item na foto",
|
||||||
|
"scanDiff.reason.vanished": "Ausente na nova foto — confirme que acabou"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -601,5 +601,13 @@
|
|||||||
"onboarding.allergen.mustard": "Senap",
|
"onboarding.allergen.mustard": "Senap",
|
||||||
"onboarding.allergen.sulphites": "Sulfiter",
|
"onboarding.allergen.sulphites": "Sulfiter",
|
||||||
"onboarding.allergen.lupin": "Lupin",
|
"onboarding.allergen.lupin": "Lupin",
|
||||||
"onboarding.allergen.molluscs": "Blötdjur"
|
"onboarding.allergen.molluscs": "Blötdjur",
|
||||||
|
"myday.computedNote": "Värdena är beräknade ur recept och livsmedelsdata och visas som uppskattningar.",
|
||||||
|
"plan.rescheduleReason": "{title} flyttades hit eftersom råvarorna bör användas först.",
|
||||||
|
"scanDiff.reason.moved_and_qty": "Flyttad och ändrad mängd",
|
||||||
|
"scanDiff.reason.moved": "Flyttad till annan plats",
|
||||||
|
"scanDiff.reason.qty_changed": "Ändrad mängd",
|
||||||
|
"scanDiff.reason.unchanged": "Oförändrad",
|
||||||
|
"scanDiff.reason.new_item": "Ny vara på bilden",
|
||||||
|
"scanDiff.reason.vanished": "Saknas på nya bilden — bekräfta att den är slut"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,19 @@ export interface NewScanObservation {
|
|||||||
|
|
||||||
export type ScanDiffRowKind = "new_item" | "moved" | "quantity_changed" | "vanished" | "unchanged";
|
export type ScanDiffRowKind = "new_item" | "moved" | "quantity_changed" | "vanished" | "unchanged";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stabil, översättbar kod för varje distinkt motivering. Klienten översätter
|
||||||
|
* via i18n-nyckeln `scanDiff.reason.<code>`; `reason` (svenska) är fallback.
|
||||||
|
* En kod per unikt meddelande – finare än `kind` (t.ex. flyttad vs flyttad+mängd).
|
||||||
|
*/
|
||||||
|
export type ScanDiffReasonCode =
|
||||||
|
| "moved_and_qty"
|
||||||
|
| "moved"
|
||||||
|
| "qty_changed"
|
||||||
|
| "unchanged"
|
||||||
|
| "new_item"
|
||||||
|
| "vanished";
|
||||||
|
|
||||||
export interface ScanDiffRow {
|
export interface ScanDiffRow {
|
||||||
kind: ScanDiffRowKind;
|
kind: ScanDiffRowKind;
|
||||||
previousItemId?: string;
|
previousItemId?: string;
|
||||||
@@ -32,8 +45,10 @@ export interface ScanDiffRow {
|
|||||||
newLocationId?: string;
|
newLocationId?: string;
|
||||||
/** Konfidens för slutsatsen (0–1). */
|
/** Konfidens för slutsatsen (0–1). */
|
||||||
confidence: number;
|
confidence: number;
|
||||||
/** Mänsklig läsbar motivering. */
|
/** Mänsklig läsbar motivering (svenska) – fallback när klienten saknar översättning. */
|
||||||
reason: string;
|
reason: string;
|
||||||
|
/** Stabil kod för klientöversättning (`scanDiff.reason.<code>`). */
|
||||||
|
reasonCode: ScanDiffReasonCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScanDiffResult {
|
export interface ScanDiffResult {
|
||||||
@@ -104,6 +119,7 @@ export function diffScans(
|
|||||||
newLocationId: obs.storageLocationId,
|
newLocationId: obs.storageLocationId,
|
||||||
confidence: round2(bestMatch.score * obs.observationConfidence),
|
confidence: round2(bestMatch.score * obs.observationConfidence),
|
||||||
reason: "Flyttad och ändrad mängd",
|
reason: "Flyttad och ändrad mängd",
|
||||||
|
reasonCode: "moved_and_qty",
|
||||||
});
|
});
|
||||||
} else if (locationChanged) {
|
} else if (locationChanged) {
|
||||||
rows.push({
|
rows.push({
|
||||||
@@ -117,6 +133,7 @@ export function diffScans(
|
|||||||
newLocationId: obs.storageLocationId,
|
newLocationId: obs.storageLocationId,
|
||||||
confidence: round2(bestMatch.score * obs.observationConfidence),
|
confidence: round2(bestMatch.score * obs.observationConfidence),
|
||||||
reason: "Flyttad till annan plats",
|
reason: "Flyttad till annan plats",
|
||||||
|
reasonCode: "moved",
|
||||||
});
|
});
|
||||||
} else if (quantityChanged) {
|
} else if (quantityChanged) {
|
||||||
rows.push({
|
rows.push({
|
||||||
@@ -130,6 +147,7 @@ export function diffScans(
|
|||||||
newLocationId: obs.storageLocationId,
|
newLocationId: obs.storageLocationId,
|
||||||
confidence: round2(bestMatch.score * obs.observationConfidence),
|
confidence: round2(bestMatch.score * obs.observationConfidence),
|
||||||
reason: "Ändrad mängd",
|
reason: "Ändrad mängd",
|
||||||
|
reasonCode: "qty_changed",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
rows.push({
|
rows.push({
|
||||||
@@ -143,6 +161,7 @@ export function diffScans(
|
|||||||
newLocationId: obs.storageLocationId,
|
newLocationId: obs.storageLocationId,
|
||||||
confidence: round2(bestMatch.score * obs.observationConfidence),
|
confidence: round2(bestMatch.score * obs.observationConfidence),
|
||||||
reason: "Oförändrad",
|
reason: "Oförändrad",
|
||||||
|
reasonCode: "unchanged",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,6 +179,7 @@ export function diffScans(
|
|||||||
newLocationId: obs.storageLocationId,
|
newLocationId: obs.storageLocationId,
|
||||||
confidence: round2(obs.observationConfidence),
|
confidence: round2(obs.observationConfidence),
|
||||||
reason: "Ny vara på bilden",
|
reason: "Ny vara på bilden",
|
||||||
|
reasonCode: "new_item",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +196,7 @@ export function diffScans(
|
|||||||
previousLocationId: item.storageLocationId,
|
previousLocationId: item.storageLocationId,
|
||||||
confidence: 0.6,
|
confidence: 0.6,
|
||||||
reason: "Saknas på nya bilden — bekräfta att den är slut",
|
reason: "Saknas på nya bilden — bekräfta att den är slut",
|
||||||
|
reasonCode: "vanished",
|
||||||
});
|
});
|
||||||
pendingVanishedIds.push(item.id);
|
pendingVanishedIds.push(item.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -484,6 +484,9 @@ export interface WeekPlanEntry {
|
|||||||
status: "planned" | "cooked" | "skipped" | "moved";
|
status: "planned" | "cooked" | "skipped" | "moved";
|
||||||
/** Förklaring vid dynamisk omplanering (spec §25). */
|
/** Förklaring vid dynamisk omplanering (spec §25). */
|
||||||
rescheduleReasonSv?: string;
|
rescheduleReasonSv?: string;
|
||||||
|
/** Översättbar form av omplaneringsmotiveringen: i18n-nyckel + parametrar. */
|
||||||
|
rescheduleReasonKey?: string;
|
||||||
|
rescheduleReasonParams?: Record<string, string | number>;
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user