From e78656bc07559442cf8c02240a783165e91b27b1 Mon Sep 17 00:00:00 2001 From: vjrj Date: Tue, 14 Jul 2026 11:19:04 +0200 Subject: [PATCH] =?UTF-8?q?feat(plantare):=20UI=20=E2=80=94=20propose=20fr?= =?UTF-8?q?om=20chat,=20review=20&=20sign=20on=20the=20ledger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The viral loop, made usable end-to-end: - Propose sheet (plantare_propose_sheet.dart): from a chat with a peer (their key already in hand), pick your side, name the seed, choose what comes back (similar · non-GMO organic / work hours / other) and an optional return-by, then sign and send. Reached from the chat overflow menu; confirms "waiting for them to sign". - Plantares screen: incoming proposals show Accept & sign / Decline right on the tile; every bilateral row wears a handshake badge — awaiting signature, signed by both, or declined. Accept falls back to a gentle "offline" nudge if the proposal isn't in hand yet. - i18n: full en + es for the bilateral flow; other locales fall back to base per slang config. Wire-up only touches human copy (no jargon) and stays behind the optional PlantareService, so inventory-only builds are unaffected. Tests: propose sheet drives a real sign+send; the ledger renders the signed/awaiting badges. Fake SocialSession in your_people test gains the new transport getter. Widget tests targeted + timed. All green. --- apps/app_seeds/lib/i18n/en.i18n.json | 1473 +++++++++-------- apps/app_seeds/lib/i18n/es.i18n.json | 1471 ++++++++-------- apps/app_seeds/lib/i18n/strings.g.dart | 4 +- apps/app_seeds/lib/i18n/strings_en.g.dart | 96 +- apps/app_seeds/lib/i18n/strings_es.g.dart | 50 +- apps/app_seeds/lib/ui/chat_screen.dart | 25 + .../lib/ui/plantare_propose_sheet.dart | 289 ++++ apps/app_seeds/lib/ui/plantares_screen.dart | 121 +- .../test/ui/plantare_propose_sheet_test.dart | 100 ++ .../test/ui/plantares_screen_test.dart | 51 + .../test/ui/your_people_screen_test.dart | 2 + 11 files changed, 2223 insertions(+), 1459 deletions(-) create mode 100644 apps/app_seeds/lib/ui/plantare_propose_sheet.dart create mode 100644 apps/app_seeds/test/ui/plantare_propose_sheet_test.dart diff --git a/apps/app_seeds/lib/i18n/en.i18n.json b/apps/app_seeds/lib/i18n/en.i18n.json index 9cce88b..cf52914 100644 --- a/apps/app_seeds/lib/i18n/en.i18n.json +++ b/apps/app_seeds/lib/i18n/en.i18n.json @@ -1,728 +1,751 @@ { - "avatar": { - "title": "Your photo or avatar", - "fromPhoto": "Take or choose a photo", - "illustration": "Or pick an illustration", - "remove": "Remove" - }, - "favorites": { - "title": "Favorites", - "empty": "No favorites yet. Save offers you like from the market.", - "save": "Save to favorites", - "remove": "Remove from favorites", - "unavailable": "No longer available" - }, - "seedSaving": { - "title": "Saving its seed", - "subtitle": "What it takes to keep the variety true", - "lifeCycle": "Cycle", - "cycleAnnual": "Annual", - "cycleBiennial": "Biennial — seeds in year 2", - "cyclePerennial": "Perennial", - "pollination": "Pollination", - "pollSelf": "Self-pollinating", - "pollCross": "Crosses with others", - "pollMixed": "Self-pollinates, sometimes crosses", - "byInsect": "by insects", - "byWind": "on the wind", - "isolation": "Keep apart", - "isolationRange": "{min}–{max} m from other varieties", - "isolationSingle": "{min} m from other varieties", - "plants": "Save from several plants", - "plantsValue": "From at least {n} plants", - "processing": "Cleaning the seed", - "procDry": "Dry seed (thresh)", - "procWet": "Wet seed (ferment and rinse)", - "difficulty": "Difficulty", - "diffEasy": "Easy", - "diffMedium": "Medium", - "diffHard": "Hard", - "advisory": "General guidance — adapt it to your climate and variety.", - "sourcePrefix": "Source" - }, - "calendar": { - "title": "This month", - "filterChip": "This month", - "selfNote": "What you've noted in your varieties.", - "nothing": "Nothing noted for {month}." - }, - "app": { - "title": "Tane" - }, - "bootstrap": { - "failed": "Tane couldn't start", - "retry": "Try again" - }, - "common": { - "save": "Save", - "cancel": "Cancel", - "delete": "Delete", - "edit": "Edit", - "type": "Type", - "comingSoon": "Coming soon", - "offline": "You're offline — sharing is paused" - }, - "home": { - "tagline": "Share and grow local seeds", - "openMarket": "Market", - "openMarketSubtitle": "Discover and share seeds nearby", - "yourInventory": "Your inventory", - "yourInventorySubtitle": "Manage your seeds" - }, - "photo": { - "camera": "Take a photo", - "gallery": "Choose from gallery", - "setAsCover": "Set as cover", - "isCover": "Cover photo", - "deleteConfirm": "Delete this photo?" - }, - "menu": { - "tagline": "your seed bank", - "inventory": "Inventory", - "market": "Market", - "profile": "Your profile", - "chat": "Chat", - "wishlist": "Favorites", - "following": "Following", - "plantares": "Plantares", - "sales": "Sales", - "calendar": "Calendar", - "settings": "Settings" - }, - "settings": { - "language": "Language", - "systemLanguage": "System language", - "langEs": "Español", - "langEn": "English", - "langPt": "Português", - "langAst": "Asturianu", - "langFr": "Français", - "langDe": "Deutsch", - "langJa": "日本語", - "about": "About", - "aboutText": "Local-first, encrypted inventory for traditional seeds. AGPL-3.0.", - "aboutOpen": "About Tane" - }, - "backup": { - "title": "Backup & restore", - "autoBackupTitle": "Automatic backups", - "autoBackupLast": "Last copy saved {date}", - "autoBackupNone": "A copy is kept automatically in the background", - "exportJson": "Save a backup", - "exportJsonSubtitle": "A complete copy to keep safe, restore later or move to another device", - "importJson": "Restore a backup", - "importJsonSubtitle": "Bring a saved copy back — nothing gets duplicated", - "exportCsv": "Export to a spreadsheet", - "exportCsvSubtitle": "A simple list for Excel or LibreOffice — no photos", - "importCsv": "Import a list", - "importCsvSubtitle": "Add entries from a spreadsheet", - "importConfirmTitle": "Restore a backup?", - "importConfirmBody": "Entries merge with your inventory; when the same entry exists on both sides, the newest version wins. Nothing gets duplicated.", - "importCsvConfirmTitle": "Import a list?", - "importCsvConfirmBody": "Every row is added as a new entry. This does not merge or replace, so importing the same file twice adds it twice.", - "importAction": "Import", - "exportSaved": "Copy saved", - "cancelled": "Cancelled", - "importDone": "Imported: {added} new, {updated} updated", - "importCsvDone": "Added {count} entries", - "importFailed": "This file could not be read as a Tane copy", - "failed": "Something went wrong", - "recoveryTitle": "Your recovery code", - "recoverySubtitle": "Print it and keep it safe — it opens your copies on a new device", - "recoveryIntro": "This code opens your saved copies and brings your bank back on any device. Keep two paper copies in safe places, like your best seed. Anyone holding it can read your copies, so share it with no one.", - "recoveryCopy": "Copy", - "recoverySave": "Save the sheet", - "recoverySheetTitle": "Tane — your recovery sheet", - "recoveryPromptTitle": "Enter your recovery code", - "recoveryPromptBody": "This copy was saved with another code. Type the code from your recovery sheet to open it.", - "recoveryWrongCode": "That code doesn't open this copy" - }, - "about": { - "title": "About", - "kanji": "種", - "tagline": "A local-first, decentralized app for managing and sharing traditional seeds and seedlings.", - "intro": "Tane (種, \"seed\" in Japanese) helps people and collectives keep a friendly inventory of their seed bank, decide what they offer, and share it locally — without a central intermediary that could control, censor, or be fined for it. Its name comes from tanemaki (種まき), \"to sow / scatter seeds\". The goal is both practical and political: to support traditional seed varieties and push back against the seed monopoly.", - "heritage": "The name honors the old Japanese mutual-aid traditions around rice — yui (shared community labour) and tanomoshi (reciprocity funds) — that inspired the paper Plantare, the \"community currency for seed exchange\" (BAH-Semillero, 2009, CC-BY-SA). Tane is the digital Plantare.", - "version": "Version", - "license": "License", - "licenseValue": "AGPL-3.0", - "website": "Website", - "openSourceLicenses": "Open-source licenses", - "openSourceLicensesSubtitle": "Third-party libraries and their licenses", - "copyright": "© {years} Comunes Association, under AGPLv3" - }, - "intro": { - "skip": "Skip", - "next": "Next", - "start": "Get started", - "menuEntry": "How Tane works", - "slides": { - "welcome": { - "title": "The seed that brought you here", - "body": "Every traditional seed is a letter written by thousands of generations, passed from hand to hand. We are what we are thanks to that sharing." - }, - "inventory": { - "title": "Your seed bank, in your pocket", - "body": "Note what you have, from which year, how much and where it came from — with the name you use. A photo and a name are enough to start." - }, - "privacy": { - "title": "Yours, and only yours", - "body": "No account, no internet, no trackers. Your data lives encrypted on your device, and only what you choose is ever shared." - }, - "share": { - "title": "Share, the way it's always been done", - "body": "Offer what you have spare — gift, swap or sell — and let someone nearby find it. Only an approximate distance is shown, never your address; you close the deal in person." - }, - "plantare": { - "title": "To sow is to multiply", - "body": "With a Plantare, whoever receives seed promises to return some later. And since returning it means growing it, every loan multiplies the commons." - } - } - }, - "inventory": { - "title": "Inventory", - "searchHint": "Search seeds", - "empty": "No seeds yet. Tap + to add your first.", - "noMatches": "No seeds match your filters.", - "clearFilters": "Clear filters", - "uncategorized": "Uncategorized", - "needsReproductionFilter": "To regrow", - "loadError": "Couldn't open your seed bank. It may just have been busy — try again.", - "retry": "Try again" - }, - "draft": { - "capture": "Capture photos", - "captured": "{n} captured to catalogue", - "triageTitle": "To catalogue", - "triageCount": "{n} to catalogue", - "untitled": "Unnamed", - "nameField": "Name this seed", - "nameHint": "What is it?", - "suggestFromPhoto": "Suggest name from photo", - "discard": "Discard" - }, - "quickAdd": { - "title": "Add a seed", - "labelField": "Name", - "labelRequired": "Give it a name", - "addPhoto": "Add photo", - "quantity": "How much?", - "more": "Add more…", - "save": "Save", - "saveAndAddAnother": "Save & add another", - "addedCount": "{n} added", - "cancel": "Cancel" - }, - "detail": { - "notFound": "This seed is no longer here.", - "lots": "Lots", - "noLots": "No lots yet.", - "names": "Also known as", - "addName": "Add name", - "links": "Links", - "addLink": "Add link", - "linkUrl": "URL", - "linkTitle": "Title (optional)", - "reference": "Learn more", - "refGbif": "GBIF", - "refWikipedia": "Wikipedia", - "refWikispecies": "Wikispecies", - "notes": "Notes", - "addLot": "Add lot", - "editLot": "Edit lot", - "deleteConfirm": "Delete this seed?", - "year": "Year {year}", - "noYear": "Year unknown" - }, - "germination": { - "title": "Germination", - "add": "Add test", - "sampleSize": "Sample size", - "germinated": "Germinated", - "none": "No germination tests yet.", - "result": "{percent}%" - }, - "viability": { - "expiringSoon": "Use or reproduce this season", - "expiringSoonYears": "Use or reproduce this season · keeps ~{years} yr", - "expired": "Past typical viability — reproduce", - "expiredYears": "Past typical viability (~{years} yr) — reproduce" - }, - "editVariety": { - "title": "Edit seed", - "name": "Name", - "category": "Category", - "notes": "Notes", - "species": "Species (from catalog)", - "speciesHint": "Search a species…", - "speciesSuggested": "Suggested from the name", - "organic": "Organic", - "organicHint": "Grown organically (eco)" - }, - "addLot": { - "title": "Add lot", - "year": "Harvest date", - "quantity": "How much?", - "amount": "Amount" - }, - "harvest": { - "pickTitle": "Select month / year", - "anyMonth": "Any month", - "noDate": "Set harvest date", - "monthNames": [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December" - ] - }, - "lotType": { - "seed": "Seeds", - "plant": "Plant", - "seedling": "Seedling", - "tree": "Tree / shrub", - "bulb": "Bulb / tuber", - "cutting": "Cutting" - }, - "presentation": { - "title": "Packaging", - "none": "Unspecified", - "pot": "Pot", - "tray": "Tray", - "plug": "Plug", - "bareRoot": "Bare-root", - "rootBall": "Root-ball" - }, - "provenance": { - "section": "Where it's from", - "seedsFrom": "Seeds from", - "seedsFromHint": "Who grew or gave them", - "place": "Place", - "placeHint": "Where they come from (with region)", - "addSeedsFrom": "Seeds from", - "addPlace": "Place" - }, - "abundance": { - "add": "How much I have", - "title": "How much I have", - "none": "Not set", - "plentyToShare": "Plenty to share", - "enoughToShare": "Enough to share a little", - "enoughForMe": "Enough for me", - "runningLow": "Running low" - }, - "share": { - "add": "Do you share it?", - "title": "Do you share it?", - "nudge": "You have plenty — you could share some.", - "price": "Price", - "priceHint": "Leave it empty to agree it later", - "private": "Just for me", - "gift": "To give away", - "exchange": "To swap", - "sell": "For sale", - "filterChip": "I share", - "printCatalog": "Print what I share", - "catalogTitle": "What I share", - "catalogSaved": "Catalog saved", - "cancelled": "Cancelled" - }, - "printLabels": { - "action": "Print labels", - "title": "Print labels", - "selectHint": "Pick the seeds to print labels for", - "selectAll": "Select all", - "selected": "{n} selected", - "none": "Select seeds first", - "format": "Label size", - "formatStickers": "Small stickers", - "formatStickersHint": "Many small labels per page — to peel onto packets", - "formatCards": "Large cards", - "formatCardsHint": "Fewer, bigger labels — for jars and boxes", - "count": "{n} labels", - "save": "Save labels", - "saved": "Labels saved", - "cancelled": "Cancelled" - }, - "cropCalendar": { - "add": "Crop calendar", - "title": "Crop calendar", - "sow": "Sow", - "transplant": "Transplant", - "flowering": "Flowering", - "fruiting": "Fruiting", - "seedHarvest": "Seed harvest", - "editorHint": "Note the months typical for this variety in your area — these are your own notes.", - "unset": "—" - }, - "needsReproduction": { - "label": "To regrow this season", - "hint": "Grow it out before the seed runs out", - "badge": "To regrow" - }, - "preservation": { - "add": "How it's kept", - "title": "How it's kept", - "none": "Unspecified", - "jarWithDesiccant": "Jar with drying agent", - "glassJar": "Glass jar", - "paperEnvelope": "Paper envelope", - "paperBag": "Paper bag", - "plasticBag": "Plastic bag" - }, - "conditionCheck": { - "advanced": "Storage & seed-bank details", - "title": "Storage checks", - "add": "Add check", - "containers": "Jars / containers", - "desiccant": "Drying agent", - "none": "No storage checks yet.", - "summary": "{count} jar(s) · {state}" - }, - "desiccant": { - "none": "None", - "add": "Add some", - "replace": "Replace it", - "dry": "Blue — dry", - "fresh": "Just renewed" - }, - "unit": { - "aFew": "a few", - "some": "some", - "plenty": "plenty", - "pinch": "a pinch", - "handful": { - "singular": "handful", - "plural": "handfuls" - }, - "teaspoon": { - "singular": "teaspoon", - "plural": "teaspoons" - }, - "spoon": { - "singular": "spoon", - "plural": "spoons" - }, - "cup": { - "singular": "cup", - "plural": "cups" - }, - "jar": { - "singular": "jar", - "plural": "jars" - }, - "sack": { - "singular": "sack", - "plural": "sacks" - }, - "packet": { - "singular": "packet", - "plural": "packets" - }, - "cob": { - "singular": "cob", - "plural": "cobs" - }, - "pod": { - "singular": "pod", - "plural": "pods" - }, - "ear": { - "singular": "ear", - "plural": "ears" - }, - "head": { - "singular": "head", - "plural": "heads" - }, - "fruit": { - "singular": "fruit", - "plural": "fruits" - }, - "bulb": { - "singular": "bulb", - "plural": "bulbs" - }, - "tuber": { - "singular": "tuber", - "plural": "tubers" - }, - "seedHead": { - "singular": "seed head", - "plural": "seed heads" - }, - "bunch": { - "singular": "bunch", - "plural": "bunches" - }, - "plant": { - "singular": "plant", - "plural": "plants" - }, - "pot": { - "singular": "pot", - "plural": "pots" - }, - "tray": { - "singular": "tray", - "plural": "trays" - }, - "seedling": { - "singular": "seedling", - "plural": "seedlings" - }, - "tree": { - "singular": "tree", - "plural": "trees" - }, - "cutting": { - "singular": "cutting", - "plural": "cuttings" - }, - "grams": { - "singular": "gram", - "plural": "grams" - }, - "count": { - "singular": "seed", - "plural": "seeds" - } - }, - "market": { - "title": "Seeds near you", - "subtitle": "What others are sharing nearby", - "notSetUp": "Sharing isn't set up yet", - "notSetUpBody": "Turn on sharing to see and give seeds to people near you. It stays rough — your zone, never your exact address.", - "setUp": "Set up sharing", - "cantReach": "Can't reach the community servers right now", - "cantReachBody": "Try again, or check the servers under Advanced setup.", - "retry": "Retry", - "setArea": "Set your area", - "setAreaBody": "Tell the market roughly where you are to see seeds nearby.", - "searching": "Looking around your area…", - "empty": "No seeds shared near you yet", - "searchHint": "Search these seeds", - "noMatches": "No shared seeds match your search", - "near": "Near you", - "contact": "Message", - "mine": "You", - "configTitle": "Sharing setup", - "setupIntro": "Sharing with people nearby is optional. Just set your rough area — you're already connected to shared community servers so people can find what you offer, with no company in the middle.", - "areaLabel": "Your area", - "areaHelp": "Kept rough on purpose — your zone, never an exact spot.", - "areaSet": "Your area is set — kept coarse, never your exact spot", - "areaNotSet": "Area not set yet — use your location, or add a code under Advanced", - "advanced": "Advanced", - "areaCodeLabel": "Area code", - "areaCodeHint": "A short code like sp3e9 — not a place name", - "serversLabel": "Community servers", - "serversHelp": "One address per line (wss://…). Leave the defaults if unsure.", - "serversAdvanced": "Add another server", - "save": "Save", - "saved": "Saved", - "wanted": "Wanted", - "shareMine": "Share my seeds", - "sharedCount": "Shared {n} seeds nearby", - "nothingToShare": "Mark some seeds to give, swap or sell first", - "useLocation": "Use my approximate location", - "locationFailed": "Couldn't get your location — check that location is on and the permission is granted", - "queued": "Saved — we'll share these when you're connected", - "shareFailed": "Couldn't reach the community servers — your seeds weren't shared. Try again in a moment.", - "rangeLabel": "How far to look", - "rangeNear": "Very close", - "rangeArea": "Around here", - "rangeRegion": "My region", - "sharedBy": "Shared by", - "noProfile": "This person hasn't shared a profile yet", - "copyId": "Copy code", - "idCopied": "Code copied", - "photo": "Photo" - }, - "profile": { - "title": "Your profile", - "name": "Display name", - "nameHint": "How others see you", - "about": "About", - "aboutHint": "A short line — what you grow, where", - "g1": "Ğ1 address (optional)", - "g1Hint": "So people can pay you in Ğ1 — separate from your key", - "yourId": "Your identity", - "idHelp": "Share this so people can recognise you", - "copy": "Copy", - "copied": "Copied", - "save": "Save", - "saved": "Profile saved", - "identities": "Your identities", - "identitiesHelp": "Keep separate identities — each with its own messages and contacts. They all come from your one backup, so switching adds nothing to remember.", - "identityLabel": "Identity {n}", - "current": "In use", - "newIdentity": "New identity", - "switchTitle": "Switch identity?", - "switchBody": "Messages and contacts are kept separate for each identity. You can switch back anytime.", - "switchAction": "Switch" - }, - "chatList": { - "title": "Messages", - "empty": "No conversations yet. Message someone from the market." - }, - "chat": { - "title": "Chat", - "hint": "Write a message…", - "send": "Send", - "empty": "No messages yet — say hello", - "offline": "Set up sharing to send messages", - "payG1": "Pay in Ğ1", - "g1Copied": "Ğ1 address copied — paste it in your wallet", - "today": "Today", - "yesterday": "Yesterday", - "sendError": "Couldn't send — check your connection", - "noLinks": "Links aren't allowed in messages" - }, - "trust": { - "none": "No one vouches for them yet", - "count": "Vouched for by {n}", - "vouch": "I know this person", - "vouched": "You vouch for them", - "circle": "In your circle" - }, - "yourPeople": { - "title": "Your people", - "help": "People you've met and vouch for, and people who vouch for you.", - "youVouchFor": "You vouch for", - "vouchesForYou": "They vouch for you", - "youVouchForEmpty": "You don't vouch for anyone yet. When you meet someone, open your chat with them and tap \"I know this person\".", - "vouchesForYouEmpty": "No one vouches for you yet", - "revoke": "Stop vouching", - "revokeConfirm": "Stop vouching for this person?", - "offline": "You're offline — try again when you're connected" - }, - "ratings": { - "rate": "Rate this person", - "edit": "Edit your rating", - "commentHint": "How did it go? (optional)", - "fromYourCircle": "{n} from people you know", - "retract": "Remove your rating", - "saved": "Rating saved" - }, - "notifications": { - "newMessageFrom": "New message from {name}" - }, - "plantare": { - "title": "Plantares", - "help": "A Plantare is a promise to reproduce a seed and return some — how a variety keeps travelling from hand to hand. It's not a sale.", - "add": "Add a commitment", - "empty": "No commitments yet. When you share or receive seed with a promise to grow it out and return some, note it here.", - "iReturn": "I'll grow it out & return some", - "owedToMe": "They'll return some to me", - "direction": "Who reproduces & returns", - "counterparty": "With whom?", - "counterpartyHint": "A person or a collective (optional)", - "owed": "What comes back?", - "owedHint": "e.g. a handful next season (optional)", - "note": "Note (optional)", - "save": "Save", - "markReturned": "Mark returned", - "markForgiven": "Let it go", - "reopen": "Reopen", - "delete": "Remove", - "statusReturned": "Returned", - "statusForgiven": "Settled", - "openSection": "Open", - "settledSection": "Done", - "removeConfirm": "Remove this commitment?", - "returnBy": "Return by {date}", - "overdue": "overdue", - "dueByLabel": "Return by (optional)", - "dueByHint": "A gentle reminder, never enforced", - "pickDate": "Pick a date", - "clearDate": "Clear date", - "sectionTitle": "Commitments" - }, - "handover": { - "title": "Seeds changed hands", - "help": "A gift, a swap or a sale — note it down, with a return promise or without.", - "iGave": "I gave seeds", - "iReceived": "I received seeds", - "whichLot": "Which batch?", - "howMuch": "How much?", - "allOfIt": "All of it", - "partOfIt": "A part", - "paymentChip": "Money changed hands", - "promiseGave": "They'll return me seed", - "promiseReceived": "I'll return seed" - }, - "sale": { - "title": "Sales", - "help": "Record seed you sold or bought — money, Ğ1, or any currency. A separate model from a gift or a Plantare. No commission is ever taken on seeds.", - "add": "Record a sale", - "empty": "No sales yet. Note here what you sell or buy.", - "iSold": "I sold", - "iBought": "I bought", - "direction": "Sold or bought?", - "counterparty": "With whom?", - "counterpartyHint": "A person or a collective (optional)", - "amount": "Amount", - "currency": "Currency", - "currencyHint": "€, Ğ1, hours… (optional)", - "hours": "hours", - "note": "Note (optional)", - "save": "Save", - "delete": "Remove", - "removeConfirm": "Remove this sale?" - }, - "legal": { - "title": "Privacy & rules", - "subtitle": "Your privacy, the market rules, and sharing seeds legally", - "privacyTitle": "Your privacy", - "privacyBody": "Tane works without an account, and everything you record stays on your device, encrypted. There are no ads, no trackers, and no Tane server.\n\nNothing is shared unless you choose to: when you publish an offer or your profile, or send a message, it travels through community-run servers. Offers only ever carry a rough zone — never your address.\n\nWhat you publish is public, and copies may remain even after you remove it — so share with care.", - "rulesTitle": "Rules of the road", - "rulesBody": "Tane is a tool, not a shop: exchanges are agreed directly between people, and nobody takes a cut. That also means you are responsible for what you offer and send.\n\nIn the market: be honest about your seeds, only offer what you may share, treat people well, and don't spam. You can block anyone, and report offers or people that break the rules — reports are acted on in the community servers.", - "seedsTitle": "About sharing seeds", - "seedsBody": "Gifting and swapping seeds between amateurs is broadly recognized in most countries. Selling can be different: in many places, selling seed of varieties that aren't officially registered is restricted — check your local rules before asking a price.\n\nSending seeds to another country is often restricted too, and commercially protected varieties must not be propagated without permission. When in doubt, keep it local and keep it a gift.", - "readFull": "Read the full documents online" - }, - "marketGate": { - "title": "Before you join the market", - "intro": "The market is a shared space between neighbours. By continuing, you agree to a few simple rules:", - "ruleHonest": "Be honest about the seeds you offer", - "ruleLegal": "Only share what you're allowed to share where you live", - "ruleRespect": "Treat people well — no spam, no abuse", - "publicNote": "What you publish here is public, and copies may remain even if you remove it later.", - "viewLegal": "Privacy & rules", - "accept": "I agree", - "decline": "Not now" - }, - "report": { - "offer": "Report this offer", - "person": "Report this person", - "title": "Report", - "prompt": "What's wrong?", - "reasonSpam": "Spam or a scam", - "reasonAbuse": "Abusive or disrespectful", - "reasonIllegal": "Seeds that shouldn't be offered", - "reasonOther": "Something else", - "detailsHint": "Add details (optional)", - "send": "Send report", - "sentHidden": "Report sent — you won't see this anymore", - "failed": "Couldn't send the report — check your connection", - "alsoBlock": "Also block this person" - }, - "block": { - "action": "Block this person", - "confirmTitle": "Block this person?", - "confirmBody": "You won't see their offers or messages anymore. You can unblock them later under Blocked people in the sharing setup.", - "confirm": "Block", - "blockedToast": "Blocked — their offers and messages are hidden", - "manageTitle": "Blocked people", - "manageEmpty": "You haven't blocked anyone", - "unblock": "Unblock" + "avatar": { + "title": "Your photo or avatar", + "fromPhoto": "Take or choose a photo", + "illustration": "Or pick an illustration", + "remove": "Remove" + }, + "favorites": { + "title": "Favorites", + "empty": "No favorites yet. Save offers you like from the market.", + "save": "Save to favorites", + "remove": "Remove from favorites", + "unavailable": "No longer available" + }, + "seedSaving": { + "title": "Saving its seed", + "subtitle": "What it takes to keep the variety true", + "lifeCycle": "Cycle", + "cycleAnnual": "Annual", + "cycleBiennial": "Biennial — seeds in year 2", + "cyclePerennial": "Perennial", + "pollination": "Pollination", + "pollSelf": "Self-pollinating", + "pollCross": "Crosses with others", + "pollMixed": "Self-pollinates, sometimes crosses", + "byInsect": "by insects", + "byWind": "on the wind", + "isolation": "Keep apart", + "isolationRange": "{min}–{max} m from other varieties", + "isolationSingle": "{min} m from other varieties", + "plants": "Save from several plants", + "plantsValue": "From at least {n} plants", + "processing": "Cleaning the seed", + "procDry": "Dry seed (thresh)", + "procWet": "Wet seed (ferment and rinse)", + "difficulty": "Difficulty", + "diffEasy": "Easy", + "diffMedium": "Medium", + "diffHard": "Hard", + "advisory": "General guidance — adapt it to your climate and variety.", + "sourcePrefix": "Source" + }, + "calendar": { + "title": "This month", + "filterChip": "This month", + "selfNote": "What you've noted in your varieties.", + "nothing": "Nothing noted for {month}." + }, + "app": { + "title": "Tane" + }, + "bootstrap": { + "failed": "Tane couldn't start", + "retry": "Try again" + }, + "common": { + "save": "Save", + "cancel": "Cancel", + "delete": "Delete", + "edit": "Edit", + "type": "Type", + "comingSoon": "Coming soon", + "offline": "You're offline — sharing is paused" + }, + "home": { + "tagline": "Share and grow local seeds", + "openMarket": "Market", + "openMarketSubtitle": "Discover and share seeds nearby", + "yourInventory": "Your inventory", + "yourInventorySubtitle": "Manage your seeds" + }, + "photo": { + "camera": "Take a photo", + "gallery": "Choose from gallery", + "setAsCover": "Set as cover", + "isCover": "Cover photo", + "deleteConfirm": "Delete this photo?" + }, + "menu": { + "tagline": "your seed bank", + "inventory": "Inventory", + "market": "Market", + "profile": "Your profile", + "chat": "Chat", + "wishlist": "Favorites", + "following": "Following", + "plantares": "Plantares", + "sales": "Sales", + "calendar": "Calendar", + "settings": "Settings" + }, + "settings": { + "language": "Language", + "systemLanguage": "System language", + "langEs": "Español", + "langEn": "English", + "langPt": "Português", + "langAst": "Asturianu", + "langFr": "Français", + "langDe": "Deutsch", + "langJa": "日本語", + "about": "About", + "aboutText": "Local-first, encrypted inventory for traditional seeds. AGPL-3.0.", + "aboutOpen": "About Tane" + }, + "backup": { + "title": "Backup & restore", + "autoBackupTitle": "Automatic backups", + "autoBackupLast": "Last copy saved {date}", + "autoBackupNone": "A copy is kept automatically in the background", + "exportJson": "Save a backup", + "exportJsonSubtitle": "A complete copy to keep safe, restore later or move to another device", + "importJson": "Restore a backup", + "importJsonSubtitle": "Bring a saved copy back — nothing gets duplicated", + "exportCsv": "Export to a spreadsheet", + "exportCsvSubtitle": "A simple list for Excel or LibreOffice — no photos", + "importCsv": "Import a list", + "importCsvSubtitle": "Add entries from a spreadsheet", + "importConfirmTitle": "Restore a backup?", + "importConfirmBody": "Entries merge with your inventory; when the same entry exists on both sides, the newest version wins. Nothing gets duplicated.", + "importCsvConfirmTitle": "Import a list?", + "importCsvConfirmBody": "Every row is added as a new entry. This does not merge or replace, so importing the same file twice adds it twice.", + "importAction": "Import", + "exportSaved": "Copy saved", + "cancelled": "Cancelled", + "importDone": "Imported: {added} new, {updated} updated", + "importCsvDone": "Added {count} entries", + "importFailed": "This file could not be read as a Tane copy", + "failed": "Something went wrong", + "recoveryTitle": "Your recovery code", + "recoverySubtitle": "Print it and keep it safe — it opens your copies on a new device", + "recoveryIntro": "This code opens your saved copies and brings your bank back on any device. Keep two paper copies in safe places, like your best seed. Anyone holding it can read your copies, so share it with no one.", + "recoveryCopy": "Copy", + "recoverySave": "Save the sheet", + "recoverySheetTitle": "Tane — your recovery sheet", + "recoveryPromptTitle": "Enter your recovery code", + "recoveryPromptBody": "This copy was saved with another code. Type the code from your recovery sheet to open it.", + "recoveryWrongCode": "That code doesn't open this copy" + }, + "about": { + "title": "About", + "kanji": "種", + "tagline": "A local-first, decentralized app for managing and sharing traditional seeds and seedlings.", + "intro": "Tane (種, \"seed\" in Japanese) helps people and collectives keep a friendly inventory of their seed bank, decide what they offer, and share it locally — without a central intermediary that could control, censor, or be fined for it. Its name comes from tanemaki (種まき), \"to sow / scatter seeds\". The goal is both practical and political: to support traditional seed varieties and push back against the seed monopoly.", + "heritage": "The name honors the old Japanese mutual-aid traditions around rice — yui (shared community labour) and tanomoshi (reciprocity funds) — that inspired the paper Plantare, the \"community currency for seed exchange\" (BAH-Semillero, 2009, CC-BY-SA). Tane is the digital Plantare.", + "version": "Version", + "license": "License", + "licenseValue": "AGPL-3.0", + "website": "Website", + "openSourceLicenses": "Open-source licenses", + "openSourceLicensesSubtitle": "Third-party libraries and their licenses", + "copyright": "© {years} Comunes Association, under AGPLv3" + }, + "intro": { + "skip": "Skip", + "next": "Next", + "start": "Get started", + "menuEntry": "How Tane works", + "slides": { + "welcome": { + "title": "The seed that brought you here", + "body": "Every traditional seed is a letter written by thousands of generations, passed from hand to hand. We are what we are thanks to that sharing." + }, + "inventory": { + "title": "Your seed bank, in your pocket", + "body": "Note what you have, from which year, how much and where it came from — with the name you use. A photo and a name are enough to start." + }, + "privacy": { + "title": "Yours, and only yours", + "body": "No account, no internet, no trackers. Your data lives encrypted on your device, and only what you choose is ever shared." + }, + "share": { + "title": "Share, the way it's always been done", + "body": "Offer what you have spare — gift, swap or sell — and let someone nearby find it. Only an approximate distance is shown, never your address; you close the deal in person." + }, + "plantare": { + "title": "To sow is to multiply", + "body": "With a Plantare, whoever receives seed promises to return some later. And since returning it means growing it, every loan multiplies the commons." + } } + }, + "inventory": { + "title": "Inventory", + "searchHint": "Search seeds", + "empty": "No seeds yet. Tap + to add your first.", + "noMatches": "No seeds match your filters.", + "clearFilters": "Clear filters", + "uncategorized": "Uncategorized", + "needsReproductionFilter": "To regrow", + "loadError": "Couldn't open your seed bank. It may just have been busy — try again.", + "retry": "Try again" + }, + "draft": { + "capture": "Capture photos", + "captured": "{n} captured to catalogue", + "triageTitle": "To catalogue", + "triageCount": "{n} to catalogue", + "untitled": "Unnamed", + "nameField": "Name this seed", + "nameHint": "What is it?", + "suggestFromPhoto": "Suggest name from photo", + "discard": "Discard" + }, + "quickAdd": { + "title": "Add a seed", + "labelField": "Name", + "labelRequired": "Give it a name", + "addPhoto": "Add photo", + "quantity": "How much?", + "more": "Add more…", + "save": "Save", + "saveAndAddAnother": "Save & add another", + "addedCount": "{n} added", + "cancel": "Cancel" + }, + "detail": { + "notFound": "This seed is no longer here.", + "lots": "Lots", + "noLots": "No lots yet.", + "names": "Also known as", + "addName": "Add name", + "links": "Links", + "addLink": "Add link", + "linkUrl": "URL", + "linkTitle": "Title (optional)", + "reference": "Learn more", + "refGbif": "GBIF", + "refWikipedia": "Wikipedia", + "refWikispecies": "Wikispecies", + "notes": "Notes", + "addLot": "Add lot", + "editLot": "Edit lot", + "deleteConfirm": "Delete this seed?", + "year": "Year {year}", + "noYear": "Year unknown" + }, + "germination": { + "title": "Germination", + "add": "Add test", + "sampleSize": "Sample size", + "germinated": "Germinated", + "none": "No germination tests yet.", + "result": "{percent}%" + }, + "viability": { + "expiringSoon": "Use or reproduce this season", + "expiringSoonYears": "Use or reproduce this season · keeps ~{years} yr", + "expired": "Past typical viability — reproduce", + "expiredYears": "Past typical viability (~{years} yr) — reproduce" + }, + "editVariety": { + "title": "Edit seed", + "name": "Name", + "category": "Category", + "notes": "Notes", + "species": "Species (from catalog)", + "speciesHint": "Search a species…", + "speciesSuggested": "Suggested from the name", + "organic": "Organic", + "organicHint": "Grown organically (eco)" + }, + "addLot": { + "title": "Add lot", + "year": "Harvest date", + "quantity": "How much?", + "amount": "Amount" + }, + "harvest": { + "pickTitle": "Select month / year", + "anyMonth": "Any month", + "noDate": "Set harvest date", + "monthNames": [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + ] + }, + "lotType": { + "seed": "Seeds", + "plant": "Plant", + "seedling": "Seedling", + "tree": "Tree / shrub", + "bulb": "Bulb / tuber", + "cutting": "Cutting" + }, + "presentation": { + "title": "Packaging", + "none": "Unspecified", + "pot": "Pot", + "tray": "Tray", + "plug": "Plug", + "bareRoot": "Bare-root", + "rootBall": "Root-ball" + }, + "provenance": { + "section": "Where it's from", + "seedsFrom": "Seeds from", + "seedsFromHint": "Who grew or gave them", + "place": "Place", + "placeHint": "Where they come from (with region)", + "addSeedsFrom": "Seeds from", + "addPlace": "Place" + }, + "abundance": { + "add": "How much I have", + "title": "How much I have", + "none": "Not set", + "plentyToShare": "Plenty to share", + "enoughToShare": "Enough to share a little", + "enoughForMe": "Enough for me", + "runningLow": "Running low" + }, + "share": { + "add": "Do you share it?", + "title": "Do you share it?", + "nudge": "You have plenty — you could share some.", + "price": "Price", + "priceHint": "Leave it empty to agree it later", + "private": "Just for me", + "gift": "To give away", + "exchange": "To swap", + "sell": "For sale", + "filterChip": "I share", + "printCatalog": "Print what I share", + "catalogTitle": "What I share", + "catalogSaved": "Catalog saved", + "cancelled": "Cancelled" + }, + "printLabels": { + "action": "Print labels", + "title": "Print labels", + "selectHint": "Pick the seeds to print labels for", + "selectAll": "Select all", + "selected": "{n} selected", + "none": "Select seeds first", + "format": "Label size", + "formatStickers": "Small stickers", + "formatStickersHint": "Many small labels per page — to peel onto packets", + "formatCards": "Large cards", + "formatCardsHint": "Fewer, bigger labels — for jars and boxes", + "count": "{n} labels", + "save": "Save labels", + "saved": "Labels saved", + "cancelled": "Cancelled" + }, + "cropCalendar": { + "add": "Crop calendar", + "title": "Crop calendar", + "sow": "Sow", + "transplant": "Transplant", + "flowering": "Flowering", + "fruiting": "Fruiting", + "seedHarvest": "Seed harvest", + "editorHint": "Note the months typical for this variety in your area — these are your own notes.", + "unset": "—" + }, + "needsReproduction": { + "label": "To regrow this season", + "hint": "Grow it out before the seed runs out", + "badge": "To regrow" + }, + "preservation": { + "add": "How it's kept", + "title": "How it's kept", + "none": "Unspecified", + "jarWithDesiccant": "Jar with drying agent", + "glassJar": "Glass jar", + "paperEnvelope": "Paper envelope", + "paperBag": "Paper bag", + "plasticBag": "Plastic bag" + }, + "conditionCheck": { + "advanced": "Storage & seed-bank details", + "title": "Storage checks", + "add": "Add check", + "containers": "Jars / containers", + "desiccant": "Drying agent", + "none": "No storage checks yet.", + "summary": "{count} jar(s) · {state}" + }, + "desiccant": { + "none": "None", + "add": "Add some", + "replace": "Replace it", + "dry": "Blue — dry", + "fresh": "Just renewed" + }, + "unit": { + "aFew": "a few", + "some": "some", + "plenty": "plenty", + "pinch": "a pinch", + "handful": { + "singular": "handful", + "plural": "handfuls" + }, + "teaspoon": { + "singular": "teaspoon", + "plural": "teaspoons" + }, + "spoon": { + "singular": "spoon", + "plural": "spoons" + }, + "cup": { + "singular": "cup", + "plural": "cups" + }, + "jar": { + "singular": "jar", + "plural": "jars" + }, + "sack": { + "singular": "sack", + "plural": "sacks" + }, + "packet": { + "singular": "packet", + "plural": "packets" + }, + "cob": { + "singular": "cob", + "plural": "cobs" + }, + "pod": { + "singular": "pod", + "plural": "pods" + }, + "ear": { + "singular": "ear", + "plural": "ears" + }, + "head": { + "singular": "head", + "plural": "heads" + }, + "fruit": { + "singular": "fruit", + "plural": "fruits" + }, + "bulb": { + "singular": "bulb", + "plural": "bulbs" + }, + "tuber": { + "singular": "tuber", + "plural": "tubers" + }, + "seedHead": { + "singular": "seed head", + "plural": "seed heads" + }, + "bunch": { + "singular": "bunch", + "plural": "bunches" + }, + "plant": { + "singular": "plant", + "plural": "plants" + }, + "pot": { + "singular": "pot", + "plural": "pots" + }, + "tray": { + "singular": "tray", + "plural": "trays" + }, + "seedling": { + "singular": "seedling", + "plural": "seedlings" + }, + "tree": { + "singular": "tree", + "plural": "trees" + }, + "cutting": { + "singular": "cutting", + "plural": "cuttings" + }, + "grams": { + "singular": "gram", + "plural": "grams" + }, + "count": { + "singular": "seed", + "plural": "seeds" + } + }, + "market": { + "title": "Seeds near you", + "subtitle": "What others are sharing nearby", + "notSetUp": "Sharing isn't set up yet", + "notSetUpBody": "Turn on sharing to see and give seeds to people near you. It stays rough — your zone, never your exact address.", + "setUp": "Set up sharing", + "cantReach": "Can't reach the community servers right now", + "cantReachBody": "Try again, or check the servers under Advanced setup.", + "retry": "Retry", + "setArea": "Set your area", + "setAreaBody": "Tell the market roughly where you are to see seeds nearby.", + "searching": "Looking around your area…", + "empty": "No seeds shared near you yet", + "searchHint": "Search these seeds", + "noMatches": "No shared seeds match your search", + "near": "Near you", + "contact": "Message", + "mine": "You", + "configTitle": "Sharing setup", + "setupIntro": "Sharing with people nearby is optional. Just set your rough area — you're already connected to shared community servers so people can find what you offer, with no company in the middle.", + "areaLabel": "Your area", + "areaHelp": "Kept rough on purpose — your zone, never an exact spot.", + "areaSet": "Your area is set — kept coarse, never your exact spot", + "areaNotSet": "Area not set yet — use your location, or add a code under Advanced", + "advanced": "Advanced", + "areaCodeLabel": "Area code", + "areaCodeHint": "A short code like sp3e9 — not a place name", + "serversLabel": "Community servers", + "serversHelp": "One address per line (wss://…). Leave the defaults if unsure.", + "serversAdvanced": "Add another server", + "save": "Save", + "saved": "Saved", + "wanted": "Wanted", + "shareMine": "Share my seeds", + "sharedCount": "Shared {n} seeds nearby", + "nothingToShare": "Mark some seeds to give, swap or sell first", + "useLocation": "Use my approximate location", + "locationFailed": "Couldn't get your location — check that location is on and the permission is granted", + "queued": "Saved — we'll share these when you're connected", + "shareFailed": "Couldn't reach the community servers — your seeds weren't shared. Try again in a moment.", + "rangeLabel": "How far to look", + "rangeNear": "Very close", + "rangeArea": "Around here", + "rangeRegion": "My region", + "sharedBy": "Shared by", + "noProfile": "This person hasn't shared a profile yet", + "copyId": "Copy code", + "idCopied": "Code copied", + "photo": "Photo" + }, + "profile": { + "title": "Your profile", + "name": "Display name", + "nameHint": "How others see you", + "about": "About", + "aboutHint": "A short line — what you grow, where", + "g1": "Ğ1 address (optional)", + "g1Hint": "So people can pay you in Ğ1 — separate from your key", + "yourId": "Your identity", + "idHelp": "Share this so people can recognise you", + "copy": "Copy", + "copied": "Copied", + "save": "Save", + "saved": "Profile saved", + "identities": "Your identities", + "identitiesHelp": "Keep separate identities — each with its own messages and contacts. They all come from your one backup, so switching adds nothing to remember.", + "identityLabel": "Identity {n}", + "current": "In use", + "newIdentity": "New identity", + "switchTitle": "Switch identity?", + "switchBody": "Messages and contacts are kept separate for each identity. You can switch back anytime.", + "switchAction": "Switch" + }, + "chatList": { + "title": "Messages", + "empty": "No conversations yet. Message someone from the market." + }, + "chat": { + "title": "Chat", + "hint": "Write a message…", + "send": "Send", + "empty": "No messages yet — say hello", + "offline": "Set up sharing to send messages", + "payG1": "Pay in Ğ1", + "g1Copied": "Ğ1 address copied — paste it in your wallet", + "today": "Today", + "yesterday": "Yesterday", + "sendError": "Couldn't send — check your connection", + "noLinks": "Links aren't allowed in messages" + }, + "trust": { + "none": "No one vouches for them yet", + "count": "Vouched for by {n}", + "vouch": "I know this person", + "vouched": "You vouch for them", + "circle": "In your circle" + }, + "yourPeople": { + "title": "Your people", + "help": "People you've met and vouch for, and people who vouch for you.", + "youVouchFor": "You vouch for", + "vouchesForYou": "They vouch for you", + "youVouchForEmpty": "You don't vouch for anyone yet. When you meet someone, open your chat with them and tap \"I know this person\".", + "vouchesForYouEmpty": "No one vouches for you yet", + "revoke": "Stop vouching", + "revokeConfirm": "Stop vouching for this person?", + "offline": "You're offline — try again when you're connected" + }, + "ratings": { + "rate": "Rate this person", + "edit": "Edit your rating", + "commentHint": "How did it go? (optional)", + "fromYourCircle": "{n} from people you know", + "retract": "Remove your rating", + "saved": "Rating saved" + }, + "notifications": { + "newMessageFrom": "New message from {name}" + }, + "plantare": { + "title": "Plantares", + "help": "A Plantare is a promise to reproduce a seed and return some — how a variety keeps travelling from hand to hand. It's not a sale.", + "add": "Add a commitment", + "empty": "No commitments yet. When you share or receive seed with a promise to grow it out and return some, note it here.", + "iReturn": "I'll grow it out & return some", + "owedToMe": "They'll return some to me", + "direction": "Who reproduces & returns", + "counterparty": "With whom?", + "counterpartyHint": "A person or a collective (optional)", + "owed": "What comes back?", + "owedHint": "e.g. a handful next season (optional)", + "note": "Note (optional)", + "save": "Save", + "markReturned": "Mark returned", + "markForgiven": "Let it go", + "reopen": "Reopen", + "delete": "Remove", + "statusReturned": "Returned", + "statusForgiven": "Settled", + "openSection": "Open", + "settledSection": "Done", + "removeConfirm": "Remove this commitment?", + "returnBy": "Return by {date}", + "overdue": "overdue", + "dueByLabel": "Return by (optional)", + "dueByHint": "A gentle reminder, never enforced", + "pickDate": "Pick a date", + "clearDate": "Clear date", + "sectionTitle": "Commitments", + "propose": "Propose a signed Plantaré", + "proposeHelp": "Both of you keep the same promise, signed by both — proof that this seed changed hands and will be grown out and returned.", + "proposeTo": "With {name}", + "sent": "Proposal sent — waiting for them to sign", + "seedLabel": "Which seed?", + "seedHint": "The variety this promise is about", + "returnKindLabel": "What comes back?", + "returnSimilar": "A similar amount of seed", + "returnSimilarNote": "open-pollinated · non-GMO · organically grown", + "returnWork": "Some hours of work", + "returnOther": "Something else", + "workHoursLabel": "How many hours?", + "proposalsSection": "Waiting for your reply", + "incomingFrom": "{name} proposes a Plantaré", + "accept": "Accept & sign", + "declineAction": "Decline", + "declineReasonHint": "Reason (optional)", + "acceptedToast": "Signed — you both hold it now", + "declinedToast": "Declined", + "badgeAwaiting": "Awaiting signature", + "badgeSigned": "Signed by both", + "badgeDeclined": "Declined", + "offline": "You're offline — it'll be sent when you reconnect" + }, + "handover": { + "title": "Seeds changed hands", + "help": "A gift, a swap or a sale — note it down, with a return promise or without.", + "iGave": "I gave seeds", + "iReceived": "I received seeds", + "whichLot": "Which batch?", + "howMuch": "How much?", + "allOfIt": "All of it", + "partOfIt": "A part", + "paymentChip": "Money changed hands", + "promiseGave": "They'll return me seed", + "promiseReceived": "I'll return seed" + }, + "sale": { + "title": "Sales", + "help": "Record seed you sold or bought — money, Ğ1, or any currency. A separate model from a gift or a Plantare. No commission is ever taken on seeds.", + "add": "Record a sale", + "empty": "No sales yet. Note here what you sell or buy.", + "iSold": "I sold", + "iBought": "I bought", + "direction": "Sold or bought?", + "counterparty": "With whom?", + "counterpartyHint": "A person or a collective (optional)", + "amount": "Amount", + "currency": "Currency", + "currencyHint": "€, Ğ1, hours… (optional)", + "hours": "hours", + "note": "Note (optional)", + "save": "Save", + "delete": "Remove", + "removeConfirm": "Remove this sale?" + }, + "legal": { + "title": "Privacy & rules", + "subtitle": "Your privacy, the market rules, and sharing seeds legally", + "privacyTitle": "Your privacy", + "privacyBody": "Tane works without an account, and everything you record stays on your device, encrypted. There are no ads, no trackers, and no Tane server.\n\nNothing is shared unless you choose to: when you publish an offer or your profile, or send a message, it travels through community-run servers. Offers only ever carry a rough zone — never your address.\n\nWhat you publish is public, and copies may remain even after you remove it — so share with care.", + "rulesTitle": "Rules of the road", + "rulesBody": "Tane is a tool, not a shop: exchanges are agreed directly between people, and nobody takes a cut. That also means you are responsible for what you offer and send.\n\nIn the market: be honest about your seeds, only offer what you may share, treat people well, and don't spam. You can block anyone, and report offers or people that break the rules — reports are acted on in the community servers.", + "seedsTitle": "About sharing seeds", + "seedsBody": "Gifting and swapping seeds between amateurs is broadly recognized in most countries. Selling can be different: in many places, selling seed of varieties that aren't officially registered is restricted — check your local rules before asking a price.\n\nSending seeds to another country is often restricted too, and commercially protected varieties must not be propagated without permission. When in doubt, keep it local and keep it a gift.", + "readFull": "Read the full documents online" + }, + "marketGate": { + "title": "Before you join the market", + "intro": "The market is a shared space between neighbours. By continuing, you agree to a few simple rules:", + "ruleHonest": "Be honest about the seeds you offer", + "ruleLegal": "Only share what you're allowed to share where you live", + "ruleRespect": "Treat people well — no spam, no abuse", + "publicNote": "What you publish here is public, and copies may remain even if you remove it later.", + "viewLegal": "Privacy & rules", + "accept": "I agree", + "decline": "Not now" + }, + "report": { + "offer": "Report this offer", + "person": "Report this person", + "title": "Report", + "prompt": "What's wrong?", + "reasonSpam": "Spam or a scam", + "reasonAbuse": "Abusive or disrespectful", + "reasonIllegal": "Seeds that shouldn't be offered", + "reasonOther": "Something else", + "detailsHint": "Add details (optional)", + "send": "Send report", + "sentHidden": "Report sent — you won't see this anymore", + "failed": "Couldn't send the report — check your connection", + "alsoBlock": "Also block this person" + }, + "block": { + "action": "Block this person", + "confirmTitle": "Block this person?", + "confirmBody": "You won't see their offers or messages anymore. You can unblock them later under Blocked people in the sharing setup.", + "confirm": "Block", + "blockedToast": "Blocked — their offers and messages are hidden", + "manageTitle": "Blocked people", + "manageEmpty": "You haven't blocked anyone", + "unblock": "Unblock" + } } diff --git a/apps/app_seeds/lib/i18n/es.i18n.json b/apps/app_seeds/lib/i18n/es.i18n.json index 36b885d..d4742b6 100644 --- a/apps/app_seeds/lib/i18n/es.i18n.json +++ b/apps/app_seeds/lib/i18n/es.i18n.json @@ -1,727 +1,750 @@ { - "avatar": { - "title": "Tu foto o avatar", - "fromPhoto": "Hacer o elegir una foto", - "illustration": "O elige un dibujo", - "remove": "Quitar" - }, - "favorites": { - "title": "Favoritos", - "empty": "Aún no tienes favoritos. Guarda ofertas que te gusten del mercado.", - "save": "Guardar en favoritos", - "remove": "Quitar de favoritos", - "unavailable": "Ya no está disponible" - }, - "seedSaving": { - "title": "Conservar su semilla", - "subtitle": "Lo que hace falta para mantener la variedad fiel", - "lifeCycle": "Ciclo", - "cycleAnnual": "Anual", - "cycleBiennial": "Bienal — da semilla el 2.º año", - "cyclePerennial": "Perenne", - "pollination": "Polinización", - "pollSelf": "Se autopoliniza", - "pollCross": "Se cruza con otras", - "pollMixed": "Se autopoliniza, pero se cruza a veces", - "byInsect": "por insectos", - "byWind": "por el viento", - "isolation": "Sepárala", - "isolationRange": "{min}–{max} m de otras variedades", - "isolationSingle": "{min} m de otras variedades", - "plants": "Guarda de varias plantas", - "plantsValue": "De al menos {n} plantas", - "processing": "Cómo limpiarla", - "procDry": "Semilla seca (trillar)", - "procWet": "Semilla húmeda (fermentar y lavar)", - "difficulty": "Dificultad", - "diffEasy": "Fácil", - "diffMedium": "Media", - "diffHard": "Difícil", - "advisory": "Orientativo — adáptalo a tu clima y variedad.", - "sourcePrefix": "Fuente" - }, - "calendar": { - "title": "Este mes", - "filterChip": "Este mes", - "selfNote": "Lo que has anotado en tus variedades.", - "nothing": "Nada anotado para {month}." - }, - "app": { - "title": "Tane" - }, - "bootstrap": { - "failed": "Tane no pudo arrancar", - "retry": "Reintentar" - }, - "common": { - "save": "Guardar", - "cancel": "Cancelar", - "delete": "Eliminar", - "edit": "Editar", - "type": "Tipo", - "comingSoon": "Pronto", - "offline": "Sin conexión — el compartir está en pausa" - }, - "home": { - "tagline": "Comparte y cultiva semillas locales", - "openMarket": "Mercado", - "openMarketSubtitle": "Descubre y comparte semillas cerca", - "yourInventory": "Tu inventario", - "yourInventorySubtitle": "Gestiona tus semillas" - }, - "photo": { - "camera": "Hacer una foto", - "gallery": "Elegir de la galería", - "setAsCover": "Poner de portada", - "isCover": "Foto de portada", - "deleteConfirm": "¿Borrar esta foto?" - }, - "menu": { - "tagline": "tu banco de semillas", - "inventory": "Inventario", - "market": "Mercado", - "profile": "Tu perfil", - "chat": "Chat", - "wishlist": "Favoritos", - "following": "Siguiendo", - "plantares": "Plantares", - "sales": "Ventas", - "calendar": "Calendario", - "settings": "Ajustes" - }, - "settings": { - "language": "Idioma", - "systemLanguage": "Idioma del sistema", - "langEs": "Español", - "langEn": "English", - "langPt": "Português", - "langFr": "Français", - "langDe": "Deutsch", - "langJa": "日本語", - "about": "Acerca de", - "aboutText": "Inventario local y cifrado para semillas tradicionales. AGPL-3.0.", - "aboutOpen": "Acerca de Tane" - }, - "backup": { - "title": "Copia de seguridad", - "autoBackupTitle": "Copias automáticas", - "autoBackupLast": "Última copia guardada el {date}", - "autoBackupNone": "Se guarda una copia automáticamente en segundo plano", - "exportJson": "Guardar una copia de seguridad", - "exportJsonSubtitle": "Una copia completa para guardar a salvo, restaurar luego o pasar a otro dispositivo", - "importJson": "Restaurar una copia", - "importJsonSubtitle": "Recupera una copia guardada — no se duplica nada", - "exportCsv": "Exportar a una hoja de cálculo", - "exportCsvSubtitle": "Una lista sencilla para Excel o LibreOffice — sin fotos", - "importCsv": "Importar una lista", - "importCsvSubtitle": "Añade entradas desde una hoja de cálculo", - "importConfirmTitle": "¿Restaurar una copia?", - "importConfirmBody": "Las entradas se fusionan con tu inventario; si una entrada existe en ambos lados, gana la versión más reciente. No se duplica nada.", - "importCsvConfirmTitle": "¿Importar una lista?", - "importCsvConfirmBody": "Cada fila se añade como una entrada nueva. No fusiona ni reemplaza, así que importar el mismo fichero dos veces lo añade dos veces.", - "importAction": "Importar", - "exportSaved": "Copia guardada", - "cancelled": "Cancelado", - "importDone": "Importado: {added} nuevas, {updated} actualizadas", - "importCsvDone": "Añadidas {count} entradas", - "importFailed": "Este fichero no se pudo leer como una copia de Tane", - "failed": "Algo ha salido mal", - "recoveryTitle": "Tu código de recuperación", - "recoverySubtitle": "Imprímelo y guárdalo bien: abre tus copias en otro dispositivo", - "recoveryIntro": "Este código abre tus copias guardadas y recupera tu banco en cualquier dispositivo. Guarda dos copias en papel en sitios seguros, como tu mejor semilla. Quien lo tenga puede leer tus copias, así que no lo compartas con nadie.", - "recoveryCopy": "Copiar", - "recoverySave": "Guardar la hoja", - "recoverySheetTitle": "Tane — tu hoja de recuperación", - "recoveryPromptTitle": "Escribe tu código de recuperación", - "recoveryPromptBody": "Esta copia se guardó con otro código. Escribe el código de tu hoja de recuperación para abrirla.", - "recoveryWrongCode": "Ese código no abre esta copia" - }, - "about": { - "title": "Acerca de", - "kanji": "種", - "tagline": "Una app local-first y descentralizada para gestionar y compartir semillas y plantones tradicionales.", - "intro": "Tane (種, «semilla» en japonés) ayuda a personas y colectivos a llevar un inventario amable de su banco de semillas, decidir qué ofrecen y compartirlo localmente — sin un intermediario central que pueda controlarlo, censurarlo o ser multado por ello. Su nombre viene de tanemaki (種まき), «sembrar / esparcir semillas». El objetivo es a la vez práctico y político: apoyar las variedades tradicionales de semillas y plantar cara al monopolio semillero.", - "heritage": "El nombre honra las viejas tradiciones japonesas de ayuda mutua en torno al arroz — yui (trabajo comunitario compartido) y tanomoshi (fondos de reciprocidad) — que inspiraron el papel Plantare, la «moneda comunitaria para el intercambio de semillas» (BAH-Semillero, 2009, CC-BY-SA). Tane es el Plantare digital.", - "version": "Versión", - "license": "Licencia", - "licenseValue": "AGPL-3.0", - "website": "Sitio web", - "openSourceLicenses": "Licencias de código abierto", - "openSourceLicensesSubtitle": "Bibliotecas de terceros y sus licencias", - "copyright": "© {years} Asociación Comunes, bajo AGPLv3" - }, - "intro": { - "skip": "Saltar", - "next": "Siguiente", - "start": "Empezar", - "menuEntry": "Cómo funciona Tane", - "slides": { - "welcome": { - "title": "La semilla que te trajo hasta aquí", - "body": "Cada semilla tradicional es una carta escrita por miles de generaciones, que pasó de mano en mano. Somos lo que somos gracias a ese intercambio." - }, - "inventory": { - "title": "Tu banco de semillas, en el bolsillo", - "body": "Apunta qué tienes, de qué año, cuánto y de dónde vino, con el nombre que tú usas. Una foto y un nombre bastan para empezar." - }, - "privacy": { - "title": "Tuyo y solo tuyo", - "body": "Sin cuenta, sin internet, sin rastreadores. Tus datos viven cifrados en tu dispositivo y solo se comparte lo que tú decidas." - }, - "share": { - "title": "Compartir, como siempre se hizo", - "body": "Ofrece lo que te sobra —regalo, trueque o venta— y que alguien cerca lo encuentre. Solo se muestra la distancia aproximada, nunca tu dirección; el trato lo cierras en persona." - }, - "plantare": { - "title": "Sembrar es multiplicar", - "body": "Con el Plantare, quien recibe promete devolver semilla más adelante. Y como para devolverla hay que cultivarla, cada préstamo multiplica el común." - } - } - }, - "inventory": { - "title": "Inventario", - "searchHint": "Buscar semillas", - "empty": "Aún no hay semillas. Toca + para añadir la primera.", - "noMatches": "Ninguna semilla coincide con los filtros.", - "clearFilters": "Quitar filtros", - "uncategorized": "Sin categoría", - "needsReproductionFilter": "Por reproducir", - "loadError": "No se pudo abrir tu banco de semillas. Quizá estaba ocupado: inténtalo de nuevo.", - "retry": "Reintentar" - }, - "draft": { - "capture": "Capturar fotos", - "captured": "{n} capturadas por catalogar", - "triageTitle": "Por catalogar", - "triageCount": "{n} por catalogar", - "untitled": "Sin nombre", - "nameField": "Nombra esta semilla", - "nameHint": "¿Qué es?", - "suggestFromPhoto": "Sugerir nombre de la foto", - "discard": "Descartar" - }, - "quickAdd": { - "title": "Añadir una semilla", - "labelField": "Nombre", - "labelRequired": "Ponle un nombre", - "addPhoto": "Añadir foto", - "quantity": "¿Cuánta?", - "more": "Añadir más…", - "save": "Guardar", - "saveAndAddAnother": "Guardar y añadir otra", - "addedCount": "{n} añadidas", - "cancel": "Cancelar" - }, - "detail": { - "notFound": "Esta semilla ya no está aquí.", - "lots": "Lotes", - "noLots": "Aún no hay lotes.", - "names": "También conocida como", - "addName": "Añadir nombre", - "links": "Enlaces", - "addLink": "Añadir enlace", - "linkUrl": "URL", - "linkTitle": "Título (opcional)", - "reference": "Saber más", - "refGbif": "GBIF", - "refWikipedia": "Wikipedia", - "refWikispecies": "Wikispecies", - "notes": "Notas", - "addLot": "Añadir lote", - "editLot": "Editar lote", - "deleteConfirm": "¿Eliminar esta semilla?", - "year": "Año {year}", - "noYear": "Año desconocido" - }, - "germination": { - "title": "Germinación", - "add": "Añadir prueba", - "sampleSize": "Muestra", - "germinated": "Germinadas", - "none": "Aún no hay pruebas de germinación.", - "result": "{percent}%" - }, - "viability": { - "expiringSoon": "Úsala o multiplícala esta temporada", - "expiringSoonYears": "Úsala o multiplícala esta temporada · dura ~{years} años", - "expired": "Supera su viabilidad típica — a multiplicar", - "expiredYears": "Supera su viabilidad típica (~{years} años) — a multiplicar" - }, - "editVariety": { - "title": "Editar semilla", - "name": "Nombre", - "category": "Categoría", - "notes": "Notas", - "species": "Especie (del catálogo)", - "speciesHint": "Buscar una especie…", - "speciesSuggested": "Sugerida por el nombre", - "organic": "Ecológica", - "organicHint": "Cultivada de forma ecológica (eco)" - }, - "addLot": { - "title": "Añadir lote", - "year": "Fecha de cosecha", - "quantity": "¿Cuánta?", - "amount": "Cantidad" - }, - "harvest": { - "pickTitle": "Seleccionar mes / año", - "anyMonth": "Cualquier mes", - "noDate": "Indicar fecha de cosecha", - "monthNames": [ - "enero", - "febrero", - "marzo", - "abril", - "mayo", - "junio", - "julio", - "agosto", - "septiembre", - "octubre", - "noviembre", - "diciembre" - ] - }, - "lotType": { - "seed": "Semillas", - "plant": "Planta", - "seedling": "Plantón", - "tree": "Árbol / arbusto", - "bulb": "Bulbo / tubérculo", - "cutting": "Esqueje" - }, - "presentation": { - "title": "Envase", - "none": "Sin especificar", - "pot": "Maceta", - "tray": "Bandeja", - "plug": "Alvéolo", - "bareRoot": "Raíz desnuda", - "rootBall": "Cepellón" - }, - "provenance": { - "section": "De dónde viene", - "seedsFrom": "Semillas de", - "seedsFromHint": "Quién las cultivó o las dio", - "place": "Lugar", - "placeHint": "De dónde vienen (con provincia)", - "addSeedsFrom": "Semillas de", - "addPlace": "Lugar" - }, - "abundance": { - "add": "Cuánta tengo", - "title": "Cuánta tengo", - "none": "Sin indicar", - "plentyToShare": "De sobra para compartir", - "enoughToShare": "Bastante, para compartir con moderación", - "enoughForMe": "Suficiente para mí", - "runningLow": "Queda poca" - }, - "share": { - "add": "¿La compartes?", - "title": "¿La compartes?", - "nudge": "Tienes de sobra: podrías compartir un poco.", - "price": "Precio", - "priceHint": "Déjalo vacío para acordarlo luego", - "private": "Solo para mí", - "gift": "Para regalar", - "exchange": "Para intercambiar", - "sell": "En venta", - "filterChip": "Comparto", - "printCatalog": "Imprimir lo que comparto", - "catalogTitle": "Lo que comparto", - "catalogSaved": "Catálogo guardado", - "cancelled": "Cancelado" - }, - "printLabels": { - "action": "Imprimir etiquetas", - "title": "Imprimir etiquetas", - "selectHint": "Elige las semillas para imprimir sus etiquetas", - "selectAll": "Seleccionar todo", - "selected": "{n} seleccionadas", - "none": "Selecciona semillas primero", - "format": "Tamaño de etiqueta", - "formatStickers": "Pegatinas pequeñas", - "formatStickersHint": "Muchas etiquetas pequeñas por hoja — para pegar en sobres", - "formatCards": "Tarjetas grandes", - "formatCardsHint": "Menos etiquetas, más grandes — para botes y cajas", - "count": "{n} etiquetas", - "save": "Guardar etiquetas", - "saved": "Etiquetas guardadas", - "cancelled": "Cancelado" - }, - "cropCalendar": { - "add": "Calendario de cultivo", - "title": "Calendario de cultivo", - "sow": "Siembra", - "transplant": "Trasplante", - "flowering": "Floración", - "fruiting": "Fructificación", - "seedHarvest": "Cosecha de semilla", - "editorHint": "Anota tú los meses típicos de esta variedad en tu zona — son tus propias notas.", - "unset": "—" - }, - "needsReproduction": { - "label": "Reproducir esta temporada", - "hint": "Cultívala antes de que se acabe la semilla", - "badge": "Por reproducir" - }, - "preservation": { - "add": "Cómo se conserva", - "title": "Cómo se conserva", - "none": "Sin especificar", - "jarWithDesiccant": "Bote con desecante", - "glassJar": "Bote de cristal", - "paperEnvelope": "Sobre de papel", - "paperBag": "Bolsa de papel", - "plasticBag": "Bolsa de plástico" - }, - "conditionCheck": { - "advanced": "Conservación y detalles del banco", - "title": "Revisiones de conservación", - "add": "Añadir revisión", - "containers": "Botes / recipientes", - "desiccant": "Desecante", - "none": "Aún no hay revisiones.", - "summary": "{count} bote(s) · {state}" - }, - "desiccant": { - "none": "No tiene", - "add": "Se pone", - "replace": "Se cambia", - "dry": "Azul — seco", - "fresh": "Recién puesto" - }, - "unit": { - "aFew": "unas pocas", - "some": "algunas", - "plenty": "muchas", - "pinch": "una pizca", - "handful": { - "singular": "puñado", - "plural": "puñados" - }, - "teaspoon": { - "singular": "cucharadita", - "plural": "cucharaditas" - }, - "spoon": { - "singular": "cuchara", - "plural": "cucharas" - }, - "cup": { - "singular": "taza", - "plural": "tazas" - }, - "jar": { - "singular": "bote", - "plural": "botes" - }, - "sack": { - "singular": "saco", - "plural": "sacos" - }, - "packet": { - "singular": "sobre", - "plural": "sobres" - }, - "cob": { - "singular": "mazorca", - "plural": "mazorcas" - }, - "pod": { - "singular": "vaina", - "plural": "vainas" - }, - "ear": { - "singular": "espiga", - "plural": "espigas" - }, - "head": { - "singular": "cabezuela", - "plural": "cabezuelas" - }, - "fruit": { - "singular": "fruto", - "plural": "frutos" - }, - "bulb": { - "singular": "bulbo", - "plural": "bulbos" - }, - "tuber": { - "singular": "tubérculo", - "plural": "tubérculos" - }, - "seedHead": { - "singular": "cabeza de semillas", - "plural": "cabezas de semillas" - }, - "bunch": { - "singular": "manojo", - "plural": "manojos" - }, - "plant": { - "singular": "planta", - "plural": "plantas" - }, - "pot": { - "singular": "maceta", - "plural": "macetas" - }, - "tray": { - "singular": "bandeja", - "plural": "bandejas" - }, - "seedling": { - "singular": "plántula", - "plural": "plántulas" - }, - "tree": { - "singular": "árbol", - "plural": "árboles" - }, - "cutting": { - "singular": "esqueje", - "plural": "esquejes" - }, - "grams": { - "singular": "gramo", - "plural": "gramos" - }, - "count": { - "singular": "semilla", - "plural": "semillas" - } - }, - "market": { - "title": "Semillas cerca de ti", - "subtitle": "Lo que otras personas comparten cerca", - "notSetUp": "Aún no has configurado el compartir", - "notSetUpBody": "Activa el compartir para ver y dar semillas a gente cercana. Se mantiene aproximado — tu zona, nunca tu dirección exacta.", - "setUp": "Configurar el compartir", - "cantReach": "No se puede conectar con los servidores ahora mismo", - "cantReachBody": "Inténtalo de nuevo, o revisa los servidores en la configuración avanzada.", - "retry": "Reintentar", - "setArea": "Indica tu zona", - "setAreaBody": "Dile al mercado tu zona aproximada para ver semillas cerca.", - "searching": "Buscando por tu zona…", - "empty": "Aún no hay semillas compartidas cerca de ti", - "searchHint": "Buscar entre estas semillas", - "noMatches": "Ninguna semilla compartida coincide con tu búsqueda", - "near": "Cerca de ti", - "contact": "Mensaje", - "mine": "Tú", - "configTitle": "Configuración de compartir", - "setupIntro": "Compartir con gente cercana es opcional. Solo indica tu zona aproximada — ya estás conectado a servidores comunitarios compartidos para que otras personas encuentren lo que ofreces, sin ninguna empresa en medio.", - "areaLabel": "Tu zona", - "areaHelp": "Se mantiene aproximado a propósito — tu zona, nunca un punto exacto.", - "areaSet": "Tu zona está puesta — aproximada, nunca tu punto exacto", - "areaNotSet": "Zona sin definir — usa tu ubicación, o añade un código en Avanzado", - "advanced": "Avanzado", - "areaCodeLabel": "Código de zona", - "areaCodeHint": "Un código corto como sp3e9 — no un nombre de lugar", - "serversLabel": "Servidores de la comunidad", - "serversHelp": "Una dirección por línea (wss://…). Déjalo así si no sabes.", - "serversAdvanced": "Añadir otro servidor", - "save": "Guardar", - "saved": "Guardado", - "wanted": "Busco", - "shareMine": "Compartir mis semillas", - "sharedCount": "Compartidas {n} semillas", - "nothingToShare": "Marca antes algunas semillas para regalar, cambiar o vender", - "useLocation": "Usar mi ubicación aproximada", - "locationFailed": "No se pudo obtener tu ubicación — comprueba que la ubicación está activada y el permiso concedido", - "queued": "Guardado — las compartiremos cuando tengas conexión", - "shareFailed": "No se pudo conectar con los servidores — tus semillas no se compartieron. Inténtalo de nuevo en un momento.", - "rangeLabel": "Hasta dónde buscar", - "rangeNear": "Muy cerca", - "rangeArea": "Por mi zona", - "rangeRegion": "Mi región", - "sharedBy": "Compartido por", - "noProfile": "Esta persona aún no ha compartido su perfil", - "copyId": "Copiar código", - "idCopied": "Código copiado", - "photo": "Foto" - }, - "profile": { - "title": "Tu perfil", - "name": "Nombre", - "nameHint": "Cómo te ven los demás", - "about": "Sobre ti", - "aboutHint": "Una línea — qué cultivas, dónde", - "g1": "Dirección Ğ1 (opcional)", - "g1Hint": "Para que te paguen en Ğ1 — aparte de tu clave", - "yourId": "Tu identidad", - "idHelp": "Compártela para que te reconozcan", - "copy": "Copiar", - "copied": "Copiado", - "save": "Guardar", - "saved": "Perfil guardado", - "identities": "Tus identidades", - "identitiesHelp": "Ten identidades separadas — cada una con sus mensajes y contactos. Todas salen de tu única copia de seguridad, así que cambiar no añade nada que recordar.", - "identityLabel": "Identidad {n}", - "current": "En uso", - "newIdentity": "Nueva identidad", - "switchTitle": "¿Cambiar de identidad?", - "switchBody": "Los mensajes y contactos se guardan por separado para cada identidad. Puedes volver cuando quieras.", - "switchAction": "Cambiar" - }, - "chatList": { - "title": "Mensajes", - "empty": "Aún no hay conversaciones. Escribe a alguien desde el mercado." - }, - "chat": { - "title": "Chat", - "hint": "Escribe un mensaje…", - "send": "Enviar", - "empty": "Aún no hay mensajes — saluda", - "offline": "Configura el compartir para enviar mensajes", - "payG1": "Pagar en Ğ1", - "g1Copied": "Dirección Ğ1 copiada — pégala en tu cartera", - "today": "Hoy", - "yesterday": "Ayer", - "sendError": "No se pudo enviar — revisa tu conexión", - "noLinks": "No se permiten enlaces en los mensajes" - }, - "trust": { - "none": "Nadie los avala aún", - "count": "Avalada por {n}", - "vouch": "Conozco a esta persona", - "vouched": "Avalas a esta persona", - "circle": "En tu círculo" - }, - "yourPeople": { - "title": "Tu gente", - "help": "Personas que conoces y avalas, y personas que te avalan.", - "youVouchFor": "Tú avalas a", - "vouchesForYou": "Te avalan", - "youVouchForEmpty": "Aún no avalas a nadie. Cuando conozcas a alguien, abre vuestro chat y toca \"Conozco a esta persona\".", - "vouchesForYouEmpty": "Nadie te avala todavía", - "revoke": "Dejar de avalar", - "revokeConfirm": "¿Dejar de avalar a esta persona?", - "offline": "Sin conexión — inténtalo cuando estés en línea" - }, - "ratings": { - "rate": "Valorar a esta persona", - "edit": "Editar tu valoración", - "commentHint": "¿Qué tal fue? (opcional)", - "fromYourCircle": "{n} de gente que conoces", - "retract": "Quitar tu valoración", - "saved": "Valoración guardada" - }, - "notifications": { - "newMessageFrom": "Nuevo mensaje de {name}" - }, - "plantare": { - "title": "Plantares", - "help": "Un Plantare es el compromiso de reproducir una semilla y devolver una parte — así una variedad sigue viajando de mano en mano. No es una venta.", - "add": "Añadir compromiso", - "empty": "Aún no hay compromisos. Cuando compartas o recibas semilla con el compromiso de reproducirla y devolver algo, anótalo aquí.", - "iReturn": "La reproduzco y devuelvo yo", - "owedToMe": "Me la devuelven a mí", - "direction": "Quién reproduce y devuelve", - "counterparty": "¿Con quién?", - "counterpartyHint": "Una persona o un colectivo (opcional)", - "owed": "¿Qué se devuelve?", - "owedHint": "p. ej. un puñado la próxima temporada (opcional)", - "note": "Nota (opcional)", - "save": "Guardar", - "markReturned": "Marcar devuelto", - "markForgiven": "Dar por saldado", - "reopen": "Reabrir", - "delete": "Quitar", - "statusReturned": "Devuelto", - "statusForgiven": "Saldado", - "openSection": "Pendientes", - "settledSection": "Hechos", - "removeConfirm": "¿Quitar este compromiso?", - "returnBy": "Devolver antes del {date}", - "overdue": "vencido", - "dueByLabel": "Devolver antes de (opcional)", - "dueByHint": "Un recordatorio suave, nunca obligatorio", - "pickDate": "Elegir fecha", - "clearDate": "Quitar fecha", - "sectionTitle": "Compromisos" - }, - "handover": { - "title": "Di o recibí semillas", - "help": "Un regalo, un trueque o una venta: apúntalo, con promesa de devolver semilla o sin ella.", - "iGave": "Di semillas", - "iReceived": "Recibí semillas", - "whichLot": "¿De qué lote?", - "howMuch": "¿Cuánto?", - "allOfIt": "Todo", - "partOfIt": "Una parte", - "paymentChip": "Hubo dinero por medio", - "promiseGave": "Me devolverán semilla", - "promiseReceived": "Devolveré semilla" - }, - "sale": { - "title": "Ventas", - "help": "Registra semilla vendida o comprada — dinero, Ğ1 o cualquier moneda. Un modelo aparte del regalo y del Plantare. Nunca se cobra comisión por las semillas.", - "add": "Registrar venta", - "empty": "Aún no hay ventas. Anota aquí lo que vendes o compras.", - "iSold": "Vendí", - "iBought": "Compré", - "direction": "¿Vendes o compras?", - "counterparty": "¿Con quién?", - "counterpartyHint": "Una persona o un colectivo (opcional)", - "amount": "Importe", - "currency": "Moneda", - "currencyHint": "€, Ğ1, horas… (opcional)", - "hours": "horas", - "note": "Nota (opcional)", - "save": "Guardar", - "delete": "Quitar", - "removeConfirm": "¿Quitar esta venta?" - }, - "legal": { - "title": "Privacidad y normas", - "subtitle": "Tu privacidad, las normas del mercado y la legalidad de las semillas", - "privacyTitle": "Tu privacidad", - "privacyBody": "Tane funciona sin cuenta, y todo lo que registras se queda en tu dispositivo, cifrado. No hay publicidad, ni rastreadores, ni servidor de Tane.\n\nNo se comparte nada salvo que tú quieras: cuando publicas una oferta o tu perfil, o envías un mensaje, viaja por servidores comunitarios. Las ofertas solo llevan una zona aproximada — nunca tu dirección.\n\nLo que publicas es público, y pueden quedar copias incluso después de retirarlo — así que comparte con cuidado.", - "rulesTitle": "Las reglas del juego", - "rulesBody": "Tane es una herramienta, no una tienda: los intercambios se acuerdan directamente entre personas y nadie se lleva comisión. Eso también significa que tú respondes de lo que ofreces y envías.\n\nEn el mercado: sé honesto con tus semillas, ofrece solo lo que puedas compartir, trata bien a la gente y no hagas spam. Puedes bloquear a cualquiera y denunciar ofertas o personas que incumplan las normas — las denuncias se atienden en los servidores comunitarios.", - "seedsTitle": "Sobre compartir semillas", - "seedsBody": "Regalar e intercambiar semillas entre aficionados está ampliamente reconocido en la mayoría de países. Vender puede ser distinto: en muchos sitios, vender semilla de variedades no registradas oficialmente está restringido — consulta tu normativa antes de pedir un precio.\n\nEnviar semillas a otro país también suele estar restringido, y las variedades protegidas comercialmente no pueden propagarse sin permiso. Ante la duda, mejor local y mejor regalo.", - "readFull": "Leer los documentos completos en la web" - }, - "marketGate": { - "title": "Antes de entrar al mercado", - "intro": "El mercado es un espacio compartido entre vecinas y vecinos. Al continuar, aceptas unas pocas normas sencillas:", - "ruleHonest": "Sé honesto con las semillas que ofreces", - "ruleLegal": "Comparte solo lo que puedas compartir donde vives", - "ruleRespect": "Trata bien a la gente — sin spam ni abusos", - "publicNote": "Lo que publiques aquí es público, y pueden quedar copias aunque lo retires después.", - "viewLegal": "Privacidad y normas", - "accept": "Acepto", - "decline": "Ahora no" - }, - "report": { - "offer": "Denunciar esta oferta", - "person": "Denunciar a esta persona", - "title": "Denunciar", - "prompt": "¿Qué ocurre?", - "reasonSpam": "Spam o un engaño", - "reasonAbuse": "Abusivo o irrespetuoso", - "reasonIllegal": "Semillas que no deberían ofrecerse", - "reasonOther": "Otra cosa", - "detailsHint": "Añade detalles (opcional)", - "send": "Enviar denuncia", - "sentHidden": "Denuncia enviada — ya no verás esto", - "failed": "No se pudo enviar la denuncia — revisa tu conexión", - "alsoBlock": "Bloquear también a esta persona" - }, - "block": { - "action": "Bloquear a esta persona", - "confirmTitle": "¿Bloquear a esta persona?", - "confirmBody": "Dejarás de ver sus ofertas y mensajes. Puedes desbloquearla más adelante en Personas bloqueadas, en la configuración de compartir.", - "confirm": "Bloquear", - "blockedToast": "Persona bloqueada — sus ofertas y mensajes quedan ocultos", - "manageTitle": "Personas bloqueadas", - "manageEmpty": "No has bloqueado a nadie", - "unblock": "Desbloquear" + "avatar": { + "title": "Tu foto o avatar", + "fromPhoto": "Hacer o elegir una foto", + "illustration": "O elige un dibujo", + "remove": "Quitar" + }, + "favorites": { + "title": "Favoritos", + "empty": "Aún no tienes favoritos. Guarda ofertas que te gusten del mercado.", + "save": "Guardar en favoritos", + "remove": "Quitar de favoritos", + "unavailable": "Ya no está disponible" + }, + "seedSaving": { + "title": "Conservar su semilla", + "subtitle": "Lo que hace falta para mantener la variedad fiel", + "lifeCycle": "Ciclo", + "cycleAnnual": "Anual", + "cycleBiennial": "Bienal — da semilla el 2.º año", + "cyclePerennial": "Perenne", + "pollination": "Polinización", + "pollSelf": "Se autopoliniza", + "pollCross": "Se cruza con otras", + "pollMixed": "Se autopoliniza, pero se cruza a veces", + "byInsect": "por insectos", + "byWind": "por el viento", + "isolation": "Sepárala", + "isolationRange": "{min}–{max} m de otras variedades", + "isolationSingle": "{min} m de otras variedades", + "plants": "Guarda de varias plantas", + "plantsValue": "De al menos {n} plantas", + "processing": "Cómo limpiarla", + "procDry": "Semilla seca (trillar)", + "procWet": "Semilla húmeda (fermentar y lavar)", + "difficulty": "Dificultad", + "diffEasy": "Fácil", + "diffMedium": "Media", + "diffHard": "Difícil", + "advisory": "Orientativo — adáptalo a tu clima y variedad.", + "sourcePrefix": "Fuente" + }, + "calendar": { + "title": "Este mes", + "filterChip": "Este mes", + "selfNote": "Lo que has anotado en tus variedades.", + "nothing": "Nada anotado para {month}." + }, + "app": { + "title": "Tane" + }, + "bootstrap": { + "failed": "Tane no pudo arrancar", + "retry": "Reintentar" + }, + "common": { + "save": "Guardar", + "cancel": "Cancelar", + "delete": "Eliminar", + "edit": "Editar", + "type": "Tipo", + "comingSoon": "Pronto", + "offline": "Sin conexión — el compartir está en pausa" + }, + "home": { + "tagline": "Comparte y cultiva semillas locales", + "openMarket": "Mercado", + "openMarketSubtitle": "Descubre y comparte semillas cerca", + "yourInventory": "Tu inventario", + "yourInventorySubtitle": "Gestiona tus semillas" + }, + "photo": { + "camera": "Hacer una foto", + "gallery": "Elegir de la galería", + "setAsCover": "Poner de portada", + "isCover": "Foto de portada", + "deleteConfirm": "¿Borrar esta foto?" + }, + "menu": { + "tagline": "tu banco de semillas", + "inventory": "Inventario", + "market": "Mercado", + "profile": "Tu perfil", + "chat": "Chat", + "wishlist": "Favoritos", + "following": "Siguiendo", + "plantares": "Plantares", + "sales": "Ventas", + "calendar": "Calendario", + "settings": "Ajustes" + }, + "settings": { + "language": "Idioma", + "systemLanguage": "Idioma del sistema", + "langEs": "Español", + "langEn": "English", + "langPt": "Português", + "langFr": "Français", + "langDe": "Deutsch", + "langJa": "日本語", + "about": "Acerca de", + "aboutText": "Inventario local y cifrado para semillas tradicionales. AGPL-3.0.", + "aboutOpen": "Acerca de Tane" + }, + "backup": { + "title": "Copia de seguridad", + "autoBackupTitle": "Copias automáticas", + "autoBackupLast": "Última copia guardada el {date}", + "autoBackupNone": "Se guarda una copia automáticamente en segundo plano", + "exportJson": "Guardar una copia de seguridad", + "exportJsonSubtitle": "Una copia completa para guardar a salvo, restaurar luego o pasar a otro dispositivo", + "importJson": "Restaurar una copia", + "importJsonSubtitle": "Recupera una copia guardada — no se duplica nada", + "exportCsv": "Exportar a una hoja de cálculo", + "exportCsvSubtitle": "Una lista sencilla para Excel o LibreOffice — sin fotos", + "importCsv": "Importar una lista", + "importCsvSubtitle": "Añade entradas desde una hoja de cálculo", + "importConfirmTitle": "¿Restaurar una copia?", + "importConfirmBody": "Las entradas se fusionan con tu inventario; si una entrada existe en ambos lados, gana la versión más reciente. No se duplica nada.", + "importCsvConfirmTitle": "¿Importar una lista?", + "importCsvConfirmBody": "Cada fila se añade como una entrada nueva. No fusiona ni reemplaza, así que importar el mismo fichero dos veces lo añade dos veces.", + "importAction": "Importar", + "exportSaved": "Copia guardada", + "cancelled": "Cancelado", + "importDone": "Importado: {added} nuevas, {updated} actualizadas", + "importCsvDone": "Añadidas {count} entradas", + "importFailed": "Este fichero no se pudo leer como una copia de Tane", + "failed": "Algo ha salido mal", + "recoveryTitle": "Tu código de recuperación", + "recoverySubtitle": "Imprímelo y guárdalo bien: abre tus copias en otro dispositivo", + "recoveryIntro": "Este código abre tus copias guardadas y recupera tu banco en cualquier dispositivo. Guarda dos copias en papel en sitios seguros, como tu mejor semilla. Quien lo tenga puede leer tus copias, así que no lo compartas con nadie.", + "recoveryCopy": "Copiar", + "recoverySave": "Guardar la hoja", + "recoverySheetTitle": "Tane — tu hoja de recuperación", + "recoveryPromptTitle": "Escribe tu código de recuperación", + "recoveryPromptBody": "Esta copia se guardó con otro código. Escribe el código de tu hoja de recuperación para abrirla.", + "recoveryWrongCode": "Ese código no abre esta copia" + }, + "about": { + "title": "Acerca de", + "kanji": "種", + "tagline": "Una app local-first y descentralizada para gestionar y compartir semillas y plantones tradicionales.", + "intro": "Tane (種, «semilla» en japonés) ayuda a personas y colectivos a llevar un inventario amable de su banco de semillas, decidir qué ofrecen y compartirlo localmente — sin un intermediario central que pueda controlarlo, censurarlo o ser multado por ello. Su nombre viene de tanemaki (種まき), «sembrar / esparcir semillas». El objetivo es a la vez práctico y político: apoyar las variedades tradicionales de semillas y plantar cara al monopolio semillero.", + "heritage": "El nombre honra las viejas tradiciones japonesas de ayuda mutua en torno al arroz — yui (trabajo comunitario compartido) y tanomoshi (fondos de reciprocidad) — que inspiraron el papel Plantare, la «moneda comunitaria para el intercambio de semillas» (BAH-Semillero, 2009, CC-BY-SA). Tane es el Plantare digital.", + "version": "Versión", + "license": "Licencia", + "licenseValue": "AGPL-3.0", + "website": "Sitio web", + "openSourceLicenses": "Licencias de código abierto", + "openSourceLicensesSubtitle": "Bibliotecas de terceros y sus licencias", + "copyright": "© {years} Asociación Comunes, bajo AGPLv3" + }, + "intro": { + "skip": "Saltar", + "next": "Siguiente", + "start": "Empezar", + "menuEntry": "Cómo funciona Tane", + "slides": { + "welcome": { + "title": "La semilla que te trajo hasta aquí", + "body": "Cada semilla tradicional es una carta escrita por miles de generaciones, que pasó de mano en mano. Somos lo que somos gracias a ese intercambio." + }, + "inventory": { + "title": "Tu banco de semillas, en el bolsillo", + "body": "Apunta qué tienes, de qué año, cuánto y de dónde vino, con el nombre que tú usas. Una foto y un nombre bastan para empezar." + }, + "privacy": { + "title": "Tuyo y solo tuyo", + "body": "Sin cuenta, sin internet, sin rastreadores. Tus datos viven cifrados en tu dispositivo y solo se comparte lo que tú decidas." + }, + "share": { + "title": "Compartir, como siempre se hizo", + "body": "Ofrece lo que te sobra —regalo, trueque o venta— y que alguien cerca lo encuentre. Solo se muestra la distancia aproximada, nunca tu dirección; el trato lo cierras en persona." + }, + "plantare": { + "title": "Sembrar es multiplicar", + "body": "Con el Plantare, quien recibe promete devolver semilla más adelante. Y como para devolverla hay que cultivarla, cada préstamo multiplica el común." + } } + }, + "inventory": { + "title": "Inventario", + "searchHint": "Buscar semillas", + "empty": "Aún no hay semillas. Toca + para añadir la primera.", + "noMatches": "Ninguna semilla coincide con los filtros.", + "clearFilters": "Quitar filtros", + "uncategorized": "Sin categoría", + "needsReproductionFilter": "Por reproducir", + "loadError": "No se pudo abrir tu banco de semillas. Quizá estaba ocupado: inténtalo de nuevo.", + "retry": "Reintentar" + }, + "draft": { + "capture": "Capturar fotos", + "captured": "{n} capturadas por catalogar", + "triageTitle": "Por catalogar", + "triageCount": "{n} por catalogar", + "untitled": "Sin nombre", + "nameField": "Nombra esta semilla", + "nameHint": "¿Qué es?", + "suggestFromPhoto": "Sugerir nombre de la foto", + "discard": "Descartar" + }, + "quickAdd": { + "title": "Añadir una semilla", + "labelField": "Nombre", + "labelRequired": "Ponle un nombre", + "addPhoto": "Añadir foto", + "quantity": "¿Cuánta?", + "more": "Añadir más…", + "save": "Guardar", + "saveAndAddAnother": "Guardar y añadir otra", + "addedCount": "{n} añadidas", + "cancel": "Cancelar" + }, + "detail": { + "notFound": "Esta semilla ya no está aquí.", + "lots": "Lotes", + "noLots": "Aún no hay lotes.", + "names": "También conocida como", + "addName": "Añadir nombre", + "links": "Enlaces", + "addLink": "Añadir enlace", + "linkUrl": "URL", + "linkTitle": "Título (opcional)", + "reference": "Saber más", + "refGbif": "GBIF", + "refWikipedia": "Wikipedia", + "refWikispecies": "Wikispecies", + "notes": "Notas", + "addLot": "Añadir lote", + "editLot": "Editar lote", + "deleteConfirm": "¿Eliminar esta semilla?", + "year": "Año {year}", + "noYear": "Año desconocido" + }, + "germination": { + "title": "Germinación", + "add": "Añadir prueba", + "sampleSize": "Muestra", + "germinated": "Germinadas", + "none": "Aún no hay pruebas de germinación.", + "result": "{percent}%" + }, + "viability": { + "expiringSoon": "Úsala o multiplícala esta temporada", + "expiringSoonYears": "Úsala o multiplícala esta temporada · dura ~{years} años", + "expired": "Supera su viabilidad típica — a multiplicar", + "expiredYears": "Supera su viabilidad típica (~{years} años) — a multiplicar" + }, + "editVariety": { + "title": "Editar semilla", + "name": "Nombre", + "category": "Categoría", + "notes": "Notas", + "species": "Especie (del catálogo)", + "speciesHint": "Buscar una especie…", + "speciesSuggested": "Sugerida por el nombre", + "organic": "Ecológica", + "organicHint": "Cultivada de forma ecológica (eco)" + }, + "addLot": { + "title": "Añadir lote", + "year": "Fecha de cosecha", + "quantity": "¿Cuánta?", + "amount": "Cantidad" + }, + "harvest": { + "pickTitle": "Seleccionar mes / año", + "anyMonth": "Cualquier mes", + "noDate": "Indicar fecha de cosecha", + "monthNames": [ + "enero", + "febrero", + "marzo", + "abril", + "mayo", + "junio", + "julio", + "agosto", + "septiembre", + "octubre", + "noviembre", + "diciembre" + ] + }, + "lotType": { + "seed": "Semillas", + "plant": "Planta", + "seedling": "Plantón", + "tree": "Árbol / arbusto", + "bulb": "Bulbo / tubérculo", + "cutting": "Esqueje" + }, + "presentation": { + "title": "Envase", + "none": "Sin especificar", + "pot": "Maceta", + "tray": "Bandeja", + "plug": "Alvéolo", + "bareRoot": "Raíz desnuda", + "rootBall": "Cepellón" + }, + "provenance": { + "section": "De dónde viene", + "seedsFrom": "Semillas de", + "seedsFromHint": "Quién las cultivó o las dio", + "place": "Lugar", + "placeHint": "De dónde vienen (con provincia)", + "addSeedsFrom": "Semillas de", + "addPlace": "Lugar" + }, + "abundance": { + "add": "Cuánta tengo", + "title": "Cuánta tengo", + "none": "Sin indicar", + "plentyToShare": "De sobra para compartir", + "enoughToShare": "Bastante, para compartir con moderación", + "enoughForMe": "Suficiente para mí", + "runningLow": "Queda poca" + }, + "share": { + "add": "¿La compartes?", + "title": "¿La compartes?", + "nudge": "Tienes de sobra: podrías compartir un poco.", + "price": "Precio", + "priceHint": "Déjalo vacío para acordarlo luego", + "private": "Solo para mí", + "gift": "Para regalar", + "exchange": "Para intercambiar", + "sell": "En venta", + "filterChip": "Comparto", + "printCatalog": "Imprimir lo que comparto", + "catalogTitle": "Lo que comparto", + "catalogSaved": "Catálogo guardado", + "cancelled": "Cancelado" + }, + "printLabels": { + "action": "Imprimir etiquetas", + "title": "Imprimir etiquetas", + "selectHint": "Elige las semillas para imprimir sus etiquetas", + "selectAll": "Seleccionar todo", + "selected": "{n} seleccionadas", + "none": "Selecciona semillas primero", + "format": "Tamaño de etiqueta", + "formatStickers": "Pegatinas pequeñas", + "formatStickersHint": "Muchas etiquetas pequeñas por hoja — para pegar en sobres", + "formatCards": "Tarjetas grandes", + "formatCardsHint": "Menos etiquetas, más grandes — para botes y cajas", + "count": "{n} etiquetas", + "save": "Guardar etiquetas", + "saved": "Etiquetas guardadas", + "cancelled": "Cancelado" + }, + "cropCalendar": { + "add": "Calendario de cultivo", + "title": "Calendario de cultivo", + "sow": "Siembra", + "transplant": "Trasplante", + "flowering": "Floración", + "fruiting": "Fructificación", + "seedHarvest": "Cosecha de semilla", + "editorHint": "Anota tú los meses típicos de esta variedad en tu zona — son tus propias notas.", + "unset": "—" + }, + "needsReproduction": { + "label": "Reproducir esta temporada", + "hint": "Cultívala antes de que se acabe la semilla", + "badge": "Por reproducir" + }, + "preservation": { + "add": "Cómo se conserva", + "title": "Cómo se conserva", + "none": "Sin especificar", + "jarWithDesiccant": "Bote con desecante", + "glassJar": "Bote de cristal", + "paperEnvelope": "Sobre de papel", + "paperBag": "Bolsa de papel", + "plasticBag": "Bolsa de plástico" + }, + "conditionCheck": { + "advanced": "Conservación y detalles del banco", + "title": "Revisiones de conservación", + "add": "Añadir revisión", + "containers": "Botes / recipientes", + "desiccant": "Desecante", + "none": "Aún no hay revisiones.", + "summary": "{count} bote(s) · {state}" + }, + "desiccant": { + "none": "No tiene", + "add": "Se pone", + "replace": "Se cambia", + "dry": "Azul — seco", + "fresh": "Recién puesto" + }, + "unit": { + "aFew": "unas pocas", + "some": "algunas", + "plenty": "muchas", + "pinch": "una pizca", + "handful": { + "singular": "puñado", + "plural": "puñados" + }, + "teaspoon": { + "singular": "cucharadita", + "plural": "cucharaditas" + }, + "spoon": { + "singular": "cuchara", + "plural": "cucharas" + }, + "cup": { + "singular": "taza", + "plural": "tazas" + }, + "jar": { + "singular": "bote", + "plural": "botes" + }, + "sack": { + "singular": "saco", + "plural": "sacos" + }, + "packet": { + "singular": "sobre", + "plural": "sobres" + }, + "cob": { + "singular": "mazorca", + "plural": "mazorcas" + }, + "pod": { + "singular": "vaina", + "plural": "vainas" + }, + "ear": { + "singular": "espiga", + "plural": "espigas" + }, + "head": { + "singular": "cabezuela", + "plural": "cabezuelas" + }, + "fruit": { + "singular": "fruto", + "plural": "frutos" + }, + "bulb": { + "singular": "bulbo", + "plural": "bulbos" + }, + "tuber": { + "singular": "tubérculo", + "plural": "tubérculos" + }, + "seedHead": { + "singular": "cabeza de semillas", + "plural": "cabezas de semillas" + }, + "bunch": { + "singular": "manojo", + "plural": "manojos" + }, + "plant": { + "singular": "planta", + "plural": "plantas" + }, + "pot": { + "singular": "maceta", + "plural": "macetas" + }, + "tray": { + "singular": "bandeja", + "plural": "bandejas" + }, + "seedling": { + "singular": "plántula", + "plural": "plántulas" + }, + "tree": { + "singular": "árbol", + "plural": "árboles" + }, + "cutting": { + "singular": "esqueje", + "plural": "esquejes" + }, + "grams": { + "singular": "gramo", + "plural": "gramos" + }, + "count": { + "singular": "semilla", + "plural": "semillas" + } + }, + "market": { + "title": "Semillas cerca de ti", + "subtitle": "Lo que otras personas comparten cerca", + "notSetUp": "Aún no has configurado el compartir", + "notSetUpBody": "Activa el compartir para ver y dar semillas a gente cercana. Se mantiene aproximado — tu zona, nunca tu dirección exacta.", + "setUp": "Configurar el compartir", + "cantReach": "No se puede conectar con los servidores ahora mismo", + "cantReachBody": "Inténtalo de nuevo, o revisa los servidores en la configuración avanzada.", + "retry": "Reintentar", + "setArea": "Indica tu zona", + "setAreaBody": "Dile al mercado tu zona aproximada para ver semillas cerca.", + "searching": "Buscando por tu zona…", + "empty": "Aún no hay semillas compartidas cerca de ti", + "searchHint": "Buscar entre estas semillas", + "noMatches": "Ninguna semilla compartida coincide con tu búsqueda", + "near": "Cerca de ti", + "contact": "Mensaje", + "mine": "Tú", + "configTitle": "Configuración de compartir", + "setupIntro": "Compartir con gente cercana es opcional. Solo indica tu zona aproximada — ya estás conectado a servidores comunitarios compartidos para que otras personas encuentren lo que ofreces, sin ninguna empresa en medio.", + "areaLabel": "Tu zona", + "areaHelp": "Se mantiene aproximado a propósito — tu zona, nunca un punto exacto.", + "areaSet": "Tu zona está puesta — aproximada, nunca tu punto exacto", + "areaNotSet": "Zona sin definir — usa tu ubicación, o añade un código en Avanzado", + "advanced": "Avanzado", + "areaCodeLabel": "Código de zona", + "areaCodeHint": "Un código corto como sp3e9 — no un nombre de lugar", + "serversLabel": "Servidores de la comunidad", + "serversHelp": "Una dirección por línea (wss://…). Déjalo así si no sabes.", + "serversAdvanced": "Añadir otro servidor", + "save": "Guardar", + "saved": "Guardado", + "wanted": "Busco", + "shareMine": "Compartir mis semillas", + "sharedCount": "Compartidas {n} semillas", + "nothingToShare": "Marca antes algunas semillas para regalar, cambiar o vender", + "useLocation": "Usar mi ubicación aproximada", + "locationFailed": "No se pudo obtener tu ubicación — comprueba que la ubicación está activada y el permiso concedido", + "queued": "Guardado — las compartiremos cuando tengas conexión", + "shareFailed": "No se pudo conectar con los servidores — tus semillas no se compartieron. Inténtalo de nuevo en un momento.", + "rangeLabel": "Hasta dónde buscar", + "rangeNear": "Muy cerca", + "rangeArea": "Por mi zona", + "rangeRegion": "Mi región", + "sharedBy": "Compartido por", + "noProfile": "Esta persona aún no ha compartido su perfil", + "copyId": "Copiar código", + "idCopied": "Código copiado", + "photo": "Foto" + }, + "profile": { + "title": "Tu perfil", + "name": "Nombre", + "nameHint": "Cómo te ven los demás", + "about": "Sobre ti", + "aboutHint": "Una línea — qué cultivas, dónde", + "g1": "Dirección Ğ1 (opcional)", + "g1Hint": "Para que te paguen en Ğ1 — aparte de tu clave", + "yourId": "Tu identidad", + "idHelp": "Compártela para que te reconozcan", + "copy": "Copiar", + "copied": "Copiado", + "save": "Guardar", + "saved": "Perfil guardado", + "identities": "Tus identidades", + "identitiesHelp": "Ten identidades separadas — cada una con sus mensajes y contactos. Todas salen de tu única copia de seguridad, así que cambiar no añade nada que recordar.", + "identityLabel": "Identidad {n}", + "current": "En uso", + "newIdentity": "Nueva identidad", + "switchTitle": "¿Cambiar de identidad?", + "switchBody": "Los mensajes y contactos se guardan por separado para cada identidad. Puedes volver cuando quieras.", + "switchAction": "Cambiar" + }, + "chatList": { + "title": "Mensajes", + "empty": "Aún no hay conversaciones. Escribe a alguien desde el mercado." + }, + "chat": { + "title": "Chat", + "hint": "Escribe un mensaje…", + "send": "Enviar", + "empty": "Aún no hay mensajes — saluda", + "offline": "Configura el compartir para enviar mensajes", + "payG1": "Pagar en Ğ1", + "g1Copied": "Dirección Ğ1 copiada — pégala en tu cartera", + "today": "Hoy", + "yesterday": "Ayer", + "sendError": "No se pudo enviar — revisa tu conexión", + "noLinks": "No se permiten enlaces en los mensajes" + }, + "trust": { + "none": "Nadie los avala aún", + "count": "Avalada por {n}", + "vouch": "Conozco a esta persona", + "vouched": "Avalas a esta persona", + "circle": "En tu círculo" + }, + "yourPeople": { + "title": "Tu gente", + "help": "Personas que conoces y avalas, y personas que te avalan.", + "youVouchFor": "Tú avalas a", + "vouchesForYou": "Te avalan", + "youVouchForEmpty": "Aún no avalas a nadie. Cuando conozcas a alguien, abre vuestro chat y toca \"Conozco a esta persona\".", + "vouchesForYouEmpty": "Nadie te avala todavía", + "revoke": "Dejar de avalar", + "revokeConfirm": "¿Dejar de avalar a esta persona?", + "offline": "Sin conexión — inténtalo cuando estés en línea" + }, + "ratings": { + "rate": "Valorar a esta persona", + "edit": "Editar tu valoración", + "commentHint": "¿Qué tal fue? (opcional)", + "fromYourCircle": "{n} de gente que conoces", + "retract": "Quitar tu valoración", + "saved": "Valoración guardada" + }, + "notifications": { + "newMessageFrom": "Nuevo mensaje de {name}" + }, + "plantare": { + "title": "Plantares", + "help": "Un Plantare es el compromiso de reproducir una semilla y devolver una parte — así una variedad sigue viajando de mano en mano. No es una venta.", + "add": "Añadir compromiso", + "empty": "Aún no hay compromisos. Cuando compartas o recibas semilla con el compromiso de reproducirla y devolver algo, anótalo aquí.", + "iReturn": "La reproduzco y devuelvo yo", + "owedToMe": "Me la devuelven a mí", + "direction": "Quién reproduce y devuelve", + "counterparty": "¿Con quién?", + "counterpartyHint": "Una persona o un colectivo (opcional)", + "owed": "¿Qué se devuelve?", + "owedHint": "p. ej. un puñado la próxima temporada (opcional)", + "note": "Nota (opcional)", + "save": "Guardar", + "markReturned": "Marcar devuelto", + "markForgiven": "Dar por saldado", + "reopen": "Reabrir", + "delete": "Quitar", + "statusReturned": "Devuelto", + "statusForgiven": "Saldado", + "openSection": "Pendientes", + "settledSection": "Hechos", + "removeConfirm": "¿Quitar este compromiso?", + "returnBy": "Devolver antes del {date}", + "overdue": "vencido", + "dueByLabel": "Devolver antes de (opcional)", + "dueByHint": "Un recordatorio suave, nunca obligatorio", + "pickDate": "Elegir fecha", + "clearDate": "Quitar fecha", + "sectionTitle": "Compromisos", + "propose": "Proponer un Plantaré firmado", + "proposeHelp": "Los dos guardáis la misma promesa, firmada por ambos: prueba de que esta semilla cambió de manos y se cultivará y devolverá.", + "proposeTo": "Con {name}", + "sent": "Propuesta enviada — esperando su firma", + "seedLabel": "¿Qué semilla?", + "seedHint": "La variedad a la que se refiere la promesa", + "returnKindLabel": "¿Qué se devuelve?", + "returnSimilar": "Una cantidad similar de semilla", + "returnSimilarNote": "polinización abierta · no transgénica · cultivada en ecológico", + "returnWork": "Unas horas de trabajo", + "returnOther": "Otra cosa", + "workHoursLabel": "¿Cuántas horas?", + "proposalsSection": "Esperando tu respuesta", + "incomingFrom": "{name} te propone un Plantaré", + "accept": "Aceptar y firmar", + "declineAction": "Rechazar", + "declineReasonHint": "Motivo (opcional)", + "acceptedToast": "Firmado — ahora lo guardáis los dos", + "declinedToast": "Rechazado", + "badgeAwaiting": "Falta firma", + "badgeSigned": "Firmado por ambos", + "badgeDeclined": "Rechazado", + "offline": "Estás sin conexión — se enviará al reconectar" + }, + "handover": { + "title": "Di o recibí semillas", + "help": "Un regalo, un trueque o una venta: apúntalo, con promesa de devolver semilla o sin ella.", + "iGave": "Di semillas", + "iReceived": "Recibí semillas", + "whichLot": "¿De qué lote?", + "howMuch": "¿Cuánto?", + "allOfIt": "Todo", + "partOfIt": "Una parte", + "paymentChip": "Hubo dinero por medio", + "promiseGave": "Me devolverán semilla", + "promiseReceived": "Devolveré semilla" + }, + "sale": { + "title": "Ventas", + "help": "Registra semilla vendida o comprada — dinero, Ğ1 o cualquier moneda. Un modelo aparte del regalo y del Plantare. Nunca se cobra comisión por las semillas.", + "add": "Registrar venta", + "empty": "Aún no hay ventas. Anota aquí lo que vendes o compras.", + "iSold": "Vendí", + "iBought": "Compré", + "direction": "¿Vendes o compras?", + "counterparty": "¿Con quién?", + "counterpartyHint": "Una persona o un colectivo (opcional)", + "amount": "Importe", + "currency": "Moneda", + "currencyHint": "€, Ğ1, horas… (opcional)", + "hours": "horas", + "note": "Nota (opcional)", + "save": "Guardar", + "delete": "Quitar", + "removeConfirm": "¿Quitar esta venta?" + }, + "legal": { + "title": "Privacidad y normas", + "subtitle": "Tu privacidad, las normas del mercado y la legalidad de las semillas", + "privacyTitle": "Tu privacidad", + "privacyBody": "Tane funciona sin cuenta, y todo lo que registras se queda en tu dispositivo, cifrado. No hay publicidad, ni rastreadores, ni servidor de Tane.\n\nNo se comparte nada salvo que tú quieras: cuando publicas una oferta o tu perfil, o envías un mensaje, viaja por servidores comunitarios. Las ofertas solo llevan una zona aproximada — nunca tu dirección.\n\nLo que publicas es público, y pueden quedar copias incluso después de retirarlo — así que comparte con cuidado.", + "rulesTitle": "Las reglas del juego", + "rulesBody": "Tane es una herramienta, no una tienda: los intercambios se acuerdan directamente entre personas y nadie se lleva comisión. Eso también significa que tú respondes de lo que ofreces y envías.\n\nEn el mercado: sé honesto con tus semillas, ofrece solo lo que puedas compartir, trata bien a la gente y no hagas spam. Puedes bloquear a cualquiera y denunciar ofertas o personas que incumplan las normas — las denuncias se atienden en los servidores comunitarios.", + "seedsTitle": "Sobre compartir semillas", + "seedsBody": "Regalar e intercambiar semillas entre aficionados está ampliamente reconocido en la mayoría de países. Vender puede ser distinto: en muchos sitios, vender semilla de variedades no registradas oficialmente está restringido — consulta tu normativa antes de pedir un precio.\n\nEnviar semillas a otro país también suele estar restringido, y las variedades protegidas comercialmente no pueden propagarse sin permiso. Ante la duda, mejor local y mejor regalo.", + "readFull": "Leer los documentos completos en la web" + }, + "marketGate": { + "title": "Antes de entrar al mercado", + "intro": "El mercado es un espacio compartido entre vecinas y vecinos. Al continuar, aceptas unas pocas normas sencillas:", + "ruleHonest": "Sé honesto con las semillas que ofreces", + "ruleLegal": "Comparte solo lo que puedas compartir donde vives", + "ruleRespect": "Trata bien a la gente — sin spam ni abusos", + "publicNote": "Lo que publiques aquí es público, y pueden quedar copias aunque lo retires después.", + "viewLegal": "Privacidad y normas", + "accept": "Acepto", + "decline": "Ahora no" + }, + "report": { + "offer": "Denunciar esta oferta", + "person": "Denunciar a esta persona", + "title": "Denunciar", + "prompt": "¿Qué ocurre?", + "reasonSpam": "Spam o un engaño", + "reasonAbuse": "Abusivo o irrespetuoso", + "reasonIllegal": "Semillas que no deberían ofrecerse", + "reasonOther": "Otra cosa", + "detailsHint": "Añade detalles (opcional)", + "send": "Enviar denuncia", + "sentHidden": "Denuncia enviada — ya no verás esto", + "failed": "No se pudo enviar la denuncia — revisa tu conexión", + "alsoBlock": "Bloquear también a esta persona" + }, + "block": { + "action": "Bloquear a esta persona", + "confirmTitle": "¿Bloquear a esta persona?", + "confirmBody": "Dejarás de ver sus ofertas y mensajes. Puedes desbloquearla más adelante en Personas bloqueadas, en la configuración de compartir.", + "confirm": "Bloquear", + "blockedToast": "Persona bloqueada — sus ofertas y mensajes quedan ocultos", + "manageTitle": "Personas bloqueadas", + "manageEmpty": "No has bloqueado a nadie", + "unblock": "Desbloquear" + } } diff --git a/apps/app_seeds/lib/i18n/strings.g.dart b/apps/app_seeds/lib/i18n/strings.g.dart index 89ac0b4..9bde9e5 100644 --- a/apps/app_seeds/lib/i18n/strings.g.dart +++ b/apps/app_seeds/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 7 -/// Strings: 3396 (485 per locale) +/// Strings: 3442 (491 per locale) /// -/// Built on 2026-07-14 at 09:00 UTC +/// Built on 2026-07-14 at 09:22 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/apps/app_seeds/lib/i18n/strings_en.g.dart b/apps/app_seeds/lib/i18n/strings_en.g.dart index 7cf9808..3f028ed 100644 --- a/apps/app_seeds/lib/i18n/strings_en.g.dart +++ b/apps/app_seeds/lib/i18n/strings_en.g.dart @@ -1809,6 +1809,75 @@ class Translations$plantare$en { /// en: 'Commitments' String get sectionTitle => 'Commitments'; + + /// en: 'Propose a signed Plantaré' + String get propose => 'Propose a signed Plantaré'; + + /// en: 'Both of you keep the same promise, signed by both — proof that this seed changed hands and will be grown out and returned.' + String get proposeHelp => 'Both of you keep the same promise, signed by both — proof that this seed changed hands and will be grown out and returned.'; + + /// en: 'With {name}' + String proposeTo({required Object name}) => 'With ${name}'; + + /// en: 'Proposal sent — waiting for them to sign' + String get sent => 'Proposal sent — waiting for them to sign'; + + /// en: 'Which seed?' + String get seedLabel => 'Which seed?'; + + /// en: 'The variety this promise is about' + String get seedHint => 'The variety this promise is about'; + + /// en: 'What comes back?' + String get returnKindLabel => 'What comes back?'; + + /// en: 'A similar amount of seed' + String get returnSimilar => 'A similar amount of seed'; + + /// en: 'open-pollinated · non-GMO · organically grown' + String get returnSimilarNote => 'open-pollinated · non-GMO · organically grown'; + + /// en: 'Some hours of work' + String get returnWork => 'Some hours of work'; + + /// en: 'Something else' + String get returnOther => 'Something else'; + + /// en: 'How many hours?' + String get workHoursLabel => 'How many hours?'; + + /// en: 'Waiting for your reply' + String get proposalsSection => 'Waiting for your reply'; + + /// en: '{name} proposes a Plantaré' + String incomingFrom({required Object name}) => '${name} proposes a Plantaré'; + + /// en: 'Accept & sign' + String get accept => 'Accept & sign'; + + /// en: 'Decline' + String get declineAction => 'Decline'; + + /// en: 'Reason (optional)' + String get declineReasonHint => 'Reason (optional)'; + + /// en: 'Signed — you both hold it now' + String get acceptedToast => 'Signed — you both hold it now'; + + /// en: 'Declined' + String get declinedToast => 'Declined'; + + /// en: 'Awaiting signature' + String get badgeAwaiting => 'Awaiting signature'; + + /// en: 'Signed by both' + String get badgeSigned => 'Signed by both'; + + /// en: 'Declined' + String get badgeDeclined => 'Declined'; + + /// en: 'You're offline — it'll be sent when you reconnect' + String get offline => 'You\'re offline — it\'ll be sent when you reconnect'; } // Path: handover @@ -3020,6 +3089,31 @@ extension on Translations { 'plantare.pickDate' => 'Pick a date', 'plantare.clearDate' => 'Clear date', 'plantare.sectionTitle' => 'Commitments', + 'plantare.propose' => 'Propose a signed Plantaré', + 'plantare.proposeHelp' => 'Both of you keep the same promise, signed by both — proof that this seed changed hands and will be grown out and returned.', + 'plantare.proposeTo' => ({required Object name}) => 'With ${name}', + 'plantare.sent' => 'Proposal sent — waiting for them to sign', + 'plantare.seedLabel' => 'Which seed?', + 'plantare.seedHint' => 'The variety this promise is about', + 'plantare.returnKindLabel' => 'What comes back?', + 'plantare.returnSimilar' => 'A similar amount of seed', + 'plantare.returnSimilarNote' => 'open-pollinated · non-GMO · organically grown', + 'plantare.returnWork' => 'Some hours of work', + 'plantare.returnOther' => 'Something else', + 'plantare.workHoursLabel' => 'How many hours?', + 'plantare.proposalsSection' => 'Waiting for your reply', + 'plantare.incomingFrom' => ({required Object name}) => '${name} proposes a Plantaré', + 'plantare.accept' => 'Accept & sign', + _ => null, + } ?? switch (path) { + 'plantare.declineAction' => 'Decline', + 'plantare.declineReasonHint' => 'Reason (optional)', + 'plantare.acceptedToast' => 'Signed — you both hold it now', + 'plantare.declinedToast' => 'Declined', + 'plantare.badgeAwaiting' => 'Awaiting signature', + 'plantare.badgeSigned' => 'Signed by both', + 'plantare.badgeDeclined' => 'Declined', + 'plantare.offline' => 'You\'re offline — it\'ll be sent when you reconnect', 'handover.title' => 'Seeds changed hands', 'handover.help' => 'A gift, a swap or a sale — note it down, with a return promise or without.', 'handover.iGave' => 'I gave seeds', @@ -3035,8 +3129,6 @@ extension on Translations { 'sale.help' => 'Record seed you sold or bought — money, Ğ1, or any currency. A separate model from a gift or a Plantare. No commission is ever taken on seeds.', 'sale.add' => 'Record a sale', 'sale.empty' => 'No sales yet. Note here what you sell or buy.', - _ => null, - } ?? switch (path) { 'sale.iSold' => 'I sold', 'sale.iBought' => 'I bought', 'sale.direction' => 'Sold or bought?', diff --git a/apps/app_seeds/lib/i18n/strings_es.g.dart b/apps/app_seeds/lib/i18n/strings_es.g.dart index 9722a74..991d83b 100644 --- a/apps/app_seeds/lib/i18n/strings_es.g.dart +++ b/apps/app_seeds/lib/i18n/strings_es.g.dart @@ -950,6 +950,29 @@ class _Translations$plantare$es extends Translations$plantare$en { @override String get pickDate => 'Elegir fecha'; @override String get clearDate => 'Quitar fecha'; @override String get sectionTitle => 'Compromisos'; + @override String get propose => 'Proponer un Plantaré firmado'; + @override String get proposeHelp => 'Los dos guardáis la misma promesa, firmada por ambos: prueba de que esta semilla cambió de manos y se cultivará y devolverá.'; + @override String proposeTo({required Object name}) => 'Con ${name}'; + @override String get sent => 'Propuesta enviada — esperando su firma'; + @override String get seedLabel => '¿Qué semilla?'; + @override String get seedHint => 'La variedad a la que se refiere la promesa'; + @override String get returnKindLabel => '¿Qué se devuelve?'; + @override String get returnSimilar => 'Una cantidad similar de semilla'; + @override String get returnSimilarNote => 'polinización abierta · no transgénica · cultivada en ecológico'; + @override String get returnWork => 'Unas horas de trabajo'; + @override String get returnOther => 'Otra cosa'; + @override String get workHoursLabel => '¿Cuántas horas?'; + @override String get proposalsSection => 'Esperando tu respuesta'; + @override String incomingFrom({required Object name}) => '${name} te propone un Plantaré'; + @override String get accept => 'Aceptar y firmar'; + @override String get declineAction => 'Rechazar'; + @override String get declineReasonHint => 'Motivo (opcional)'; + @override String get acceptedToast => 'Firmado — ahora lo guardáis los dos'; + @override String get declinedToast => 'Rechazado'; + @override String get badgeAwaiting => 'Falta firma'; + @override String get badgeSigned => 'Firmado por ambos'; + @override String get badgeDeclined => 'Rechazado'; + @override String get offline => 'Estás sin conexión — se enviará al reconectar'; } // Path: handover @@ -1910,6 +1933,31 @@ extension on TranslationsEs { 'plantare.pickDate' => 'Elegir fecha', 'plantare.clearDate' => 'Quitar fecha', 'plantare.sectionTitle' => 'Compromisos', + 'plantare.propose' => 'Proponer un Plantaré firmado', + 'plantare.proposeHelp' => 'Los dos guardáis la misma promesa, firmada por ambos: prueba de que esta semilla cambió de manos y se cultivará y devolverá.', + 'plantare.proposeTo' => ({required Object name}) => 'Con ${name}', + 'plantare.sent' => 'Propuesta enviada — esperando su firma', + 'plantare.seedLabel' => '¿Qué semilla?', + 'plantare.seedHint' => 'La variedad a la que se refiere la promesa', + 'plantare.returnKindLabel' => '¿Qué se devuelve?', + 'plantare.returnSimilar' => 'Una cantidad similar de semilla', + 'plantare.returnSimilarNote' => 'polinización abierta · no transgénica · cultivada en ecológico', + 'plantare.returnWork' => 'Unas horas de trabajo', + 'plantare.returnOther' => 'Otra cosa', + 'plantare.workHoursLabel' => '¿Cuántas horas?', + 'plantare.proposalsSection' => 'Esperando tu respuesta', + 'plantare.incomingFrom' => ({required Object name}) => '${name} te propone un Plantaré', + 'plantare.accept' => 'Aceptar y firmar', + 'plantare.declineAction' => 'Rechazar', + _ => null, + } ?? switch (path) { + 'plantare.declineReasonHint' => 'Motivo (opcional)', + 'plantare.acceptedToast' => 'Firmado — ahora lo guardáis los dos', + 'plantare.declinedToast' => 'Rechazado', + 'plantare.badgeAwaiting' => 'Falta firma', + 'plantare.badgeSigned' => 'Firmado por ambos', + 'plantare.badgeDeclined' => 'Rechazado', + 'plantare.offline' => 'Estás sin conexión — se enviará al reconectar', 'handover.title' => 'Di o recibí semillas', 'handover.help' => 'Un regalo, un trueque o una venta: apúntalo, con promesa de devolver semilla o sin ella.', 'handover.iGave' => 'Di semillas', @@ -1926,8 +1974,6 @@ extension on TranslationsEs { 'sale.add' => 'Registrar venta', 'sale.empty' => 'Aún no hay ventas. Anota aquí lo que vendes o compras.', 'sale.iSold' => 'Vendí', - _ => null, - } ?? switch (path) { 'sale.iBought' => 'Compré', 'sale.direction' => '¿Vendes o compras?', 'sale.counterparty' => '¿Con quién?', diff --git a/apps/app_seeds/lib/ui/chat_screen.dart b/apps/app_seeds/lib/ui/chat_screen.dart index ec8a492..d9fa5f7 100644 --- a/apps/app_seeds/lib/ui/chat_screen.dart +++ b/apps/app_seeds/lib/ui/chat_screen.dart @@ -12,6 +12,7 @@ import '../domain/message_rules.dart'; import '../i18n/strings.g.dart'; import '../services/message_store.dart'; import '../services/onboarding_store.dart'; +import '../services/plantare_service.dart'; import '../services/profile_cache.dart'; import '../services/profile_store.dart'; import '../services/social_connection.dart'; @@ -23,6 +24,7 @@ import '../state/peer_rating_cubit.dart'; import '../state/trust_cubit.dart'; import 'market_gate.dart'; import 'peer_avatar.dart'; +import 'plantare_propose_sheet.dart'; import 'rating_sheet.dart'; import 'report_sheet.dart'; import 'theme.dart'; @@ -205,6 +207,23 @@ class _ChatScreenState extends State { /// Reports this peer (a standard report event to the community servers); /// when they were also blocked from the sheet, leaves the chat too. + /// Opens the "propose a signed Plantaré" sheet against this peer (their key is + /// in hand from the conversation), and confirms when a proposal is sent. + Future _proposePlantare() async { + if (!getIt.isRegistered()) return; + final sent = await showProposePlantareSheet( + context, + service: getIt(), + peerPubkey: widget.peerPubkey, + peerName: _peerName, + ); + if (sent == true && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.t.plantare.sent)), + ); + } + } + Future _reportPeer() async { final outcome = await showReportSheet( context, @@ -305,10 +324,16 @@ class _ChatScreenState extends State { PopupMenuButton( key: const Key('chat.menu'), onSelected: (value) { + if (value == 'plantare') _proposePlantare(); if (value == 'report') _reportPeer(); if (value == 'block') _blockPeer(); }, itemBuilder: (context) => [ + if (getIt.isRegistered()) + PopupMenuItem( + value: 'plantare', + child: Text(t.plantare.propose), + ), PopupMenuItem( value: 'report', child: Text(t.report.person), diff --git a/apps/app_seeds/lib/ui/plantare_propose_sheet.dart b/apps/app_seeds/lib/ui/plantare_propose_sheet.dart new file mode 100644 index 0000000..1915276 --- /dev/null +++ b/apps/app_seeds/lib/ui/plantare_propose_sheet.dart @@ -0,0 +1,289 @@ +import 'package:flutter/material.dart'; + +import '../db/enums.dart'; +import '../i18n/strings.g.dart'; +import '../services/plantare_service.dart'; +import '../services/profile_cache.dart' show shortPubkey; +import 'theme.dart'; + +/// Opens the "propose a signed Plantaré" sheet against a specific peer, whose +/// [peerPubkey] is already in hand (the flow starts from a chat or a closed +/// offer). Fills the terms, self-signs, and sends the proposal via +/// [PlantareService]; the peer counter-signs to close it. Returns true if a +/// proposal was sent. +Future showProposePlantareSheet( + BuildContext context, { + required PlantareService service, + required String peerPubkey, + String? peerName, + String? varietyId, + String? seedLabel, +}) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (_) => _ProposeSheet( + service: service, + peerPubkey: peerPubkey, + peerName: peerName, + varietyId: varietyId, + seedLabel: seedLabel, + ), + ); +} + +class _ProposeSheet extends StatefulWidget { + const _ProposeSheet({ + required this.service, + required this.peerPubkey, + this.peerName, + this.varietyId, + this.seedLabel, + }); + + final PlantareService service; + final String peerPubkey; + final String? peerName; + final String? varietyId; + final String? seedLabel; + + @override + State<_ProposeSheet> createState() => _ProposeSheetState(); +} + +class _ProposeSheetState extends State<_ProposeSheet> { + // Default: I gave the seed and they'll return some (I'm the creditor) — the + // common "I'm sharing seed" case. + PlantareDirection _direction = PlantareDirection.owedToMe; + PlantareReturnKind _returnKind = PlantareReturnKind.similar; + late final TextEditingController _seed = + TextEditingController(text: widget.seedLabel ?? ''); + final _owed = TextEditingController(); + final _hours = TextEditingController(); + DateTime? _dueBy; + bool _saving = false; + + @override + void dispose() { + _seed.dispose(); + _owed.dispose(); + _hours.dispose(); + super.dispose(); + } + + String? _nullIfBlank(String s) => s.trim().isEmpty ? null : s.trim(); + + Future _pickDueBy() async { + final now = DateTime.now(); + final picked = await showDatePicker( + context: context, + initialDate: _dueBy ?? DateTime(now.year + 1, now.month, now.day), + firstDate: DateTime(now.year - 1), + lastDate: DateTime(now.year + 10), + ); + if (picked != null) setState(() => _dueBy = picked); + } + + Future _send() async { + final label = _seed.text.trim(); + if (label.isEmpty) return; // the seed name is the one thing we need + setState(() => _saving = true); + await widget.service.propose( + direction: _direction, + counterpartyKey: widget.peerPubkey, + counterpartyName: widget.peerName, + varietyId: widget.varietyId, + label: label, + owedDescription: _nullIfBlank(_owed.text), + returnKind: _returnKind, + workHours: _returnKind == PlantareReturnKind.workHours + ? double.tryParse(_hours.text.trim().replaceAll(',', '.')) + : null, + dueBy: _dueBy, + ); + if (mounted) Navigator.of(context).pop(true); + } + + @override + Widget build(BuildContext context) { + final t = context.t; + final peer = widget.peerName ?? shortPubkey(widget.peerPubkey); + return Padding( + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 16, + bottom: MediaQuery.of(context).viewInsets.bottom + 16, + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text(t.plantare.propose, + style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 4), + Text(t.plantare.proposeTo(name: peer), + style: const TextStyle(color: seedGreen, fontSize: 13)), + const SizedBox(height: 8), + Text(t.plantare.proposeHelp, + style: const TextStyle(color: seedMuted, fontSize: 13)), + const SizedBox(height: 16), + Text(t.plantare.direction, + style: Theme.of(context).textTheme.labelLarge), + const SizedBox(height: 6), + for (final d in PlantareDirection.values) + ListTile( + key: Key('propose.direction.${d.name}'), + contentPadding: EdgeInsets.zero, + leading: Icon( + _direction == d + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + color: _direction == d ? seedGreen : seedMuted, + ), + title: Text(d == PlantareDirection.iReturn + ? t.plantare.iReturn + : t.plantare.owedToMe), + onTap: () => setState(() => _direction = d), + ), + const SizedBox(height: 8), + TextField( + key: const Key('propose.seed'), + controller: _seed, + textCapitalization: TextCapitalization.sentences, + decoration: InputDecoration( + labelText: t.plantare.seedLabel, + helperText: t.plantare.seedHint, + border: const OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + Text(t.plantare.returnKindLabel, + style: Theme.of(context).textTheme.labelLarge), + const SizedBox(height: 6), + _ReturnChoice( + value: PlantareReturnKind.similar, + group: _returnKind, + title: t.plantare.returnSimilar, + note: t.plantare.returnSimilarNote, + onTap: (v) => setState(() => _returnKind = v), + ), + _ReturnChoice( + value: PlantareReturnKind.workHours, + group: _returnKind, + title: t.plantare.returnWork, + onTap: (v) => setState(() => _returnKind = v), + ), + _ReturnChoice( + value: PlantareReturnKind.other, + group: _returnKind, + title: t.plantare.returnOther, + onTap: (v) => setState(() => _returnKind = v), + ), + if (_returnKind == PlantareReturnKind.workHours) ...[ + const SizedBox(height: 8), + TextField( + key: const Key('propose.hours'), + controller: _hours, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: InputDecoration( + labelText: t.plantare.workHoursLabel, + border: const OutlineInputBorder(), + ), + ), + ], + const SizedBox(height: 12), + TextField( + key: const Key('propose.owed'), + controller: _owed, + decoration: InputDecoration( + labelText: t.plantare.owed, + helperText: t.plantare.owedHint, + helperMaxLines: 2, + border: const OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + InputDecorator( + decoration: InputDecoration( + labelText: t.plantare.dueByLabel, + helperText: t.plantare.dueByHint, + border: const OutlineInputBorder(), + ), + child: Row( + children: [ + Expanded( + child: Text( + _dueBy == null + ? t.plantare.pickDate + : MaterialLocalizations.of(context) + .formatShortDate(_dueBy!), + style: TextStyle( + color: _dueBy == null ? seedMuted : seedOnSurface, + ), + ), + ), + if (_dueBy != null) + IconButton( + icon: const Icon(Icons.clear, size: 20), + tooltip: t.plantare.clearDate, + onPressed: () => setState(() => _dueBy = null), + ), + IconButton( + key: const Key('propose.dueBy.pick'), + icon: const Icon(Icons.event_outlined), + tooltip: t.plantare.pickDate, + onPressed: _pickDueBy, + ), + ], + ), + ), + const SizedBox(height: 20), + FilledButton.icon( + key: const Key('propose.send'), + onPressed: _saving ? null : _send, + icon: const Icon(Icons.draw_outlined), + label: Text(t.plantare.propose), + ), + ], + ), + ), + ); + } +} + +class _ReturnChoice extends StatelessWidget { + const _ReturnChoice({ + required this.value, + required this.group, + required this.title, + required this.onTap, + this.note, + }); + + final PlantareReturnKind value; + final PlantareReturnKind group; + final String title; + final String? note; + final ValueChanged onTap; + + @override + Widget build(BuildContext context) { + final selected = value == group; + return ListTile( + key: Key('propose.return.${value.name}'), + contentPadding: EdgeInsets.zero, + leading: Icon( + selected ? Icons.radio_button_checked : Icons.radio_button_unchecked, + color: selected ? seedGreen : seedMuted, + ), + title: Text(title), + subtitle: note == null + ? null + : Text(note!, style: const TextStyle(color: seedMuted, fontSize: 12)), + onTap: () => onTap(value), + ); + } +} diff --git a/apps/app_seeds/lib/ui/plantares_screen.dart b/apps/app_seeds/lib/ui/plantares_screen.dart index 7a16e22..5140802 100644 --- a/apps/app_seeds/lib/ui/plantares_screen.dart +++ b/apps/app_seeds/lib/ui/plantares_screen.dart @@ -4,7 +4,9 @@ import 'package:go_router/go_router.dart'; import '../data/variety_repository.dart'; import '../db/enums.dart'; +import '../di/injector.dart'; import '../i18n/strings.g.dart'; +import '../services/plantare_service.dart'; import 'plantare_sheet.dart'; import 'theme.dart'; @@ -18,6 +20,9 @@ class PlantaresScreen extends StatelessWidget { Widget build(BuildContext context) { final t = context.t; final repo = context.read(); + final service = getIt.isRegistered() + ? getIt() + : null; return Scaffold( appBar: AppBar(title: Text(t.plantare.title)), floatingActionButton: FloatingActionButton.extended( @@ -53,9 +58,9 @@ class PlantaresScreen extends StatelessWidget { padding: const EdgeInsets.only(bottom: 88), children: [ if (open.isNotEmpty) _SectionHeader(t.plantare.openSection), - for (final v in open) _PlantareTile(v, repo), + for (final v in open) _PlantareTile(v, repo, service), if (settled.isNotEmpty) _SectionHeader(t.plantare.settledSection), - for (final v in settled) _PlantareTile(v, repo), + for (final v in settled) _PlantareTile(v, repo, service), ], ); }, @@ -64,6 +69,50 @@ class PlantaresScreen extends StatelessWidget { } } +/// A small chip for the bilateral handshake state — "awaiting signature", +/// "signed by both", or "declined". Absent for a plain v1 local note. +class _RemoteStateBadge extends StatelessWidget { + const _RemoteStateBadge(this.state); + final PlantareRemoteState state; + + @override + Widget build(BuildContext context) { + final t = context.t; + final (label, color, icon) = switch (state) { + PlantareRemoteState.proposed => ( + t.plantare.badgeAwaiting, + seedMuted, + Icons.hourglass_empty, + ), + PlantareRemoteState.accepted => ( + t.plantare.badgeSigned, + seedGreen, + Icons.verified_outlined, + ), + PlantareRemoteState.declined => ( + t.plantare.badgeDeclined, + seedWarning, + Icons.do_not_disturb_on_outlined, + ), + }; + return Padding( + padding: const EdgeInsets.only(top: 4), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 14, color: color), + const SizedBox(width: 4), + Text( + label, + style: TextStyle( + color: color, fontSize: 12, fontWeight: FontWeight.w600), + ), + ], + ), + ); + } +} + class _SectionHeader extends StatelessWidget { const _SectionHeader(this.label); final String label; @@ -89,10 +138,49 @@ class _SectionHeader extends StatelessWidget { /// it's still ahead or overdue. The overflow menu carries the rest (let go / /// reopen / remove). class _PlantareTile extends StatelessWidget { - const _PlantareTile(this.view, this.repo); + const _PlantareTile(this.view, this.repo, this.service); final PlantareView view; final VarietyRepository repo; + final PlantareService? service; + + /// A proposal is "awaiting me" (I still have to accept/decline) when the + /// handshake is `proposed` and MY stub is missing — my slot is the debtor one + /// if I'm the one who returns, else the creditor one. + bool get _awaitingMe { + final p = view.plantare; + if (p.remoteState != PlantareRemoteState.proposed) return false; + final mySig = p.direction == PlantareDirection.iReturn + ? p.debtorSignature + : p.creditorSignature; + return mySig == null; + } + + Future _accept(BuildContext context) async { + final id = view.plantare.pledgeId; + if (service == null || id == null) return; + final t = context.t; + try { + await service!.accept(id); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(t.plantare.acceptedToast)), + ); + } + } catch (_) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(t.plantare.offline)), + ); + } + } + } + + Future _decline(BuildContext context) async { + final id = view.plantare.pledgeId; + if (service == null || id == null) return; + await service!.decline(id); + } @override Widget build(BuildContext context) { @@ -156,12 +244,37 @@ class _PlantareTile extends StatelessWidget { fontWeight: overdue ? FontWeight.w600 : null, ), ), + if (p.remoteState != null) _RemoteStateBadge(p.remoteState!), + // An incoming proposal awaiting my signature: sign it or turn it down, + // right here. + if (_awaitingMe && service != null) + Padding( + padding: const EdgeInsets.only(top: 6), + child: Row( + children: [ + FilledButton.icon( + key: Key('plantare.accept.${p.id}'), + onPressed: () => _accept(context), + icon: const Icon(Icons.draw_outlined, size: 18), + label: Text(t.plantare.accept), + ), + const SizedBox(width: 8), + TextButton( + key: Key('plantare.decline.${p.id}'), + onPressed: () => _decline(context), + child: Text(t.plantare.declineAction), + ), + ], + ), + ), ], ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ - if (!done) + // A returned/mark button only makes sense once the deal is agreed + // (or for a plain local note). Hide it while a proposal is pending. + if (!done && !_awaitingMe) IconButton( key: Key('plantare.return.${p.id}'), icon: const Icon(Icons.check_circle_outline), diff --git a/apps/app_seeds/test/ui/plantare_propose_sheet_test.dart b/apps/app_seeds/test/ui/plantare_propose_sheet_test.dart new file mode 100644 index 0000000..129d4f0 --- /dev/null +++ b/apps/app_seeds/test/ui/plantare_propose_sheet_test.dart @@ -0,0 +1,100 @@ +import 'package:commons_core/commons_core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tane/db/enums.dart'; +import 'package:tane/i18n/strings.g.dart'; +import 'package:tane/services/plantare_service.dart'; +import 'package:tane/ui/plantare_propose_sheet.dart'; + +import '../support/test_support.dart'; + +/// A records-only transport so the sheet can be driven without a relay. +class _CapturingTransport implements PlantareTransport { + final proposed = []; + @override + Future propose(PlantarePledge pledge) async => proposed.add(pledge); + @override + Future accept(PlantarePledge pledge) async {} + @override + Future decline({ + required String toPubkey, + required String pledgeId, + String reason = '', + }) async {} + @override + Stream incoming() => const Stream.empty(); + @override + Future close() async {} +} + +void main() { + testWidgets('filling the seed and sending proposes a signed pledge', + (tester) async { + LocaleSettings.setLocaleSync(AppLocale.en); + final db = newTestDatabase(); + addTearDown(db.close); + final repo = newTestRepository(db); + final tx = _CapturingTransport(); + final service = PlantareService( + repo: repo, + selfPubkey: 'a' * 64, + selfSecretKey: + '0000000000000000000000000000000000000000000000000000000000000001', + )..bindTransport(tx); + + await tester.pumpWidget( + TranslationProvider( + child: MaterialApp( + locale: const Locale('en'), + supportedLocales: AppLocaleUtils.supportedLocales, + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + home: Builder( + builder: (context) => Scaffold( + body: Center( + child: ElevatedButton( + onPressed: () => showProposePlantareSheet( + context, + service: service, + peerPubkey: 'b' * 64, + peerName: 'Ana', + ), + child: const Text('open'), + ), + ), + ), + ), + ), + ), + ); + + await tester.tap(find.text('open')); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('propose.seed')), findsOneWidget); + + // The seed name is the one required field. + await tester.enterText( + find.byKey(const Key('propose.seed')), 'Tomate rosa'); + await tester.pump(); + expect(find.text('Tomate rosa'), findsOneWidget); + await tester.ensureVisible(find.byKey(const Key('propose.send'))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('propose.send'))); + await tester.pumpAndSettle(); + + expect(tx.proposed, hasLength(1)); + expect(tx.proposed.single.label, 'Tomate rosa'); + // Default direction is owedToMe → I'm the creditor and signed my stub. + expect(tx.proposed.single.creditorSignature, isNotNull); + expect(tx.proposed.single.debtorSignature, isNull); + // The local row is recorded as proposed. + final row = await repo.plantareByPledgeId(tx.proposed.single.pledgeId); + expect(row!.remoteState, PlantareRemoteState.proposed); + + await service.stop(); + }); +} diff --git a/apps/app_seeds/test/ui/plantares_screen_test.dart b/apps/app_seeds/test/ui/plantares_screen_test.dart index 2034afe..66b84a6 100644 --- a/apps/app_seeds/test/ui/plantares_screen_test.dart +++ b/apps/app_seeds/test/ui/plantares_screen_test.dart @@ -63,6 +63,57 @@ void main() { await disposeTree(tester); }); + testWidgets('a fully-signed bilateral pledge shows the "signed by both" badge', + (tester) async { + LocaleSettings.setLocaleSync(AppLocale.en); + final repo = newTestRepository(db); + await repo.createPlantare( + direction: PlantareDirection.owedToMe, + counterparty: 'Ana', + pledgeId: 'p-1', + debtorKey: 'dk', + creditorKey: 'ck', + debtorSignature: 'ds', + creditorSignature: 'cs', + remoteState: PlantareRemoteState.accepted, + ); + + await tester.pumpWidget( + wrapScreen(repository: repo, child: const PlantaresScreen()), + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.text('Signed by both'), findsOneWidget); + // No accept/decline for a closed deal. + expect(find.byKey(const Key('plantare.accept.')), findsNothing); + await disposeTree(tester); + }); + + testWidgets('a pending proposal shows the "awaiting signature" badge', + (tester) async { + LocaleSettings.setLocaleSync(AppLocale.en); + final repo = newTestRepository(db); + await repo.createPlantare( + direction: PlantareDirection.iReturn, + counterparty: 'Ben', + pledgeId: 'p-2', + debtorKey: 'dk', + creditorKey: 'ck', + creditorSignature: 'cs', // proposer (creditor) signed; I (debtor) haven't + remoteState: PlantareRemoteState.proposed, + ); + + await tester.pumpWidget( + wrapScreen(repository: repo, child: const PlantaresScreen()), + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.text('Awaiting signature'), findsOneWidget); + await disposeTree(tester); + }); + testWidgets('tapping a linked commitment opens its variety', (tester) async { final repo = newTestRepository(db); final vid = await repo.addQuickVariety(label: 'Judía'); diff --git a/apps/app_seeds/test/ui/your_people_screen_test.dart b/apps/app_seeds/test/ui/your_people_screen_test.dart index d868fba..1b7d2ba 100644 --- a/apps/app_seeds/test/ui/your_people_screen_test.dart +++ b/apps/app_seeds/test/ui/your_people_screen_test.dart @@ -67,6 +67,8 @@ class FakeSession implements SocialSession { @override ProfileTransport get profile => throw UnimplementedError(); @override + PlantareTransport get plantares => throw UnimplementedError(); + @override SyncTransport get sync => throw UnimplementedError(); @override Future close() async {}