feat(streckkod): okand produkt -> fota forpackning -> spara i delad katalog
Okand streckkod erbjuder nu 'Fota forpackningen' direkt i streckkodsvyn. Fotot lases av (naring/allergener/innehall) och produkten UPSERTas i den DELADE products-katalogen kopplat till GTIN (dataSource=user_scanned, verificationStatus=unverified, createdByUserId=anvandaren) sa nasta person som skannar samma kod far den direkt. Produkten laggs INTE till i lagret. Naringsdeklaration-knappen tas bort som egen ingang - flodet vavs in i streckkodsskanningen. product_package/nutrition_label -> READ_NUTRITION_LABEL avslutar som 'completed' med result.product (ingen awaiting_confirmation). - validation: barcode-falt i createScanInput.context - worker: ny gren i processScanJob som skriver till products + returnerar tidigt - mobil: inline contributePhoto (fota -> ladda upp -> polla -> visa info) - scan: ta bort naringsdeklaration-knappen - i18n: barcode.contributing.* + uppdaterad unknownBody pa 12 sprak Verifierat: typecheck (validation/worker/api/mobil), worker 23/23, api 104/104, och repro mot testdatabas (products-upsert, ingen inventory, idempotent aterskann).
This commit is contained in:
@@ -21,7 +21,9 @@ import { useAnalytics } from "@/lib/analytics";
|
||||
import { scanStarted } from "@app/analytics";
|
||||
import { FirstScanCoach, useFirstScanCoach } from "@/components/FirstScanCoach";
|
||||
|
||||
/** Skanna (spec §4.2): kyl, frys, skafferi, ingredienser, tallrik, kvitto, streckkod, datum, näringsdeklaration. */
|
||||
/** Skanna (spec §4.2): kyl, frys, skafferi, ingredienser, tallrik, kvitto, streckkod, datum.
|
||||
* Näringsdeklaration är INGEN egen knapp – den vävs in i streckkodsflödet: okänd kod → fota
|
||||
* förpackningen → produkten läses av och sparas i den delade katalogen (se barcode.tsx). */
|
||||
|
||||
const SCAN_TYPES = [
|
||||
{ type: "fridge", glyph: "🧊", labelKey: "scan.fridge" },
|
||||
@@ -32,7 +34,6 @@ const SCAN_TYPES = [
|
||||
{ type: "receipt", glyph: "🧾", labelKey: "scan.receipt" },
|
||||
{ type: "barcode", glyph: "🏷️", labelKey: "scan.barcode" },
|
||||
{ type: "expiry_date", glyph: "📅", labelKey: "scan.expiry" },
|
||||
{ type: "nutrition_label", glyph: "🔬", labelKey: "scan.nutrition" },
|
||||
] as const;
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { ScrollView, StyleSheet, View } from "react-native";
|
||||
import { router } from "expo-router";
|
||||
import * as ImagePicker from "expo-image-picker";
|
||||
import { CameraView, useCameraPermissions } from "expo-camera";
|
||||
import { ALLERGEN_LABELS_SV, type Allergen } from "@app/shared-types";
|
||||
import { api } from "@/lib/api";
|
||||
import { api, uploadImage, ApiError } from "@/lib/api";
|
||||
import { t } from "@/lib/i18n";
|
||||
import { Body, Button, Card, Heading, Row, Screen, Small, Spacer } from "@/components/ui";
|
||||
import { colors, spacing } from "@/lib/theme";
|
||||
@@ -48,6 +49,30 @@ interface ProductInfo {
|
||||
interface BarcodeScanResponse {
|
||||
product: ProductInfo | null;
|
||||
}
|
||||
interface CreateScanResponse {
|
||||
scan: { id: string };
|
||||
uploads: Array<{ key: string; uploadUrl: string; headers: Record<string, string> }>;
|
||||
}
|
||||
interface ScanJobResponse {
|
||||
status: string;
|
||||
result: { product: ProductInfo | null } | null;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pollar en produktskanning tills AAMOS läst av förpackningen. Vid "completed"
|
||||
* returneras produkten (redan sparad i delade katalogen av workern), vid
|
||||
* "failed"/timeout returneras null. Max ~60 s (40 × 1,5 s).
|
||||
*/
|
||||
async function pollScan(id: string): Promise<ProductInfo | null> {
|
||||
for (let i = 0; i < 40; i++) {
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
const job = await api<ScanJobResponse>(`/v1/scans/${id}`);
|
||||
if (job.status === "completed") return job.result?.product ?? null;
|
||||
if (job.status === "failed") return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const fmtG = (n: number) => `${(Math.round(n * 10) / 10).toString().replace(".", ",")} g`;
|
||||
const allergenLabels = (codes: string[] | null | undefined): string =>
|
||||
@@ -61,15 +86,19 @@ export default function BarcodeScreen() {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [product, setProduct] = useState<ProductInfo | null>(null);
|
||||
const [unknown, setUnknown] = useState(false);
|
||||
const [contributing, setContributing] = useState(false);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
// Synkron lås: hindrar att kamerans upprepade träffar startar parallella uppslag.
|
||||
const locked = useRef(false);
|
||||
// Senast avlästa streckkod – används när användaren fotar en okänd produkt.
|
||||
const lastGtin = useRef<string | null>(null);
|
||||
|
||||
const scanning = !busy && !product && !unknown && !errorMsg;
|
||||
|
||||
const onScanned = async (gtin: string) => {
|
||||
if (locked.current) return;
|
||||
locked.current = true;
|
||||
lastGtin.current = gtin;
|
||||
setBusy(true);
|
||||
setErrorMsg(null);
|
||||
try {
|
||||
@@ -86,11 +115,66 @@ export default function BarcodeScreen() {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Okänd produkt → fota förpackningen. Bilden läses av (näring/allergener/
|
||||
* innehåll) och produkten sparas i den DELADE katalogen kopplat till
|
||||
* streckkoden – så nästa person som skannar samma kod får den direkt.
|
||||
* Lägger INTE till något bland användarens egna varor (spec §11).
|
||||
*/
|
||||
const contributePhoto = async () => {
|
||||
const gtin = lastGtin.current;
|
||||
if (!gtin) return;
|
||||
try {
|
||||
// Fota förpackningen (kamera, med galleri-fallback som i skanningsflödet).
|
||||
const perm = await ImagePicker.requestCameraPermissionsAsync();
|
||||
const picked = perm.granted
|
||||
? await ImagePicker.launchCameraAsync({ quality: 0.7 })
|
||||
: await ImagePicker.launchImageLibraryAsync({ quality: 0.7 });
|
||||
const asset = picked.assets?.[0];
|
||||
if (picked.canceled || !asset) return; // avbröt – stanna kvar i "okänd"-vyn
|
||||
|
||||
setContributing(true);
|
||||
setErrorMsg(null);
|
||||
|
||||
// Produktskanning kopplad till streckkoden – delad katalog, inte lagret.
|
||||
const created = await api<CreateScanResponse>("/v1/scans", {
|
||||
method: "POST",
|
||||
body: {
|
||||
scanType: "product_package",
|
||||
imageCount: 1,
|
||||
contentType: "image/jpeg",
|
||||
context: { barcode: gtin },
|
||||
},
|
||||
});
|
||||
const upload = created.uploads[0];
|
||||
if (!upload) throw new Error(t("common.error"));
|
||||
await uploadImage(upload, asset.uri);
|
||||
await api(`/v1/scans/${created.scan.id}/start`, { method: "POST" });
|
||||
|
||||
const found = await pollScan(created.scan.id);
|
||||
if (found) {
|
||||
setUnknown(false);
|
||||
setProduct(found);
|
||||
} else {
|
||||
setErrorMsg(t("barcode.contributing.failed"));
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.code === "QUOTA_EXCEEDED") {
|
||||
router.push("/paywall");
|
||||
} else {
|
||||
setErrorMsg(err instanceof Error ? err.message : t("common.error"));
|
||||
}
|
||||
} finally {
|
||||
setContributing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const scanAgain = () => {
|
||||
setProduct(null);
|
||||
setUnknown(false);
|
||||
setErrorMsg(null);
|
||||
locked.current = false;
|
||||
lastGtin.current = null;
|
||||
};
|
||||
|
||||
if (!permission) return <Screen><Body>{t("scan.analyzing")}</Body></Screen>;
|
||||
@@ -203,23 +287,30 @@ export default function BarcodeScreen() {
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Okänd produkt */}
|
||||
{unknown && (
|
||||
{/* Okänd produkt → erbjud att fota förpackningen (bidra till delade katalogen) */}
|
||||
{unknown && !contributing && !errorMsg && (
|
||||
<View style={styles.panel}>
|
||||
<View style={{ padding: spacing.lg }}>
|
||||
<Heading>{t("barcode.unknownTitle")}</Heading>
|
||||
<Body muted>{t("barcode.unknownBody")}</Body>
|
||||
<Spacer size={spacing.md} />
|
||||
<Button
|
||||
label={t("barcode.photoPackage")}
|
||||
onPress={() => router.replace("/(tabs)/scan")}
|
||||
/>
|
||||
<Button label={t("barcode.photoPackage")} onPress={() => void contributePhoto()} />
|
||||
<Spacer size={spacing.xs} />
|
||||
<Button label={t("barcode.info.scanAgain")} variant="ghost" onPress={scanAgain} />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Läser av fotad förpackning */}
|
||||
{contributing && (
|
||||
<View style={styles.panel}>
|
||||
<View style={{ padding: spacing.lg }}>
|
||||
<Heading>{t("barcode.contributing.title")}</Heading>
|
||||
<Body muted>{t("barcode.contributing.body")}</Body>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Fel */}
|
||||
{errorMsg && (
|
||||
<View style={styles.panel}>
|
||||
|
||||
@@ -30,12 +30,15 @@
|
||||
"barcode.allowCamera": "Tillad kamera",
|
||||
"barcode.cameraBody": "{brand} skal bruge kameraet til at scanne stregkoder.",
|
||||
"barcode.cameraTitle": "Kameraet er nødvendigt",
|
||||
"barcode.contributing.title": "Aflæser emballagen…",
|
||||
"barcode.contributing.body": "Vi finder næring, allergener og indhold – og gemmer produktet i det delte katalog, så den næste slipper for at fotografere det.",
|
||||
"barcode.contributing.failed": "Kunne ikke aflæse emballagen. Prøv igen med et skarpt billede og godt lys.",
|
||||
"barcode.kcalPer100": "{kcal} kcal/100 g",
|
||||
"barcode.needHousehold": "Du skal først have en husstand.",
|
||||
"barcode.noLocation": "Ingen opbevaringssteder fundet.",
|
||||
"barcode.noNutrition": "Ingen næringsdata",
|
||||
"barcode.photoPackage": "Tag billede af emballagen",
|
||||
"barcode.unknownBody": "Produktet er ikke i databasen endnu. Tag et billede af forsiden og næringsdeklarationen, så tilføjer vi det.",
|
||||
"barcode.unknownBody": "Produktet er ikke i databasen endnu. Tag et billede af forsiden og næringsdeklarationen, så aflæser vi indholdet og gemmer produktet i det delte katalog – så slipper den næste. (Det tilføjes ikke til dine varer.)",
|
||||
"barcode.unknownTitle": "Ukendt produkt",
|
||||
"common.add": "Tilføj",
|
||||
"common.back": "Tilbage",
|
||||
|
||||
@@ -30,12 +30,15 @@
|
||||
"barcode.allowCamera": "Kamera erlauben",
|
||||
"barcode.cameraBody": "{brand} benötigt die Kamera, um Barcodes zu scannen.",
|
||||
"barcode.cameraTitle": "Kamera erforderlich",
|
||||
"barcode.contributing.title": "Verpackung wird gelesen…",
|
||||
"barcode.contributing.body": "Wir ermitteln Nährwerte, Allergene und Zutaten – und speichern das Produkt im gemeinsamen Katalog, damit es der Nächste nicht fotografieren muss.",
|
||||
"barcode.contributing.failed": "Die Verpackung konnte nicht gelesen werden. Versuche es mit einem scharfen Foto und gutem Licht erneut.",
|
||||
"barcode.kcalPer100": "{kcal} kcal/100 g",
|
||||
"barcode.needHousehold": "Du brauchst zuerst einen Haushalt.",
|
||||
"barcode.noLocation": "Kein Lagerort gefunden.",
|
||||
"barcode.noNutrition": "Keine Nährwertdaten",
|
||||
"barcode.photoPackage": "Verpackung fotografieren",
|
||||
"barcode.unknownBody": "Dieses Produkt ist noch nicht in der Datenbank. Fotografiere Vorderseite und Nährwerttabelle, dann fügen wir es hinzu.",
|
||||
"barcode.unknownBody": "Dieses Produkt ist noch nicht in der Datenbank. Fotografiere die Vorderseite und die Nährwerttabelle – wir lesen den Inhalt aus und speichern das Produkt im gemeinsamen Katalog, damit es der Nächste leichter hat. (Es wird nicht zu deinen Artikeln hinzugefügt.)",
|
||||
"barcode.unknownTitle": "Unbekanntes Produkt",
|
||||
"common.add": "Hinzufügen",
|
||||
"common.back": "Zurück",
|
||||
|
||||
@@ -30,12 +30,15 @@
|
||||
"barcode.allowCamera": "Allow camera",
|
||||
"barcode.cameraBody": "{brand} needs the camera to scan barcodes.",
|
||||
"barcode.cameraTitle": "Camera needed",
|
||||
"barcode.contributing.title": "Reading the package…",
|
||||
"barcode.contributing.body": "We're extracting nutrition, allergens and ingredients – and saving the product to the shared catalogue so the next person doesn't have to photograph it.",
|
||||
"barcode.contributing.failed": "Couldn't read the package. Try again with a sharp photo and good lighting.",
|
||||
"barcode.kcalPer100": "{kcal} kcal/100 g",
|
||||
"barcode.needHousehold": "You need a household first.",
|
||||
"barcode.noLocation": "No storage location found.",
|
||||
"barcode.noNutrition": "No nutrition data",
|
||||
"barcode.photoPackage": "Photograph the package",
|
||||
"barcode.unknownBody": "This product isn't in the database yet. Photograph the front and the nutrition label and we'll add it.",
|
||||
"barcode.unknownBody": "This product isn't in the database yet. Photograph the front and the nutrition label – we'll read the contents and save the product to the shared catalogue so the next person doesn't have to. (It won't be added to your items.)",
|
||||
"barcode.unknownTitle": "Unknown product",
|
||||
"common.add": "Add",
|
||||
"common.back": "Back",
|
||||
|
||||
@@ -30,12 +30,15 @@
|
||||
"barcode.allowCamera": "Permitir cámara",
|
||||
"barcode.cameraBody": "{brand} necesita la cámara para escanear códigos de barras.",
|
||||
"barcode.cameraTitle": "Se necesita la cámara",
|
||||
"barcode.contributing.title": "Leyendo el envase…",
|
||||
"barcode.contributing.body": "Estamos extrayendo la información nutricional, los alérgenos y los ingredientes, y guardando el producto en el catálogo compartido para que la próxima persona no tenga que fotografiarlo.",
|
||||
"barcode.contributing.failed": "No se pudo leer el envase. Inténtalo de nuevo con una foto nítida y buena luz.",
|
||||
"barcode.kcalPer100": "{kcal} kcal/100 g",
|
||||
"barcode.needHousehold": "Primero necesitas un hogar.",
|
||||
"barcode.noLocation": "No se encontró ningún lugar de almacenamiento.",
|
||||
"barcode.noNutrition": "Sin datos nutricionales",
|
||||
"barcode.photoPackage": "Fotografiar el envase",
|
||||
"barcode.unknownBody": "Este producto aún no está en la base de datos. Fotografía el frontal y la etiqueta nutricional y lo añadiremos.",
|
||||
"barcode.unknownBody": "Este producto aún no está en la base de datos. Fotografía la parte delantera y la información nutricional: leeremos el contenido y guardaremos el producto en el catálogo compartido para que la próxima persona no tenga que hacerlo. (No se añadirá a tus productos.)",
|
||||
"barcode.unknownTitle": "Producto desconocido",
|
||||
"common.add": "Añadir",
|
||||
"common.back": "Atrás",
|
||||
|
||||
@@ -30,12 +30,15 @@
|
||||
"barcode.allowCamera": "Salli kamera",
|
||||
"barcode.cameraBody": "{brand} tarvitsee kameraa viivakoodien skannaamiseen.",
|
||||
"barcode.cameraTitle": "Kamera tarvitaan",
|
||||
"barcode.contributing.title": "Luetaan pakkausta…",
|
||||
"barcode.contributing.body": "Poimimme ravintoarvot, allergeenit ja ainesosat – ja tallennamme tuotteen jaettuun luetteloon, jottei seuraavan tarvitse kuvata sitä.",
|
||||
"barcode.contributing.failed": "Pakkausta ei voitu lukea. Yritä uudelleen terävällä kuvalla ja hyvässä valossa.",
|
||||
"barcode.kcalPer100": "{kcal} kcal/100 g",
|
||||
"barcode.needHousehold": "Tarvitset ensin kotitalouden.",
|
||||
"barcode.noLocation": "Säilytyspaikkaa ei löytynyt.",
|
||||
"barcode.noNutrition": "Ei ravintotietoja",
|
||||
"barcode.photoPackage": "Kuvaa pakkaus",
|
||||
"barcode.unknownBody": "Tuotetta ei ole vielä tietokannassa. Kuvaa etupuoli ja ravintosisältö, niin lisäämme sen.",
|
||||
"barcode.unknownBody": "Tuotetta ei ole vielä tietokannassa. Kuvaa etupuoli ja ravintosisältö – luemme sisällön ja tallennamme tuotteen jaettuun luetteloon, jotta seuraavan ei tarvitse. (Sitä ei lisätä tavaroihisi.)",
|
||||
"barcode.unknownTitle": "Tuntematon tuote",
|
||||
"common.add": "Lisää",
|
||||
"common.back": "Takaisin",
|
||||
|
||||
@@ -30,12 +30,15 @@
|
||||
"barcode.allowCamera": "Autoriser l'appareil photo",
|
||||
"barcode.cameraBody": "{brand} a besoin de l'appareil photo pour scanner les codes-barres.",
|
||||
"barcode.cameraTitle": "Appareil photo requis",
|
||||
"barcode.contributing.title": "Lecture de l'emballage…",
|
||||
"barcode.contributing.body": "Nous extrayons les valeurs nutritionnelles, les allergènes et les ingrédients, et enregistrons le produit dans le catalogue partagé pour que la prochaine personne n'ait pas à le photographier.",
|
||||
"barcode.contributing.failed": "Impossible de lire l'emballage. Réessayez avec une photo nette et un bon éclairage.",
|
||||
"barcode.kcalPer100": "{kcal} kcal/100 g",
|
||||
"barcode.needHousehold": "Il vous faut d'abord un foyer.",
|
||||
"barcode.noLocation": "Aucun lieu de stockage trouvé.",
|
||||
"barcode.noNutrition": "Pas de données nutritionnelles",
|
||||
"barcode.photoPackage": "Photographier l'emballage",
|
||||
"barcode.unknownBody": "Ce produit n'est pas encore dans la base. Photographiez l'avant et l'étiquette nutritionnelle et nous l'ajouterons.",
|
||||
"barcode.unknownBody": "Ce produit n'est pas encore dans la base de données. Photographiez l'avant et la déclaration nutritionnelle : nous lirons le contenu et enregistrerons le produit dans le catalogue partagé pour que la prochaine personne n'ait pas à le faire. (Il ne sera pas ajouté à vos articles.)",
|
||||
"barcode.unknownTitle": "Produit inconnu",
|
||||
"common.add": "Ajouter",
|
||||
"common.back": "Retour",
|
||||
|
||||
@@ -30,12 +30,15 @@
|
||||
"barcode.allowCamera": "Consenti fotocamera",
|
||||
"barcode.cameraBody": "{brand} ha bisogno della fotocamera per leggere i codici a barre.",
|
||||
"barcode.cameraTitle": "Serve la fotocamera",
|
||||
"barcode.contributing.title": "Lettura della confezione…",
|
||||
"barcode.contributing.body": "Stiamo estraendo valori nutrizionali, allergeni e ingredienti e salvando il prodotto nel catalogo condiviso, così il prossimo non dovrà fotografarlo.",
|
||||
"barcode.contributing.failed": "Impossibile leggere la confezione. Riprova con una foto nitida e una buona illuminazione.",
|
||||
"barcode.kcalPer100": "{kcal} kcal/100 g",
|
||||
"barcode.needHousehold": "Prima ti serve una famiglia.",
|
||||
"barcode.noLocation": "Nessun luogo di conservazione trovato.",
|
||||
"barcode.noNutrition": "Dati nutrizionali mancanti",
|
||||
"barcode.photoPackage": "Fotografa la confezione",
|
||||
"barcode.unknownBody": "Questo prodotto non è ancora nel database. Fotografa il fronte e l'etichetta nutrizionale e lo aggiungeremo.",
|
||||
"barcode.unknownBody": "Questo prodotto non è ancora nel database. Fotografa il fronte e la tabella nutrizionale: leggeremo il contenuto e salveremo il prodotto nel catalogo condiviso, così il prossimo non dovrà farlo. (Non verrà aggiunto ai tuoi articoli.)",
|
||||
"barcode.unknownTitle": "Prodotto sconosciuto",
|
||||
"common.add": "Aggiungi",
|
||||
"common.back": "Indietro",
|
||||
|
||||
@@ -30,12 +30,15 @@
|
||||
"barcode.allowCamera": "Tillat kamera",
|
||||
"barcode.cameraBody": "{brand} trenger kameraet for å skanne strekkoder.",
|
||||
"barcode.cameraTitle": "Kameraet trengs",
|
||||
"barcode.contributing.title": "Leser av emballasjen…",
|
||||
"barcode.contributing.body": "Vi henter næring, allergener og innhold – og lagrer produktet i den delte katalogen, så den neste slipper å fotografere det.",
|
||||
"barcode.contributing.failed": "Kunne ikke lese av emballasjen. Prøv igjen med et skarpt bilde og godt lys.",
|
||||
"barcode.kcalPer100": "{kcal} kcal/100 g",
|
||||
"barcode.needHousehold": "Du trenger en husstand først.",
|
||||
"barcode.noLocation": "Fant ingen oppbevaringssteder.",
|
||||
"barcode.noNutrition": "Ingen næringsdata",
|
||||
"barcode.photoPackage": "Ta bilde av pakken",
|
||||
"barcode.unknownBody": "Produktet er ikke i databasen ennå. Ta bilde av forsiden og næringsdeklarasjonen, så legger vi det til.",
|
||||
"barcode.unknownBody": "Produktet er ikke i databasen ennå. Ta bilde av forsiden og næringsinnholdet – vi leser av innholdet og lagrer produktet i den delte katalogen, så slipper den neste. (Det legges ikke til blant varene dine.)",
|
||||
"barcode.unknownTitle": "Ukjent produkt",
|
||||
"common.add": "Legg til",
|
||||
"common.back": "Tilbake",
|
||||
|
||||
@@ -30,12 +30,15 @@
|
||||
"barcode.allowCamera": "Camera toestaan",
|
||||
"barcode.cameraBody": "{brand} heeft de camera nodig om streepjescodes te scannen.",
|
||||
"barcode.cameraTitle": "Camera nodig",
|
||||
"barcode.contributing.title": "Verpakking wordt gelezen…",
|
||||
"barcode.contributing.body": "We halen voedingswaarde, allergenen en ingrediënten op – en slaan het product op in de gedeelde catalogus, zodat de volgende het niet hoeft te fotograferen.",
|
||||
"barcode.contributing.failed": "Kon de verpakking niet lezen. Probeer het opnieuw met een scherpe foto en goed licht.",
|
||||
"barcode.kcalPer100": "{kcal} kcal/100 g",
|
||||
"barcode.needHousehold": "Je hebt eerst een huishouden nodig.",
|
||||
"barcode.noLocation": "Geen bewaarplek gevonden.",
|
||||
"barcode.noNutrition": "Geen voedingswaarden",
|
||||
"barcode.photoPackage": "Fotografeer de verpakking",
|
||||
"barcode.unknownBody": "Dit product staat nog niet in de database. Fotografeer de voorkant en het voedingsetiket, dan voegen we het toe.",
|
||||
"barcode.unknownBody": "Dit product staat nog niet in de database. Fotografeer de voorkant en de voedingswaarde – we lezen de inhoud af en slaan het product op in de gedeelde catalogus, zodat de volgende dat niet hoeft. (Het wordt niet aan je artikelen toegevoegd.)",
|
||||
"barcode.unknownTitle": "Onbekend product",
|
||||
"common.add": "Toevoegen",
|
||||
"common.back": "Terug",
|
||||
|
||||
@@ -30,12 +30,15 @@
|
||||
"barcode.allowCamera": "Zezwól na aparat",
|
||||
"barcode.cameraBody": "{brand} potrzebuje aparatu do skanowania kodów kreskowych.",
|
||||
"barcode.cameraTitle": "Potrzebny aparat",
|
||||
"barcode.contributing.title": "Odczytywanie opakowania…",
|
||||
"barcode.contributing.body": "Odczytujemy wartości odżywcze, alergeny i składniki – i zapisujemy produkt we wspólnym katalogu, aby następna osoba nie musiała go fotografować.",
|
||||
"barcode.contributing.failed": "Nie udało się odczytać opakowania. Spróbuj ponownie z ostrym zdjęciem i dobrym oświetleniem.",
|
||||
"barcode.kcalPer100": "{kcal} kcal/100 g",
|
||||
"barcode.needHousehold": "Najpierw potrzebujesz gospodarstwa.",
|
||||
"barcode.noLocation": "Nie znaleziono miejsca przechowywania.",
|
||||
"barcode.noNutrition": "Brak danych żywieniowych",
|
||||
"barcode.photoPackage": "Sfotografuj opakowanie",
|
||||
"barcode.unknownBody": "Tego produktu nie ma jeszcze w bazie. Sfotografuj przód i etykietę wartości odżywczych, a my go dodamy.",
|
||||
"barcode.unknownBody": "Tego produktu nie ma jeszcze w bazie. Zrób zdjęcie przodu i tabeli wartości odżywczych – odczytamy zawartość i zapiszemy produkt we wspólnym katalogu, aby następna osoba nie musiała. (Nie zostanie dodany do Twoich produktów.)",
|
||||
"barcode.unknownTitle": "Nieznany produkt",
|
||||
"common.add": "Dodaj",
|
||||
"common.back": "Wstecz",
|
||||
|
||||
@@ -30,12 +30,15 @@
|
||||
"barcode.allowCamera": "Permitir câmara",
|
||||
"barcode.cameraBody": "O {brand} precisa da câmara para ler códigos de barras.",
|
||||
"barcode.cameraTitle": "É precisa a câmara",
|
||||
"barcode.contributing.title": "A ler a embalagem…",
|
||||
"barcode.contributing.body": "Estamos a extrair informação nutricional, alergénios e ingredientes – e a guardar o produto no catálogo partilhado, para que a próxima pessoa não precise de o fotografar.",
|
||||
"barcode.contributing.failed": "Não foi possível ler a embalagem. Tente novamente com uma foto nítida e boa iluminação.",
|
||||
"barcode.kcalPer100": "{kcal} kcal/100 g",
|
||||
"barcode.needHousehold": "Primeiro precisa de um agregado.",
|
||||
"barcode.noLocation": "Nenhum local de armazenamento encontrado.",
|
||||
"barcode.noNutrition": "Sem dados nutricionais",
|
||||
"barcode.photoPackage": "Fotografar a embalagem",
|
||||
"barcode.unknownBody": "Este produto ainda não está na base de dados. Fotografe a frente e o rótulo nutricional e nós adicionamo-lo.",
|
||||
"barcode.unknownBody": "Este produto ainda não está na base de dados. Fotografe a frente e a declaração nutricional – vamos ler o conteúdo e guardar o produto no catálogo partilhado, para que a próxima pessoa não precise. (Não será adicionado aos seus artigos.)",
|
||||
"barcode.unknownTitle": "Produto desconhecido",
|
||||
"common.add": "Adicionar",
|
||||
"common.back": "Voltar",
|
||||
|
||||
@@ -30,12 +30,15 @@
|
||||
"barcode.allowCamera": "Tillåt kamera",
|
||||
"barcode.cameraBody": "För att skanna streckkoder behöver {brand} kameran.",
|
||||
"barcode.cameraTitle": "Kameran behövs",
|
||||
"barcode.contributing.title": "Läser av förpackningen…",
|
||||
"barcode.contributing.body": "Vi tar fram näring, allergener och innehåll – och sparar produkten i den delade katalogen så nästa person slipper fota den.",
|
||||
"barcode.contributing.failed": "Kunde inte läsa av förpackningen. Försök igen med skarp bild och bra ljus.",
|
||||
"barcode.kcalPer100": "{kcal} kcal/100 g",
|
||||
"barcode.needHousehold": "Du behöver ett hushåll först.",
|
||||
"barcode.noLocation": "Ingen förvaringsplats hittades.",
|
||||
"barcode.noNutrition": "Näringsdata saknas",
|
||||
"barcode.photoPackage": "Fota förpackningen",
|
||||
"barcode.unknownBody": "Produkten finns inte i databasen ännu. Fota framsidan och näringsdeklarationen så lägger vi till den.",
|
||||
"barcode.unknownBody": "Produkten finns inte i databasen ännu. Fota framsidan och näringsdeklarationen så läser vi av innehållet och sparar produkten i den delade katalogen – nästa person slipper fota den. (Läggs inte till bland dina varor.)",
|
||||
"barcode.unknownTitle": "Okänd produkt",
|
||||
"common.add": "Lägg till",
|
||||
"common.back": "Tillbaka",
|
||||
|
||||
Reference in New Issue
Block a user