i18n(app-svep) + i18n-vakt: hela mobil-UI:t översatt + CI-vakt mot regress

- Svepte 51 hårdkodade strängar -> t() över konto, sök/bläddra, sparade recept,
  kök, planera, skanna, byt måltid, inköp, minne, cooking, api-fel (+ locLabel/
  kategorier via nycklar). 47 nya nycklar, översatta till alla 12 språk.
- NY: apps/mobile/scripts/i18n-check.mjs (i18n:check) – fäller bygget om ett språk
  saknar en nyckel, en använd nyckel saknas, eller det finns hårdkodad UI-text.
  Wire:ad i CI (.github/workflows/ci.yml) efter typecheck. => 'lägg till språk'
  blir: kör vakten, den listar exakt vad som fattas. Aldrig mer leta för hand.
- Prettier-fixade 5 filer från tidigare leveranser så format:check blir grön.
- submit-recipe fri-text-payload + firebase dev-stubs markerade // i18n-ignore.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2026-08-21 01:50:08 +00:00
parent cadaa1a41f
commit 2d59a3e636
36 changed files with 1123 additions and 288 deletions
+33 -27
View File
@@ -37,14 +37,8 @@ interface InventoryItem {
}
const LOCATION_ORDER = ["fridge", "freezer", "pantry", "garage_freezer", "wine_fridge"];
const LOCATION_LABELS: Record<string, string> = {
fridge: "Kyl",
freezer: "Frys",
pantry: "Skafferi",
garage_freezer: "Garagefrys",
wine_fridge: "Vinkyl",
};
const locLabel = (type: string) => LOCATION_LABELS[type] ?? type;
const locLabel = (type: string) =>
LOCATION_ORDER.includes(type) ? t(`home.location.${type}`) : type;
const orderIndex = (type: string) => {
const i = LOCATION_ORDER.indexOf(type);
return i === -1 ? LOCATION_ORDER.length : i;
@@ -121,25 +115,30 @@ export default function KitchenScreen() {
const confirmDelete = (ids: string[], label: string) => {
if (ids.length === 0) return;
Alert.alert(
"Ta bort",
`Är du säker på att du vill ta bort ${ids.length} ${ids.length === 1 ? "vara" : "varor"} ur ${label}?`,
[
{ text: t("common.cancel"), style: "cancel" },
{ text: "Ta bort", style: "destructive", onPress: () => bulkRemove.mutate(ids) },
],
);
Alert.alert(t("common.remove"), t("kitchen.deleteConfirm", { label }), [
{ text: t("common.cancel"), style: "cancel" },
{ text: t("common.remove"), style: "destructive", onPress: () => bulkRemove.mutate(ids) },
]);
};
const emptyLabel = locFilter ? locLabel(locFilter) : q ? "sökningen" : "hela lagret";
const emptyLabel = locFilter
? locLabel(locFilter)
: q
? t("kitchen.scopeSearch")
: t("kitchen.scopeAll");
return (
<Screen>
<Input value={search} onChangeText={setSearch} placeholder="Sök vara…" autoCorrect={false} />
<Input
value={search}
onChangeText={setSearch}
placeholder={t("kitchen.searchPlaceholder")}
autoCorrect={false}
/>
<Row>
<Button
label="Alla"
label={t("common.all")}
variant={locFilter === null ? "primary" : "ghost"}
onPress={() => setLocFilter(null)}
/>
@@ -162,22 +161,25 @@ export default function KitchenScreen() {
<Button
label={`Ta bort valda (${selected.size})`}
variant="danger"
onPress={() => confirmDelete([...selected], "markeringen")}
onPress={() => confirmDelete([...selected], t("kitchen.scopeSelection"))}
/>
)}
{visible.length > 0 && (
<Button
label="Töm listan"
label={t("kitchen.clearList")}
variant="ghost"
onPress={() => confirmDelete(visible.map((i) => i.id), emptyLabel)}
onPress={() =>
confirmDelete(
visible.map((i) => i.id),
emptyLabel,
)
}
/>
)}
</Row>
</Row>
{visible.length === 0 && (
<EmptyState text="Inga varor att visa. Skanna eller lägg till varor." />
)}
{visible.length === 0 && <EmptyState text={t("kitchen.empty")} />}
{groups.map((g) => (
<View key={g.type} style={{ gap: spacing.xs, marginTop: spacing.sm }}>
@@ -191,7 +193,11 @@ export default function KitchenScreen() {
<Card
key={item.id}
onPress={() => toggle(item.id)}
style={isSel ? { borderColor: colors.primary, backgroundColor: colors.primarySoft } : undefined}
style={
isSel
? { borderColor: colors.primary, backgroundColor: colors.primarySoft }
: undefined
}
>
<Row style={{ justifyContent: "space-between", alignItems: "center" }}>
<Body>
@@ -199,7 +205,7 @@ export default function KitchenScreen() {
{item.displayName} · {formatQuantity(item.quantity, item.unit)}
</Body>
<Row style={{ alignItems: "center", gap: 12 }}>
{item.expiry.pastBestBefore && <Small> bäst före</Small>}
{item.expiry.pastBestBefore && <Small> {t("kitchen.pastBefore")}</Small>}
<Pressable
onPress={() => confirmDelete([item.id], locLabel(item.locationType))}
hitSlop={8}