diff --git a/apps/app_seeds/lib/app.dart b/apps/app_seeds/lib/app.dart index ffb87b1..c05f58d 100644 --- a/apps/app_seeds/lib/app.dart +++ b/apps/app_seeds/lib/app.dart @@ -206,7 +206,11 @@ class TaneApp extends StatelessWidget { // desktop (Flutter disables that by default), so the photo carousel and // lists swipe there too. scrollBehavior: const _DragScrollBehavior(), - locale: TranslationProvider.of(context).flutterLocale, + // Flutter ships no Material/Cupertino localizations for Asturian + // (`ast`), so render the framework chrome (date pickers, native dialog + // buttons) in Spanish — the closest shipped language — while the app's + // own text stays Asturian via slang. Every other locale maps 1:1. + locale: materialLocaleFor(TranslationProvider.of(context).flutterLocale), supportedLocales: AppLocaleUtils.supportedLocales, localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, @@ -224,6 +228,14 @@ class TaneApp extends StatelessWidget { } } +/// Maps the app's active locale to one Flutter's bundled Material/Cupertino +/// localizations can serve. Asturian (`ast`) has none, so it borrows Spanish +/// (`es`) for the framework chrome; the app's own strings stay Asturian (they +/// come from slang, not from `Localizations`). All other locales pass through. +@visibleForTesting +Locale materialLocaleFor(Locale locale) => + locale.languageCode == 'ast' ? const Locale('es') : locale; + /// Enables mouse/trackpad drag scrolling (off by default on desktop) so the /// photo carousel and lists can be swiped with a pointer. class _DragScrollBehavior extends MaterialScrollBehavior { diff --git a/apps/app_seeds/lib/data/species_catalog.dart b/apps/app_seeds/lib/data/species_catalog.dart index f381617..00cc7e7 100644 --- a/apps/app_seeds/lib/data/species_catalog.dart +++ b/apps/app_seeds/lib/data/species_catalog.dart @@ -31,3 +31,16 @@ Future> loadBundledSpecies() async { final jsonString = await rootBundle.loadString(_catalogAsset); return parseSpeciesCatalog(jsonString); } + +/// Version of the bundled catalog, mirrored from the asset's `version` field +/// (written by `tool/gen_species_catalog.dart`). A test asserts the two stay in +/// sync. The app records the version it last seeded and skips the ~1.5 MB parse +/// and the table scan in [SpeciesRepository.seedBundled] on every subsequent +/// launch — so bumping this (alongside the asset) is what triggers a one-time +/// re-seed after the catalog changes. +const int speciesCatalogVersion = 3; + +/// Reads just the `version` field from a catalog JSON string. Used by the sync +/// test to keep [speciesCatalogVersion] aligned with the asset. +int parseSpeciesCatalogVersion(String jsonString) => + (jsonDecode(jsonString) as Map)['version'] as int; diff --git a/apps/app_seeds/lib/data/species_repository.dart b/apps/app_seeds/lib/data/species_repository.dart index d521bc2..7a18f8c 100644 --- a/apps/app_seeds/lib/data/species_repository.dart +++ b/apps/app_seeds/lib/data/species_repository.dart @@ -70,6 +70,24 @@ class SpeciesRepository { /// Reads all existing species once and writes in a single batch: with a /// catalog of ~1000 species a per-row SELECT on every startup would be a real /// cost on the encrypted database. + /// Seeds the bundled catalog only when [readVersion] doesn't already report + /// [version]. [loadSeeds] is awaited lazily and skipped entirely when this + /// install already holds [version] — so the ~1.5 MB catalog parse and the + /// table scan in [seedBundled] are paid once per catalog version, not on + /// every launch (the main startup cost). On a (re)seed, [writeVersion] records + /// [version] only after [seedBundled] commits, so an interrupted seed is + /// retried on the next launch. + Future seedBundledIfNeeded({ + required int version, + required Future Function() readVersion, + required Future Function(String version) writeVersion, + required Future> Function() loadSeeds, + }) async { + if (await readVersion() == version.toString()) return; + await seedBundled(await loadSeeds()); + await writeVersion(version.toString()); + } + Future seedBundled(List seeds) async { final stamp = Hlc.zero(nodeId).pack(); await _db.transaction(() async { diff --git a/apps/app_seeds/lib/di/injector.dart b/apps/app_seeds/lib/di/injector.dart index 4d45472..f96d188 100644 --- a/apps/app_seeds/lib/di/injector.dart +++ b/apps/app_seeds/lib/di/injector.dart @@ -21,6 +21,7 @@ import '../services/auto_backup_store.dart'; import '../services/export_import_service.dart'; import '../services/file_picker_file_service.dart'; import '../services/file_service.dart'; +import '../services/locale_store.dart'; import '../services/ocr/label_text_extractor.dart'; import '../services/ocr/ocr_language.dart'; import '../services/ocr/tesseract_label_extractor.dart'; @@ -68,9 +69,18 @@ Future configureDependencies() async { // contacted here (local-first — the social layer only enriches). final socialService = await SocialService.fromRootSeedHex(rootSeedHex); - // Seed the bundled species catalog (idempotent) before the UI opens. + // Seed the bundled species catalog before the UI opens — but only once per + // catalog version. Parsing the ~1.5 MB catalog and scanning the species table + // on every launch was the main startup jank; the version marker skips both + // when this install already holds the current catalog. + const catalogVersionKey = 'species_catalog_version'; final speciesRepository = SpeciesRepository(database, idGen: IdGen()); - await speciesRepository.seedBundled(await loadBundledSpecies()); + await speciesRepository.seedBundledIfNeeded( + version: speciesCatalogVersion, + readVersion: () => secretStore.read(catalogVersionKey), + writeVersion: (v) => secretStore.write(catalogVersionKey, v), + loadSeeds: loadBundledSpecies, + ); final varietyRepository = VarietyRepository( database, @@ -97,6 +107,7 @@ Future configureDependencies() async { ..registerSingleton(fileService) ..registerSingleton(labelExtractor) ..registerSingleton(OnboardingStore(secretStore)) + ..registerSingleton(LocaleStore(secretStore)) ..registerSingleton(socialService) ..registerSingleton(SocialSettings(secretStore)) ..registerSingleton(OfferOutbox(secretStore)) diff --git a/apps/app_seeds/lib/i18n/ast.i18n.json b/apps/app_seeds/lib/i18n/ast.i18n.json new file mode 100644 index 0000000..16928a7 --- /dev/null +++ b/apps/app_seeds/lib/i18n/ast.i18n.json @@ -0,0 +1,410 @@ +{ + "app": { + "title": "Tanemaki" + }, + "common": { + "save": "Guardar", + "cancel": "Encaboxar", + "delete": "Desaniciar", + "edit": "Editar", + "type": "Triba", + "comingSoon": "Aína" + }, + "home": { + "tagline": "Comparte y cultiva simiente llocal", + "openMarket": "Mercáu", + "openMarketSubtitle": "Descubri y comparte simiente cerca", + "yourInventory": "El to inventariu", + "yourInventorySubtitle": "Remana la to simiente" + }, + "photo": { + "camera": "Facer una semeya", + "gallery": "Escoyer de la galería", + "setAsCover": "Poner de portada", + "isCover": "Semeya de portada", + "deleteConfirm": "¿Desaniciar esta semeya?" + }, + "menu": { + "tagline": "el to bancu de simiente", + "inventory": "Inventariu", + "market": "Mercáu", + "profile": "El to perfil", + "chat": "Charra", + "wishlist": "Llista de deseos", + "following": "Siguiendo", + "settings": "Axustes" + }, + "settings": { + "language": "Llingua", + "systemLanguage": "Llingua del sistema", + "langEs": "Español", + "langEn": "English", + "langPt": "Português", + "langAst": "Asturianu", + "about": "Tocante a", + "aboutText": "Inventariu llocal y cifráu pa simiente tradicional. AGPL-3.0.", + "aboutOpen": "Tocante a Tanemaki" + }, + "backup": { + "title": "Copia de seguranza", + "autoBackupTitle": "Copies automátiques", + "autoBackupLast": "Cabera copia guardada'l {date}", + "autoBackupNone": "Guárdase una copia automáticamente en segundu planu", + "exportJson": "Guardar una copia de seguranza", + "exportJsonSubtitle": "Una copia completa pa guardar a salvo, restaurar dempués o pasar a otru preséu", + "importJson": "Restaurar una copia", + "importJsonSubtitle": "Recupera una copia guardada — nun se dobla nada", + "exportCsv": "Esportar a una fueya de cálculu", + "exportCsvSubtitle": "Una llista cenciella pa Excel o LibreOffice — ensin semeyes", + "importCsv": "Importar una llista", + "importCsvSubtitle": "Amiesta entraes dende una fueya de cálculu", + "importConfirmTitle": "¿Restaurar una copia?", + "importConfirmBody": "Les entraes fusiónense col to inventariu; si una entrada esiste en dambos llaos, gana la versión más nueva. Nun se dobla nada.", + "importCsvConfirmTitle": "¿Importar una llista?", + "importCsvConfirmBody": "Cada filera amiéstase como una entrada nueva. Nun fusiona nin troca, asina qu'importar el mesmu ficheru dos veces amiéstalu dos veces.", + "importAction": "Importar", + "exportSaved": "Copia guardada", + "cancelled": "Encaboxáu", + "importDone": "Importáu: {added} nueves, {updated} anovaes", + "importCsvDone": "Amestaes {count} entraes", + "importFailed": "Esti ficheru nun se pudo lleer como una copia de Tanemaki", + "failed": "Daqué salió mal", + "recoveryTitle": "El to códigu de recuperación", + "recoverySubtitle": "Imprémelu y guárdalu bien: abre les tos copies n'otru preséu", + "recoveryIntro": "Esti códigu abre les tos copies guardaes y recupera'l to bancu en cualquier preséu. Guarda dos copies en papel en sitios seguros, como la to meyor simiente. Quien lu tenga pue lleer les tos copies, asina que nun lu compartas con naide.", + "recoveryCopy": "Copiar", + "recoverySave": "Guardar la fueya", + "recoverySheetTitle": "Tanemaki — la to fueya de recuperación", + "recoveryPromptTitle": "Escribi'l to códigu de recuperación", + "recoveryPromptBody": "Esta copia guardóse con otru códigu. Escribi'l códigu de la to fueya de recuperación p'abrilla.", + "recoveryWrongCode": "Esi códigu nun abre esta copia" + }, + "about": { + "title": "Tocante a", + "kanji": "種まき", + "tagline": "Una app local-first y descentralizada pa remanar y compartir simiente y plantones tradicionales.", + "intro": "Tanemaki (種まき, «semar / esparder simiente» en xaponés; nome curtiu Tane, 種 «grana») ayuda a persones y coleutivos a llevar un inventariu amable del so bancu de simiente, decidir qué ufierten y compartilo llocalmente — ensin un intermediariu central que pueda controlalo, censuralo o ser multáu por ello. L'oxetivu ye a la vez práuticu y políticu: sofitar les variedaes tradicionales de simiente y plantar cara al monopoliu simienteru.", + "heritage": "El nome honra les vieyes tradiciones xaponeses d'ayuda mutua alredor del arroz — yui (trabayu comunitariu compartíu) y tanomoshi (fondos de reciprocidá) — qu'inspiraron el papel Plantare, la «moneda comunitaria pal intercambiu de simiente» (BAH-Semilleru, 2009, CC-BY-SA). Tanemaki ye'l Plantare dixital.", + "version": "Versión", + "license": "Llicencia", + "licenseValue": "AGPL-3.0", + "website": "Sitiu web", + "openSourceLicenses": "Llicencies de códigu abiertu", + "openSourceLicensesSubtitle": "Biblioteques de terceros y les sos llicencies", + "copyright": "© {years} Asociación Comunes, baxo AGPLv3" + }, + "intro": { + "skip": "Saltar", + "next": "Siguiente", + "start": "Entamar", + "menuEntry": "Cómo furrula Tanemaki", + "slides": { + "welcome": { + "title": "La grana que te traxo hasta equí", + "body": "Cada simiente tradicional ye una carta escrita por miles de xeneraciones, que pasó de mano en mano. Somos lo que somos gracies a esi intercambiu." + }, + "inventory": { + "title": "El to bancu de simiente, nel bolsu", + "body": "Apunta qué tienes, de qué añu, cuánto y d'ónde vieno, col nome que tu uses. Una semeya y un nome abasten pa entamar." + }, + "privacy": { + "title": "Tuyo y namás tuyo", + "body": "Ensin cuenta, ensin internet, ensin rastrexadores. Los tos datos viven cifraos nel to preséu y namás se comparte lo que tu decidas." + }, + "share": { + "title": "Compartir, como siempres se fizo", + "body": "Ufierta lo que te sobra —regalu, truecu o venta— y qu'alguien cerca lo atope. Namás s'amuesa la distancia averada, enxamás la to direición; el tratu ciérraslu en persona." + }, + "plantare": { + "title": "Semar ye multiplicar", + "body": "Col Plantare, quien recibe promete devolver simiente más alantre. Y como pa devolvela hai que cultivala, cada préstamu multiplica'l común." + } + } + }, + "inventory": { + "title": "Inventariu", + "searchHint": "Guetar simiente", + "empty": "Entá nun hai simiente. Toca + p'amestar la primera.", + "noMatches": "Nenguna simiente concasa colos filtros.", + "clearFilters": "Quitar filtros", + "uncategorized": "Ensin categoría", + "needsReproductionFilter": "Por reproducir" + }, + "draft": { + "capture": "Capturar semeyes", + "captured": "{n} capturaes por catalogar", + "triageTitle": "Por catalogar", + "triageCount": "{n} por catalogar", + "untitled": "Ensin nome", + "nameField": "Ponle nome a esta simiente", + "nameHint": "¿Qué ye?", + "suggestFromPhoto": "Suxerir nome de la semeya", + "discard": "Descartar" + }, + "quickAdd": { + "title": "Amestar una simiente", + "labelField": "Nome", + "labelRequired": "Ponle un nome", + "addPhoto": "Amestar semeya", + "quantity": "¿Cuánta?", + "more": "Amestar más…", + "save": "Guardar", + "saveAndAddAnother": "Guardar y amestar otra", + "addedCount": "{n} amestaes", + "cancel": "Encaboxar" + }, + "detail": { + "notFound": "Esta simiente yá nun ta equí.", + "lots": "Llotes", + "noLots": "Entá nun hai llotes.", + "names": "Conocida tamién como", + "addName": "Amestar nome", + "links": "Enllaces", + "addLink": "Amestar enllaz", + "linkUrl": "URL", + "linkTitle": "Títulu (opcional)", + "reference": "Saber más", + "refGbif": "GBIF", + "refWikipedia": "Wikipedia", + "refWikispecies": "Wikispecies", + "notes": "Notes", + "addLot": "Amestar llote", + "editLot": "Editar llote", + "deleteConfirm": "¿Desaniciar esta simiente?", + "year": "Añu {year}", + "noYear": "Añu desconocíu" + }, + "germination": { + "title": "Germinación", + "add": "Amestar prueba", + "sampleSize": "Amuesa", + "germinated": "Germinaes", + "none": "Entá nun hai pruebes 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": "Pasa la so viabilidá típica — a multiplicar", + "expiredYears": "Pasa la so viabilidá típica (~{years} años) — a multiplicar" + }, + "editVariety": { + "title": "Editar simiente", + "name": "Nome", + "category": "Categoría", + "notes": "Notes", + "species": "Especie (del catálogu)", + "speciesHint": "Guetar una especie…", + "speciesSuggested": "Suxerida pol nome", + "organic": "Ecolóxica", + "organicHint": "Cultivada de mou ecolóxicu (eco)" + }, + "addLot": { + "title": "Amestar llote", + "year": "Data de coyecha", + "quantity": "¿Cuánta?", + "amount": "Cantidá" + }, + "harvest": { + "pickTitle": "Escoyer mes / añu", + "anyMonth": "Cualquier mes", + "noDate": "Indicar data de coyecha", + "monthNames": [ + "xineru", "febreru", "marzu", "abril", "mayu", "xunu", + "xunetu", "agostu", "setiembre", "ochobre", "payares", "avientu" + ] + }, + "lotType": { + "seed": "Simiente", + "plant": "Planta", + "seedling": "Planton", + "tree": "Árbol / matu", + "bulb": "Bulbu / tubérculu", + "cutting": "Esqueje" + }, + "presentation": { + "title": "Envase", + "none": "Ensin especificar", + "pot": "Tiestu", + "tray": "Bandexa", + "plug": "Alvéolu", + "bareRoot": "Raíz esnuda", + "rootBall": "Cepellón" + }, + "provenance": { + "section": "D'ónde vien", + "seedsFrom": "Simiente de", + "seedsFromHint": "Quién la cultivó o la dio", + "place": "Llugar", + "placeHint": "D'ónde vien (con provincia)", + "addSeedsFrom": "Simiente de", + "addPlace": "Llugar" + }, + "abundance": { + "add": "Cuánta tengo", + "title": "Cuánta tengo", + "none": "Ensin indicar", + "plentyToShare": "A esgaya pa compartir", + "enoughToShare": "Abondo, pa compartir con mesura", + "enoughForMe": "Abondo pa min", + "runningLow": "Queda poca" + }, + "share": { + "add": "¿Compártesla?", + "title": "¿Compártesla?", + "nudge": "Tienes a esgaya: podríes compartir un poco.", + "private": "Namás pa min", + "gift": "Pa regalar", + "exchange": "Pa trocar", + "sell": "En venta", + "filterChip": "Comparto", + "printCatalog": "Imprentar lo que comparto", + "catalogTitle": "Lo que comparto", + "catalogSaved": "Catálogu guardáu", + "cancelled": "Encaboxáu" + }, + "cropCalendar": { + "add": "Calendariu de cultivu", + "title": "Calendariu de cultivu", + "sow": "Sema", + "transplant": "Tresplante", + "flowering": "Floriamientu", + "fruiting": "Fructificación", + "seedHarvest": "Coyecha de simiente", + "unset": "—" + }, + "needsReproduction": { + "label": "Reproducir esta temporada", + "hint": "Cultívala enantes de que s'acabe la simiente", + "badge": "Por reproducir" + }, + "preservation": { + "add": "Cómo se caltién", + "title": "Cómo se caltién", + "none": "Ensin especificar", + "jarWithDesiccant": "Bote con desecante", + "glassJar": "Bote de cristal", + "paperEnvelope": "Sobre de papel", + "paperBag": "Bolsa de papel", + "plasticBag": "Bolsa de plásticu" + }, + "conditionCheck": { + "advanced": "Caltenimientu y detalles del bancu", + "title": "Revisiones de caltenimientu", + "add": "Amestar revisión", + "containers": "Botes / recipientes", + "desiccant": "Desecante", + "none": "Entá nun hai revisiones.", + "summary": "{count} bote(s) · {state}" + }, + "desiccant": { + "none": "Nun tien", + "add": "Pónse", + "replace": "Cámbiase", + "dry": "Azul — secu", + "fresh": "Recién puestu" + }, + "unit": { + "aFew": "delles poques", + "some": "dalgunes", + "plenty": "munches", + "pinch": "una migaya", + "handful": { "singular": "puñáu", "plural": "puñaos" }, + "teaspoon": { "singular": "cuyaradina", "plural": "cuyaradines" }, + "spoon": { "singular": "cuyar", "plural": "cuyares" }, + "cup": { "singular": "taza", "plural": "tazes" }, + "jar": { "singular": "bote", "plural": "botes" }, + "sack": { "singular": "sacu", "plural": "sacos" }, + "packet": { "singular": "sobre", "plural": "sobres" }, + "cob": { "singular": "panoya", "plural": "panoyes" }, + "pod": { "singular": "vaina", "plural": "vaines" }, + "ear": { "singular": "espiga", "plural": "espigues" }, + "head": { "singular": "cabezuela", "plural": "cabezueles" }, + "fruit": { "singular": "frutu", "plural": "frutos" }, + "bulb": { "singular": "bulbu", "plural": "bulbos" }, + "tuber": { "singular": "tubérculu", "plural": "tubérculos" }, + "seedHead": { "singular": "cabeza de simiente", "plural": "cabeces de simiente" }, + "bunch": { "singular": "manoyu", "plural": "manoyos" }, + "plant": { "singular": "planta", "plural": "plantes" }, + "pot": { "singular": "tiestu", "plural": "tiestos" }, + "tray": { "singular": "bandexa", "plural": "bandexes" }, + "seedling": { "singular": "plántula", "plural": "plántules" }, + "tree": { "singular": "árbol", "plural": "árboles" }, + "cutting": { "singular": "esqueje", "plural": "esquejes" }, + "grams": { "singular": "gramu", "plural": "gramos" }, + "count": { "singular": "grana", "plural": "granes" } + }, + "market": { + "title": "Simiente cerca de ti", + "subtitle": "Lo qu'otres persones comparten cerca", + "notSetUp": "Entá nun configurasti'l compartir", + "notSetUpBody": "Activa'l compartir pa ver y dar simiente a xente cercano. Caltiénse averao — la to zona, enxamás la to direición esacta.", + "setUp": "Configurar el compartir", + "cantReach": "Nun se pue coneutar colos servidores agora mesmo", + "cantReachBody": "Inténtalo otra vez, o revisa los servidores na configuración avanzada.", + "retry": "Retentar", + "setArea": "Indica la to zona", + "setAreaBody": "Dile al mercáu la to zona averada pa ver simiente cerca.", + "searching": "Guetando pela to zona…", + "empty": "Entá nun hai simiente compartida cerca de ti", + "near": "Cerca de ti", + "contact": "Mensaxe", + "mine": "Tu", + "configTitle": "Configuración de compartir", + "setupIntro": "Compartir con xente cercano ye opcional. Namás indica la to zona averada — yá tas coneutáu a servidores comunitarios compartíos pa qu'otres persones atopen lo qu'ufiertes, ensin nenguna empresa en mediu.", + "areaLabel": "La to zona", + "areaHelp": "Caltiénse averao a costafecha — la to zona, enxamás un puntu esactu.", + "areaSet": "La to zona ta puesta — averada, enxamás el to puntu esactu", + "areaNotSet": "Zona ensin definir — usa'l to allugamientu, o amiesta un códigu n'Avanzáu", + "advanced": "Avanzáu", + "areaCodeLabel": "Códigu de zona", + "areaCodeHint": "Un códigu curtiu como sp3e9 — non un nome de llugar", + "serversLabel": "Servidores de la comunidá", + "serversHelp": "Una direición per llinia (wss://…). Déxalo asina si nun sabes.", + "serversAdvanced": "Amestar otru servidor", + "save": "Guardar", + "saved": "Guardáu", + "wanted": "Gueto", + "shareMine": "Compartir la mio simiente", + "sharedCount": "Compartíes {n} simientes", + "nothingToShare": "Marca primero dalguna simiente pa regalar, trocar o vender", + "useLocation": "Usar el mio allugamientu averáu", + "locationFailed": "Nun se pudo consiguir el to allugamientu — comprueba que l'allugamientu ta activáu y el permisu concedíu", + "queued": "Guardáu — compartirémosles cuando tengas conexón" + }, + "profile": { + "title": "El to perfil", + "name": "Nome", + "nameHint": "Cómo te ven los demás", + "about": "Sobre ti", + "aboutHint": "Una llinia — qué cultives, ónde", + "g1": "Direición Ğ1 (opcional)", + "g1Hint": "Pa que te paguen en Ğ1 — aparte de la to clave", + "yourId": "La to identidá", + "idHelp": "Compártela pa que te reconozan", + "copy": "Copiar", + "copied": "Copiao", + "save": "Guardar", + "saved": "Perfil guardáu" + }, + "chatList": { + "title": "Mensaxes", + "empty": "Entá nun hai charres. Escríbi-y a alguien dende'l mercáu." + }, + "chat": { + "title": "Charra", + "hint": "Escribi un mensaxe…", + "send": "Unviar", + "empty": "Entá nun hai mensaxes — saluda", + "offline": "Configura'l compartir pa unviar mensaxes", + "payG1": "Pagar en Ğ1", + "g1Copied": "Direición Ğ1 copiada — apégala na to cartera" + }, + "trust": { + "none": "Naide los avala entá", + "count": "Avalada por {n}", + "vouch": "Conozo a esta persona", + "vouched": "Avales a esta persona", + "circle": "Nel to círculu" + } +} diff --git a/apps/app_seeds/lib/i18n/en.i18n.json b/apps/app_seeds/lib/i18n/en.i18n.json index 42be1cd..e9dbfda 100644 --- a/apps/app_seeds/lib/i18n/en.i18n.json +++ b/apps/app_seeds/lib/i18n/en.i18n.json @@ -41,6 +41,7 @@ "langEs": "Español", "langEn": "English", "langPt": "Português", + "langAst": "Asturianu", "about": "About", "aboutText": "Local-first, encrypted inventory for traditional seeds. AGPL-3.0.", "aboutOpen": "About Tanemaki" @@ -347,6 +348,8 @@ "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", diff --git a/apps/app_seeds/lib/i18n/es.i18n.json b/apps/app_seeds/lib/i18n/es.i18n.json index 990c711..bfac317 100644 --- a/apps/app_seeds/lib/i18n/es.i18n.json +++ b/apps/app_seeds/lib/i18n/es.i18n.json @@ -347,6 +347,8 @@ "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ú", diff --git a/apps/app_seeds/lib/i18n/pt.i18n.json b/apps/app_seeds/lib/i18n/pt.i18n.json index ac483c9..f3f8cb5 100644 --- a/apps/app_seeds/lib/i18n/pt.i18n.json +++ b/apps/app_seeds/lib/i18n/pt.i18n.json @@ -41,6 +41,7 @@ "langEs": "Español", "langEn": "English", "langPt": "Português", + "langAst": "Asturianu", "about": "Acerca de", "aboutText": "Inventário local e cifrado para sementes tradicionais. AGPL-3.0.", "aboutOpen": "Acerca do Tanemaki" @@ -343,6 +344,8 @@ "setAreaBody": "Diz ao mercado a tua zona aproximada para ver sementes por perto.", "searching": "A procurar pela tua zona…", "empty": "Ainda não há sementes partilhadas perto de ti", + "searchHint": "Procurar nestas sementes", + "noMatches": "Nenhuma semente partilhada corresponde à procura", "near": "Perto de ti", "contact": "Mensagem", "mine": "Tu", diff --git a/apps/app_seeds/lib/i18n/strings.g.dart b/apps/app_seeds/lib/i18n/strings.g.dart index 9c313ee..990740b 100644 --- a/apps/app_seeds/lib/i18n/strings.g.dart +++ b/apps/app_seeds/lib/i18n/strings.g.dart @@ -3,10 +3,10 @@ /// Source: lib/i18n /// To regenerate, run: `dart run slang` /// -/// Locales: 3 -/// Strings: 1076 (358 per locale) +/// Locales: 4 +/// Strings: 1444 (361 per locale) /// -/// Built on 2026-07-10 at 13:27 UTC +/// Built on 2026-07-10 at 13:48 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import @@ -18,6 +18,7 @@ import 'package:slang/generated.dart'; import 'package:slang_flutter/slang_flutter.dart'; export 'package:slang_flutter/slang_flutter.dart'; +import 'strings_ast.g.dart' as l_ast; import 'strings_es.g.dart' as l_es; import 'strings_pt.g.dart' as l_pt; part 'strings_en.g.dart'; @@ -30,6 +31,7 @@ part 'strings_en.g.dart'; /// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check enum AppLocale with BaseAppLocale { en(languageCode: 'en'), + ast(languageCode: 'ast'), es(languageCode: 'es'), pt(languageCode: 'pt'); @@ -69,6 +71,12 @@ enum AppLocale with BaseAppLocale { cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, ); + case AppLocale.ast: + return l_ast.TranslationsAst( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); case AppLocale.es: return l_es.TranslationsEs( overrides: overrides, diff --git a/apps/app_seeds/lib/i18n/strings_ast.g.dart b/apps/app_seeds/lib/i18n/strings_ast.g.dart new file mode 100644 index 0000000..e3f2d4a --- /dev/null +++ b/apps/app_seeds/lib/i18n/strings_ast.g.dart @@ -0,0 +1,1417 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import +// dart format off + +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:slang/generated.dart'; +import 'strings.g.dart'; + +// Path: +class TranslationsAst extends Translations with BaseTranslations { + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + TranslationsAst({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver, TranslationMetadata? meta}) + : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = meta ?? TranslationMetadata( + locale: AppLocale.ast, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ), + super(cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver) { + super.$meta.setFlatMapFunction($meta.getTranslation); // copy base translations to super.$meta + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override final TranslationMetadata $meta; + + /// Access flat map + @override dynamic operator[](String key) => $meta.getTranslation(key) ?? super.$meta.getTranslation(key); + + late final TranslationsAst _root = this; // ignore: unused_field + + @override + TranslationsAst $copyWith({TranslationMetadata? meta}) => TranslationsAst(meta: meta ?? this.$meta); + + // Translations + @override late final _Translations$app$ast app = _Translations$app$ast._(_root); + @override late final _Translations$common$ast common = _Translations$common$ast._(_root); + @override late final _Translations$home$ast home = _Translations$home$ast._(_root); + @override late final _Translations$photo$ast photo = _Translations$photo$ast._(_root); + @override late final _Translations$menu$ast menu = _Translations$menu$ast._(_root); + @override late final _Translations$settings$ast settings = _Translations$settings$ast._(_root); + @override late final _Translations$backup$ast backup = _Translations$backup$ast._(_root); + @override late final _Translations$about$ast about = _Translations$about$ast._(_root); + @override late final _Translations$intro$ast intro = _Translations$intro$ast._(_root); + @override late final _Translations$inventory$ast inventory = _Translations$inventory$ast._(_root); + @override late final _Translations$draft$ast draft = _Translations$draft$ast._(_root); + @override late final _Translations$quickAdd$ast quickAdd = _Translations$quickAdd$ast._(_root); + @override late final _Translations$detail$ast detail = _Translations$detail$ast._(_root); + @override late final _Translations$germination$ast germination = _Translations$germination$ast._(_root); + @override late final _Translations$viability$ast viability = _Translations$viability$ast._(_root); + @override late final _Translations$editVariety$ast editVariety = _Translations$editVariety$ast._(_root); + @override late final _Translations$addLot$ast addLot = _Translations$addLot$ast._(_root); + @override late final _Translations$harvest$ast harvest = _Translations$harvest$ast._(_root); + @override late final _Translations$lotType$ast lotType = _Translations$lotType$ast._(_root); + @override late final _Translations$presentation$ast presentation = _Translations$presentation$ast._(_root); + @override late final _Translations$provenance$ast provenance = _Translations$provenance$ast._(_root); + @override late final _Translations$abundance$ast abundance = _Translations$abundance$ast._(_root); + @override late final _Translations$share$ast share = _Translations$share$ast._(_root); + @override late final _Translations$cropCalendar$ast cropCalendar = _Translations$cropCalendar$ast._(_root); + @override late final _Translations$needsReproduction$ast needsReproduction = _Translations$needsReproduction$ast._(_root); + @override late final _Translations$preservation$ast preservation = _Translations$preservation$ast._(_root); + @override late final _Translations$conditionCheck$ast conditionCheck = _Translations$conditionCheck$ast._(_root); + @override late final _Translations$desiccant$ast desiccant = _Translations$desiccant$ast._(_root); + @override late final _Translations$unit$ast unit = _Translations$unit$ast._(_root); + @override late final _Translations$market$ast market = _Translations$market$ast._(_root); + @override late final _Translations$profile$ast profile = _Translations$profile$ast._(_root); + @override late final _Translations$chatList$ast chatList = _Translations$chatList$ast._(_root); + @override late final _Translations$chat$ast chat = _Translations$chat$ast._(_root); + @override late final _Translations$trust$ast trust = _Translations$trust$ast._(_root); +} + +// Path: app +class _Translations$app$ast extends Translations$app$en { + _Translations$app$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get title => 'Tanemaki'; +} + +// Path: common +class _Translations$common$ast extends Translations$common$en { + _Translations$common$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get save => 'Guardar'; + @override String get cancel => 'Encaboxar'; + @override String get delete => 'Desaniciar'; + @override String get edit => 'Editar'; + @override String get type => 'Triba'; + @override String get comingSoon => 'Aína'; +} + +// Path: home +class _Translations$home$ast extends Translations$home$en { + _Translations$home$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get tagline => 'Comparte y cultiva simiente llocal'; + @override String get openMarket => 'Mercáu'; + @override String get openMarketSubtitle => 'Descubri y comparte simiente cerca'; + @override String get yourInventory => 'El to inventariu'; + @override String get yourInventorySubtitle => 'Remana la to simiente'; +} + +// Path: photo +class _Translations$photo$ast extends Translations$photo$en { + _Translations$photo$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get camera => 'Facer una semeya'; + @override String get gallery => 'Escoyer de la galería'; + @override String get setAsCover => 'Poner de portada'; + @override String get isCover => 'Semeya de portada'; + @override String get deleteConfirm => '¿Desaniciar esta semeya?'; +} + +// Path: menu +class _Translations$menu$ast extends Translations$menu$en { + _Translations$menu$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get tagline => 'el to bancu de simiente'; + @override String get inventory => 'Inventariu'; + @override String get market => 'Mercáu'; + @override String get profile => 'El to perfil'; + @override String get chat => 'Charra'; + @override String get wishlist => 'Llista de deseos'; + @override String get following => 'Siguiendo'; + @override String get settings => 'Axustes'; +} + +// Path: settings +class _Translations$settings$ast extends Translations$settings$en { + _Translations$settings$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get language => 'Llingua'; + @override String get systemLanguage => 'Llingua del sistema'; + @override String get langEs => 'Español'; + @override String get langEn => 'English'; + @override String get langPt => 'Português'; + @override String get langAst => 'Asturianu'; + @override String get about => 'Tocante a'; + @override String get aboutText => 'Inventariu llocal y cifráu pa simiente tradicional. AGPL-3.0.'; + @override String get aboutOpen => 'Tocante a Tanemaki'; +} + +// Path: backup +class _Translations$backup$ast extends Translations$backup$en { + _Translations$backup$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get title => 'Copia de seguranza'; + @override String get autoBackupTitle => 'Copies automátiques'; + @override String autoBackupLast({required Object date}) => 'Cabera copia guardada\'l ${date}'; + @override String get autoBackupNone => 'Guárdase una copia automáticamente en segundu planu'; + @override String get exportJson => 'Guardar una copia de seguranza'; + @override String get exportJsonSubtitle => 'Una copia completa pa guardar a salvo, restaurar dempués o pasar a otru preséu'; + @override String get importJson => 'Restaurar una copia'; + @override String get importJsonSubtitle => 'Recupera una copia guardada — nun se dobla nada'; + @override String get exportCsv => 'Esportar a una fueya de cálculu'; + @override String get exportCsvSubtitle => 'Una llista cenciella pa Excel o LibreOffice — ensin semeyes'; + @override String get importCsv => 'Importar una llista'; + @override String get importCsvSubtitle => 'Amiesta entraes dende una fueya de cálculu'; + @override String get importConfirmTitle => '¿Restaurar una copia?'; + @override String get importConfirmBody => 'Les entraes fusiónense col to inventariu; si una entrada esiste en dambos llaos, gana la versión más nueva. Nun se dobla nada.'; + @override String get importCsvConfirmTitle => '¿Importar una llista?'; + @override String get importCsvConfirmBody => 'Cada filera amiéstase como una entrada nueva. Nun fusiona nin troca, asina qu\'importar el mesmu ficheru dos veces amiéstalu dos veces.'; + @override String get importAction => 'Importar'; + @override String get exportSaved => 'Copia guardada'; + @override String get cancelled => 'Encaboxáu'; + @override String importDone({required Object added, required Object updated}) => 'Importáu: ${added} nueves, ${updated} anovaes'; + @override String importCsvDone({required Object count}) => 'Amestaes ${count} entraes'; + @override String get importFailed => 'Esti ficheru nun se pudo lleer como una copia de Tanemaki'; + @override String get failed => 'Daqué salió mal'; + @override String get recoveryTitle => 'El to códigu de recuperación'; + @override String get recoverySubtitle => 'Imprémelu y guárdalu bien: abre les tos copies n\'otru preséu'; + @override String get recoveryIntro => 'Esti códigu abre les tos copies guardaes y recupera\'l to bancu en cualquier preséu. Guarda dos copies en papel en sitios seguros, como la to meyor simiente. Quien lu tenga pue lleer les tos copies, asina que nun lu compartas con naide.'; + @override String get recoveryCopy => 'Copiar'; + @override String get recoverySave => 'Guardar la fueya'; + @override String get recoverySheetTitle => 'Tanemaki — la to fueya de recuperación'; + @override String get recoveryPromptTitle => 'Escribi\'l to códigu de recuperación'; + @override String get recoveryPromptBody => 'Esta copia guardóse con otru códigu. Escribi\'l códigu de la to fueya de recuperación p\'abrilla.'; + @override String get recoveryWrongCode => 'Esi códigu nun abre esta copia'; +} + +// Path: about +class _Translations$about$ast extends Translations$about$en { + _Translations$about$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get title => 'Tocante a'; + @override String get kanji => '種まき'; + @override String get tagline => 'Una app local-first y descentralizada pa remanar y compartir simiente y plantones tradicionales.'; + @override String get intro => 'Tanemaki (種まき, «semar / esparder simiente» en xaponés; nome curtiu Tane, 種 «grana») ayuda a persones y coleutivos a llevar un inventariu amable del so bancu de simiente, decidir qué ufierten y compartilo llocalmente — ensin un intermediariu central que pueda controlalo, censuralo o ser multáu por ello. L\'oxetivu ye a la vez práuticu y políticu: sofitar les variedaes tradicionales de simiente y plantar cara al monopoliu simienteru.'; + @override String get heritage => 'El nome honra les vieyes tradiciones xaponeses d\'ayuda mutua alredor del arroz — yui (trabayu comunitariu compartíu) y tanomoshi (fondos de reciprocidá) — qu\'inspiraron el papel Plantare, la «moneda comunitaria pal intercambiu de simiente» (BAH-Semilleru, 2009, CC-BY-SA). Tanemaki ye\'l Plantare dixital.'; + @override String get version => 'Versión'; + @override String get license => 'Llicencia'; + @override String get licenseValue => 'AGPL-3.0'; + @override String get website => 'Sitiu web'; + @override String get openSourceLicenses => 'Llicencies de códigu abiertu'; + @override String get openSourceLicensesSubtitle => 'Biblioteques de terceros y les sos llicencies'; + @override String copyright({required Object years}) => '© ${years} Asociación Comunes, baxo AGPLv3'; +} + +// Path: intro +class _Translations$intro$ast extends Translations$intro$en { + _Translations$intro$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get skip => 'Saltar'; + @override String get next => 'Siguiente'; + @override String get start => 'Entamar'; + @override String get menuEntry => 'Cómo furrula Tanemaki'; + @override late final _Translations$intro$slides$ast slides = _Translations$intro$slides$ast._(_root); +} + +// Path: inventory +class _Translations$inventory$ast extends Translations$inventory$en { + _Translations$inventory$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get title => 'Inventariu'; + @override String get searchHint => 'Guetar simiente'; + @override String get empty => 'Entá nun hai simiente. Toca + p\'amestar la primera.'; + @override String get noMatches => 'Nenguna simiente concasa colos filtros.'; + @override String get clearFilters => 'Quitar filtros'; + @override String get uncategorized => 'Ensin categoría'; + @override String get needsReproductionFilter => 'Por reproducir'; +} + +// Path: draft +class _Translations$draft$ast extends Translations$draft$en { + _Translations$draft$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get capture => 'Capturar semeyes'; + @override String captured({required Object n}) => '${n} capturaes por catalogar'; + @override String get triageTitle => 'Por catalogar'; + @override String triageCount({required Object n}) => '${n} por catalogar'; + @override String get untitled => 'Ensin nome'; + @override String get nameField => 'Ponle nome a esta simiente'; + @override String get nameHint => '¿Qué ye?'; + @override String get suggestFromPhoto => 'Suxerir nome de la semeya'; + @override String get discard => 'Descartar'; +} + +// Path: quickAdd +class _Translations$quickAdd$ast extends Translations$quickAdd$en { + _Translations$quickAdd$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get title => 'Amestar una simiente'; + @override String get labelField => 'Nome'; + @override String get labelRequired => 'Ponle un nome'; + @override String get addPhoto => 'Amestar semeya'; + @override String get quantity => '¿Cuánta?'; + @override String get more => 'Amestar más…'; + @override String get save => 'Guardar'; + @override String get saveAndAddAnother => 'Guardar y amestar otra'; + @override String addedCount({required Object n}) => '${n} amestaes'; + @override String get cancel => 'Encaboxar'; +} + +// Path: detail +class _Translations$detail$ast extends Translations$detail$en { + _Translations$detail$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get notFound => 'Esta simiente yá nun ta equí.'; + @override String get lots => 'Llotes'; + @override String get noLots => 'Entá nun hai llotes.'; + @override String get names => 'Conocida tamién como'; + @override String get addName => 'Amestar nome'; + @override String get links => 'Enllaces'; + @override String get addLink => 'Amestar enllaz'; + @override String get linkUrl => 'URL'; + @override String get linkTitle => 'Títulu (opcional)'; + @override String get reference => 'Saber más'; + @override String get refGbif => 'GBIF'; + @override String get refWikipedia => 'Wikipedia'; + @override String get refWikispecies => 'Wikispecies'; + @override String get notes => 'Notes'; + @override String get addLot => 'Amestar llote'; + @override String get editLot => 'Editar llote'; + @override String get deleteConfirm => '¿Desaniciar esta simiente?'; + @override String year({required Object year}) => 'Añu ${year}'; + @override String get noYear => 'Añu desconocíu'; +} + +// Path: germination +class _Translations$germination$ast extends Translations$germination$en { + _Translations$germination$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get title => 'Germinación'; + @override String get add => 'Amestar prueba'; + @override String get sampleSize => 'Amuesa'; + @override String get germinated => 'Germinaes'; + @override String get none => 'Entá nun hai pruebes de germinación.'; + @override String result({required Object percent}) => '${percent}%'; +} + +// Path: viability +class _Translations$viability$ast extends Translations$viability$en { + _Translations$viability$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get expiringSoon => 'Úsala o multiplícala esta temporada'; + @override String expiringSoonYears({required Object years}) => 'Úsala o multiplícala esta temporada · dura ~${years} años'; + @override String get expired => 'Pasa la so viabilidá típica — a multiplicar'; + @override String expiredYears({required Object years}) => 'Pasa la so viabilidá típica (~${years} años) — a multiplicar'; +} + +// Path: editVariety +class _Translations$editVariety$ast extends Translations$editVariety$en { + _Translations$editVariety$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get title => 'Editar simiente'; + @override String get name => 'Nome'; + @override String get category => 'Categoría'; + @override String get notes => 'Notes'; + @override String get species => 'Especie (del catálogu)'; + @override String get speciesHint => 'Guetar una especie…'; + @override String get speciesSuggested => 'Suxerida pol nome'; + @override String get organic => 'Ecolóxica'; + @override String get organicHint => 'Cultivada de mou ecolóxicu (eco)'; +} + +// Path: addLot +class _Translations$addLot$ast extends Translations$addLot$en { + _Translations$addLot$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get title => 'Amestar llote'; + @override String get year => 'Data de coyecha'; + @override String get quantity => '¿Cuánta?'; + @override String get amount => 'Cantidá'; +} + +// Path: harvest +class _Translations$harvest$ast extends Translations$harvest$en { + _Translations$harvest$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get pickTitle => 'Escoyer mes / añu'; + @override String get anyMonth => 'Cualquier mes'; + @override String get noDate => 'Indicar data de coyecha'; + @override List get monthNames => [ + 'xineru', + 'febreru', + 'marzu', + 'abril', + 'mayu', + 'xunu', + 'xunetu', + 'agostu', + 'setiembre', + 'ochobre', + 'payares', + 'avientu', + ]; +} + +// Path: lotType +class _Translations$lotType$ast extends Translations$lotType$en { + _Translations$lotType$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get seed => 'Simiente'; + @override String get plant => 'Planta'; + @override String get seedling => 'Planton'; + @override String get tree => 'Árbol / matu'; + @override String get bulb => 'Bulbu / tubérculu'; + @override String get cutting => 'Esqueje'; +} + +// Path: presentation +class _Translations$presentation$ast extends Translations$presentation$en { + _Translations$presentation$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get title => 'Envase'; + @override String get none => 'Ensin especificar'; + @override String get pot => 'Tiestu'; + @override String get tray => 'Bandexa'; + @override String get plug => 'Alvéolu'; + @override String get bareRoot => 'Raíz esnuda'; + @override String get rootBall => 'Cepellón'; +} + +// Path: provenance +class _Translations$provenance$ast extends Translations$provenance$en { + _Translations$provenance$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get section => 'D\'ónde vien'; + @override String get seedsFrom => 'Simiente de'; + @override String get seedsFromHint => 'Quién la cultivó o la dio'; + @override String get place => 'Llugar'; + @override String get placeHint => 'D\'ónde vien (con provincia)'; + @override String get addSeedsFrom => 'Simiente de'; + @override String get addPlace => 'Llugar'; +} + +// Path: abundance +class _Translations$abundance$ast extends Translations$abundance$en { + _Translations$abundance$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get add => 'Cuánta tengo'; + @override String get title => 'Cuánta tengo'; + @override String get none => 'Ensin indicar'; + @override String get plentyToShare => 'A esgaya pa compartir'; + @override String get enoughToShare => 'Abondo, pa compartir con mesura'; + @override String get enoughForMe => 'Abondo pa min'; + @override String get runningLow => 'Queda poca'; +} + +// Path: share +class _Translations$share$ast extends Translations$share$en { + _Translations$share$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get add => '¿Compártesla?'; + @override String get title => '¿Compártesla?'; + @override String get nudge => 'Tienes a esgaya: podríes compartir un poco.'; + @override String get private => 'Namás pa min'; + @override String get gift => 'Pa regalar'; + @override String get exchange => 'Pa trocar'; + @override String get sell => 'En venta'; + @override String get filterChip => 'Comparto'; + @override String get printCatalog => 'Imprentar lo que comparto'; + @override String get catalogTitle => 'Lo que comparto'; + @override String get catalogSaved => 'Catálogu guardáu'; + @override String get cancelled => 'Encaboxáu'; +} + +// Path: cropCalendar +class _Translations$cropCalendar$ast extends Translations$cropCalendar$en { + _Translations$cropCalendar$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get add => 'Calendariu de cultivu'; + @override String get title => 'Calendariu de cultivu'; + @override String get sow => 'Sema'; + @override String get transplant => 'Tresplante'; + @override String get flowering => 'Floriamientu'; + @override String get fruiting => 'Fructificación'; + @override String get seedHarvest => 'Coyecha de simiente'; + @override String get unset => '—'; +} + +// Path: needsReproduction +class _Translations$needsReproduction$ast extends Translations$needsReproduction$en { + _Translations$needsReproduction$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get label => 'Reproducir esta temporada'; + @override String get hint => 'Cultívala enantes de que s\'acabe la simiente'; + @override String get badge => 'Por reproducir'; +} + +// Path: preservation +class _Translations$preservation$ast extends Translations$preservation$en { + _Translations$preservation$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get add => 'Cómo se caltién'; + @override String get title => 'Cómo se caltién'; + @override String get none => 'Ensin especificar'; + @override String get jarWithDesiccant => 'Bote con desecante'; + @override String get glassJar => 'Bote de cristal'; + @override String get paperEnvelope => 'Sobre de papel'; + @override String get paperBag => 'Bolsa de papel'; + @override String get plasticBag => 'Bolsa de plásticu'; +} + +// Path: conditionCheck +class _Translations$conditionCheck$ast extends Translations$conditionCheck$en { + _Translations$conditionCheck$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get advanced => 'Caltenimientu y detalles del bancu'; + @override String get title => 'Revisiones de caltenimientu'; + @override String get add => 'Amestar revisión'; + @override String get containers => 'Botes / recipientes'; + @override String get desiccant => 'Desecante'; + @override String get none => 'Entá nun hai revisiones.'; + @override String summary({required Object count, required Object state}) => '${count} bote(s) · ${state}'; +} + +// Path: desiccant +class _Translations$desiccant$ast extends Translations$desiccant$en { + _Translations$desiccant$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get none => 'Nun tien'; + @override String get add => 'Pónse'; + @override String get replace => 'Cámbiase'; + @override String get dry => 'Azul — secu'; + @override String get fresh => 'Recién puestu'; +} + +// Path: unit +class _Translations$unit$ast extends Translations$unit$en { + _Translations$unit$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get aFew => 'delles poques'; + @override String get some => 'dalgunes'; + @override String get plenty => 'munches'; + @override String get pinch => 'una migaya'; + @override late final _Translations$unit$handful$ast handful = _Translations$unit$handful$ast._(_root); + @override late final _Translations$unit$teaspoon$ast teaspoon = _Translations$unit$teaspoon$ast._(_root); + @override late final _Translations$unit$spoon$ast spoon = _Translations$unit$spoon$ast._(_root); + @override late final _Translations$unit$cup$ast cup = _Translations$unit$cup$ast._(_root); + @override late final _Translations$unit$jar$ast jar = _Translations$unit$jar$ast._(_root); + @override late final _Translations$unit$sack$ast sack = _Translations$unit$sack$ast._(_root); + @override late final _Translations$unit$packet$ast packet = _Translations$unit$packet$ast._(_root); + @override late final _Translations$unit$cob$ast cob = _Translations$unit$cob$ast._(_root); + @override late final _Translations$unit$pod$ast pod = _Translations$unit$pod$ast._(_root); + @override late final _Translations$unit$ear$ast ear = _Translations$unit$ear$ast._(_root); + @override late final _Translations$unit$head$ast head = _Translations$unit$head$ast._(_root); + @override late final _Translations$unit$fruit$ast fruit = _Translations$unit$fruit$ast._(_root); + @override late final _Translations$unit$bulb$ast bulb = _Translations$unit$bulb$ast._(_root); + @override late final _Translations$unit$tuber$ast tuber = _Translations$unit$tuber$ast._(_root); + @override late final _Translations$unit$seedHead$ast seedHead = _Translations$unit$seedHead$ast._(_root); + @override late final _Translations$unit$bunch$ast bunch = _Translations$unit$bunch$ast._(_root); + @override late final _Translations$unit$plant$ast plant = _Translations$unit$plant$ast._(_root); + @override late final _Translations$unit$pot$ast pot = _Translations$unit$pot$ast._(_root); + @override late final _Translations$unit$tray$ast tray = _Translations$unit$tray$ast._(_root); + @override late final _Translations$unit$seedling$ast seedling = _Translations$unit$seedling$ast._(_root); + @override late final _Translations$unit$tree$ast tree = _Translations$unit$tree$ast._(_root); + @override late final _Translations$unit$cutting$ast cutting = _Translations$unit$cutting$ast._(_root); + @override late final _Translations$unit$grams$ast grams = _Translations$unit$grams$ast._(_root); + @override late final _Translations$unit$count$ast count = _Translations$unit$count$ast._(_root); +} + +// Path: market +class _Translations$market$ast extends Translations$market$en { + _Translations$market$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get title => 'Simiente cerca de ti'; + @override String get subtitle => 'Lo qu\'otres persones comparten cerca'; + @override String get notSetUp => 'Entá nun configurasti\'l compartir'; + @override String get notSetUpBody => 'Activa\'l compartir pa ver y dar simiente a xente cercano. Caltiénse averao — la to zona, enxamás la to direición esacta.'; + @override String get setUp => 'Configurar el compartir'; + @override String get cantReach => 'Nun se pue coneutar colos servidores agora mesmo'; + @override String get cantReachBody => 'Inténtalo otra vez, o revisa los servidores na configuración avanzada.'; + @override String get retry => 'Retentar'; + @override String get setArea => 'Indica la to zona'; + @override String get setAreaBody => 'Dile al mercáu la to zona averada pa ver simiente cerca.'; + @override String get searching => 'Guetando pela to zona…'; + @override String get empty => 'Entá nun hai simiente compartida cerca de ti'; + @override String get near => 'Cerca de ti'; + @override String get contact => 'Mensaxe'; + @override String get mine => 'Tu'; + @override String get configTitle => 'Configuración de compartir'; + @override String get setupIntro => 'Compartir con xente cercano ye opcional. Namás indica la to zona averada — yá tas coneutáu a servidores comunitarios compartíos pa qu\'otres persones atopen lo qu\'ufiertes, ensin nenguna empresa en mediu.'; + @override String get areaLabel => 'La to zona'; + @override String get areaHelp => 'Caltiénse averao a costafecha — la to zona, enxamás un puntu esactu.'; + @override String get areaSet => 'La to zona ta puesta — averada, enxamás el to puntu esactu'; + @override String get areaNotSet => 'Zona ensin definir — usa\'l to allugamientu, o amiesta un códigu n\'Avanzáu'; + @override String get advanced => 'Avanzáu'; + @override String get areaCodeLabel => 'Códigu de zona'; + @override String get areaCodeHint => 'Un códigu curtiu como sp3e9 — non un nome de llugar'; + @override String get serversLabel => 'Servidores de la comunidá'; + @override String get serversHelp => 'Una direición per llinia (wss://…). Déxalo asina si nun sabes.'; + @override String get serversAdvanced => 'Amestar otru servidor'; + @override String get save => 'Guardar'; + @override String get saved => 'Guardáu'; + @override String get wanted => 'Gueto'; + @override String get shareMine => 'Compartir la mio simiente'; + @override String sharedCount({required Object n}) => 'Compartíes ${n} simientes'; + @override String get nothingToShare => 'Marca primero dalguna simiente pa regalar, trocar o vender'; + @override String get useLocation => 'Usar el mio allugamientu averáu'; + @override String get locationFailed => 'Nun se pudo consiguir el to allugamientu — comprueba que l\'allugamientu ta activáu y el permisu concedíu'; + @override String get queued => 'Guardáu — compartirémosles cuando tengas conexón'; +} + +// Path: profile +class _Translations$profile$ast extends Translations$profile$en { + _Translations$profile$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get title => 'El to perfil'; + @override String get name => 'Nome'; + @override String get nameHint => 'Cómo te ven los demás'; + @override String get about => 'Sobre ti'; + @override String get aboutHint => 'Una llinia — qué cultives, ónde'; + @override String get g1 => 'Direición Ğ1 (opcional)'; + @override String get g1Hint => 'Pa que te paguen en Ğ1 — aparte de la to clave'; + @override String get yourId => 'La to identidá'; + @override String get idHelp => 'Compártela pa que te reconozan'; + @override String get copy => 'Copiar'; + @override String get copied => 'Copiao'; + @override String get save => 'Guardar'; + @override String get saved => 'Perfil guardáu'; +} + +// Path: chatList +class _Translations$chatList$ast extends Translations$chatList$en { + _Translations$chatList$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get title => 'Mensaxes'; + @override String get empty => 'Entá nun hai charres. Escríbi-y a alguien dende\'l mercáu.'; +} + +// Path: chat +class _Translations$chat$ast extends Translations$chat$en { + _Translations$chat$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get title => 'Charra'; + @override String get hint => 'Escribi un mensaxe…'; + @override String get send => 'Unviar'; + @override String get empty => 'Entá nun hai mensaxes — saluda'; + @override String get offline => 'Configura\'l compartir pa unviar mensaxes'; + @override String get payG1 => 'Pagar en Ğ1'; + @override String get g1Copied => 'Direición Ğ1 copiada — apégala na to cartera'; +} + +// Path: trust +class _Translations$trust$ast extends Translations$trust$en { + _Translations$trust$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get none => 'Naide los avala entá'; + @override String count({required Object n}) => 'Avalada por ${n}'; + @override String get vouch => 'Conozo a esta persona'; + @override String get vouched => 'Avales a esta persona'; + @override String get circle => 'Nel to círculu'; +} + +// Path: intro.slides +class _Translations$intro$slides$ast extends Translations$intro$slides$en { + _Translations$intro$slides$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override late final _Translations$intro$slides$welcome$ast welcome = _Translations$intro$slides$welcome$ast._(_root); + @override late final _Translations$intro$slides$inventory$ast inventory = _Translations$intro$slides$inventory$ast._(_root); + @override late final _Translations$intro$slides$privacy$ast privacy = _Translations$intro$slides$privacy$ast._(_root); + @override late final _Translations$intro$slides$share$ast share = _Translations$intro$slides$share$ast._(_root); + @override late final _Translations$intro$slides$plantare$ast plantare = _Translations$intro$slides$plantare$ast._(_root); +} + +// Path: unit.handful +class _Translations$unit$handful$ast extends Translations$unit$handful$en { + _Translations$unit$handful$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'puñáu'; + @override String get plural => 'puñaos'; +} + +// Path: unit.teaspoon +class _Translations$unit$teaspoon$ast extends Translations$unit$teaspoon$en { + _Translations$unit$teaspoon$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'cuyaradina'; + @override String get plural => 'cuyaradines'; +} + +// Path: unit.spoon +class _Translations$unit$spoon$ast extends Translations$unit$spoon$en { + _Translations$unit$spoon$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'cuyar'; + @override String get plural => 'cuyares'; +} + +// Path: unit.cup +class _Translations$unit$cup$ast extends Translations$unit$cup$en { + _Translations$unit$cup$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'taza'; + @override String get plural => 'tazes'; +} + +// Path: unit.jar +class _Translations$unit$jar$ast extends Translations$unit$jar$en { + _Translations$unit$jar$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'bote'; + @override String get plural => 'botes'; +} + +// Path: unit.sack +class _Translations$unit$sack$ast extends Translations$unit$sack$en { + _Translations$unit$sack$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'sacu'; + @override String get plural => 'sacos'; +} + +// Path: unit.packet +class _Translations$unit$packet$ast extends Translations$unit$packet$en { + _Translations$unit$packet$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'sobre'; + @override String get plural => 'sobres'; +} + +// Path: unit.cob +class _Translations$unit$cob$ast extends Translations$unit$cob$en { + _Translations$unit$cob$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'panoya'; + @override String get plural => 'panoyes'; +} + +// Path: unit.pod +class _Translations$unit$pod$ast extends Translations$unit$pod$en { + _Translations$unit$pod$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'vaina'; + @override String get plural => 'vaines'; +} + +// Path: unit.ear +class _Translations$unit$ear$ast extends Translations$unit$ear$en { + _Translations$unit$ear$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'espiga'; + @override String get plural => 'espigues'; +} + +// Path: unit.head +class _Translations$unit$head$ast extends Translations$unit$head$en { + _Translations$unit$head$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'cabezuela'; + @override String get plural => 'cabezueles'; +} + +// Path: unit.fruit +class _Translations$unit$fruit$ast extends Translations$unit$fruit$en { + _Translations$unit$fruit$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'frutu'; + @override String get plural => 'frutos'; +} + +// Path: unit.bulb +class _Translations$unit$bulb$ast extends Translations$unit$bulb$en { + _Translations$unit$bulb$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'bulbu'; + @override String get plural => 'bulbos'; +} + +// Path: unit.tuber +class _Translations$unit$tuber$ast extends Translations$unit$tuber$en { + _Translations$unit$tuber$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'tubérculu'; + @override String get plural => 'tubérculos'; +} + +// Path: unit.seedHead +class _Translations$unit$seedHead$ast extends Translations$unit$seedHead$en { + _Translations$unit$seedHead$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'cabeza de simiente'; + @override String get plural => 'cabeces de simiente'; +} + +// Path: unit.bunch +class _Translations$unit$bunch$ast extends Translations$unit$bunch$en { + _Translations$unit$bunch$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'manoyu'; + @override String get plural => 'manoyos'; +} + +// Path: unit.plant +class _Translations$unit$plant$ast extends Translations$unit$plant$en { + _Translations$unit$plant$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'planta'; + @override String get plural => 'plantes'; +} + +// Path: unit.pot +class _Translations$unit$pot$ast extends Translations$unit$pot$en { + _Translations$unit$pot$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'tiestu'; + @override String get plural => 'tiestos'; +} + +// Path: unit.tray +class _Translations$unit$tray$ast extends Translations$unit$tray$en { + _Translations$unit$tray$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'bandexa'; + @override String get plural => 'bandexes'; +} + +// Path: unit.seedling +class _Translations$unit$seedling$ast extends Translations$unit$seedling$en { + _Translations$unit$seedling$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'plántula'; + @override String get plural => 'plántules'; +} + +// Path: unit.tree +class _Translations$unit$tree$ast extends Translations$unit$tree$en { + _Translations$unit$tree$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'árbol'; + @override String get plural => 'árboles'; +} + +// Path: unit.cutting +class _Translations$unit$cutting$ast extends Translations$unit$cutting$en { + _Translations$unit$cutting$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'esqueje'; + @override String get plural => 'esquejes'; +} + +// Path: unit.grams +class _Translations$unit$grams$ast extends Translations$unit$grams$en { + _Translations$unit$grams$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'gramu'; + @override String get plural => 'gramos'; +} + +// Path: unit.count +class _Translations$unit$count$ast extends Translations$unit$count$en { + _Translations$unit$count$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get singular => 'grana'; + @override String get plural => 'granes'; +} + +// Path: intro.slides.welcome +class _Translations$intro$slides$welcome$ast extends Translations$intro$slides$welcome$en { + _Translations$intro$slides$welcome$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get title => 'La grana que te traxo hasta equí'; + @override String get body => 'Cada simiente tradicional ye una carta escrita por miles de xeneraciones, que pasó de mano en mano. Somos lo que somos gracies a esi intercambiu.'; +} + +// Path: intro.slides.inventory +class _Translations$intro$slides$inventory$ast extends Translations$intro$slides$inventory$en { + _Translations$intro$slides$inventory$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get title => 'El to bancu de simiente, nel bolsu'; + @override String get body => 'Apunta qué tienes, de qué añu, cuánto y d\'ónde vieno, col nome que tu uses. Una semeya y un nome abasten pa entamar.'; +} + +// Path: intro.slides.privacy +class _Translations$intro$slides$privacy$ast extends Translations$intro$slides$privacy$en { + _Translations$intro$slides$privacy$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get title => 'Tuyo y namás tuyo'; + @override String get body => 'Ensin cuenta, ensin internet, ensin rastrexadores. Los tos datos viven cifraos nel to preséu y namás se comparte lo que tu decidas.'; +} + +// Path: intro.slides.share +class _Translations$intro$slides$share$ast extends Translations$intro$slides$share$en { + _Translations$intro$slides$share$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get title => 'Compartir, como siempres se fizo'; + @override String get body => 'Ufierta lo que te sobra —regalu, truecu o venta— y qu\'alguien cerca lo atope. Namás s\'amuesa la distancia averada, enxamás la to direición; el tratu ciérraslu en persona.'; +} + +// Path: intro.slides.plantare +class _Translations$intro$slides$plantare$ast extends Translations$intro$slides$plantare$en { + _Translations$intro$slides$plantare$ast._(TranslationsAst root) : this._root = root, super.internal(root); + + final TranslationsAst _root; // ignore: unused_field + + // Translations + @override String get title => 'Semar ye multiplicar'; + @override String get body => 'Col Plantare, quien recibe promete devolver simiente más alantre. Y como pa devolvela hai que cultivala, cada préstamu multiplica\'l común.'; +} + +/// The flat map containing all translations for locale . +/// Only for edge cases! For simple maps, use the map function of this library. +/// +/// The Dart AOT compiler has issues with very large switch statements, +/// so the map is split into smaller functions (512 entries each). +extension on TranslationsAst { + dynamic _flatMapFunction(String path) { + return switch (path) { + 'app.title' => 'Tanemaki', + 'common.save' => 'Guardar', + 'common.cancel' => 'Encaboxar', + 'common.delete' => 'Desaniciar', + 'common.edit' => 'Editar', + 'common.type' => 'Triba', + 'common.comingSoon' => 'Aína', + 'home.tagline' => 'Comparte y cultiva simiente llocal', + 'home.openMarket' => 'Mercáu', + 'home.openMarketSubtitle' => 'Descubri y comparte simiente cerca', + 'home.yourInventory' => 'El to inventariu', + 'home.yourInventorySubtitle' => 'Remana la to simiente', + 'photo.camera' => 'Facer una semeya', + 'photo.gallery' => 'Escoyer de la galería', + 'photo.setAsCover' => 'Poner de portada', + 'photo.isCover' => 'Semeya de portada', + 'photo.deleteConfirm' => '¿Desaniciar esta semeya?', + 'menu.tagline' => 'el to bancu de simiente', + 'menu.inventory' => 'Inventariu', + 'menu.market' => 'Mercáu', + 'menu.profile' => 'El to perfil', + 'menu.chat' => 'Charra', + 'menu.wishlist' => 'Llista de deseos', + 'menu.following' => 'Siguiendo', + 'menu.settings' => 'Axustes', + 'settings.language' => 'Llingua', + 'settings.systemLanguage' => 'Llingua del sistema', + 'settings.langEs' => 'Español', + 'settings.langEn' => 'English', + 'settings.langPt' => 'Português', + 'settings.langAst' => 'Asturianu', + 'settings.about' => 'Tocante a', + 'settings.aboutText' => 'Inventariu llocal y cifráu pa simiente tradicional. AGPL-3.0.', + 'settings.aboutOpen' => 'Tocante a Tanemaki', + 'backup.title' => 'Copia de seguranza', + 'backup.autoBackupTitle' => 'Copies automátiques', + 'backup.autoBackupLast' => ({required Object date}) => 'Cabera copia guardada\'l ${date}', + 'backup.autoBackupNone' => 'Guárdase una copia automáticamente en segundu planu', + 'backup.exportJson' => 'Guardar una copia de seguranza', + 'backup.exportJsonSubtitle' => 'Una copia completa pa guardar a salvo, restaurar dempués o pasar a otru preséu', + 'backup.importJson' => 'Restaurar una copia', + 'backup.importJsonSubtitle' => 'Recupera una copia guardada — nun se dobla nada', + 'backup.exportCsv' => 'Esportar a una fueya de cálculu', + 'backup.exportCsvSubtitle' => 'Una llista cenciella pa Excel o LibreOffice — ensin semeyes', + 'backup.importCsv' => 'Importar una llista', + 'backup.importCsvSubtitle' => 'Amiesta entraes dende una fueya de cálculu', + 'backup.importConfirmTitle' => '¿Restaurar una copia?', + 'backup.importConfirmBody' => 'Les entraes fusiónense col to inventariu; si una entrada esiste en dambos llaos, gana la versión más nueva. Nun se dobla nada.', + 'backup.importCsvConfirmTitle' => '¿Importar una llista?', + 'backup.importCsvConfirmBody' => 'Cada filera amiéstase como una entrada nueva. Nun fusiona nin troca, asina qu\'importar el mesmu ficheru dos veces amiéstalu dos veces.', + 'backup.importAction' => 'Importar', + 'backup.exportSaved' => 'Copia guardada', + 'backup.cancelled' => 'Encaboxáu', + 'backup.importDone' => ({required Object added, required Object updated}) => 'Importáu: ${added} nueves, ${updated} anovaes', + 'backup.importCsvDone' => ({required Object count}) => 'Amestaes ${count} entraes', + 'backup.importFailed' => 'Esti ficheru nun se pudo lleer como una copia de Tanemaki', + 'backup.failed' => 'Daqué salió mal', + 'backup.recoveryTitle' => 'El to códigu de recuperación', + 'backup.recoverySubtitle' => 'Imprémelu y guárdalu bien: abre les tos copies n\'otru preséu', + 'backup.recoveryIntro' => 'Esti códigu abre les tos copies guardaes y recupera\'l to bancu en cualquier preséu. Guarda dos copies en papel en sitios seguros, como la to meyor simiente. Quien lu tenga pue lleer les tos copies, asina que nun lu compartas con naide.', + 'backup.recoveryCopy' => 'Copiar', + 'backup.recoverySave' => 'Guardar la fueya', + 'backup.recoverySheetTitle' => 'Tanemaki — la to fueya de recuperación', + 'backup.recoveryPromptTitle' => 'Escribi\'l to códigu de recuperación', + 'backup.recoveryPromptBody' => 'Esta copia guardóse con otru códigu. Escribi\'l códigu de la to fueya de recuperación p\'abrilla.', + 'backup.recoveryWrongCode' => 'Esi códigu nun abre esta copia', + 'about.title' => 'Tocante a', + 'about.kanji' => '種まき', + 'about.tagline' => 'Una app local-first y descentralizada pa remanar y compartir simiente y plantones tradicionales.', + 'about.intro' => 'Tanemaki (種まき, «semar / esparder simiente» en xaponés; nome curtiu Tane, 種 «grana») ayuda a persones y coleutivos a llevar un inventariu amable del so bancu de simiente, decidir qué ufierten y compartilo llocalmente — ensin un intermediariu central que pueda controlalo, censuralo o ser multáu por ello. L\'oxetivu ye a la vez práuticu y políticu: sofitar les variedaes tradicionales de simiente y plantar cara al monopoliu simienteru.', + 'about.heritage' => 'El nome honra les vieyes tradiciones xaponeses d\'ayuda mutua alredor del arroz — yui (trabayu comunitariu compartíu) y tanomoshi (fondos de reciprocidá) — qu\'inspiraron el papel Plantare, la «moneda comunitaria pal intercambiu de simiente» (BAH-Semilleru, 2009, CC-BY-SA). Tanemaki ye\'l Plantare dixital.', + 'about.version' => 'Versión', + 'about.license' => 'Llicencia', + 'about.licenseValue' => 'AGPL-3.0', + 'about.website' => 'Sitiu web', + 'about.openSourceLicenses' => 'Llicencies de códigu abiertu', + 'about.openSourceLicensesSubtitle' => 'Biblioteques de terceros y les sos llicencies', + 'about.copyright' => ({required Object years}) => '© ${years} Asociación Comunes, baxo AGPLv3', + 'intro.skip' => 'Saltar', + 'intro.next' => 'Siguiente', + 'intro.start' => 'Entamar', + 'intro.menuEntry' => 'Cómo furrula Tanemaki', + 'intro.slides.welcome.title' => 'La grana que te traxo hasta equí', + 'intro.slides.welcome.body' => 'Cada simiente tradicional ye una carta escrita por miles de xeneraciones, que pasó de mano en mano. Somos lo que somos gracies a esi intercambiu.', + 'intro.slides.inventory.title' => 'El to bancu de simiente, nel bolsu', + 'intro.slides.inventory.body' => 'Apunta qué tienes, de qué añu, cuánto y d\'ónde vieno, col nome que tu uses. Una semeya y un nome abasten pa entamar.', + 'intro.slides.privacy.title' => 'Tuyo y namás tuyo', + 'intro.slides.privacy.body' => 'Ensin cuenta, ensin internet, ensin rastrexadores. Los tos datos viven cifraos nel to preséu y namás se comparte lo que tu decidas.', + 'intro.slides.share.title' => 'Compartir, como siempres se fizo', + 'intro.slides.share.body' => 'Ufierta lo que te sobra —regalu, truecu o venta— y qu\'alguien cerca lo atope. Namás s\'amuesa la distancia averada, enxamás la to direición; el tratu ciérraslu en persona.', + 'intro.slides.plantare.title' => 'Semar ye multiplicar', + 'intro.slides.plantare.body' => 'Col Plantare, quien recibe promete devolver simiente más alantre. Y como pa devolvela hai que cultivala, cada préstamu multiplica\'l común.', + 'inventory.title' => 'Inventariu', + 'inventory.searchHint' => 'Guetar simiente', + 'inventory.empty' => 'Entá nun hai simiente. Toca + p\'amestar la primera.', + 'inventory.noMatches' => 'Nenguna simiente concasa colos filtros.', + 'inventory.clearFilters' => 'Quitar filtros', + 'inventory.uncategorized' => 'Ensin categoría', + 'inventory.needsReproductionFilter' => 'Por reproducir', + 'draft.capture' => 'Capturar semeyes', + 'draft.captured' => ({required Object n}) => '${n} capturaes por catalogar', + 'draft.triageTitle' => 'Por catalogar', + 'draft.triageCount' => ({required Object n}) => '${n} por catalogar', + 'draft.untitled' => 'Ensin nome', + 'draft.nameField' => 'Ponle nome a esta simiente', + 'draft.nameHint' => '¿Qué ye?', + 'draft.suggestFromPhoto' => 'Suxerir nome de la semeya', + 'draft.discard' => 'Descartar', + 'quickAdd.title' => 'Amestar una simiente', + 'quickAdd.labelField' => 'Nome', + 'quickAdd.labelRequired' => 'Ponle un nome', + 'quickAdd.addPhoto' => 'Amestar semeya', + 'quickAdd.quantity' => '¿Cuánta?', + 'quickAdd.more' => 'Amestar más…', + 'quickAdd.save' => 'Guardar', + 'quickAdd.saveAndAddAnother' => 'Guardar y amestar otra', + 'quickAdd.addedCount' => ({required Object n}) => '${n} amestaes', + 'quickAdd.cancel' => 'Encaboxar', + 'detail.notFound' => 'Esta simiente yá nun ta equí.', + 'detail.lots' => 'Llotes', + 'detail.noLots' => 'Entá nun hai llotes.', + 'detail.names' => 'Conocida tamién como', + 'detail.addName' => 'Amestar nome', + 'detail.links' => 'Enllaces', + 'detail.addLink' => 'Amestar enllaz', + 'detail.linkUrl' => 'URL', + 'detail.linkTitle' => 'Títulu (opcional)', + 'detail.reference' => 'Saber más', + 'detail.refGbif' => 'GBIF', + 'detail.refWikipedia' => 'Wikipedia', + 'detail.refWikispecies' => 'Wikispecies', + 'detail.notes' => 'Notes', + 'detail.addLot' => 'Amestar llote', + 'detail.editLot' => 'Editar llote', + 'detail.deleteConfirm' => '¿Desaniciar esta simiente?', + 'detail.year' => ({required Object year}) => 'Añu ${year}', + 'detail.noYear' => 'Añu desconocíu', + 'germination.title' => 'Germinación', + 'germination.add' => 'Amestar prueba', + 'germination.sampleSize' => 'Amuesa', + 'germination.germinated' => 'Germinaes', + 'germination.none' => 'Entá nun hai pruebes de germinación.', + 'germination.result' => ({required Object percent}) => '${percent}%', + 'viability.expiringSoon' => 'Úsala o multiplícala esta temporada', + 'viability.expiringSoonYears' => ({required Object years}) => 'Úsala o multiplícala esta temporada · dura ~${years} años', + 'viability.expired' => 'Pasa la so viabilidá típica — a multiplicar', + 'viability.expiredYears' => ({required Object years}) => 'Pasa la so viabilidá típica (~${years} años) — a multiplicar', + 'editVariety.title' => 'Editar simiente', + 'editVariety.name' => 'Nome', + 'editVariety.category' => 'Categoría', + 'editVariety.notes' => 'Notes', + 'editVariety.species' => 'Especie (del catálogu)', + 'editVariety.speciesHint' => 'Guetar una especie…', + 'editVariety.speciesSuggested' => 'Suxerida pol nome', + 'editVariety.organic' => 'Ecolóxica', + 'editVariety.organicHint' => 'Cultivada de mou ecolóxicu (eco)', + 'addLot.title' => 'Amestar llote', + 'addLot.year' => 'Data de coyecha', + 'addLot.quantity' => '¿Cuánta?', + 'addLot.amount' => 'Cantidá', + 'harvest.pickTitle' => 'Escoyer mes / añu', + 'harvest.anyMonth' => 'Cualquier mes', + 'harvest.noDate' => 'Indicar data de coyecha', + 'harvest.monthNames.0' => 'xineru', + 'harvest.monthNames.1' => 'febreru', + 'harvest.monthNames.2' => 'marzu', + 'harvest.monthNames.3' => 'abril', + 'harvest.monthNames.4' => 'mayu', + 'harvest.monthNames.5' => 'xunu', + 'harvest.monthNames.6' => 'xunetu', + 'harvest.monthNames.7' => 'agostu', + 'harvest.monthNames.8' => 'setiembre', + 'harvest.monthNames.9' => 'ochobre', + 'harvest.monthNames.10' => 'payares', + 'harvest.monthNames.11' => 'avientu', + 'lotType.seed' => 'Simiente', + 'lotType.plant' => 'Planta', + 'lotType.seedling' => 'Planton', + 'lotType.tree' => 'Árbol / matu', + 'lotType.bulb' => 'Bulbu / tubérculu', + 'lotType.cutting' => 'Esqueje', + 'presentation.title' => 'Envase', + 'presentation.none' => 'Ensin especificar', + 'presentation.pot' => 'Tiestu', + 'presentation.tray' => 'Bandexa', + 'presentation.plug' => 'Alvéolu', + 'presentation.bareRoot' => 'Raíz esnuda', + 'presentation.rootBall' => 'Cepellón', + 'provenance.section' => 'D\'ónde vien', + 'provenance.seedsFrom' => 'Simiente de', + 'provenance.seedsFromHint' => 'Quién la cultivó o la dio', + 'provenance.place' => 'Llugar', + 'provenance.placeHint' => 'D\'ónde vien (con provincia)', + 'provenance.addSeedsFrom' => 'Simiente de', + 'provenance.addPlace' => 'Llugar', + 'abundance.add' => 'Cuánta tengo', + 'abundance.title' => 'Cuánta tengo', + 'abundance.none' => 'Ensin indicar', + 'abundance.plentyToShare' => 'A esgaya pa compartir', + 'abundance.enoughToShare' => 'Abondo, pa compartir con mesura', + 'abundance.enoughForMe' => 'Abondo pa min', + 'abundance.runningLow' => 'Queda poca', + 'share.add' => '¿Compártesla?', + 'share.title' => '¿Compártesla?', + 'share.nudge' => 'Tienes a esgaya: podríes compartir un poco.', + 'share.private' => 'Namás pa min', + 'share.gift' => 'Pa regalar', + 'share.exchange' => 'Pa trocar', + 'share.sell' => 'En venta', + 'share.filterChip' => 'Comparto', + 'share.printCatalog' => 'Imprentar lo que comparto', + 'share.catalogTitle' => 'Lo que comparto', + 'share.catalogSaved' => 'Catálogu guardáu', + 'share.cancelled' => 'Encaboxáu', + 'cropCalendar.add' => 'Calendariu de cultivu', + 'cropCalendar.title' => 'Calendariu de cultivu', + 'cropCalendar.sow' => 'Sema', + 'cropCalendar.transplant' => 'Tresplante', + 'cropCalendar.flowering' => 'Floriamientu', + 'cropCalendar.fruiting' => 'Fructificación', + 'cropCalendar.seedHarvest' => 'Coyecha de simiente', + 'cropCalendar.unset' => '—', + 'needsReproduction.label' => 'Reproducir esta temporada', + 'needsReproduction.hint' => 'Cultívala enantes de que s\'acabe la simiente', + 'needsReproduction.badge' => 'Por reproducir', + 'preservation.add' => 'Cómo se caltién', + 'preservation.title' => 'Cómo se caltién', + 'preservation.none' => 'Ensin especificar', + 'preservation.jarWithDesiccant' => 'Bote con desecante', + 'preservation.glassJar' => 'Bote de cristal', + 'preservation.paperEnvelope' => 'Sobre de papel', + 'preservation.paperBag' => 'Bolsa de papel', + 'preservation.plasticBag' => 'Bolsa de plásticu', + 'conditionCheck.advanced' => 'Caltenimientu y detalles del bancu', + 'conditionCheck.title' => 'Revisiones de caltenimientu', + 'conditionCheck.add' => 'Amestar revisión', + 'conditionCheck.containers' => 'Botes / recipientes', + 'conditionCheck.desiccant' => 'Desecante', + 'conditionCheck.none' => 'Entá nun hai revisiones.', + 'conditionCheck.summary' => ({required Object count, required Object state}) => '${count} bote(s) · ${state}', + 'desiccant.none' => 'Nun tien', + 'desiccant.add' => 'Pónse', + 'desiccant.replace' => 'Cámbiase', + 'desiccant.dry' => 'Azul — secu', + 'desiccant.fresh' => 'Recién puestu', + 'unit.aFew' => 'delles poques', + 'unit.some' => 'dalgunes', + 'unit.plenty' => 'munches', + 'unit.pinch' => 'una migaya', + 'unit.handful.singular' => 'puñáu', + 'unit.handful.plural' => 'puñaos', + 'unit.teaspoon.singular' => 'cuyaradina', + 'unit.teaspoon.plural' => 'cuyaradines', + 'unit.spoon.singular' => 'cuyar', + 'unit.spoon.plural' => 'cuyares', + 'unit.cup.singular' => 'taza', + 'unit.cup.plural' => 'tazes', + 'unit.jar.singular' => 'bote', + 'unit.jar.plural' => 'botes', + 'unit.sack.singular' => 'sacu', + 'unit.sack.plural' => 'sacos', + 'unit.packet.singular' => 'sobre', + 'unit.packet.plural' => 'sobres', + 'unit.cob.singular' => 'panoya', + 'unit.cob.plural' => 'panoyes', + 'unit.pod.singular' => 'vaina', + 'unit.pod.plural' => 'vaines', + 'unit.ear.singular' => 'espiga', + 'unit.ear.plural' => 'espigues', + 'unit.head.singular' => 'cabezuela', + 'unit.head.plural' => 'cabezueles', + 'unit.fruit.singular' => 'frutu', + 'unit.fruit.plural' => 'frutos', + 'unit.bulb.singular' => 'bulbu', + 'unit.bulb.plural' => 'bulbos', + 'unit.tuber.singular' => 'tubérculu', + 'unit.tuber.plural' => 'tubérculos', + 'unit.seedHead.singular' => 'cabeza de simiente', + 'unit.seedHead.plural' => 'cabeces de simiente', + 'unit.bunch.singular' => 'manoyu', + 'unit.bunch.plural' => 'manoyos', + 'unit.plant.singular' => 'planta', + 'unit.plant.plural' => 'plantes', + 'unit.pot.singular' => 'tiestu', + 'unit.pot.plural' => 'tiestos', + 'unit.tray.singular' => 'bandexa', + 'unit.tray.plural' => 'bandexes', + 'unit.seedling.singular' => 'plántula', + 'unit.seedling.plural' => 'plántules', + 'unit.tree.singular' => 'árbol', + 'unit.tree.plural' => 'árboles', + 'unit.cutting.singular' => 'esqueje', + 'unit.cutting.plural' => 'esquejes', + 'unit.grams.singular' => 'gramu', + 'unit.grams.plural' => 'gramos', + 'unit.count.singular' => 'grana', + 'unit.count.plural' => 'granes', + 'market.title' => 'Simiente cerca de ti', + 'market.subtitle' => 'Lo qu\'otres persones comparten cerca', + 'market.notSetUp' => 'Entá nun configurasti\'l compartir', + 'market.notSetUpBody' => 'Activa\'l compartir pa ver y dar simiente a xente cercano. Caltiénse averao — la to zona, enxamás la to direición esacta.', + 'market.setUp' => 'Configurar el compartir', + 'market.cantReach' => 'Nun se pue coneutar colos servidores agora mesmo', + 'market.cantReachBody' => 'Inténtalo otra vez, o revisa los servidores na configuración avanzada.', + 'market.retry' => 'Retentar', + 'market.setArea' => 'Indica la to zona', + 'market.setAreaBody' => 'Dile al mercáu la to zona averada pa ver simiente cerca.', + 'market.searching' => 'Guetando pela to zona…', + 'market.empty' => 'Entá nun hai simiente compartida cerca de ti', + 'market.near' => 'Cerca de ti', + 'market.contact' => 'Mensaxe', + 'market.mine' => 'Tu', + 'market.configTitle' => 'Configuración de compartir', + 'market.setupIntro' => 'Compartir con xente cercano ye opcional. Namás indica la to zona averada — yá tas coneutáu a servidores comunitarios compartíos pa qu\'otres persones atopen lo qu\'ufiertes, ensin nenguna empresa en mediu.', + 'market.areaLabel' => 'La to zona', + 'market.areaHelp' => 'Caltiénse averao a costafecha — la to zona, enxamás un puntu esactu.', + 'market.areaSet' => 'La to zona ta puesta — averada, enxamás el to puntu esactu', + 'market.areaNotSet' => 'Zona ensin definir — usa\'l to allugamientu, o amiesta un códigu n\'Avanzáu', + 'market.advanced' => 'Avanzáu', + 'market.areaCodeLabel' => 'Códigu de zona', + 'market.areaCodeHint' => 'Un códigu curtiu como sp3e9 — non un nome de llugar', + 'market.serversLabel' => 'Servidores de la comunidá', + 'market.serversHelp' => 'Una direición per llinia (wss://…). Déxalo asina si nun sabes.', + 'market.serversAdvanced' => 'Amestar otru servidor', + 'market.save' => 'Guardar', + 'market.saved' => 'Guardáu', + 'market.wanted' => 'Gueto', + 'market.shareMine' => 'Compartir la mio simiente', + 'market.sharedCount' => ({required Object n}) => 'Compartíes ${n} simientes', + 'market.nothingToShare' => 'Marca primero dalguna simiente pa regalar, trocar o vender', + 'market.useLocation' => 'Usar el mio allugamientu averáu', + 'market.locationFailed' => 'Nun se pudo consiguir el to allugamientu — comprueba que l\'allugamientu ta activáu y el permisu concedíu', + 'market.queued' => 'Guardáu — compartirémosles cuando tengas conexón', + 'profile.title' => 'El to perfil', + 'profile.name' => 'Nome', + 'profile.nameHint' => 'Cómo te ven los demás', + 'profile.about' => 'Sobre ti', + 'profile.aboutHint' => 'Una llinia — qué cultives, ónde', + 'profile.g1' => 'Direición Ğ1 (opcional)', + 'profile.g1Hint' => 'Pa que te paguen en Ğ1 — aparte de la to clave', + 'profile.yourId' => 'La to identidá', + 'profile.idHelp' => 'Compártela pa que te reconozan', + 'profile.copy' => 'Copiar', + 'profile.copied' => 'Copiao', + 'profile.save' => 'Guardar', + 'profile.saved' => 'Perfil guardáu', + 'chatList.title' => 'Mensaxes', + 'chatList.empty' => 'Entá nun hai charres. Escríbi-y a alguien dende\'l mercáu.', + 'chat.title' => 'Charra', + 'chat.hint' => 'Escribi un mensaxe…', + 'chat.send' => 'Unviar', + 'chat.empty' => 'Entá nun hai mensaxes — saluda', + 'chat.offline' => 'Configura\'l compartir pa unviar mensaxes', + 'chat.payG1' => 'Pagar en Ğ1', + 'chat.g1Copied' => 'Direición Ğ1 copiada — apégala na to cartera', + 'trust.none' => 'Naide los avala entá', + 'trust.count' => ({required Object n}) => 'Avalada por ${n}', + 'trust.vouch' => 'Conozo a esta persona', + 'trust.vouched' => 'Avales a esta persona', + 'trust.circle' => 'Nel to círculu', + _ => null, + }; + } +} diff --git a/apps/app_seeds/lib/i18n/strings_en.g.dart b/apps/app_seeds/lib/i18n/strings_en.g.dart index 537e057..af2c0e6 100644 --- a/apps/app_seeds/lib/i18n/strings_en.g.dart +++ b/apps/app_seeds/lib/i18n/strings_en.g.dart @@ -222,6 +222,9 @@ class Translations$settings$en { /// en: 'Português' String get langPt => 'Português'; + /// en: 'Asturianu' + String get langAst => 'Asturianu'; + /// en: 'About' String get about => 'About'; @@ -1104,6 +1107,12 @@ class Translations$market$en { /// en: 'No seeds shared near you yet' String get empty => 'No seeds shared near you yet'; + /// en: 'Search these seeds' + String get searchHint => 'Search these seeds'; + + /// en: 'No shared seeds match your search' + String get noMatches => 'No shared seeds match your search'; + /// en: 'Near you' String get near => 'Near you'; @@ -1782,6 +1791,7 @@ extension on Translations { 'settings.langEs' => 'Español', 'settings.langEn' => 'English', 'settings.langPt' => 'Português', + 'settings.langAst' => 'Asturianu', 'settings.about' => 'About', 'settings.aboutText' => 'Local-first, encrypted inventory for traditional seeds. AGPL-3.0.', 'settings.aboutOpen' => 'About Tanemaki', @@ -2060,6 +2070,8 @@ extension on Translations { 'market.setAreaBody' => 'Tell the market roughly where you are to see seeds nearby.', 'market.searching' => 'Looking around your area…', 'market.empty' => 'No seeds shared near you yet', + 'market.searchHint' => 'Search these seeds', + 'market.noMatches' => 'No shared seeds match your search', 'market.near' => 'Near you', 'market.contact' => 'Message', 'market.mine' => 'You', diff --git a/apps/app_seeds/lib/i18n/strings_es.g.dart b/apps/app_seeds/lib/i18n/strings_es.g.dart index 341cc31..7695e63 100644 --- a/apps/app_seeds/lib/i18n/strings_es.g.dart +++ b/apps/app_seeds/lib/i18n/strings_es.g.dart @@ -621,6 +621,8 @@ class _Translations$market$es extends Translations$market$en { @override String get setAreaBody => 'Dile al mercado tu zona aproximada para ver semillas cerca.'; @override String get searching => 'Buscando por tu zona…'; @override String get empty => 'Aún no hay semillas compartidas cerca de ti'; + @override String get searchHint => 'Buscar entre estas semillas'; + @override String get noMatches => 'Ninguna semilla compartida coincide con tu búsqueda'; @override String get near => 'Cerca de ti'; @override String get contact => 'Mensaje'; @override String get mine => 'Tú'; @@ -1360,6 +1362,8 @@ extension on TranslationsEs { 'market.setAreaBody' => 'Dile al mercado tu zona aproximada para ver semillas cerca.', 'market.searching' => 'Buscando por tu zona…', 'market.empty' => 'Aún no hay semillas compartidas cerca de ti', + 'market.searchHint' => 'Buscar entre estas semillas', + 'market.noMatches' => 'Ninguna semilla compartida coincide con tu búsqueda', 'market.near' => 'Cerca de ti', 'market.contact' => 'Mensaje', 'market.mine' => 'Tú', diff --git a/apps/app_seeds/lib/i18n/strings_pt.g.dart b/apps/app_seeds/lib/i18n/strings_pt.g.dart index 8dcdf17..d872cc2 100644 --- a/apps/app_seeds/lib/i18n/strings_pt.g.dart +++ b/apps/app_seeds/lib/i18n/strings_pt.g.dart @@ -158,6 +158,7 @@ class _Translations$settings$pt extends Translations$settings$en { @override String get langEs => 'Español'; @override String get langEn => 'English'; @override String get langPt => 'Português'; + @override String get langAst => 'Asturianu'; @override String get about => 'Acerca de'; @override String get aboutText => 'Inventário local e cifrado para sementes tradicionais. AGPL-3.0.'; @override String get aboutOpen => 'Acerca do Tanemaki'; @@ -617,6 +618,8 @@ class _Translations$market$pt extends Translations$market$en { @override String get setAreaBody => 'Diz ao mercado a tua zona aproximada para ver sementes por perto.'; @override String get searching => 'A procurar pela tua zona…'; @override String get empty => 'Ainda não há sementes partilhadas perto de ti'; + @override String get searchHint => 'Procurar nestas sementes'; + @override String get noMatches => 'Nenhuma semente partilhada corresponde à procura'; @override String get near => 'Perto de ti'; @override String get contact => 'Mensagem'; @override String get mine => 'Tu'; @@ -1078,6 +1081,7 @@ extension on TranslationsPt { 'settings.langEs' => 'Español', 'settings.langEn' => 'English', 'settings.langPt' => 'Português', + 'settings.langAst' => 'Asturianu', 'settings.about' => 'Acerca de', 'settings.aboutText' => 'Inventário local e cifrado para sementes tradicionais. AGPL-3.0.', 'settings.aboutOpen' => 'Acerca do Tanemaki', @@ -1352,6 +1356,8 @@ extension on TranslationsPt { 'market.setAreaBody' => 'Diz ao mercado a tua zona aproximada para ver sementes por perto.', 'market.searching' => 'A procurar pela tua zona…', 'market.empty' => 'Ainda não há sementes partilhadas perto de ti', + 'market.searchHint' => 'Procurar nestas sementes', + 'market.noMatches' => 'Nenhuma semente partilhada corresponde à procura', 'market.near' => 'Perto de ti', 'market.contact' => 'Mensagem', 'market.mine' => 'Tu', diff --git a/apps/app_seeds/lib/main.dart b/apps/app_seeds/lib/main.dart index f978dfd..977c9f7 100644 --- a/apps/app_seeds/lib/main.dart +++ b/apps/app_seeds/lib/main.dart @@ -7,6 +7,7 @@ import 'di/injector.dart'; import 'i18n/strings.g.dart'; import 'services/auto_backup_service.dart'; import 'services/coarse_location.dart'; +import 'services/locale_store.dart'; import 'services/message_store.dart'; import 'services/offer_outbox.dart'; import 'services/onboarding_store.dart'; @@ -17,8 +18,13 @@ import 'services/social_settings.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); + // Start from the device language, then let an explicit in-app choice win — + // it's the only reliable way to reach languages the OS picker doesn't offer + // (e.g. Asturian). Restored after DI, since the store lives in the keystore. LocaleSettings.useDeviceLocaleSync(); await configureDependencies(); + final savedLocale = await getIt().saved(); + if (savedLocale != null) LocaleSettings.setLocaleSync(savedLocale); final onboarding = getIt(); runApp( TranslationProvider( diff --git a/apps/app_seeds/lib/services/locale_store.dart b/apps/app_seeds/lib/services/locale_store.dart new file mode 100644 index 0000000..2abf399 --- /dev/null +++ b/apps/app_seeds/lib/services/locale_store.dart @@ -0,0 +1,45 @@ +import '../i18n/strings.g.dart'; +import '../security/secret_store.dart'; + +/// Remembers the language the user picked in Settings so it survives restarts. +/// An empty/absent value means "follow the device language". Backed by the OS +/// keystore (via [SecretStore]), like the rest of the app's small preferences — +/// honouring "no plaintext at rest". +/// +/// Why persist at all: the device language picker is an unreliable way to reach +/// minority languages (Android often doesn't offer Asturian, and slang would +/// fall back to the device default on every launch), so the choice made in-app +/// must be remembered explicitly. Asturian itself is modelled as the `es-AST` +/// locale (Spanish language + `AST` region), not `ast`, so Flutter's +/// Material/Cupertino localizations — which don't ship `ast` — resolve it +/// through Spanish instead of throwing. See [SettingsScreen] for the picker. +class LocaleStore { + LocaleStore(this._store); + + final SecretStore _store; + + static const _key = 'tane.locale'; + + /// The saved locale, or null when the user follows the device language. + /// Unknown tags (e.g. a locale removed in a later build) also read as null. + Future saved() async { + final raw = await _store.read(_key); + if (raw == null || raw.isEmpty) return null; + for (final locale in AppLocale.values) { + if (locale.name == raw) return locale; + } + return null; + } + + /// Persists [locale] and applies it immediately. + Future setLocale(AppLocale locale) async { + await _store.write(_key, locale.name); + await LocaleSettings.setLocale(locale); + } + + /// Forgets the explicit choice and follows the device language from now on. + Future useDeviceLocale() async { + await _store.write(_key, ''); + await LocaleSettings.useDeviceLocale(); + } +} diff --git a/apps/app_seeds/lib/state/offers_cubit.dart b/apps/app_seeds/lib/state/offers_cubit.dart index 053f6e7..979c5fc 100644 --- a/apps/app_seeds/lib/state/offers_cubit.dart +++ b/apps/app_seeds/lib/state/offers_cubit.dart @@ -16,6 +16,7 @@ class OffersState extends Equatable { const OffersState({ this.offers = const [], this.areaGeohash = '', + this.query = '', this.searching = false, this.publishing = false, this.hasSearched = false, @@ -28,6 +29,9 @@ class OffersState extends Equatable { /// The coarse area currently being browsed. final String areaGeohash; + /// Free-text filter over [offers] typed in the search box (empty = show all). + final String query; + final bool searching; final bool publishing; @@ -38,9 +42,18 @@ class OffersState extends Equatable { /// Last error, in human terms for the UI (null when fine). final String? error; + /// [offers] narrowed by [query], matched against the offer summary. Kept + /// separate from [offers] so a search never drops the underlying discoveries. + List get visibleOffers { + final q = query.trim().toLowerCase(); + if (q.isEmpty) return offers; + return offers.where((o) => o.summary.toLowerCase().contains(q)).toList(); + } + OffersState copyWith({ List? offers, String? areaGeohash, + String? query, bool? searching, bool? publishing, bool? hasSearched, @@ -49,6 +62,7 @@ class OffersState extends Equatable { return OffersState( offers: offers ?? this.offers, areaGeohash: areaGeohash ?? this.areaGeohash, + query: query ?? this.query, searching: searching ?? this.searching, publishing: publishing ?? this.publishing, hasSearched: hasSearched ?? this.hasSearched, @@ -58,7 +72,7 @@ class OffersState extends Equatable { @override List get props => - [offers, areaGeohash, searching, publishing, hasSearched, error]; + [offers, areaGeohash, query, searching, publishing, hasSearched, error]; } /// Drives offer discovery and publishing over an [OfferTransport]. Depends on @@ -92,6 +106,7 @@ class OffersCubit extends Cubit { _searchTimeout?.cancel(); emit(OffersState( areaGeohash: geohashPrefix, + query: state.query, // keep the text filter across a refresh searching: true, hasSearched: true, )); @@ -111,6 +126,10 @@ class OffersCubit extends Cubit { }); } + /// Narrows the visible offers to those whose summary matches [query]. Purely + /// local over the already-discovered list; does not re-hit the transport. + void search(String query) => emit(state.copyWith(query: query)); + /// Publishes [offer]; returns the transport's verdict. No-op result when /// offline. Future publish(Offer offer) async { diff --git a/apps/app_seeds/lib/ui/backup_section.dart b/apps/app_seeds/lib/ui/backup_section.dart index 953c1cd..5a0ca82 100644 --- a/apps/app_seeds/lib/ui/backup_section.dart +++ b/apps/app_seeds/lib/ui/backup_section.dart @@ -360,8 +360,11 @@ class _AutoBackupTileState extends State<_AutoBackupTile> { final subtitle = last == null ? t.backup.autoBackupNone : t.backup.autoBackupLast( + // Format via the Localizations locale, not the raw app locale: + // Asturian (`ast`) has no `intl` date symbols and would throw, + // but it's mapped to Spanish for the framework (see app.dart). date: DateFormat.yMMMd( - LocaleSettings.currentLocale.languageCode, + Localizations.localeOf(context).languageCode, ).format(last), ); return ListTile( diff --git a/apps/app_seeds/lib/ui/market_screen.dart b/apps/app_seeds/lib/ui/market_screen.dart index 6784d7c..9fdb886 100644 --- a/apps/app_seeds/lib/ui/market_screen.dart +++ b/apps/app_seeds/lib/ui/market_screen.dart @@ -240,26 +240,95 @@ class MarketBody extends StatelessWidget { ), ); } + final cubit = context.read(); + // Pull-to-refresh re-runs discovery for the current area (keeping the + // text filter), so the list is never stuck on a stale scan. + Future refresh() => cubit.discover(state.areaGeohash); + + // Nothing discovered at all: keep the friendly empty state, but make it + // pull-to-refreshable so a swipe re-scans. if (state.offers.isEmpty) { - return _EmptyState( - icon: Icons.grass_outlined, - title: t.market.empty, - body: '', + return RefreshIndicator( + onRefresh: refresh, + child: ListView( + // AlwaysScrollable so the pull gesture works with a single child. + physics: const AlwaysScrollableScrollPhysics(), + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * 0.6, + child: _EmptyState( + icon: Icons.grass_outlined, + title: t.market.empty, + body: '', + ), + ), + ], + ), ); } - return ListView.separated( - padding: const EdgeInsets.all(16), - itemCount: state.offers.length, - separatorBuilder: (_, _) => const SizedBox(height: 12), - itemBuilder: (context, i) { - final o = state.offers[i]; - final mine = o.authorPubkeyHex == selfPubkey; - return _OfferCard( - offer: o, - mine: mine, - onContact: mine ? null : () => context.push('/chat/${o.authorPubkeyHex}'), - ); - }, + + final visible = state.visibleOffers; + return Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 4), + child: TextField( + key: const Key('market.search'), + decoration: InputDecoration( + hintText: t.market.searchHint, + prefixIcon: const Icon(Icons.search, color: seedMuted), + hintStyle: const TextStyle(color: seedMuted), + filled: true, + fillColor: Colors.white, + isDense: true, + contentPadding: const EdgeInsets.symmetric(vertical: 14), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(28), + borderSide: BorderSide.none, + ), + ), + onChanged: cubit.search, + ), + ), + Expanded( + child: RefreshIndicator( + onRefresh: refresh, + child: visible.isEmpty + // Discovered offers exist, but the search filtered them out. + ? ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * 0.5, + child: _EmptyState( + icon: Icons.search_off, + title: t.market.noMatches, + body: '', + ), + ), + ], + ) + : ListView.separated( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + itemCount: visible.length, + separatorBuilder: (_, _) => const SizedBox(height: 12), + itemBuilder: (context, i) { + final o = visible[i]; + final mine = o.authorPubkeyHex == selfPubkey; + return _OfferCard( + offer: o, + mine: mine, + onContact: mine + ? null + : () => + context.push('/chat/${o.authorPubkeyHex}'), + ); + }, + ), + ), + ), + ], ); }, ); diff --git a/apps/app_seeds/lib/ui/settings_screen.dart b/apps/app_seeds/lib/ui/settings_screen.dart index cdd8f7c..aeaadcd 100644 --- a/apps/app_seeds/lib/ui/settings_screen.dart +++ b/apps/app_seeds/lib/ui/settings_screen.dart @@ -1,23 +1,30 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import '../di/injector.dart'; import '../i18n/strings.g.dart'; import '../services/export_import_service.dart'; +import '../services/locale_store.dart'; import 'backup_section.dart'; import 'theme.dart'; /// Basic settings: app language, backup (export/import) and an "about" -/// section. [exportImport] is injectable so widget tests can pass a fake; -/// the app resolves it lazily from the service locator. +/// section. [exportImport] and [localeStore] are injectable so widget tests can +/// pass fakes; the app resolves them lazily from the service locator. class SettingsScreen extends StatelessWidget { - const SettingsScreen({this.exportImport, super.key}); + const SettingsScreen({this.exportImport, this.localeStore, super.key}); final ExportImportService? exportImport; + final LocaleStore? localeStore; @override Widget build(BuildContext context) { final t = context.t; - final current = TranslationProvider.of(context).flutterLocale.languageCode; + // Compare the full locale (not just the language code) so `es` and the + // Asturian `es-AST` — which share the `es` language — are told apart. + final current = TranslationProvider.of(context).locale; + void pick(AppLocale locale) => + (localeStore ?? getIt()).setLocale(locale); return Scaffold( appBar: AppBar(title: Text(t.menu.settings)), body: ListView( @@ -25,23 +32,28 @@ class SettingsScreen extends StatelessWidget { _SectionHeader(t.settings.language), _LanguageTile( label: t.settings.langEs, - selected: current == 'es', - onTap: () => LocaleSettings.setLocale(AppLocale.es), + selected: current == AppLocale.es, + onTap: () => pick(AppLocale.es), + ), + _LanguageTile( + label: t.settings.langAst, + selected: current == AppLocale.ast, + onTap: () => pick(AppLocale.ast), ), _LanguageTile( label: t.settings.langEn, - selected: current == 'en', - onTap: () => LocaleSettings.setLocale(AppLocale.en), + selected: current == AppLocale.en, + onTap: () => pick(AppLocale.en), ), _LanguageTile( label: t.settings.langPt, - selected: current == 'pt', - onTap: () => LocaleSettings.setLocale(AppLocale.pt), + selected: current == AppLocale.pt, + onTap: () => pick(AppLocale.pt), ), ListTile( leading: const Icon(Icons.smartphone_outlined), title: Text(t.settings.systemLanguage), - onTap: () => LocaleSettings.useDeviceLocale(), + onTap: () => (localeStore ?? getIt()).useDeviceLocale(), ), const Divider(), _SectionHeader(t.backup.title), diff --git a/apps/app_seeds/test/data/species_catalog_asset_test.dart b/apps/app_seeds/test/data/species_catalog_asset_test.dart index 2ea7509..17c3a27 100644 --- a/apps/app_seeds/test/data/species_catalog_asset_test.dart +++ b/apps/app_seeds/test/data/species_catalog_asset_test.dart @@ -17,6 +17,16 @@ void main() { expect(json['version'], 3); }); + test('speciesCatalogVersion mirrors the asset version', () { + // The startup seed guard skips the reseed by comparing this constant to the + // version it last recorded; it must track the asset or a bumped catalog is + // never re-seeded. + expect( + speciesCatalogVersion, + parseSpeciesCatalogVersion(file.readAsStringSync()), + ); + }); + test('is a broad catalog, not the old starter set', () { // The expanded catalog holds ~1200 species (was 14 by hand). The floor // leaves headroom for Wikidata drift across regenerations. diff --git a/apps/app_seeds/test/data/species_repository_test.dart b/apps/app_seeds/test/data/species_repository_test.dart index bb862e4..9c4a0fe 100644 --- a/apps/app_seeds/test/data/species_repository_test.dart +++ b/apps/app_seeds/test/data/species_repository_test.dart @@ -237,4 +237,50 @@ void main() { // No species named → no suggestion. expect(await repo.classifyLabel('Girasol gigante'), isNull); }); + + group('seedBundledIfNeeded', () { + test('seeds on first run and skips the reload once recorded', () async { + String? stored; + var loads = 0; + Future> load() async { + loads++; + return _seeds; + } + + await repo.seedBundledIfNeeded( + version: 3, + readVersion: () async => stored, + writeVersion: (v) async => stored = v, + loadSeeds: load, + ); + expect(loads, 1); + expect(stored, '3'); + expect(await repo.search('Tomato'), isNotEmpty); + + // Same version already recorded → the ~1.5 MB parse is never loaded again. + await repo.seedBundledIfNeeded( + version: 3, + readVersion: () async => stored, + writeVersion: (v) async => stored = v, + loadSeeds: load, + ); + expect(loads, 1); + }); + + test('reseeds when the catalog version changes', () async { + var stored = '3'; + var loads = 0; + await repo.seedBundledIfNeeded( + version: 4, + readVersion: () async => stored, + writeVersion: (v) async => stored = v, + loadSeeds: () async { + loads++; + return _seeds; + }, + ); + expect(loads, 1); + expect(stored, '4'); + }); + }); } diff --git a/apps/app_seeds/test/services/locale_store_test.dart b/apps/app_seeds/test/services/locale_store_test.dart new file mode 100644 index 0000000..8784a9d --- /dev/null +++ b/apps/app_seeds/test/services/locale_store_test.dart @@ -0,0 +1,54 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tane/i18n/strings.g.dart'; +import 'package:tane/security/secret_store.dart'; +import 'package:tane/services/locale_store.dart'; + +/// In-memory [SecretStore] for tests. +class FakeSecretStore implements SecretStore { + final Map _data = {}; + @override + Future read(String key) async => _data[key]; + @override + Future write(String key, String value) async => _data[key] = value; +} + +void main() { + // setLocale/useDeviceLocale touch the Flutter locale, which needs a binding. + TestWidgetsFlutterBinding.ensureInitialized(); + + late FakeSecretStore secrets; + late LocaleStore store; + + setUp(() { + secrets = FakeSecretStore(); + store = LocaleStore(secrets); + }); + + test('no choice saved yet → follows the device (null)', () async { + expect(await store.saved(), isNull); + }); + + test('picking Asturian persists it and applies it', () async { + await store.setLocale(AppLocale.ast); + + expect(await store.saved(), AppLocale.ast); + expect(LocaleSettings.currentLocale, AppLocale.ast); + // Asturian is modelled as its real `ast` code so the schema stays honest; + // Material chrome borrows Spanish elsewhere (see app.dart). + expect(AppLocale.ast.languageCode, 'ast'); + }); + + test('choosing "system language" forgets the explicit choice', () async { + await store.setLocale(AppLocale.es); + expect(await store.saved(), AppLocale.es); + + await store.useDeviceLocale(); + expect(await store.saved(), isNull); + }); + + test('an unknown stored tag reads as "follow the device", never throws', + () async { + await secrets.write('tane.locale', 'zz-ZZ-removed-in-a-later-build'); + expect(await store.saved(), isNull); + }); +} diff --git a/apps/app_seeds/test/state/offers_cubit_test.dart b/apps/app_seeds/test/state/offers_cubit_test.dart index b8800e1..e1d3f05 100644 --- a/apps/app_seeds/test/state/offers_cubit_test.dart +++ b/apps/app_seeds/test/state/offers_cubit_test.dart @@ -102,6 +102,56 @@ void main() { await cubit.close(); }); + test('search narrows visible offers by summary, keeping the full list', + () async { + final transport = FakeOfferTransport(); + for (final s in ['Tomate rosa', 'Judía verde', 'Tomate rojo']) { + await transport.publish(OfferMapper.fromSharedLot( + lotId: s, + authorPubkeyHex: 'ab' * 32, + summary: s, + sharing: OfferStatus.shared, + areaGeohash: 'sp3e9', + )); + } + final cubit = OffersCubit(transport); + await cubit.discover('sp3'); + await pumpEventQueue(); + expect(cubit.state.offers, hasLength(3)); + + cubit.search('tomate'); // case-insensitive + expect(cubit.state.visibleOffers.map((o) => o.summary), + containsAll(['Tomate rosa', 'Tomate rojo'])); + expect(cubit.state.visibleOffers, hasLength(2)); + expect(cubit.state.offers, hasLength(3)); // underlying list untouched + + cubit.search(''); // clearing shows everything again + expect(cubit.state.visibleOffers, hasLength(3)); + await cubit.close(); + }); + + test('the search filter survives a refresh (re-discovery)', () async { + final transport = FakeOfferTransport(); + await transport.publish(OfferMapper.fromSharedLot( + lotId: 'lot-1', + authorPubkeyHex: 'ab' * 32, + summary: 'Pimiento', + sharing: OfferStatus.shared, + areaGeohash: 'sp3e9', + )); + final cubit = OffersCubit(transport); + await cubit.discover('sp3'); + await pumpEventQueue(); + cubit.search('pim'); + expect(cubit.state.query, 'pim'); + + await cubit.discover('sp3'); // pull-to-refresh + await pumpEventQueue(); + expect(cubit.state.query, 'pim'); // kept across the refresh + expect(cubit.state.visibleOffers, hasLength(1)); + await cubit.close(); + }); + test('a different area finds nothing', () async { final transport = FakeOfferTransport(); await transport.publish(OfferMapper.fromSharedLot( diff --git a/apps/app_seeds/test/ui/asturian_locale_test.dart b/apps/app_seeds/test/ui/asturian_locale_test.dart new file mode 100644 index 0000000..ec7e404 --- /dev/null +++ b/apps/app_seeds/test/ui/asturian_locale_test.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tane/app.dart'; +import 'package:tane/i18n/strings.g.dart'; +import 'package:tane/services/locale_store.dart'; +import 'package:tane/ui/settings_screen.dart'; + +import '../support/test_support.dart'; + +/// Asturian (`ast`) is a first-class language, but Flutter ships no +/// Material/Cupertino localizations for it. The app carries the honest `ast` +/// locale for its own strings and borrows Spanish for the framework chrome +/// (see [materialLocaleFor]). These tests pin both halves of that contract. +void main() { + testWidgets('picking Asturian switches app text to Asturian, keeps Material ' + 'chrome resolvable in Spanish, and persists the choice', (tester) async { + LocaleSettings.setLocaleSync(AppLocale.en); + final store = LocaleStore(InMemorySecretStore()); + late Locale materialLocale; + + await tester.pumpWidget( + TranslationProvider( + child: Builder( + builder: (context) { + final appLocale = TranslationProvider.of(context).flutterLocale; + return MaterialApp( + // Mirrors app.dart: the framework localizations follow the mapped + // locale, the app's own strings follow slang. + locale: materialLocaleFor(appLocale), + supportedLocales: AppLocaleUtils.supportedLocales, + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + home: Builder( + builder: (context) { + // Resolving these must never throw for the Asturian locale. + materialLocale = Localizations.localeOf(context); + MaterialLocalizations.of(context); + return SettingsScreen(localeStore: store); + }, + ), + ); + }, + ), + ), + ); + + // Starts in English. + expect(find.text('Language'), findsOneWidget); + expect(materialLocale.languageCode, 'en'); + + await tester.tap(find.text('Asturianu')); + await tester.pumpAndSettle(); + + // App strings are now Asturian… + expect(find.text('Llingua'), findsOneWidget); + // …the framework chrome fell back to Spanish (not the unsupported `ast`)… + expect(materialLocale.languageCode, 'es'); + // …and the choice was remembered for next launch. + expect(await store.saved(), AppLocale.ast); + }); + + test('materialLocaleFor maps Asturian to Spanish and passes others through', + () { + expect(materialLocaleFor(const Locale('ast')), const Locale('es')); + expect(materialLocaleFor(const Locale('es')), const Locale('es')); + expect(materialLocaleFor(const Locale('en')), const Locale('en')); + expect(materialLocaleFor(const Locale('pt')), const Locale('pt')); + }); +} diff --git a/apps/app_seeds/test/ui/backup_section_test.dart b/apps/app_seeds/test/ui/backup_section_test.dart index 0df8dcf..474bb53 100644 --- a/apps/app_seeds/test/ui/backup_section_test.dart +++ b/apps/app_seeds/test/ui/backup_section_test.dart @@ -14,6 +14,7 @@ import 'package:tane/services/auto_backup_store.dart'; import 'package:tane/services/export_import_service.dart'; import 'package:tane/services/file_service.dart'; import 'package:tane/services/recovery_sheet_service.dart'; +import 'package:tane/app.dart'; import 'package:tane/ui/backup_section.dart'; import 'package:tane/ui/settings_screen.dart'; @@ -254,6 +255,41 @@ void main() { expect(spy.calls, 1); expect(find.text('Copy saved'), findsOneWidget); }); + + testWidgets('the last-backup date renders under Asturian without an ' + 'invalid-locale crash', (tester) async { + // Asturian (`ast`) has no `intl` date symbols; formatting the "last copy" + // date used to throw "Invalid locale ast". The framework locale is mapped + // to Spanish, so the date must format through that instead. + LocaleSettings.setLocaleSync(AppLocale.ast); + addTearDown(() => LocaleSettings.setLocaleSync(AppLocale.en)); + + await tester.pumpWidget( + TranslationProvider( + child: MaterialApp( + locale: materialLocaleFor(const Locale('ast')), + supportedLocales: AppLocaleUtils.supportedLocales, + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + home: Scaffold( + body: BackupSection( + service: service, + sheet: sheet, + autoBackup: _DatedAutoBackup(db), + ), + ), + ), + ), + ); + await tester.pump(); // resolve the "last copy" future + + // The Asturian tile (and its Spanish-formatted date subtitle) built fine. + expect(find.text('Copies automátiques'), findsOneWidget); + expect(tester.takeException(), isNull); + }); } /// Counts explicit "back up now" taps without touching the disk. @@ -277,3 +313,21 @@ class _SpyAutoBackup extends AutoBackupService { return true; } } + +/// Reports a fixed "last copy" date so the auto-backup subtitle formats a real +/// date — the path that crashed under Asturian. +class _DatedAutoBackup extends AutoBackupService { + _DatedAutoBackup(AppDatabase db) + : super( + exporter: ExportImportService( + repository: newTestRepository(db), + files: FakeFileService(), + keys: SecureKeyStore(store: InMemorySecretStore()), + ), + store: AutoBackupStore(InMemorySecretStore()), + directory: () async => throw UnimplementedError(), + ); + + @override + Future lastBackupAt() async => DateTime.utc(2026, 3, 14); +}