i18n: RTL smoke tests, Portuguese locale and a translation guide
- Add rtl_smoke_test covering inventory, detail (lot sheet) and settings under Directionality.rtl, with an Arabic label and vernacular name. - Ship pt.i18n.json (Portuguese) as proof the locale pipeline scales beyond es/en; wire its language tile in Settings. - docs/i18n.md documents the Weblate-ready flow for adding locales.
This commit is contained in:
parent
d6781870d9
commit
5d2b41a110
11 changed files with 1736 additions and 6 deletions
|
|
@ -39,6 +39,7 @@
|
||||||
"systemLanguage": "System language",
|
"systemLanguage": "System language",
|
||||||
"langEs": "Español",
|
"langEs": "Español",
|
||||||
"langEn": "English",
|
"langEn": "English",
|
||||||
|
"langPt": "Português",
|
||||||
"about": "About",
|
"about": "About",
|
||||||
"aboutText": "Local-first, encrypted inventory for traditional seeds. AGPL-3.0.",
|
"aboutText": "Local-first, encrypted inventory for traditional seeds. AGPL-3.0.",
|
||||||
"aboutOpen": "About Tanemaki"
|
"aboutOpen": "About Tanemaki"
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@
|
||||||
"systemLanguage": "Idioma del sistema",
|
"systemLanguage": "Idioma del sistema",
|
||||||
"langEs": "Español",
|
"langEs": "Español",
|
||||||
"langEn": "English",
|
"langEn": "English",
|
||||||
|
"langPt": "Português",
|
||||||
"about": "Acerca de",
|
"about": "Acerca de",
|
||||||
"aboutText": "Inventario local y cifrado para semillas tradicionales. AGPL-3.0.",
|
"aboutText": "Inventario local y cifrado para semillas tradicionales. AGPL-3.0.",
|
||||||
"aboutOpen": "Acerca de Tanemaki"
|
"aboutOpen": "Acerca de Tanemaki"
|
||||||
|
|
|
||||||
327
apps/app_seeds/lib/i18n/pt.i18n.json
Normal file
327
apps/app_seeds/lib/i18n/pt.i18n.json
Normal file
|
|
@ -0,0 +1,327 @@
|
||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"title": "Tanemaki"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"save": "Guardar",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"delete": "Eliminar",
|
||||||
|
"edit": "Editar",
|
||||||
|
"type": "Tipo",
|
||||||
|
"comingSoon": "Em breve"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"tagline": "Partilha e cultiva sementes locais",
|
||||||
|
"openMarket": "Mercado aberto",
|
||||||
|
"openMarketSubtitle": "Descobre e troca",
|
||||||
|
"yourInventory": "O teu inventário",
|
||||||
|
"yourInventorySubtitle": "Gere as tuas sementes"
|
||||||
|
},
|
||||||
|
"photo": {
|
||||||
|
"camera": "Tirar uma foto",
|
||||||
|
"gallery": "Escolher da galeria",
|
||||||
|
"setAsCover": "Usar como capa",
|
||||||
|
"isCover": "Foto de capa",
|
||||||
|
"deleteConfirm": "Eliminar esta foto?"
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"tagline": "o teu banco de sementes",
|
||||||
|
"inventory": "Inventário",
|
||||||
|
"market": "Mercado",
|
||||||
|
"profile": "O teu perfil",
|
||||||
|
"chat": "Conversas",
|
||||||
|
"wishlist": "Lista de desejos",
|
||||||
|
"following": "A seguir",
|
||||||
|
"settings": "Definições"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"language": "Idioma",
|
||||||
|
"systemLanguage": "Idioma do sistema",
|
||||||
|
"langEs": "Español",
|
||||||
|
"langEn": "English",
|
||||||
|
"langPt": "Português",
|
||||||
|
"about": "Acerca de",
|
||||||
|
"aboutText": "Inventário local e cifrado para sementes tradicionais. AGPL-3.0.",
|
||||||
|
"aboutOpen": "Acerca do Tanemaki"
|
||||||
|
},
|
||||||
|
"backup": {
|
||||||
|
"title": "Cópia de segurança",
|
||||||
|
"exportJson": "Guardar uma cópia de segurança",
|
||||||
|
"exportJsonSubtitle": "Uma cópia completa para guardar em segurança, restaurar depois ou levar para outro aparelho",
|
||||||
|
"importJson": "Restaurar uma cópia",
|
||||||
|
"importJsonSubtitle": "Recupera uma cópia guardada — nada fica duplicado",
|
||||||
|
"exportCsv": "Exportar para uma folha de cálculo",
|
||||||
|
"exportCsvSubtitle": "Uma lista simples para Excel ou LibreOffice — sem fotos",
|
||||||
|
"importCsv": "Importar uma lista",
|
||||||
|
"importCsvSubtitle": "Acrescenta entradas a partir de uma folha de cálculo",
|
||||||
|
"importConfirmTitle": "Restaurar uma cópia?",
|
||||||
|
"importConfirmBody": "As entradas fundem-se com o teu inventário; quando a mesma entrada existe dos dois lados, vence a versão mais recente. Nada fica duplicado.",
|
||||||
|
"importCsvConfirmTitle": "Importar uma lista?",
|
||||||
|
"importCsvConfirmBody": "Cada linha é acrescentada como uma entrada nova. Não funde nem substitui, por isso importar o mesmo ficheiro duas vezes acrescenta-o duas vezes.",
|
||||||
|
"importAction": "Importar",
|
||||||
|
"exportSaved": "Cópia guardada",
|
||||||
|
"cancelled": "Cancelado",
|
||||||
|
"importDone": "Importado: {added} novas, {updated} atualizadas",
|
||||||
|
"importCsvDone": "{count} entradas acrescentadas",
|
||||||
|
"importFailed": "Este ficheiro não pôde ser lido como uma cópia do Tanemaki",
|
||||||
|
"failed": "Algo correu mal",
|
||||||
|
"recoveryTitle": "O teu código de recuperação",
|
||||||
|
"recoverySubtitle": "Imprime-o e guarda-o bem: abre as tuas cópias noutro aparelho",
|
||||||
|
"recoveryIntro": "Este código abre as tuas cópias guardadas e recupera o teu banco em qualquer aparelho. Guarda duas cópias em papel em sítios seguros, como a tua melhor semente. Quem o tiver pode ler as tuas cópias, por isso não o partilhes com ninguém.",
|
||||||
|
"recoveryCopy": "Copiar",
|
||||||
|
"recoverySave": "Guardar a folha",
|
||||||
|
"recoverySheetTitle": "Tanemaki — a tua folha de recuperação",
|
||||||
|
"recoveryPromptTitle": "Escreve o teu código de recuperação",
|
||||||
|
"recoveryPromptBody": "Esta cópia foi guardada com outro código. Escreve o código da tua folha de recuperação para a abrir.",
|
||||||
|
"recoveryWrongCode": "Esse código não abre esta cópia"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"title": "Acerca de",
|
||||||
|
"kanji": "種まき",
|
||||||
|
"tagline": "Uma aplicação local e descentralizada para gerir e partilhar sementes e plântulas tradicionais.",
|
||||||
|
"intro": "O Tanemaki (種まき, \"semear / espalhar sementes\" em japonês; nome curto Tane, 種 \"semente\") ajuda pessoas e coletivos a manter um inventário amigável do seu banco de sementes, decidir o que oferecem e partilhá-lo localmente — sem um intermediário central que possa controlar, censurar ou ser multado por isso. O objetivo é prático e político ao mesmo tempo: apoiar as variedades tradicionais e fazer frente ao monopólio das sementes.",
|
||||||
|
"heritage": "O nome honra as antigas tradições japonesas de ajuda mútua à volta do arroz — yui (trabalho comunitário partilhado) e tanomoshi (fundos de reciprocidade) — que inspiraram o Plantare em papel, a \"moeda comunitária de troca de sementes\" (BAH-Semillero, 2009, CC-BY-SA). O Tanemaki é o Plantare digital.",
|
||||||
|
"version": "Versão",
|
||||||
|
"license": "Licença",
|
||||||
|
"licenseValue": "AGPL-3.0",
|
||||||
|
"website": "Sítio web",
|
||||||
|
"openSourceLicenses": "Licenças de código aberto",
|
||||||
|
"openSourceLicensesSubtitle": "Bibliotecas de terceiros e as suas licenças"
|
||||||
|
},
|
||||||
|
"intro": {
|
||||||
|
"skip": "Saltar",
|
||||||
|
"next": "Seguinte",
|
||||||
|
"start": "Começar",
|
||||||
|
"menuEntry": "Como funciona o Tanemaki",
|
||||||
|
"slides": {
|
||||||
|
"welcome": {
|
||||||
|
"title": "A semente que te trouxe até aqui",
|
||||||
|
"body": "Cada semente tradicional é uma carta escrita por milhares de gerações, passada de mão em mão. Somos o que somos graças a essa partilha."
|
||||||
|
},
|
||||||
|
"inventory": {
|
||||||
|
"title": "O teu banco de sementes, no bolso",
|
||||||
|
"body": "Anota o que tens, de que ano, quanto e de onde veio — com o nome que tu usas. Uma foto e um nome chegam para começar."
|
||||||
|
},
|
||||||
|
"privacy": {
|
||||||
|
"title": "Teu, e só teu",
|
||||||
|
"body": "Sem conta, sem internet, sem rastreadores. Os teus dados vivem cifrados no teu aparelho, e só o que tu escolheres é partilhado."
|
||||||
|
},
|
||||||
|
"share": {
|
||||||
|
"title": "Partilhar, como sempre se fez",
|
||||||
|
"body": "Oferece o que te sobra — dar, trocar ou vender — e deixa que alguém perto de ti o encontre. Só se mostra uma distância aproximada, nunca a tua morada; o acordo fecha-se em pessoa."
|
||||||
|
},
|
||||||
|
"plantare": {
|
||||||
|
"title": "Semear é multiplicar",
|
||||||
|
"body": "Com um Plantare, quem recebe semente promete devolver uma parte mais tarde. E como devolvê-la implica cultivá-la, cada empréstimo multiplica o comum."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inventory": {
|
||||||
|
"title": "Inventário",
|
||||||
|
"searchHint": "Procurar sementes",
|
||||||
|
"empty": "Ainda não há sementes. Toca em + para adicionar a primeira.",
|
||||||
|
"noMatches": "Nenhuma semente corresponde aos teus filtros.",
|
||||||
|
"clearFilters": "Limpar filtros",
|
||||||
|
"uncategorized": "Sem categoria",
|
||||||
|
"needsReproductionFilter": "Para reproduzir"
|
||||||
|
},
|
||||||
|
"draft": {
|
||||||
|
"capture": "Capturar fotos",
|
||||||
|
"captured": "{n} capturadas para catalogar",
|
||||||
|
"triageTitle": "Para catalogar",
|
||||||
|
"triageCount": "{n} para catalogar",
|
||||||
|
"untitled": "Sem nome",
|
||||||
|
"nameField": "Dá um nome a esta semente",
|
||||||
|
"nameHint": "O que é?",
|
||||||
|
"suggestFromPhoto": "Sugerir nome a partir da foto",
|
||||||
|
"discard": "Descartar"
|
||||||
|
},
|
||||||
|
"quickAdd": {
|
||||||
|
"title": "Adicionar uma semente",
|
||||||
|
"labelField": "Nome",
|
||||||
|
"labelRequired": "Dá-lhe um nome",
|
||||||
|
"addPhoto": "Adicionar foto",
|
||||||
|
"quantity": "Quanto?",
|
||||||
|
"more": "Adicionar mais…",
|
||||||
|
"save": "Guardar",
|
||||||
|
"saveAndAddAnother": "Guardar e adicionar outra",
|
||||||
|
"addedCount": "{n} adicionadas",
|
||||||
|
"cancel": "Cancelar"
|
||||||
|
},
|
||||||
|
"detail": {
|
||||||
|
"notFound": "Esta semente já não está aqui.",
|
||||||
|
"lots": "Lotes",
|
||||||
|
"noLots": "Ainda não há lotes.",
|
||||||
|
"names": "Também conhecida como",
|
||||||
|
"addName": "Adicionar nome",
|
||||||
|
"links": "Ligações",
|
||||||
|
"addLink": "Adicionar ligação",
|
||||||
|
"linkUrl": "URL",
|
||||||
|
"linkTitle": "Título (opcional)",
|
||||||
|
"notes": "Notas",
|
||||||
|
"addLot": "Adicionar lote",
|
||||||
|
"editLot": "Editar lote",
|
||||||
|
"deleteConfirm": "Eliminar esta semente?",
|
||||||
|
"year": "Ano {year}",
|
||||||
|
"noYear": "Ano desconhecido"
|
||||||
|
},
|
||||||
|
"germination": {
|
||||||
|
"title": "Germinação",
|
||||||
|
"add": "Adicionar teste",
|
||||||
|
"sampleSize": "Tamanho da amostra",
|
||||||
|
"germinated": "Germinadas",
|
||||||
|
"none": "Ainda não há testes de germinação.",
|
||||||
|
"result": "{percent}%"
|
||||||
|
},
|
||||||
|
"viability": {
|
||||||
|
"expiringSoon": "Usa ou reproduz nesta época",
|
||||||
|
"expiringSoonYears": "Usa ou reproduz nesta época · dura ~{years} anos",
|
||||||
|
"expired": "Passou a viabilidade típica — reproduzir",
|
||||||
|
"expiredYears": "Passou a viabilidade típica (~{years} anos) — reproduzir"
|
||||||
|
},
|
||||||
|
"editVariety": {
|
||||||
|
"title": "Editar semente",
|
||||||
|
"name": "Nome",
|
||||||
|
"category": "Categoria",
|
||||||
|
"notes": "Notas",
|
||||||
|
"species": "Espécie (do catálogo)",
|
||||||
|
"speciesHint": "Procura uma espécie…",
|
||||||
|
"organic": "Biológica",
|
||||||
|
"organicHint": "Cultivada em modo biológico"
|
||||||
|
},
|
||||||
|
"addLot": {
|
||||||
|
"title": "Adicionar lote",
|
||||||
|
"year": "Data de colheita",
|
||||||
|
"quantity": "Quanto?",
|
||||||
|
"amount": "Quantidade"
|
||||||
|
},
|
||||||
|
"harvest": {
|
||||||
|
"pickTitle": "Escolhe mês / ano",
|
||||||
|
"anyMonth": "Qualquer mês",
|
||||||
|
"noDate": "Definir data de colheita",
|
||||||
|
"monthNames": [
|
||||||
|
"janeiro", "fevereiro", "março", "abril", "maio", "junho",
|
||||||
|
"julho", "agosto", "setembro", "outubro", "novembro", "dezembro"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lotType": {
|
||||||
|
"seed": "Sementes",
|
||||||
|
"plant": "Planta",
|
||||||
|
"seedling": "Plântula",
|
||||||
|
"tree": "Árvore / arbusto",
|
||||||
|
"bulb": "Bolbo / tubérculo",
|
||||||
|
"cutting": "Estaca"
|
||||||
|
},
|
||||||
|
"presentation": {
|
||||||
|
"title": "Apresentação",
|
||||||
|
"none": "Sem indicar",
|
||||||
|
"pot": "Vaso",
|
||||||
|
"tray": "Tabuleiro",
|
||||||
|
"plug": "Alvéolo",
|
||||||
|
"bareRoot": "Raiz nua",
|
||||||
|
"rootBall": "Torrão"
|
||||||
|
},
|
||||||
|
"provenance": {
|
||||||
|
"section": "De onde vem",
|
||||||
|
"seedsFrom": "Sementes de",
|
||||||
|
"seedsFromHint": "Quem as cultivou ou ofereceu",
|
||||||
|
"place": "Lugar",
|
||||||
|
"placeHint": "De onde vêm (com a região)",
|
||||||
|
"addSeedsFrom": "Sementes de",
|
||||||
|
"addPlace": "Lugar"
|
||||||
|
},
|
||||||
|
"abundance": {
|
||||||
|
"add": "Quanta tenho",
|
||||||
|
"title": "Quanta tenho",
|
||||||
|
"none": "Sem indicar",
|
||||||
|
"plentyToShare": "De sobra para partilhar",
|
||||||
|
"enoughToShare": "Bastante, para partilhar com moderação",
|
||||||
|
"enoughForMe": "Suficiente para mim",
|
||||||
|
"runningLow": "Resta pouca"
|
||||||
|
},
|
||||||
|
"share": {
|
||||||
|
"add": "Partilhas esta?",
|
||||||
|
"title": "Partilhas esta?",
|
||||||
|
"nudge": "Tens de sobra: podias partilhar um pouco.",
|
||||||
|
"private": "Só para mim",
|
||||||
|
"gift": "Para dar",
|
||||||
|
"exchange": "Para trocar",
|
||||||
|
"sell": "À venda",
|
||||||
|
"filterChip": "Partilho",
|
||||||
|
"printCatalog": "Imprimir o que partilho",
|
||||||
|
"catalogTitle": "O que partilho",
|
||||||
|
"catalogSaved": "Catálogo guardado",
|
||||||
|
"cancelled": "Cancelado"
|
||||||
|
},
|
||||||
|
"cropCalendar": {
|
||||||
|
"add": "Calendário de cultivo",
|
||||||
|
"title": "Calendário de cultivo",
|
||||||
|
"sow": "Sementeira",
|
||||||
|
"transplant": "Transplante",
|
||||||
|
"flowering": "Floração",
|
||||||
|
"fruiting": "Frutificação",
|
||||||
|
"seedHarvest": "Colheita de semente",
|
||||||
|
"unset": "—"
|
||||||
|
},
|
||||||
|
"needsReproduction": {
|
||||||
|
"label": "Reproduzir nesta época",
|
||||||
|
"hint": "Cultiva-a antes que a semente acabe",
|
||||||
|
"badge": "Por reproduzir"
|
||||||
|
},
|
||||||
|
"preservation": {
|
||||||
|
"add": "Como está guardada",
|
||||||
|
"title": "Como está guardada",
|
||||||
|
"none": "Sem indicar",
|
||||||
|
"jarWithDesiccant": "Frasco com agente secante",
|
||||||
|
"glassJar": "Frasco de vidro",
|
||||||
|
"paperEnvelope": "Envelope de papel",
|
||||||
|
"paperBag": "Saco de papel",
|
||||||
|
"plasticBag": "Saco de plástico"
|
||||||
|
},
|
||||||
|
"conditionCheck": {
|
||||||
|
"advanced": "Armazenamento e detalhes de banco de sementes",
|
||||||
|
"title": "Verificações de armazenamento",
|
||||||
|
"add": "Adicionar verificação",
|
||||||
|
"containers": "Frascos / recipientes",
|
||||||
|
"desiccant": "Agente secante",
|
||||||
|
"none": "Ainda não há verificações de armazenamento.",
|
||||||
|
"summary": "{count} frasco(s) · {state}"
|
||||||
|
},
|
||||||
|
"desiccant": {
|
||||||
|
"none": "Nenhum",
|
||||||
|
"add": "Adicionar",
|
||||||
|
"replace": "Substituir",
|
||||||
|
"dry": "Azul — seco",
|
||||||
|
"fresh": "Acabado de renovar"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"aFew": "algumas",
|
||||||
|
"some": "bastantes",
|
||||||
|
"plenty": "muitas",
|
||||||
|
"pinch": "uma pitada",
|
||||||
|
"handful": { "singular": "mão-cheia", "plural": "mãos-cheias" },
|
||||||
|
"teaspoon": { "singular": "colher de chá", "plural": "colheres de chá" },
|
||||||
|
"spoon": { "singular": "colher", "plural": "colheres" },
|
||||||
|
"cup": { "singular": "chávena", "plural": "chávenas" },
|
||||||
|
"jar": { "singular": "frasco", "plural": "frascos" },
|
||||||
|
"sack": { "singular": "saco", "plural": "sacos" },
|
||||||
|
"packet": { "singular": "pacote", "plural": "pacotes" },
|
||||||
|
"cob": { "singular": "maçaroca", "plural": "maçarocas" },
|
||||||
|
"pod": { "singular": "vagem", "plural": "vagens" },
|
||||||
|
"ear": { "singular": "espiga", "plural": "espigas" },
|
||||||
|
"head": { "singular": "cabeça", "plural": "cabeças" },
|
||||||
|
"fruit": { "singular": "fruto", "plural": "frutos" },
|
||||||
|
"bulb": { "singular": "bolbo", "plural": "bolbos" },
|
||||||
|
"tuber": { "singular": "tubérculo", "plural": "tubérculos" },
|
||||||
|
"seedHead": { "singular": "capítulo", "plural": "capítulos" },
|
||||||
|
"bunch": { "singular": "molho", "plural": "molhos" },
|
||||||
|
"plant": { "singular": "planta", "plural": "plantas" },
|
||||||
|
"pot": { "singular": "vaso", "plural": "vasos" },
|
||||||
|
"tray": { "singular": "tabuleiro", "plural": "tabuleiros" },
|
||||||
|
"seedling": { "singular": "plântula", "plural": "plântulas" },
|
||||||
|
"tree": { "singular": "árvore", "plural": "árvores" },
|
||||||
|
"cutting": { "singular": "estaca", "plural": "estacas" },
|
||||||
|
"grams": { "singular": "grama", "plural": "gramas" },
|
||||||
|
"count": { "singular": "semente", "plural": "sementes" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,10 +3,10 @@
|
||||||
/// Source: lib/i18n
|
/// Source: lib/i18n
|
||||||
/// To regenerate, run: `dart run slang`
|
/// To regenerate, run: `dart run slang`
|
||||||
///
|
///
|
||||||
/// Locales: 2
|
/// Locales: 3
|
||||||
/// Strings: 572 (286 per locale)
|
/// Strings: 861 (287 per locale)
|
||||||
///
|
///
|
||||||
/// Built on 2026-07-09 at 20:28 UTC
|
/// Built on 2026-07-09 at 20:35 UTC
|
||||||
|
|
||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// ignore_for_file: type=lint, unused_import
|
||||||
|
|
@ -19,6 +19,7 @@ import 'package:slang_flutter/slang_flutter.dart';
|
||||||
export 'package:slang_flutter/slang_flutter.dart';
|
export 'package:slang_flutter/slang_flutter.dart';
|
||||||
|
|
||||||
import 'strings_es.g.dart' as l_es;
|
import 'strings_es.g.dart' as l_es;
|
||||||
|
import 'strings_pt.g.dart' as l_pt;
|
||||||
part 'strings_en.g.dart';
|
part 'strings_en.g.dart';
|
||||||
|
|
||||||
/// Supported locales.
|
/// Supported locales.
|
||||||
|
|
@ -29,7 +30,8 @@ part 'strings_en.g.dart';
|
||||||
/// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check
|
/// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check
|
||||||
enum AppLocale with BaseAppLocale<AppLocale, Translations> {
|
enum AppLocale with BaseAppLocale<AppLocale, Translations> {
|
||||||
en(languageCode: 'en'),
|
en(languageCode: 'en'),
|
||||||
es(languageCode: 'es');
|
es(languageCode: 'es'),
|
||||||
|
pt(languageCode: 'pt');
|
||||||
|
|
||||||
const AppLocale({
|
const AppLocale({
|
||||||
required this.languageCode,
|
required this.languageCode,
|
||||||
|
|
@ -73,6 +75,12 @@ enum AppLocale with BaseAppLocale<AppLocale, Translations> {
|
||||||
cardinalResolver: cardinalResolver,
|
cardinalResolver: cardinalResolver,
|
||||||
ordinalResolver: ordinalResolver,
|
ordinalResolver: ordinalResolver,
|
||||||
);
|
);
|
||||||
|
case AppLocale.pt:
|
||||||
|
return l_pt.TranslationsPt(
|
||||||
|
overrides: overrides,
|
||||||
|
cardinalResolver: cardinalResolver,
|
||||||
|
ordinalResolver: ordinalResolver,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -211,6 +211,9 @@ class Translations$settings$en {
|
||||||
/// en: 'English'
|
/// en: 'English'
|
||||||
String get langEn => 'English';
|
String get langEn => 'English';
|
||||||
|
|
||||||
|
/// en: 'Português'
|
||||||
|
String get langPt => 'Português';
|
||||||
|
|
||||||
/// en: 'About'
|
/// en: 'About'
|
||||||
String get about => 'About';
|
String get about => 'About';
|
||||||
|
|
||||||
|
|
@ -1508,6 +1511,7 @@ extension on Translations {
|
||||||
'settings.systemLanguage' => 'System language',
|
'settings.systemLanguage' => 'System language',
|
||||||
'settings.langEs' => 'Español',
|
'settings.langEs' => 'Español',
|
||||||
'settings.langEn' => 'English',
|
'settings.langEn' => 'English',
|
||||||
|
'settings.langPt' => 'Português',
|
||||||
'settings.about' => 'About',
|
'settings.about' => 'About',
|
||||||
'settings.aboutText' => 'Local-first, encrypted inventory for traditional seeds. AGPL-3.0.',
|
'settings.aboutText' => 'Local-first, encrypted inventory for traditional seeds. AGPL-3.0.',
|
||||||
'settings.aboutOpen' => 'About Tanemaki',
|
'settings.aboutOpen' => 'About Tanemaki',
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,7 @@ class _Translations$settings$es extends Translations$settings$en {
|
||||||
@override String get systemLanguage => 'Idioma del sistema';
|
@override String get systemLanguage => 'Idioma del sistema';
|
||||||
@override String get langEs => 'Español';
|
@override String get langEs => 'Español';
|
||||||
@override String get langEn => 'English';
|
@override String get langEn => 'English';
|
||||||
|
@override String get langPt => 'Português';
|
||||||
@override String get about => 'Acerca de';
|
@override String get about => 'Acerca de';
|
||||||
@override String get aboutText => 'Inventario local y cifrado para semillas tradicionales. AGPL-3.0.';
|
@override String get aboutText => 'Inventario local y cifrado para semillas tradicionales. AGPL-3.0.';
|
||||||
@override String get aboutOpen => 'Acerca de Tanemaki';
|
@override String get aboutOpen => 'Acerca de Tanemaki';
|
||||||
|
|
@ -956,6 +957,7 @@ extension on TranslationsEs {
|
||||||
'settings.systemLanguage' => 'Idioma del sistema',
|
'settings.systemLanguage' => 'Idioma del sistema',
|
||||||
'settings.langEs' => 'Español',
|
'settings.langEs' => 'Español',
|
||||||
'settings.langEn' => 'English',
|
'settings.langEn' => 'English',
|
||||||
|
'settings.langPt' => 'Português',
|
||||||
'settings.about' => 'Acerca de',
|
'settings.about' => 'Acerca de',
|
||||||
'settings.aboutText' => 'Inventario local y cifrado para semillas tradicionales. AGPL-3.0.',
|
'settings.aboutText' => 'Inventario local y cifrado para semillas tradicionales. AGPL-3.0.',
|
||||||
'settings.aboutOpen' => 'Acerca de Tanemaki',
|
'settings.aboutOpen' => 'Acerca de Tanemaki',
|
||||||
|
|
|
||||||
1221
apps/app_seeds/lib/i18n/strings_pt.g.dart
Normal file
1221
apps/app_seeds/lib/i18n/strings_pt.g.dart
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -33,6 +33,11 @@ class SettingsScreen extends StatelessWidget {
|
||||||
selected: current == 'en',
|
selected: current == 'en',
|
||||||
onTap: () => LocaleSettings.setLocale(AppLocale.en),
|
onTap: () => LocaleSettings.setLocale(AppLocale.en),
|
||||||
),
|
),
|
||||||
|
_LanguageTile(
|
||||||
|
label: t.settings.langPt,
|
||||||
|
selected: current == 'pt',
|
||||||
|
onTap: () => LocaleSettings.setLocale(AppLocale.pt),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.smartphone_outlined),
|
leading: const Icon(Icons.smartphone_outlined),
|
||||||
title: Text(t.settings.systemLanguage),
|
title: Text(t.settings.systemLanguage),
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ Widget wrapScreen({
|
||||||
required VarietyRepository repository,
|
required VarietyRepository repository,
|
||||||
required Widget child,
|
required Widget child,
|
||||||
AppLocale locale = AppLocale.en,
|
AppLocale locale = AppLocale.en,
|
||||||
|
TextDirection? textDirection,
|
||||||
}) {
|
}) {
|
||||||
LocaleSettings.setLocaleSync(locale);
|
LocaleSettings.setLocaleSync(locale);
|
||||||
return TranslationProvider(
|
return TranslationProvider(
|
||||||
|
|
@ -75,7 +76,9 @@ Widget wrapScreen({
|
||||||
GlobalWidgetsLocalizations.delegate,
|
GlobalWidgetsLocalizations.delegate,
|
||||||
GlobalCupertinoLocalizations.delegate,
|
GlobalCupertinoLocalizations.delegate,
|
||||||
],
|
],
|
||||||
home: child,
|
home: textDirection == null
|
||||||
|
? child
|
||||||
|
: Directionality(textDirection: textDirection, child: child),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -88,6 +91,7 @@ Widget wrapDetail({
|
||||||
required String varietyId,
|
required String varietyId,
|
||||||
SpeciesRepository? species,
|
SpeciesRepository? species,
|
||||||
AppLocale locale = AppLocale.en,
|
AppLocale locale = AppLocale.en,
|
||||||
|
TextDirection? textDirection,
|
||||||
}) {
|
}) {
|
||||||
LocaleSettings.setLocaleSync(locale);
|
LocaleSettings.setLocaleSync(locale);
|
||||||
return TranslationProvider(
|
return TranslationProvider(
|
||||||
|
|
@ -106,7 +110,12 @@ Widget wrapDetail({
|
||||||
GlobalWidgetsLocalizations.delegate,
|
GlobalWidgetsLocalizations.delegate,
|
||||||
GlobalCupertinoLocalizations.delegate,
|
GlobalCupertinoLocalizations.delegate,
|
||||||
],
|
],
|
||||||
home: const VarietyDetailScreen(),
|
home: textDirection == null
|
||||||
|
? const VarietyDetailScreen()
|
||||||
|
: Directionality(
|
||||||
|
textDirection: textDirection,
|
||||||
|
child: const VarietyDetailScreen(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
99
apps/app_seeds/test/ui/rtl_smoke_test.dart
Normal file
99
apps/app_seeds/test/ui/rtl_smoke_test.dart
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import 'package:commons_core/commons_core.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:tane/db/database.dart';
|
||||||
|
import 'package:tane/db/enums.dart';
|
||||||
|
import 'package:tane/ui/inventory_list_screen.dart';
|
||||||
|
import 'package:tane/ui/settings_screen.dart';
|
||||||
|
|
||||||
|
import '../support/test_support.dart';
|
||||||
|
|
||||||
|
/// RTL is a first-class requirement (CLAUDE.md): the main screens must build
|
||||||
|
/// and lay out under right-to-left without overflowing or misplacing
|
||||||
|
/// directional affordances. Overflow errors fail these tests automatically.
|
||||||
|
void main() {
|
||||||
|
late AppDatabase db;
|
||||||
|
|
||||||
|
setUp(() => db = newTestDatabase());
|
||||||
|
tearDown(() => db.close());
|
||||||
|
|
||||||
|
testWidgets('the inventory list renders under RTL', (tester) async {
|
||||||
|
final repo = newTestRepository(db);
|
||||||
|
final id = await repo.addQuickVariety(
|
||||||
|
label: 'طماطم بلدية', // an Arabic label, as a keeper would write it
|
||||||
|
category: 'Solanaceae',
|
||||||
|
);
|
||||||
|
await repo.addLot(
|
||||||
|
varietyId: id,
|
||||||
|
harvestYear: 2024,
|
||||||
|
quantity: const Quantity(kind: QuantityKind.handful),
|
||||||
|
offerStatus: OfferStatus.shared,
|
||||||
|
);
|
||||||
|
await repo.addQuickVariety(label: 'Maize', category: 'Poaceae');
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
wrapScreen(
|
||||||
|
repository: repo,
|
||||||
|
textDirection: TextDirection.rtl,
|
||||||
|
child: const InventoryListScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('طماطم بلدية'), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('inventory.filter.sharing')), findsOneWidget);
|
||||||
|
await disposeTree(tester);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('the variety detail (lots, calendar, names) renders under RTL', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final repo = newTestRepository(db);
|
||||||
|
final id = await repo.addQuickVariety(
|
||||||
|
label: 'Grandma tomato',
|
||||||
|
category: 'Solanaceae',
|
||||||
|
);
|
||||||
|
await repo.addVernacularName(id, 'طماطم');
|
||||||
|
await repo.addLot(
|
||||||
|
varietyId: id,
|
||||||
|
harvestYear: 2023,
|
||||||
|
quantity: const Quantity(kind: QuantityKind.packet, count: 3),
|
||||||
|
abundance: Abundance.plentyToShare,
|
||||||
|
offerStatus: OfferStatus.exchange,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
wrapDetail(
|
||||||
|
repository: repo,
|
||||||
|
varietyId: id,
|
||||||
|
species: newTestSpeciesRepository(db),
|
||||||
|
textDirection: TextDirection.rtl,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Grandma tomato'), findsOneWidget);
|
||||||
|
expect(find.text('طماطم'), findsOneWidget);
|
||||||
|
|
||||||
|
// The lot sheet (the busiest form) must also lay out under RTL.
|
||||||
|
await tester.tap(find.byKey(const Key('detail.addLot')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.byKey(const Key('addLot.save')), findsOneWidget);
|
||||||
|
await disposeTree(tester);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('settings renders under RTL', (tester) async {
|
||||||
|
final repo = newTestRepository(db);
|
||||||
|
await tester.pumpWidget(
|
||||||
|
wrapScreen(
|
||||||
|
repository: repo,
|
||||||
|
textDirection: TextDirection.rtl,
|
||||||
|
child: const SettingsScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Português'), findsOneWidget);
|
||||||
|
await disposeTree(tester);
|
||||||
|
});
|
||||||
|
}
|
||||||
53
docs/i18n.md
Normal file
53
docs/i18n.md
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
# Translating Tanemaki
|
||||||
|
|
||||||
|
Tanemaki is multilingual **by design** — international, not any-country-first
|
||||||
|
(see the golden rules in [`../CLAUDE.md`](../CLAUDE.md)). Translations live as
|
||||||
|
one JSON file per locale, a format Weblate (and any translation platform that
|
||||||
|
speaks nested JSON) handles natively.
|
||||||
|
|
||||||
|
## Where strings live
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/app_seeds/lib/i18n/
|
||||||
|
en.i18n.json ← base locale (source of truth for keys)
|
||||||
|
es.i18n.json
|
||||||
|
pt.i18n.json
|
||||||
|
strings*.g.dart ← generated, do not edit
|
||||||
|
```
|
||||||
|
|
||||||
|
- Placeholders use braces: `"{count} entries added"`. Keep them verbatim.
|
||||||
|
- Keys are code and never change per locale; only values are translated.
|
||||||
|
- Missing keys fall back to English at runtime (`fallback_strategy:
|
||||||
|
base_locale` in [`slang.yaml`](../apps/app_seeds/slang.yaml)), so a partial
|
||||||
|
translation is shippable from day one.
|
||||||
|
|
||||||
|
## Adding a locale
|
||||||
|
|
||||||
|
1. Copy `en.i18n.json` to `<code>.i18n.json` (e.g. `fr.i18n.json`) and
|
||||||
|
translate what you can.
|
||||||
|
2. Regenerate the Dart bindings:
|
||||||
|
```bash
|
||||||
|
cd apps/app_seeds && dart run slang
|
||||||
|
```
|
||||||
|
3. Add a language tile in
|
||||||
|
[`settings_screen.dart`](../apps/app_seeds/lib/ui/settings_screen.dart)
|
||||||
|
(label = the language's own name, e.g. "Français") plus the `langXx` key in
|
||||||
|
every locale file.
|
||||||
|
4. Run the tests: `flutter test` (an i18n test guards against hardcoded
|
||||||
|
strings; the RTL smoke test guards right-to-left layouts).
|
||||||
|
|
||||||
|
## RTL and scripts
|
||||||
|
|
||||||
|
Right-to-left locales (Arabic, Hebrew, Persian) are first-class: layouts use
|
||||||
|
directional insets and are exercised by
|
||||||
|
[`rtl_smoke_test.dart`](../apps/app_seeds/test/ui/rtl_smoke_test.dart).
|
||||||
|
Locale-specific resources ship with their locale, never hardcoded: OCR
|
||||||
|
language packs (`assets/tessdata/`), PDF fonts (`assets/fonts/`), and species
|
||||||
|
common names (`assets/catalog/species.json`, a locale-keyed map).
|
||||||
|
|
||||||
|
## Weblate
|
||||||
|
|
||||||
|
The JSON-per-locale layout is Weblate-ready (component per file, base
|
||||||
|
`en.i18n.json`). Hosting is not set up yet; when it is, this page will link
|
||||||
|
to the project. Until then, translation PRs/patches are welcome directly on
|
||||||
|
the files above.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue