diff --git a/apps/app_seeds/assets/fonts/NotoSansArabic-OFL.txt b/apps/app_seeds/assets/fonts/NotoSansArabic-OFL.txt new file mode 100644 index 0000000..14c589f --- /dev/null +++ b/apps/app_seeds/assets/fonts/NotoSansArabic-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Noto Project Authors (https://github.com/notofonts/arabic) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/app_seeds/assets/fonts/NotoSansArabic.ttf b/apps/app_seeds/assets/fonts/NotoSansArabic.ttf new file mode 100644 index 0000000..f1d01ed Binary files /dev/null and b/apps/app_seeds/assets/fonts/NotoSansArabic.ttf differ diff --git a/apps/app_seeds/assets/fonts/NotoSansJP-OFL.txt b/apps/app_seeds/assets/fonts/NotoSansJP-OFL.txt new file mode 100644 index 0000000..1c9f432 --- /dev/null +++ b/apps/app_seeds/assets/fonts/NotoSansJP-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source' + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/app_seeds/assets/fonts/NotoSansJP.ttf b/apps/app_seeds/assets/fonts/NotoSansJP.ttf new file mode 100644 index 0000000..cdd8f08 Binary files /dev/null and b/apps/app_seeds/assets/fonts/NotoSansJP.ttf differ diff --git a/apps/app_seeds/lib/i18n/ast.i18n.json b/apps/app_seeds/lib/i18n/ast.i18n.json index 4f4b26b..87aa385 100644 --- a/apps/app_seeds/lib/i18n/ast.i18n.json +++ b/apps/app_seeds/lib/i18n/ast.i18n.json @@ -97,6 +97,7 @@ "langAst": "Asturianu", "langFr": "Français", "langDe": "Deutsch", + "langJa": "日本語", "about": "Tocante a", "aboutText": "Inventariu llocal y cifráu pa simiente tradicional. AGPL-3.0.", "aboutOpen": "Tocante a Tane" diff --git a/apps/app_seeds/lib/i18n/de.i18n.json b/apps/app_seeds/lib/i18n/de.i18n.json index 2bac6b1..a65e1a6 100644 --- a/apps/app_seeds/lib/i18n/de.i18n.json +++ b/apps/app_seeds/lib/i18n/de.i18n.json @@ -100,7 +100,8 @@ "aboutText": "Lokale, verschlüsselte Saatgutbank für traditionelle Samen. AGPL-3.0.", "aboutOpen": "Über Tane", "langDe": "Deutsch", - "langFr": "Français" + "langFr": "Français", + "langJa": "日本語" }, "backup": { "title": "Sicherung und Wiederherstellung", diff --git a/apps/app_seeds/lib/i18n/en.i18n.json b/apps/app_seeds/lib/i18n/en.i18n.json index ea1bd75..9cce88b 100644 --- a/apps/app_seeds/lib/i18n/en.i18n.json +++ b/apps/app_seeds/lib/i18n/en.i18n.json @@ -98,6 +98,7 @@ "langAst": "Asturianu", "langFr": "Français", "langDe": "Deutsch", + "langJa": "日本語", "about": "About", "aboutText": "Local-first, encrypted inventory for traditional seeds. AGPL-3.0.", "aboutOpen": "About Tane" diff --git a/apps/app_seeds/lib/i18n/es.i18n.json b/apps/app_seeds/lib/i18n/es.i18n.json index 9b38efd..36b885d 100644 --- a/apps/app_seeds/lib/i18n/es.i18n.json +++ b/apps/app_seeds/lib/i18n/es.i18n.json @@ -97,6 +97,7 @@ "langPt": "Português", "langFr": "Français", "langDe": "Deutsch", + "langJa": "日本語", "about": "Acerca de", "aboutText": "Inventario local y cifrado para semillas tradicionales. AGPL-3.0.", "aboutOpen": "Acerca de Tane" diff --git a/apps/app_seeds/lib/i18n/fr.i18n.json b/apps/app_seeds/lib/i18n/fr.i18n.json index 8984454..7855a9d 100644 --- a/apps/app_seeds/lib/i18n/fr.i18n.json +++ b/apps/app_seeds/lib/i18n/fr.i18n.json @@ -98,6 +98,7 @@ "langAst": "Asturianu", "langFr": "Français", "langDe": "Deutsch", + "langJa": "日本語", "about": "À propos", "aboutText": "Inventaire local-first et chiffré pour les semences traditionnelles. AGPL-3.0.", "aboutOpen": "À propos de Tane" diff --git a/apps/app_seeds/lib/i18n/ja.i18n.json b/apps/app_seeds/lib/i18n/ja.i18n.json new file mode 100644 index 0000000..b84bc5f --- /dev/null +++ b/apps/app_seeds/lib/i18n/ja.i18n.json @@ -0,0 +1,46 @@ +{ + "app": { + "title": "Tane" + }, + "common": { + "save": "保存", + "cancel": "キャンセル", + "delete": "削除", + "edit": "編集", + "type": "種類", + "comingSoon": "近日公開", + "offline": "オフラインです — 共有を一時停止しています" + }, + "menu": { + "tagline": "あなたの種子バンク", + "inventory": "在庫", + "market": "マーケット", + "profile": "プロフィール", + "chat": "チャット", + "wishlist": "お気に入り", + "following": "フォロー中", + "calendar": "カレンダー", + "settings": "設定" + }, + "settings": { + "language": "言語", + "systemLanguage": "システムの言語", + "langEs": "Español", + "langEn": "English", + "langPt": "Português", + "langAst": "Asturianu", + "langFr": "Français", + "langDe": "Deutsch", + "langJa": "日本語", + "about": "このアプリについて", + "aboutText": "在来種のためのローカルファースト・暗号化在庫管理。AGPL-3.0。", + "aboutOpen": "Tane について" + }, + "home": { + "tagline": "地域の種を分かち合い、育てよう", + "openMarket": "マーケット", + "openMarketSubtitle": "近くの種を見つけて分かち合う", + "yourInventory": "あなたの在庫", + "yourInventorySubtitle": "種を管理する" + } +} diff --git a/apps/app_seeds/lib/i18n/pt.i18n.json b/apps/app_seeds/lib/i18n/pt.i18n.json index a6e40e4..07ebc8b 100644 --- a/apps/app_seeds/lib/i18n/pt.i18n.json +++ b/apps/app_seeds/lib/i18n/pt.i18n.json @@ -98,6 +98,7 @@ "langAst": "Asturianu", "langFr": "Français", "langDe": "Deutsch", + "langJa": "日本語", "about": "Acerca de", "aboutText": "Inventário local e cifrado para sementes tradicionais. AGPL-3.0.", "aboutOpen": "Acerca do Tane" diff --git a/apps/app_seeds/lib/i18n/strings.g.dart b/apps/app_seeds/lib/i18n/strings.g.dart index 3a7f6f4..89ac0b4 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: 6 -/// Strings: 3356 (559 per locale) +/// Locales: 7 +/// Strings: 3396 (485 per locale) /// -/// Built on 2026-07-13 at 16:11 UTC +/// Built on 2026-07-14 at 09:00 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import @@ -22,6 +22,7 @@ import 'strings_ast.g.dart' as l_ast; import 'strings_de.g.dart' as l_de; import 'strings_es.g.dart' as l_es; import 'strings_fr.g.dart' as l_fr; +import 'strings_ja.g.dart' as l_ja; import 'strings_pt.g.dart' as l_pt; part 'strings_en.g.dart'; @@ -37,6 +38,7 @@ enum AppLocale with BaseAppLocale { de(languageCode: 'de'), es(languageCode: 'es'), fr(languageCode: 'fr'), + ja(languageCode: 'ja'), pt(languageCode: 'pt'); const AppLocale({ @@ -99,6 +101,12 @@ enum AppLocale with BaseAppLocale { cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, ); + case AppLocale.ja: + return l_ja.TranslationsJa( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); case AppLocale.pt: return l_pt.TranslationsPt( overrides: overrides, diff --git a/apps/app_seeds/lib/i18n/strings_ast.g.dart b/apps/app_seeds/lib/i18n/strings_ast.g.dart index 41acb84..2167666 100644 --- a/apps/app_seeds/lib/i18n/strings_ast.g.dart +++ b/apps/app_seeds/lib/i18n/strings_ast.g.dart @@ -265,6 +265,7 @@ class _Translations$settings$ast extends Translations$settings$en { @override String get langAst => 'Asturianu'; @override String get langFr => 'Français'; @override String get langDe => 'Deutsch'; + @override String get langJa => '日本語'; @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 Tane'; @@ -1488,6 +1489,7 @@ extension on TranslationsAst { 'settings.langAst' => 'Asturianu', 'settings.langFr' => 'Français', 'settings.langDe' => 'Deutsch', + 'settings.langJa' => '日本語', 'settings.about' => 'Tocante a', 'settings.aboutText' => 'Inventariu llocal y cifráu pa simiente tradicional. AGPL-3.0.', 'settings.aboutOpen' => 'Tocante a Tane', @@ -1922,9 +1924,9 @@ extension on TranslationsAst { 'sale.iSold' => 'Vendí', 'sale.iBought' => 'Mercué', 'sale.direction' => '¿Vendes o merques?', - 'sale.counterparty' => '¿Con quién?', _ => null, } ?? switch (path) { + 'sale.counterparty' => '¿Con quién?', 'sale.counterpartyHint' => 'Una persona o un coleutivu (opcional)', 'sale.amount' => 'Importe', 'sale.currency' => 'Moneda', diff --git a/apps/app_seeds/lib/i18n/strings_de.g.dart b/apps/app_seeds/lib/i18n/strings_de.g.dart index d8f82b0..b959d59 100644 --- a/apps/app_seeds/lib/i18n/strings_de.g.dart +++ b/apps/app_seeds/lib/i18n/strings_de.g.dart @@ -269,6 +269,7 @@ class _Translations$settings$de extends Translations$settings$en { @override String get aboutOpen => 'Über Tane'; @override String get langDe => 'Deutsch'; @override String get langFr => 'Français'; + @override String get langJa => '日本語'; } // Path: backup @@ -1488,6 +1489,7 @@ extension on TranslationsDe { 'settings.aboutOpen' => 'Über Tane', 'settings.langDe' => 'Deutsch', 'settings.langFr' => 'Français', + 'settings.langJa' => '日本語', 'backup.title' => 'Sicherung und Wiederherstellung', 'backup.autoBackupTitle' => 'Automatische Sicherungen', 'backup.autoBackupLast' => ({required Object date}) => 'Letzte Sicherung ${date}', @@ -1918,9 +1920,9 @@ extension on TranslationsDe { 'sale.counterpartyHint' => 'Eine Person oder ein Kollektiv (optional)', 'sale.amount' => 'Betrag', 'sale.currency' => 'Währung', - 'sale.currencyHint' => '€, Ğ1, Stunden… (optional)', _ => null, } ?? switch (path) { + 'sale.currencyHint' => '€, Ğ1, Stunden… (optional)', 'sale.hours' => 'Stunden', 'sale.note' => 'Notiz (optional)', 'sale.save' => 'Speichern', diff --git a/apps/app_seeds/lib/i18n/strings_en.g.dart b/apps/app_seeds/lib/i18n/strings_en.g.dart index f978628..7cf9808 100644 --- a/apps/app_seeds/lib/i18n/strings_en.g.dart +++ b/apps/app_seeds/lib/i18n/strings_en.g.dart @@ -424,6 +424,9 @@ class Translations$settings$en { /// en: 'Deutsch' String get langDe => 'Deutsch'; + /// en: '日本語' + String get langJa => '日本語'; + /// en: 'About' String get about => 'About'; @@ -2598,6 +2601,7 @@ extension on Translations { 'settings.langAst' => 'Asturianu', 'settings.langFr' => 'Français', 'settings.langDe' => 'Deutsch', + 'settings.langJa' => '日本語', 'settings.about' => 'About', 'settings.aboutText' => 'Local-first, encrypted inventory for traditional seeds. AGPL-3.0.', 'settings.aboutOpen' => 'About Tane', @@ -3031,9 +3035,9 @@ extension on Translations { 'sale.help' => 'Record seed you sold or bought — money, Ğ1, or any currency. A separate model from a gift or a Plantare. No commission is ever taken on seeds.', 'sale.add' => 'Record a sale', 'sale.empty' => 'No sales yet. Note here what you sell or buy.', - 'sale.iSold' => 'I sold', _ => null, } ?? switch (path) { + 'sale.iSold' => 'I sold', 'sale.iBought' => 'I bought', 'sale.direction' => 'Sold or bought?', 'sale.counterparty' => 'With whom?', diff --git a/apps/app_seeds/lib/i18n/strings_es.g.dart b/apps/app_seeds/lib/i18n/strings_es.g.dart index e94c59d..9722a74 100644 --- a/apps/app_seeds/lib/i18n/strings_es.g.dart +++ b/apps/app_seeds/lib/i18n/strings_es.g.dart @@ -265,6 +265,7 @@ class _Translations$settings$es extends Translations$settings$en { @override String get langPt => 'Português'; @override String get langFr => 'Français'; @override String get langDe => 'Deutsch'; + @override String get langJa => '日本語'; @override String get about => 'Acerca de'; @override String get aboutText => 'Inventario local y cifrado para semillas tradicionales. AGPL-3.0.'; @override String get aboutOpen => 'Acerca de Tane'; @@ -1490,6 +1491,7 @@ extension on TranslationsEs { 'settings.langPt' => 'Português', 'settings.langFr' => 'Français', 'settings.langDe' => 'Deutsch', + 'settings.langJa' => '日本語', 'settings.about' => 'Acerca de', 'settings.aboutText' => 'Inventario local y cifrado para semillas tradicionales. AGPL-3.0.', 'settings.aboutOpen' => 'Acerca de Tane', @@ -1924,9 +1926,9 @@ extension on TranslationsEs { 'sale.add' => 'Registrar venta', 'sale.empty' => 'Aún no hay ventas. Anota aquí lo que vendes o compras.', 'sale.iSold' => 'Vendí', - 'sale.iBought' => 'Compré', _ => null, } ?? switch (path) { + 'sale.iBought' => 'Compré', 'sale.direction' => '¿Vendes o compras?', 'sale.counterparty' => '¿Con quién?', 'sale.counterpartyHint' => 'Una persona o un colectivo (opcional)', diff --git a/apps/app_seeds/lib/i18n/strings_fr.g.dart b/apps/app_seeds/lib/i18n/strings_fr.g.dart index f4273dc..9cc3259 100644 --- a/apps/app_seeds/lib/i18n/strings_fr.g.dart +++ b/apps/app_seeds/lib/i18n/strings_fr.g.dart @@ -266,6 +266,7 @@ class _Translations$settings$fr extends Translations$settings$en { @override String get langAst => 'Asturianu'; @override String get langFr => 'Français'; @override String get langDe => 'Deutsch'; + @override String get langJa => '日本語'; @override String get about => 'À propos'; @override String get aboutText => 'Inventaire local-first et chiffré pour les semences traditionnelles. AGPL-3.0.'; @override String get aboutOpen => 'À propos de Tane'; @@ -1485,6 +1486,7 @@ extension on TranslationsFr { 'settings.langAst' => 'Asturianu', 'settings.langFr' => 'Français', 'settings.langDe' => 'Deutsch', + 'settings.langJa' => '日本語', 'settings.about' => 'À propos', 'settings.aboutText' => 'Inventaire local-first et chiffré pour les semences traditionnelles. AGPL-3.0.', 'settings.aboutOpen' => 'À propos de Tane', @@ -1918,9 +1920,9 @@ extension on TranslationsFr { 'sale.counterpartyHint' => 'Une personne ou un collectif (optionnel)', 'sale.amount' => 'Montant', 'sale.currency' => 'Monnaie', - 'sale.currencyHint' => '€, Ğ1, heures… (optionnel)', _ => null, } ?? switch (path) { + 'sale.currencyHint' => '€, Ğ1, heures… (optionnel)', 'sale.hours' => 'heures', 'sale.note' => 'Note (optionnel)', 'sale.save' => 'Enregistrer', diff --git a/apps/app_seeds/lib/i18n/strings_ja.g.dart b/apps/app_seeds/lib/i18n/strings_ja.g.dart new file mode 100644 index 0000000..01cfd2e --- /dev/null +++ b/apps/app_seeds/lib/i18n/strings_ja.g.dart @@ -0,0 +1,173 @@ +/// +/// 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 TranslationsJa 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. + TranslationsJa({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.ja, + 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 TranslationsJa _root = this; // ignore: unused_field + + @override + TranslationsJa $copyWith({TranslationMetadata? meta}) => TranslationsJa(meta: meta ?? this.$meta); + + // Translations + @override late final _Translations$app$ja app = _Translations$app$ja._(_root); + @override late final _Translations$common$ja common = _Translations$common$ja._(_root); + @override late final _Translations$menu$ja menu = _Translations$menu$ja._(_root); + @override late final _Translations$settings$ja settings = _Translations$settings$ja._(_root); + @override late final _Translations$home$ja home = _Translations$home$ja._(_root); +} + +// Path: app +class _Translations$app$ja extends Translations$app$en { + _Translations$app$ja._(TranslationsJa root) : this._root = root, super.internal(root); + + final TranslationsJa _root; // ignore: unused_field + + // Translations + @override String get title => 'Tane'; +} + +// Path: common +class _Translations$common$ja extends Translations$common$en { + _Translations$common$ja._(TranslationsJa root) : this._root = root, super.internal(root); + + final TranslationsJa _root; // ignore: unused_field + + // Translations + @override String get save => '保存'; + @override String get cancel => 'キャンセル'; + @override String get delete => '削除'; + @override String get edit => '編集'; + @override String get type => '種類'; + @override String get comingSoon => '近日公開'; + @override String get offline => 'オフラインです — 共有を一時停止しています'; +} + +// Path: menu +class _Translations$menu$ja extends Translations$menu$en { + _Translations$menu$ja._(TranslationsJa root) : this._root = root, super.internal(root); + + final TranslationsJa _root; // ignore: unused_field + + // Translations + @override String get tagline => 'あなたの種子バンク'; + @override String get inventory => '在庫'; + @override String get market => 'マーケット'; + @override String get profile => 'プロフィール'; + @override String get chat => 'チャット'; + @override String get wishlist => 'お気に入り'; + @override String get following => 'フォロー中'; + @override String get calendar => 'カレンダー'; + @override String get settings => '設定'; +} + +// Path: settings +class _Translations$settings$ja extends Translations$settings$en { + _Translations$settings$ja._(TranslationsJa root) : this._root = root, super.internal(root); + + final TranslationsJa _root; // ignore: unused_field + + // Translations + @override String get language => '言語'; + @override String get systemLanguage => 'システムの言語'; + @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 langFr => 'Français'; + @override String get langDe => 'Deutsch'; + @override String get langJa => '日本語'; + @override String get about => 'このアプリについて'; + @override String get aboutText => '在来種のためのローカルファースト・暗号化在庫管理。AGPL-3.0。'; + @override String get aboutOpen => 'Tane について'; +} + +// Path: home +class _Translations$home$ja extends Translations$home$en { + _Translations$home$ja._(TranslationsJa root) : this._root = root, super.internal(root); + + final TranslationsJa _root; // ignore: unused_field + + // Translations + @override String get tagline => '地域の種を分かち合い、育てよう'; + @override String get openMarket => 'マーケット'; + @override String get openMarketSubtitle => '近くの種を見つけて分かち合う'; + @override String get yourInventory => 'あなたの在庫'; + @override String get yourInventorySubtitle => '種を管理する'; +} + +/// 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 TranslationsJa { + dynamic _flatMapFunction(String path) { + return switch (path) { + 'app.title' => 'Tane', + 'common.save' => '保存', + 'common.cancel' => 'キャンセル', + 'common.delete' => '削除', + 'common.edit' => '編集', + 'common.type' => '種類', + 'common.comingSoon' => '近日公開', + 'common.offline' => 'オフラインです — 共有を一時停止しています', + 'menu.tagline' => 'あなたの種子バンク', + 'menu.inventory' => '在庫', + 'menu.market' => 'マーケット', + 'menu.profile' => 'プロフィール', + 'menu.chat' => 'チャット', + 'menu.wishlist' => 'お気に入り', + 'menu.following' => 'フォロー中', + 'menu.calendar' => 'カレンダー', + 'menu.settings' => '設定', + 'settings.language' => '言語', + 'settings.systemLanguage' => 'システムの言語', + 'settings.langEs' => 'Español', + 'settings.langEn' => 'English', + 'settings.langPt' => 'Português', + 'settings.langAst' => 'Asturianu', + 'settings.langFr' => 'Français', + 'settings.langDe' => 'Deutsch', + 'settings.langJa' => '日本語', + 'settings.about' => 'このアプリについて', + 'settings.aboutText' => '在来種のためのローカルファースト・暗号化在庫管理。AGPL-3.0。', + 'settings.aboutOpen' => 'Tane について', + 'home.tagline' => '地域の種を分かち合い、育てよう', + 'home.openMarket' => 'マーケット', + 'home.openMarketSubtitle' => '近くの種を見つけて分かち合う', + 'home.yourInventory' => 'あなたの在庫', + 'home.yourInventorySubtitle' => '種を管理する', + _ => null, + }; + } +} diff --git a/apps/app_seeds/lib/i18n/strings_pt.g.dart b/apps/app_seeds/lib/i18n/strings_pt.g.dart index 7ff9a19..b65a307 100644 --- a/apps/app_seeds/lib/i18n/strings_pt.g.dart +++ b/apps/app_seeds/lib/i18n/strings_pt.g.dart @@ -266,6 +266,7 @@ class _Translations$settings$pt extends Translations$settings$en { @override String get langAst => 'Asturianu'; @override String get langFr => 'Français'; @override String get langDe => 'Deutsch'; + @override String get langJa => '日本語'; @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 Tane'; @@ -1488,6 +1489,7 @@ extension on TranslationsPt { 'settings.langAst' => 'Asturianu', 'settings.langFr' => 'Français', 'settings.langDe' => 'Deutsch', + 'settings.langJa' => '日本語', 'settings.about' => 'Acerca de', 'settings.aboutText' => 'Inventário local e cifrado para sementes tradicionais. AGPL-3.0.', 'settings.aboutOpen' => 'Acerca do Tane', @@ -1921,9 +1923,9 @@ extension on TranslationsPt { 'sale.iBought' => 'Comprei', 'sale.direction' => 'Vendes ou compras?', 'sale.counterparty' => 'Com quem?', - 'sale.counterpartyHint' => 'Uma pessoa ou um coletivo (opcional)', _ => null, } ?? switch (path) { + 'sale.counterpartyHint' => 'Uma pessoa ou um coletivo (opcional)', 'sale.amount' => 'Montante', 'sale.currency' => 'Moeda', 'sale.currencyHint' => '€, Ğ1, horas… (opcional)', diff --git a/apps/app_seeds/lib/services/label_sheet_service.dart b/apps/app_seeds/lib/services/label_sheet_service.dart index d0383d1..d109f08 100644 --- a/apps/app_seeds/lib/services/label_sheet_service.dart +++ b/apps/app_seeds/lib/services/label_sheet_service.dart @@ -1,11 +1,11 @@ import 'dart:math' as math; import 'dart:typed_data'; -import 'package:flutter/services.dart' show rootBundle; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; import 'file_service.dart'; +import 'pdf_fonts.dart'; /// How the labels are tiled on the page: many small stickers to peel onto /// packets, or fewer larger cards for jars and boxes. @@ -65,9 +65,10 @@ class _Geometry { /// platform channels apart from loading the bundled font, so it runs (and is /// tested) on the host. /// -/// The embedded DejaVu face covers Latin (extended), Cyrillic and Greek. Like -/// the OCR traineddata, the PDF font is a per-locale resource: wider scripts -/// (Arabic, CJK) ship alongside their locales, not hardcoded here. +/// The embedded DejaVu face covers Latin (extended), Cyrillic and Greek, with +/// Noto Arabic + Noto CJK as per-glyph fallbacks (see [buildPdfTheme]) so RTL +/// and CJK text render. Fonts are a per-locale resource, extensible to any +/// script. class LabelSheetService { LabelSheetService({required FileService files}) : _files = files; @@ -103,15 +104,8 @@ class LabelSheetService { bool rtl = false, }) async { final textDirection = rtl ? pw.TextDirection.rtl : pw.TextDirection.ltr; - final base = pw.Font.ttf( - await rootBundle.load('assets/fonts/DejaVuSans.ttf'), - ); - final bold = pw.Font.ttf( - await rootBundle.load('assets/fonts/DejaVuSans-Bold.ttf'), - ); - final doc = pw.Document( - theme: pw.ThemeData.withFont(base: base, bold: bold, italic: base), - ); + final fonts = await loadPdfFonts(); + final doc = pw.Document(theme: fonts.theme); final g = _geometryOf(format); final labelWidth = (PdfPageFormat.a4.width - _margin * 2) / g.columns; @@ -150,7 +144,7 @@ class LabelSheetService { maxLines: 1, overflow: pw.TextOverflow.clip, style: pw.TextStyle( - font: bold, + font: fonts.bold, fontSize: g.common, color: PdfColors.grey800, ), @@ -159,7 +153,7 @@ class LabelSheetService { label.varietyLabel, maxLines: 2, overflow: pw.TextOverflow.clip, - style: pw.TextStyle(font: bold, fontSize: g.title), + style: pw.TextStyle(font: fonts.bold, fontSize: g.title), ), if (label.scientificName != null) pw.Text( diff --git a/apps/app_seeds/lib/services/pdf_fonts.dart b/apps/app_seeds/lib/services/pdf_fonts.dart new file mode 100644 index 0000000..3f5f943 --- /dev/null +++ b/apps/app_seeds/lib/services/pdf_fonts.dart @@ -0,0 +1,38 @@ +import 'package:flutter/services.dart' show rootBundle; +import 'package:pdf/widgets.dart' as pw; + +/// Fonts and theme shared by every generated PDF (labels, catalog, recovery +/// sheet). [theme] is the document-wide default; [bold] is exposed for the few +/// places that set a heading weight explicitly — merging against [theme] keeps +/// its script fallbacks, so bold Arabic/CJK still renders. +typedef PdfFonts = ({pw.ThemeData theme, pw.Font bold}); + +/// Loads the PDF fonts. +/// +/// The base face is DejaVu (Latin extended, Cyrillic, Greek) with Noto Arabic +/// and Noto CJK registered as per-glyph fallbacks, so vernacular species names, +/// user notes, and localized labels in Arabic (RTL) or CJK render instead of +/// showing tofu boxes. The `pdf` package subsets fonts to the glyphs actually +/// used, so bundling the full Noto faces does not bloat the output file. +/// +/// Fonts are a per-locale resource, extensible to any script — widen the +/// fallback list as more scripts land. +Future loadPdfFonts() async { + final base = pw.Font.ttf( + await rootBundle.load('assets/fonts/DejaVuSans.ttf'), + ); + final bold = pw.Font.ttf( + await rootBundle.load('assets/fonts/DejaVuSans-Bold.ttf'), + ); + final arabic = pw.Font.ttf( + await rootBundle.load('assets/fonts/NotoSansArabic.ttf'), + ); + final cjk = pw.Font.ttf(await rootBundle.load('assets/fonts/NotoSansJP.ttf')); + final theme = pw.ThemeData.withFont( + base: base, + bold: bold, + italic: base, + fontFallback: [arabic, cjk], + ); + return (theme: theme, bold: bold); +} diff --git a/apps/app_seeds/lib/services/recovery_sheet_service.dart b/apps/app_seeds/lib/services/recovery_sheet_service.dart index ac688c5..b271c74 100644 --- a/apps/app_seeds/lib/services/recovery_sheet_service.dart +++ b/apps/app_seeds/lib/services/recovery_sheet_service.dart @@ -1,10 +1,10 @@ import 'dart:typed_data'; -import 'package:flutter/services.dart' show rootBundle; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; import 'file_service.dart'; +import 'pdf_fonts.dart'; /// Renders the printable recovery sheet — a QR plus the typed code and short /// instructions ("keep two paper copies, like your best seed"). All strings @@ -19,22 +19,15 @@ class RecoverySheetService { required String intro, required String code, }) async { - final base = pw.Font.ttf( - await rootBundle.load('assets/fonts/DejaVuSans.ttf'), - ); - final bold = pw.Font.ttf( - await rootBundle.load('assets/fonts/DejaVuSans-Bold.ttf'), - ); - final doc = pw.Document( - theme: pw.ThemeData.withFont(base: base, bold: bold), - ); + final fonts = await loadPdfFonts(); + final doc = pw.Document(theme: fonts.theme); doc.addPage( pw.Page( pageFormat: PdfPageFormat.a4, build: (context) => pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ - pw.Text(title, style: pw.TextStyle(font: bold, fontSize: 20)), + pw.Text(title, style: pw.TextStyle(font: fonts.bold, fontSize: 20)), pw.SizedBox(height: 12), pw.Text(intro, style: const pw.TextStyle(fontSize: 11)), pw.SizedBox(height: 24), @@ -54,7 +47,7 @@ class RecoverySheetService { child: pw.Text( code, textAlign: pw.TextAlign.center, - style: pw.TextStyle(font: bold, fontSize: 12), + style: pw.TextStyle(font: fonts.bold, fontSize: 12), ), ), ], diff --git a/apps/app_seeds/lib/services/share_catalog_service.dart b/apps/app_seeds/lib/services/share_catalog_service.dart index 469a7a3..7979a39 100644 --- a/apps/app_seeds/lib/services/share_catalog_service.dart +++ b/apps/app_seeds/lib/services/share_catalog_service.dart @@ -1,10 +1,10 @@ import 'dart:typed_data'; -import 'package:flutter/services.dart' show rootBundle; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; import 'file_service.dart'; +import 'pdf_fonts.dart'; /// One printable line of the "what I share" catalog. All strings arrive /// already localized — this service knows nothing about i18n or the domain. @@ -47,20 +47,13 @@ class ShareCatalogService { required String date, required List rows, }) async { - final base = pw.Font.ttf( - await rootBundle.load('assets/fonts/DejaVuSans.ttf'), - ); - final bold = pw.Font.ttf( - await rootBundle.load('assets/fonts/DejaVuSans-Bold.ttf'), - ); - final doc = pw.Document( - theme: pw.ThemeData.withFont(base: base, bold: bold, italic: base), - ); + final fonts = await loadPdfFonts(); + final doc = pw.Document(theme: fonts.theme); doc.addPage( pw.MultiPage( pageFormat: PdfPageFormat.a4, build: (context) => [ - pw.Text(title, style: pw.TextStyle(font: bold, fontSize: 20)), + pw.Text(title, style: pw.TextStyle(font: fonts.bold, fontSize: 20)), pw.SizedBox(height: 2), pw.Text( date, @@ -84,7 +77,7 @@ class ShareCatalogService { children: [ pw.Text( row.name, - style: pw.TextStyle(font: bold, fontSize: 12), + style: pw.TextStyle(font: fonts.bold, fontSize: 12), ), if (row.scientificName != null) pw.Text( diff --git a/apps/app_seeds/lib/ui/inventory_list_screen.dart b/apps/app_seeds/lib/ui/inventory_list_screen.dart index 890c022..11584ba 100644 --- a/apps/app_seeds/lib/ui/inventory_list_screen.dart +++ b/apps/app_seeds/lib/ui/inventory_list_screen.dart @@ -63,7 +63,10 @@ class InventoryListScreen extends StatelessWidget { key: const Key('inventory.search'), decoration: InputDecoration( hintText: t.inventory.searchHint, - prefixIcon: const Icon(Icons.search, color: seedMuted), + prefixIcon: const Icon( + Icons.search, + color: seedMuted, + ), hintStyle: const TextStyle(color: seedMuted), filled: true, fillColor: Colors.white, @@ -404,15 +407,21 @@ class _FilterBar extends StatelessWidget { // Lay out the groups in order, dropping a hairline divider between any two // that both have chips. - final groups = [attrChips, formChips, categoryChips].where((g) => g.isNotEmpty); + final groups = [ + attrChips, + formChips, + categoryChips, + ].where((g) => g.isNotEmpty); final row = []; for (final group in groups) { if (row.isNotEmpty) row.add(const _FilterGroupDivider()); for (final chip in group) { - row.add(Padding( - padding: const EdgeInsetsDirectional.only(end: 8), - child: chip, - )); + row.add( + Padding( + padding: const EdgeInsetsDirectional.only(end: 8), + child: chip, + ), + ); } } final scroller = EdgeFade( @@ -451,11 +460,11 @@ class _FilterGroupDivider extends StatelessWidget { @override Widget build(BuildContext context) => Container( - width: 1, - height: 22, - margin: const EdgeInsetsDirectional.only(end: 8), - color: seedOutline, - ); + width: 1, + height: 22, + margin: const EdgeInsetsDirectional.only(end: 8), + color: seedOutline, + ); } /// A [FilterChip] tinted with a [Swatch]: a soft fill when idle, a stronger @@ -587,7 +596,10 @@ class _CategoryHeader extends StatelessWidget { width: 10, height: 10, margin: const EdgeInsetsDirectional.only(end: 8), - decoration: BoxDecoration(color: swatch.ink, shape: BoxShape.circle), + decoration: BoxDecoration( + color: swatch.ink, + shape: BoxShape.circle, + ), ), Flexible( // Base on the text theme so it honours the system font-scale factor. @@ -740,7 +752,7 @@ class _Avatar extends StatelessWidget { final trimmed = item.label.trim(); final initial = trimmed.isEmpty ? '?' - : trimmed.substring(0, 1).toUpperCase(); + : trimmed.characters.first.toUpperCase(); return ExcludeSemantics( child: CircleAvatar( backgroundColor: seedAvatar, diff --git a/apps/app_seeds/lib/ui/settings_screen.dart b/apps/app_seeds/lib/ui/settings_screen.dart index 7fa3586..ff619ef 100644 --- a/apps/app_seeds/lib/ui/settings_screen.dart +++ b/apps/app_seeds/lib/ui/settings_screen.dart @@ -59,6 +59,7 @@ List<(AppLocale, String)> _languages(Translations t) => [ (AppLocale.pt, t.settings.langPt), (AppLocale.fr, t.settings.langFr), (AppLocale.de, t.settings.langDe), + (AppLocale.ja, t.settings.langJa), ]; /// A single tile showing the active language; tapping opens a picker with all diff --git a/apps/app_seeds/lib/ui/theme.dart b/apps/app_seeds/lib/ui/theme.dart index 60b372c..170a6c2 100644 --- a/apps/app_seeds/lib/ui/theme.dart +++ b/apps/app_seeds/lib/ui/theme.dart @@ -52,6 +52,11 @@ ThemeData buildTaneTheme() { return ThemeData( useMaterial3: true, colorScheme: scheme, + // Per-glyph script fallbacks so Arabic (RTL) and CJK text render even where + // the platform's default font lacks them (notably desktop). The primary + // family stays the system default for Latin; these only fill the gaps. The + // ranges are disjoint, so order is immaterial. Fonts bundled in pubspec. + fontFamilyFallback: const ['Noto Sans JP', 'Noto Sans Arabic'], scaffoldBackgroundColor: seedCanvas, // Green app bars — aligned with the v2 mockups and the real app. appBarTheme: const AppBarTheme( diff --git a/apps/app_seeds/lib/ui/variety_detail_screen.dart b/apps/app_seeds/lib/ui/variety_detail_screen.dart index 03e36bb..6eeeead 100644 --- a/apps/app_seeds/lib/ui/variety_detail_screen.dart +++ b/apps/app_seeds/lib/ui/variety_detail_screen.dart @@ -147,7 +147,8 @@ class _DetailView extends StatelessWidget { if (SeedSavingCatalog.guideFor( scientificName: detail.scientificName, family: detail.family ?? detail.category, - ) case final guide? when guide.hasAny) ...[ + ) + case final guide? when guide.hasAny) ...[ const SizedBox(height: 8), // Reference data (facts about the species, not the grower's own // records) — collapsed by default so it's opt-in depth, not noise. @@ -301,7 +302,8 @@ class _PlantareLine extends StatelessWidget { ? t.plantare.statusReturned : t.plantare.statusForgiven, ]; - final overdue = !done && + final overdue = + !done && p.dueBy != null && p.dueBy! < DateTime.now().millisecondsSinceEpoch; final returnBy = p.dueBy == null @@ -997,7 +999,9 @@ class _EditVarietySheetState extends State<_EditVarietySheet> { child: Text( t.cropCalendar.editorHint, style: const TextStyle( - color: seedMuted, fontSize: 12), + color: seedMuted, + fontSize: 12, + ), ), ), ), @@ -1099,9 +1103,13 @@ class _MonthMultiSelect extends StatelessWidget { } } -/// A short (≤3-char) label for a month name, for the compact calendar chips. -String _monthAbbrev(String name) => - name.length <= 3 ? name : name.substring(0, 3); +/// A short (≤3-grapheme) label for a month name, for the compact calendar chips. +/// Counts by grapheme (not UTF-16 units) so CJK/complex scripts don't truncate +/// mid-character. +String _monthAbbrev(String name) { + final chars = name.characters; + return chars.length <= 3 ? name : chars.take(3).toString(); +} /// Read-only crop calendar: one line per recorded phase ("Sow · Mar, Apr, Sep"). /// Renders only the phases that have months set. @@ -1201,14 +1209,24 @@ class _SeedSavingView extends StatelessWidget { } if ((guide.recommendedPlants ?? guide.minPlants) case final n?) { - rows.add(_line( - Icons.groups_outlined, t.seedSaving.plants, t.seedSaving.plantsValue(n: n))); + rows.add( + _line( + Icons.groups_outlined, + t.seedSaving.plants, + t.seedSaving.plantsValue(n: n), + ), + ); } if (guide.processing case final proc?) { final wet = proc == SeedProcessing.wet; - rows.add(_line(wet ? Icons.water_drop_outlined : Icons.grass, - t.seedSaving.processing, wet ? t.seedSaving.procWet : t.seedSaving.procDry)); + rows.add( + _line( + wet ? Icons.water_drop_outlined : Icons.grass, + t.seedSaving.processing, + wet ? t.seedSaving.procWet : t.seedSaving.procDry, + ), + ); } final note = guide.noteFor(LocaleSettings.currentLocale.languageCode); @@ -1228,10 +1246,16 @@ class _SeedSavingView extends StatelessWidget { // Advisory + a light source credit — the figures are general, not local // gospel, and drawn from the seed-saving literature. const SizedBox(height: 10), - Text(t.seedSaving.advisory, - style: const TextStyle( - color: seedMuted, fontSize: 12, fontStyle: FontStyle.italic)), - if (SeedSavingCatalog.sources case final sources when sources.isNotEmpty) + Text( + t.seedSaving.advisory, + style: const TextStyle( + color: seedMuted, + fontSize: 12, + fontStyle: FontStyle.italic, + ), + ), + if (SeedSavingCatalog.sources case final sources + when sources.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 2), child: Text( @@ -1245,23 +1269,28 @@ class _SeedSavingView extends StatelessWidget { } Widget _line(IconData icon, String label, String value) => Padding( - padding: const EdgeInsets.symmetric(vertical: 3), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(icon, size: 18, color: seedGreen), - const SizedBox(width: 10), - Expanded( - child: Text.rich(TextSpan(children: [ + padding: const EdgeInsets.symmetric(vertical: 3), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 18, color: seedGreen), + const SizedBox(width: 10), + Expanded( + child: Text.rich( + TextSpan( + children: [ TextSpan( - text: '$label · ', - style: const TextStyle(fontWeight: FontWeight.w600)), + text: '$label · ', + style: const TextStyle(fontWeight: FontWeight.w600), + ), TextSpan(text: value), - ])), + ], ), - ], + ), ), - ); + ], + ), + ); } class _DifficultyChip extends StatelessWidget { @@ -1274,24 +1303,30 @@ class _DifficultyChip extends StatelessWidget { final t = context.t; final (label, color) = switch (difficulty) { SavingDifficulty.easy => (t.seedSaving.diffEasy, const Color(0xFF3B6D11)), - SavingDifficulty.medium => - (t.seedSaving.diffMedium, const Color(0xFF8A6D1E)), + SavingDifficulty.medium => ( + t.seedSaving.diffMedium, + const Color(0xFF8A6D1E), + ), SavingDifficulty.hard => (t.seedSaving.diffHard, const Color(0xFFA14234)), }; return Row( children: [ Icon(Icons.insights, size: 18, color: seedGreen), const SizedBox(width: 10), - Text('${t.seedSaving.difficulty} · ', - style: const TextStyle(fontWeight: FontWeight.w600)), + Text( + '${t.seedSaving.difficulty} · ', + style: const TextStyle(fontWeight: FontWeight.w600), + ), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2), decoration: BoxDecoration( color: color.withValues(alpha: 0.14), borderRadius: BorderRadius.circular(10), ), - child: Text(label, - style: TextStyle(color: color, fontWeight: FontWeight.w600)), + child: Text( + label, + style: TextStyle(color: color, fontWeight: FontWeight.w600), + ), ), ], ); @@ -1324,7 +1359,6 @@ String desiccantLabel(Translations t, DesiccantState d) => switch (d) { DesiccantState.fresh => t.desiccant.fresh, }; - /// Single-select chips for the share terms. Gift and swap come first and sale /// last, never preselected — the gift is first-class, the sale a choice /// (sharing-model §4.1). There is always a selection ("just for me" default). @@ -1401,7 +1435,7 @@ class _ConditionChecksBlock extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Align( - alignment: Alignment.centerLeft, + alignment: AlignmentDirectional.centerStart, child: Text( t.conditionCheck.title, style: Theme.of(context).textTheme.labelLarge, @@ -1428,7 +1462,7 @@ class _ConditionChecksBlock extends StatelessWidget { ), ), Align( - alignment: Alignment.centerLeft, + alignment: AlignmentDirectional.centerStart, child: TextButton.icon( key: const Key('conditionCheck.open'), icon: const Icon(Icons.add), @@ -1778,7 +1812,7 @@ Future _showLotSheet( children: [ const SizedBox(height: 4), Align( - alignment: Alignment.centerLeft, + alignment: AlignmentDirectional.centerStart, child: Text( t.preservation.title, style: Theme.of( diff --git a/apps/app_seeds/pubspec.yaml b/apps/app_seeds/pubspec.yaml index c40545d..fd47a9a 100644 --- a/apps/app_seeds/pubspec.yaml +++ b/apps/app_seeds/pubspec.yaml @@ -146,13 +146,26 @@ flutter: # Remove before shipping if you don't want them in the bundle. - assets/ocr_fixtures/ # Embedded in generated PDFs (printable catalog). DejaVu covers Latin - # (extended), Cyrillic and Greek; like the OCR packs, PDF fonts are a - # per-locale resource — swap in wider coverage (Arabic, CJK) as locales - # land. Free license (Bitstream Vera / public domain additions). + # (extended), Cyrillic and Greek; Noto Arabic + Noto CJK ride alongside as + # per-glyph fallbacks so RTL and CJK text render instead of tofu. Fonts are + # a per-locale resource, extensible to any script. Licenses: DejaVu + # (Bitstream Vera / public domain), Noto (SIL OFL 1.1). See assets/fonts/. - assets/fonts/DejaVuSans.ttf - assets/fonts/DejaVuSans-Bold.ttf + - assets/fonts/NotoSansArabic.ttf + - assets/fonts/NotoSansJP.ttf fonts: # Custom seed-themed icon glyphs (chars a–k). See docs/mockups/seedks-glyphs.png. - family: seedks fonts: - asset: fonts/seedks.ttf + # Script coverage fallbacks for the UI text theme (see ui/theme.dart). Noto + # (SIL OFL 1.1) covers Arabic (RTL) and CJK (Japanese kanji/kana + shared + # Han) so those scripts render on every platform, including desktop where + # the system font may lack them. Variable fonts; Flutter picks the weight. + - family: Noto Sans Arabic + fonts: + - asset: assets/fonts/NotoSansArabic.ttf + - family: Noto Sans JP + fonts: + - asset: assets/fonts/NotoSansJP.ttf diff --git a/apps/app_seeds/test/services/i18n_ja_locale_test.dart b/apps/app_seeds/test/services/i18n_ja_locale_test.dart new file mode 100644 index 0000000..8de9e37 --- /dev/null +++ b/apps/app_seeds/test/services/i18n_ja_locale_test.dart @@ -0,0 +1,31 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tane/i18n/strings.g.dart'; + +/// Japanese (`ja`) is the CJK reference locale — fitting, since the app's own +/// name is Japanese (種, *tane*, "seed"). It proves the CJK path end to end: +/// the locale is wired, core strings resolve in Japanese, and untranslated +/// keys fall back to the English base (slang `fallback_strategy: base_locale`). +void main() { + test('ja is a supported locale', () { + expect( + AppLocaleUtils.supportedLocales.map((l) => l.languageCode), + contains('ja'), + ); + }); + + test('core strings resolve in Japanese', () { + final ja = AppLocale.ja.buildSync(); + expect(ja.menu.inventory, '在庫'); + expect(ja.common.save, '保存'); + expect(ja.settings.langJa, '日本語'); + }); + + test('untranslated keys fall back to the English base', () { + final ja = AppLocale.ja.buildSync(); + final en = AppLocale.en.buildSync(); + // `menu.plantares` is left untranslated in ja (a coined feature name), so + // it must surface the English text rather than a blank or a key. + expect(ja.menu.plantares, en.menu.plantares); + expect(ja.menu.plantares, isNotEmpty); + }); +} diff --git a/apps/app_seeds/test/services/label_sheet_service_test.dart b/apps/app_seeds/test/services/label_sheet_service_test.dart index 075e41d..75d58f0 100644 --- a/apps/app_seeds/test/services/label_sheet_service_test.dart +++ b/apps/app_seeds/test/services/label_sheet_service_test.dart @@ -76,6 +76,27 @@ void main() { expect(String.fromCharCodes(bytes.take(5)), '%PDF-'); }); + test('buildPdf embeds CJK and Arabic glyphs (no tofu)', () async { + // Japanese (CJK) and Arabic (RTL) fall outside DejaVu's coverage; the Noto + // fallbacks in the PDF theme must render them without throwing. Guards + // against a regression that drops the fallback fonts. + final service = LabelSheetService(files: _RecordingFileService()); + final bytes = await service.buildPdf( + labels: const [ + LabelSheetLabel( + varietyLabel: '日本の在来種のトマト', + commonName: 'トマト', + scientificName: 'Solanum lycopersicum', + qrData: 'tane://seed?v=tomato', + ), + LabelSheetLabel(varietyLabel: 'طماطم بلدية', qrData: 'tane://seed?v=x'), + ], + format: LabelSheetFormat.cards, + ); + expect(String.fromCharCodes(bytes.take(5)), '%PDF-'); + expect(bytes.length, greaterThan(1000)); + }); + test('saveLabels hands the PDF to the save dialog', () async { final files = _RecordingFileService(); final service = LabelSheetService(files: files); diff --git a/docs/i18n.md b/docs/i18n.md index cfc5b6b..400dbf8 100644 --- a/docs/i18n.md +++ b/docs/i18n.md @@ -10,11 +10,15 @@ speaks nested JSON) handles natively. ``` apps/app_seeds/lib/i18n/ en.i18n.json ← base locale (source of truth for keys) - es.i18n.json - pt.i18n.json + es.i18n.json ast, pt, fr, de, ja … strings*.g.dart ← generated, do not edit ``` +Live locales: `es`, `ast`, `en`, `pt`, `fr`, `de`, `ja`. Japanese (`ja`) is the +CJK reference — fittingly, the app's own name is Japanese (種, *tane*, "seed"). +It ships only a core set of strings translated; the rest fall back to English, +which is exactly how a partial locale is meant to grow. + - 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: @@ -41,6 +45,16 @@ apps/app_seeds/lib/i18n/ 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). + +**Fonts cover the scripts we claim.** The UI text theme carries Noto Sans +Arabic + Noto Sans JP as per-glyph fallbacks (see +[`theme.dart`](../apps/app_seeds/lib/ui/theme.dart)), so RTL and CJK render on +every platform — including desktop, where the system font may lack them. Every +generated PDF shares the same fallbacks via +[`pdf_fonts.dart`](../apps/app_seeds/lib/services/pdf_fonts.dart), so labels and +catalogs print Arabic/CJK instead of tofu. Fonts are a per-locale resource, +extensible to any script; widen the fallback list as more scripts land. + 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).