feat(i18n): full RTL/CJK support — fonts, Japanese locale, directional fixes
Bundle Noto Sans Arabic + Noto Sans JP (SIL OFL 1.1) as per-glyph fallbacks for both the UI text theme and every generated PDF, so RTL (Arabic) and CJK render on all platforms — including desktop, where the system font may lack them — instead of tofu. A shared pdf_fonts.dart centralizes the PDF theme and the three label/catalog/recovery services reuse it. Add Japanese (ja) as the CJK reference locale — fittingly, the app's own name is Japanese (種, tane, 'seed'). Core strings translated; the rest fall back to English via slang. Wired into supportedLocales (automatic) and the settings picker. Fix three physical Alignment.centerLeft -> AlignmentDirectional.centerStart in variety_detail_screen, and make month-abbreviation and avatar-initial truncation grapheme-safe (.characters) so CJK text is never cut mid-character. Tests: CJK/Arabic PDF glyph render, ja locale resolution + English fallback. The chat-bubble mirroring flagged by the audit was verified correct (already respects Directionality) and left unchanged.
This commit is contained in:
parent
41ea6735d2
commit
baa8867287
31 changed files with 686 additions and 104 deletions
93
apps/app_seeds/assets/fonts/NotoSansArabic-OFL.txt
Normal file
93
apps/app_seeds/assets/fonts/NotoSansArabic-OFL.txt
Normal file
|
|
@ -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.
|
||||||
BIN
apps/app_seeds/assets/fonts/NotoSansArabic.ttf
Normal file
BIN
apps/app_seeds/assets/fonts/NotoSansArabic.ttf
Normal file
Binary file not shown.
93
apps/app_seeds/assets/fonts/NotoSansJP-OFL.txt
Normal file
93
apps/app_seeds/assets/fonts/NotoSansJP-OFL.txt
Normal file
|
|
@ -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.
|
||||||
BIN
apps/app_seeds/assets/fonts/NotoSansJP.ttf
Normal file
BIN
apps/app_seeds/assets/fonts/NotoSansJP.ttf
Normal file
Binary file not shown.
|
|
@ -97,6 +97,7 @@
|
||||||
"langAst": "Asturianu",
|
"langAst": "Asturianu",
|
||||||
"langFr": "Français",
|
"langFr": "Français",
|
||||||
"langDe": "Deutsch",
|
"langDe": "Deutsch",
|
||||||
|
"langJa": "日本語",
|
||||||
"about": "Tocante a",
|
"about": "Tocante a",
|
||||||
"aboutText": "Inventariu llocal y cifráu pa simiente tradicional. AGPL-3.0.",
|
"aboutText": "Inventariu llocal y cifráu pa simiente tradicional. AGPL-3.0.",
|
||||||
"aboutOpen": "Tocante a Tane"
|
"aboutOpen": "Tocante a Tane"
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,8 @@
|
||||||
"aboutText": "Lokale, verschlüsselte Saatgutbank für traditionelle Samen. AGPL-3.0.",
|
"aboutText": "Lokale, verschlüsselte Saatgutbank für traditionelle Samen. AGPL-3.0.",
|
||||||
"aboutOpen": "Über Tane",
|
"aboutOpen": "Über Tane",
|
||||||
"langDe": "Deutsch",
|
"langDe": "Deutsch",
|
||||||
"langFr": "Français"
|
"langFr": "Français",
|
||||||
|
"langJa": "日本語"
|
||||||
},
|
},
|
||||||
"backup": {
|
"backup": {
|
||||||
"title": "Sicherung und Wiederherstellung",
|
"title": "Sicherung und Wiederherstellung",
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@
|
||||||
"langAst": "Asturianu",
|
"langAst": "Asturianu",
|
||||||
"langFr": "Français",
|
"langFr": "Français",
|
||||||
"langDe": "Deutsch",
|
"langDe": "Deutsch",
|
||||||
|
"langJa": "日本語",
|
||||||
"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 Tane"
|
"aboutOpen": "About Tane"
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,7 @@
|
||||||
"langPt": "Português",
|
"langPt": "Português",
|
||||||
"langFr": "Français",
|
"langFr": "Français",
|
||||||
"langDe": "Deutsch",
|
"langDe": "Deutsch",
|
||||||
|
"langJa": "日本語",
|
||||||
"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 Tane"
|
"aboutOpen": "Acerca de Tane"
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@
|
||||||
"langAst": "Asturianu",
|
"langAst": "Asturianu",
|
||||||
"langFr": "Français",
|
"langFr": "Français",
|
||||||
"langDe": "Deutsch",
|
"langDe": "Deutsch",
|
||||||
|
"langJa": "日本語",
|
||||||
"about": "À propos",
|
"about": "À propos",
|
||||||
"aboutText": "Inventaire local-first et chiffré pour les semences traditionnelles. AGPL-3.0.",
|
"aboutText": "Inventaire local-first et chiffré pour les semences traditionnelles. AGPL-3.0.",
|
||||||
"aboutOpen": "À propos de Tane"
|
"aboutOpen": "À propos de Tane"
|
||||||
|
|
|
||||||
46
apps/app_seeds/lib/i18n/ja.i18n.json
Normal file
46
apps/app_seeds/lib/i18n/ja.i18n.json
Normal file
|
|
@ -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": "種を管理する"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -98,6 +98,7 @@
|
||||||
"langAst": "Asturianu",
|
"langAst": "Asturianu",
|
||||||
"langFr": "Français",
|
"langFr": "Français",
|
||||||
"langDe": "Deutsch",
|
"langDe": "Deutsch",
|
||||||
|
"langJa": "日本語",
|
||||||
"about": "Acerca de",
|
"about": "Acerca de",
|
||||||
"aboutText": "Inventário local e cifrado para sementes tradicionais. AGPL-3.0.",
|
"aboutText": "Inventário local e cifrado para sementes tradicionais. AGPL-3.0.",
|
||||||
"aboutOpen": "Acerca do Tane"
|
"aboutOpen": "Acerca do Tane"
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@
|
||||||
/// Source: lib/i18n
|
/// Source: lib/i18n
|
||||||
/// To regenerate, run: `dart run slang`
|
/// To regenerate, run: `dart run slang`
|
||||||
///
|
///
|
||||||
/// Locales: 6
|
/// Locales: 7
|
||||||
/// Strings: 3356 (559 per locale)
|
/// 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
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// 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_de.g.dart' as l_de;
|
||||||
import 'strings_es.g.dart' as l_es;
|
import 'strings_es.g.dart' as l_es;
|
||||||
import 'strings_fr.g.dart' as l_fr;
|
import 'strings_fr.g.dart' as l_fr;
|
||||||
|
import 'strings_ja.g.dart' as l_ja;
|
||||||
import 'strings_pt.g.dart' as l_pt;
|
import 'strings_pt.g.dart' as l_pt;
|
||||||
part 'strings_en.g.dart';
|
part 'strings_en.g.dart';
|
||||||
|
|
||||||
|
|
@ -37,6 +38,7 @@ enum AppLocale with BaseAppLocale<AppLocale, Translations> {
|
||||||
de(languageCode: 'de'),
|
de(languageCode: 'de'),
|
||||||
es(languageCode: 'es'),
|
es(languageCode: 'es'),
|
||||||
fr(languageCode: 'fr'),
|
fr(languageCode: 'fr'),
|
||||||
|
ja(languageCode: 'ja'),
|
||||||
pt(languageCode: 'pt');
|
pt(languageCode: 'pt');
|
||||||
|
|
||||||
const AppLocale({
|
const AppLocale({
|
||||||
|
|
@ -99,6 +101,12 @@ enum AppLocale with BaseAppLocale<AppLocale, Translations> {
|
||||||
cardinalResolver: cardinalResolver,
|
cardinalResolver: cardinalResolver,
|
||||||
ordinalResolver: ordinalResolver,
|
ordinalResolver: ordinalResolver,
|
||||||
);
|
);
|
||||||
|
case AppLocale.ja:
|
||||||
|
return l_ja.TranslationsJa(
|
||||||
|
overrides: overrides,
|
||||||
|
cardinalResolver: cardinalResolver,
|
||||||
|
ordinalResolver: ordinalResolver,
|
||||||
|
);
|
||||||
case AppLocale.pt:
|
case AppLocale.pt:
|
||||||
return l_pt.TranslationsPt(
|
return l_pt.TranslationsPt(
|
||||||
overrides: overrides,
|
overrides: overrides,
|
||||||
|
|
|
||||||
|
|
@ -265,6 +265,7 @@ class _Translations$settings$ast extends Translations$settings$en {
|
||||||
@override String get langAst => 'Asturianu';
|
@override String get langAst => 'Asturianu';
|
||||||
@override String get langFr => 'Français';
|
@override String get langFr => 'Français';
|
||||||
@override String get langDe => 'Deutsch';
|
@override String get langDe => 'Deutsch';
|
||||||
|
@override String get langJa => '日本語';
|
||||||
@override String get about => 'Tocante a';
|
@override String get about => 'Tocante a';
|
||||||
@override String get aboutText => 'Inventariu llocal y cifráu pa simiente tradicional. AGPL-3.0.';
|
@override String get aboutText => 'Inventariu llocal y cifráu pa simiente tradicional. AGPL-3.0.';
|
||||||
@override String get aboutOpen => 'Tocante a Tane';
|
@override String get aboutOpen => 'Tocante a Tane';
|
||||||
|
|
@ -1488,6 +1489,7 @@ extension on TranslationsAst {
|
||||||
'settings.langAst' => 'Asturianu',
|
'settings.langAst' => 'Asturianu',
|
||||||
'settings.langFr' => 'Français',
|
'settings.langFr' => 'Français',
|
||||||
'settings.langDe' => 'Deutsch',
|
'settings.langDe' => 'Deutsch',
|
||||||
|
'settings.langJa' => '日本語',
|
||||||
'settings.about' => 'Tocante a',
|
'settings.about' => 'Tocante a',
|
||||||
'settings.aboutText' => 'Inventariu llocal y cifráu pa simiente tradicional. AGPL-3.0.',
|
'settings.aboutText' => 'Inventariu llocal y cifráu pa simiente tradicional. AGPL-3.0.',
|
||||||
'settings.aboutOpen' => 'Tocante a Tane',
|
'settings.aboutOpen' => 'Tocante a Tane',
|
||||||
|
|
@ -1922,9 +1924,9 @@ extension on TranslationsAst {
|
||||||
'sale.iSold' => 'Vendí',
|
'sale.iSold' => 'Vendí',
|
||||||
'sale.iBought' => 'Mercué',
|
'sale.iBought' => 'Mercué',
|
||||||
'sale.direction' => '¿Vendes o merques?',
|
'sale.direction' => '¿Vendes o merques?',
|
||||||
'sale.counterparty' => '¿Con quién?',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'sale.counterparty' => '¿Con quién?',
|
||||||
'sale.counterpartyHint' => 'Una persona o un coleutivu (opcional)',
|
'sale.counterpartyHint' => 'Una persona o un coleutivu (opcional)',
|
||||||
'sale.amount' => 'Importe',
|
'sale.amount' => 'Importe',
|
||||||
'sale.currency' => 'Moneda',
|
'sale.currency' => 'Moneda',
|
||||||
|
|
|
||||||
|
|
@ -269,6 +269,7 @@ class _Translations$settings$de extends Translations$settings$en {
|
||||||
@override String get aboutOpen => 'Über Tane';
|
@override String get aboutOpen => 'Über Tane';
|
||||||
@override String get langDe => 'Deutsch';
|
@override String get langDe => 'Deutsch';
|
||||||
@override String get langFr => 'Français';
|
@override String get langFr => 'Français';
|
||||||
|
@override String get langJa => '日本語';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: backup
|
// Path: backup
|
||||||
|
|
@ -1488,6 +1489,7 @@ extension on TranslationsDe {
|
||||||
'settings.aboutOpen' => 'Über Tane',
|
'settings.aboutOpen' => 'Über Tane',
|
||||||
'settings.langDe' => 'Deutsch',
|
'settings.langDe' => 'Deutsch',
|
||||||
'settings.langFr' => 'Français',
|
'settings.langFr' => 'Français',
|
||||||
|
'settings.langJa' => '日本語',
|
||||||
'backup.title' => 'Sicherung und Wiederherstellung',
|
'backup.title' => 'Sicherung und Wiederherstellung',
|
||||||
'backup.autoBackupTitle' => 'Automatische Sicherungen',
|
'backup.autoBackupTitle' => 'Automatische Sicherungen',
|
||||||
'backup.autoBackupLast' => ({required Object date}) => 'Letzte Sicherung ${date}',
|
'backup.autoBackupLast' => ({required Object date}) => 'Letzte Sicherung ${date}',
|
||||||
|
|
@ -1918,9 +1920,9 @@ extension on TranslationsDe {
|
||||||
'sale.counterpartyHint' => 'Eine Person oder ein Kollektiv (optional)',
|
'sale.counterpartyHint' => 'Eine Person oder ein Kollektiv (optional)',
|
||||||
'sale.amount' => 'Betrag',
|
'sale.amount' => 'Betrag',
|
||||||
'sale.currency' => 'Währung',
|
'sale.currency' => 'Währung',
|
||||||
'sale.currencyHint' => '€, Ğ1, Stunden… (optional)',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'sale.currencyHint' => '€, Ğ1, Stunden… (optional)',
|
||||||
'sale.hours' => 'Stunden',
|
'sale.hours' => 'Stunden',
|
||||||
'sale.note' => 'Notiz (optional)',
|
'sale.note' => 'Notiz (optional)',
|
||||||
'sale.save' => 'Speichern',
|
'sale.save' => 'Speichern',
|
||||||
|
|
|
||||||
|
|
@ -424,6 +424,9 @@ class Translations$settings$en {
|
||||||
/// en: 'Deutsch'
|
/// en: 'Deutsch'
|
||||||
String get langDe => 'Deutsch';
|
String get langDe => 'Deutsch';
|
||||||
|
|
||||||
|
/// en: '日本語'
|
||||||
|
String get langJa => '日本語';
|
||||||
|
|
||||||
/// en: 'About'
|
/// en: 'About'
|
||||||
String get about => 'About';
|
String get about => 'About';
|
||||||
|
|
||||||
|
|
@ -2598,6 +2601,7 @@ extension on Translations {
|
||||||
'settings.langAst' => 'Asturianu',
|
'settings.langAst' => 'Asturianu',
|
||||||
'settings.langFr' => 'Français',
|
'settings.langFr' => 'Français',
|
||||||
'settings.langDe' => 'Deutsch',
|
'settings.langDe' => 'Deutsch',
|
||||||
|
'settings.langJa' => '日本語',
|
||||||
'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 Tane',
|
'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.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.add' => 'Record a sale',
|
||||||
'sale.empty' => 'No sales yet. Note here what you sell or buy.',
|
'sale.empty' => 'No sales yet. Note here what you sell or buy.',
|
||||||
'sale.iSold' => 'I sold',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'sale.iSold' => 'I sold',
|
||||||
'sale.iBought' => 'I bought',
|
'sale.iBought' => 'I bought',
|
||||||
'sale.direction' => 'Sold or bought?',
|
'sale.direction' => 'Sold or bought?',
|
||||||
'sale.counterparty' => 'With whom?',
|
'sale.counterparty' => 'With whom?',
|
||||||
|
|
|
||||||
|
|
@ -265,6 +265,7 @@ class _Translations$settings$es extends Translations$settings$en {
|
||||||
@override String get langPt => 'Português';
|
@override String get langPt => 'Português';
|
||||||
@override String get langFr => 'Français';
|
@override String get langFr => 'Français';
|
||||||
@override String get langDe => 'Deutsch';
|
@override String get langDe => 'Deutsch';
|
||||||
|
@override String get langJa => '日本語';
|
||||||
@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 Tane';
|
@override String get aboutOpen => 'Acerca de Tane';
|
||||||
|
|
@ -1490,6 +1491,7 @@ extension on TranslationsEs {
|
||||||
'settings.langPt' => 'Português',
|
'settings.langPt' => 'Português',
|
||||||
'settings.langFr' => 'Français',
|
'settings.langFr' => 'Français',
|
||||||
'settings.langDe' => 'Deutsch',
|
'settings.langDe' => 'Deutsch',
|
||||||
|
'settings.langJa' => '日本語',
|
||||||
'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 Tane',
|
'settings.aboutOpen' => 'Acerca de Tane',
|
||||||
|
|
@ -1924,9 +1926,9 @@ extension on TranslationsEs {
|
||||||
'sale.add' => 'Registrar venta',
|
'sale.add' => 'Registrar venta',
|
||||||
'sale.empty' => 'Aún no hay ventas. Anota aquí lo que vendes o compras.',
|
'sale.empty' => 'Aún no hay ventas. Anota aquí lo que vendes o compras.',
|
||||||
'sale.iSold' => 'Vendí',
|
'sale.iSold' => 'Vendí',
|
||||||
'sale.iBought' => 'Compré',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'sale.iBought' => 'Compré',
|
||||||
'sale.direction' => '¿Vendes o compras?',
|
'sale.direction' => '¿Vendes o compras?',
|
||||||
'sale.counterparty' => '¿Con quién?',
|
'sale.counterparty' => '¿Con quién?',
|
||||||
'sale.counterpartyHint' => 'Una persona o un colectivo (opcional)',
|
'sale.counterpartyHint' => 'Una persona o un colectivo (opcional)',
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,7 @@ class _Translations$settings$fr extends Translations$settings$en {
|
||||||
@override String get langAst => 'Asturianu';
|
@override String get langAst => 'Asturianu';
|
||||||
@override String get langFr => 'Français';
|
@override String get langFr => 'Français';
|
||||||
@override String get langDe => 'Deutsch';
|
@override String get langDe => 'Deutsch';
|
||||||
|
@override String get langJa => '日本語';
|
||||||
@override String get about => 'À propos';
|
@override String get about => 'À propos';
|
||||||
@override String get aboutText => 'Inventaire local-first et chiffré pour les semences traditionnelles. AGPL-3.0.';
|
@override String get aboutText => 'Inventaire local-first et chiffré pour les semences traditionnelles. AGPL-3.0.';
|
||||||
@override String get aboutOpen => 'À propos de Tane';
|
@override String get aboutOpen => 'À propos de Tane';
|
||||||
|
|
@ -1485,6 +1486,7 @@ extension on TranslationsFr {
|
||||||
'settings.langAst' => 'Asturianu',
|
'settings.langAst' => 'Asturianu',
|
||||||
'settings.langFr' => 'Français',
|
'settings.langFr' => 'Français',
|
||||||
'settings.langDe' => 'Deutsch',
|
'settings.langDe' => 'Deutsch',
|
||||||
|
'settings.langJa' => '日本語',
|
||||||
'settings.about' => 'À propos',
|
'settings.about' => 'À propos',
|
||||||
'settings.aboutText' => 'Inventaire local-first et chiffré pour les semences traditionnelles. AGPL-3.0.',
|
'settings.aboutText' => 'Inventaire local-first et chiffré pour les semences traditionnelles. AGPL-3.0.',
|
||||||
'settings.aboutOpen' => 'À propos de Tane',
|
'settings.aboutOpen' => 'À propos de Tane',
|
||||||
|
|
@ -1918,9 +1920,9 @@ extension on TranslationsFr {
|
||||||
'sale.counterpartyHint' => 'Une personne ou un collectif (optionnel)',
|
'sale.counterpartyHint' => 'Une personne ou un collectif (optionnel)',
|
||||||
'sale.amount' => 'Montant',
|
'sale.amount' => 'Montant',
|
||||||
'sale.currency' => 'Monnaie',
|
'sale.currency' => 'Monnaie',
|
||||||
'sale.currencyHint' => '€, Ğ1, heures… (optionnel)',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'sale.currencyHint' => '€, Ğ1, heures… (optionnel)',
|
||||||
'sale.hours' => 'heures',
|
'sale.hours' => 'heures',
|
||||||
'sale.note' => 'Note (optionnel)',
|
'sale.note' => 'Note (optionnel)',
|
||||||
'sale.save' => 'Enregistrer',
|
'sale.save' => 'Enregistrer',
|
||||||
|
|
|
||||||
173
apps/app_seeds/lib/i18n/strings_ja.g.dart
Normal file
173
apps/app_seeds/lib/i18n/strings_ja.g.dart
Normal file
|
|
@ -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: <root>
|
||||||
|
class TranslationsJa extends Translations with BaseTranslations<AppLocale, Translations> {
|
||||||
|
/// You can call this constructor and build your own translation instance of this locale.
|
||||||
|
/// Constructing via the enum [AppLocale.build] is preferred.
|
||||||
|
TranslationsJa({Map<String, Node>? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver, TranslationMetadata<AppLocale, Translations>? 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 <ja>.
|
||||||
|
@override final TranslationMetadata<AppLocale, Translations> $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<AppLocale, Translations>? 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 <ja>.
|
||||||
|
/// 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -266,6 +266,7 @@ class _Translations$settings$pt extends Translations$settings$en {
|
||||||
@override String get langAst => 'Asturianu';
|
@override String get langAst => 'Asturianu';
|
||||||
@override String get langFr => 'Français';
|
@override String get langFr => 'Français';
|
||||||
@override String get langDe => 'Deutsch';
|
@override String get langDe => 'Deutsch';
|
||||||
|
@override String get langJa => '日本語';
|
||||||
@override String get about => 'Acerca de';
|
@override String get about => 'Acerca de';
|
||||||
@override String get aboutText => 'Inventário local e cifrado para sementes tradicionais. AGPL-3.0.';
|
@override String get aboutText => 'Inventário local e cifrado para sementes tradicionais. AGPL-3.0.';
|
||||||
@override String get aboutOpen => 'Acerca do Tane';
|
@override String get aboutOpen => 'Acerca do Tane';
|
||||||
|
|
@ -1488,6 +1489,7 @@ extension on TranslationsPt {
|
||||||
'settings.langAst' => 'Asturianu',
|
'settings.langAst' => 'Asturianu',
|
||||||
'settings.langFr' => 'Français',
|
'settings.langFr' => 'Français',
|
||||||
'settings.langDe' => 'Deutsch',
|
'settings.langDe' => 'Deutsch',
|
||||||
|
'settings.langJa' => '日本語',
|
||||||
'settings.about' => 'Acerca de',
|
'settings.about' => 'Acerca de',
|
||||||
'settings.aboutText' => 'Inventário local e cifrado para sementes tradicionais. AGPL-3.0.',
|
'settings.aboutText' => 'Inventário local e cifrado para sementes tradicionais. AGPL-3.0.',
|
||||||
'settings.aboutOpen' => 'Acerca do Tane',
|
'settings.aboutOpen' => 'Acerca do Tane',
|
||||||
|
|
@ -1921,9 +1923,9 @@ extension on TranslationsPt {
|
||||||
'sale.iBought' => 'Comprei',
|
'sale.iBought' => 'Comprei',
|
||||||
'sale.direction' => 'Vendes ou compras?',
|
'sale.direction' => 'Vendes ou compras?',
|
||||||
'sale.counterparty' => 'Com quem?',
|
'sale.counterparty' => 'Com quem?',
|
||||||
'sale.counterpartyHint' => 'Uma pessoa ou um coletivo (opcional)',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'sale.counterpartyHint' => 'Uma pessoa ou um coletivo (opcional)',
|
||||||
'sale.amount' => 'Montante',
|
'sale.amount' => 'Montante',
|
||||||
'sale.currency' => 'Moeda',
|
'sale.currency' => 'Moeda',
|
||||||
'sale.currencyHint' => '€, Ğ1, horas… (opcional)',
|
'sale.currencyHint' => '€, Ğ1, horas… (opcional)',
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/services.dart' show rootBundle;
|
|
||||||
import 'package:pdf/pdf.dart';
|
import 'package:pdf/pdf.dart';
|
||||||
import 'package:pdf/widgets.dart' as pw;
|
import 'package:pdf/widgets.dart' as pw;
|
||||||
|
|
||||||
import 'file_service.dart';
|
import 'file_service.dart';
|
||||||
|
import 'pdf_fonts.dart';
|
||||||
|
|
||||||
/// How the labels are tiled on the page: many small stickers to peel onto
|
/// How the labels are tiled on the page: many small stickers to peel onto
|
||||||
/// packets, or fewer larger cards for jars and boxes.
|
/// 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
|
/// platform channels apart from loading the bundled font, so it runs (and is
|
||||||
/// tested) on the host.
|
/// tested) on the host.
|
||||||
///
|
///
|
||||||
/// The embedded DejaVu face covers Latin (extended), Cyrillic and Greek. Like
|
/// The embedded DejaVu face covers Latin (extended), Cyrillic and Greek, with
|
||||||
/// the OCR traineddata, the PDF font is a per-locale resource: wider scripts
|
/// Noto Arabic + Noto CJK as per-glyph fallbacks (see [buildPdfTheme]) so RTL
|
||||||
/// (Arabic, CJK) ship alongside their locales, not hardcoded here.
|
/// and CJK text render. Fonts are a per-locale resource, extensible to any
|
||||||
|
/// script.
|
||||||
class LabelSheetService {
|
class LabelSheetService {
|
||||||
LabelSheetService({required FileService files}) : _files = files;
|
LabelSheetService({required FileService files}) : _files = files;
|
||||||
|
|
||||||
|
|
@ -103,15 +104,8 @@ class LabelSheetService {
|
||||||
bool rtl = false,
|
bool rtl = false,
|
||||||
}) async {
|
}) async {
|
||||||
final textDirection = rtl ? pw.TextDirection.rtl : pw.TextDirection.ltr;
|
final textDirection = rtl ? pw.TextDirection.rtl : pw.TextDirection.ltr;
|
||||||
final base = pw.Font.ttf(
|
final fonts = await loadPdfFonts();
|
||||||
await rootBundle.load('assets/fonts/DejaVuSans.ttf'),
|
final doc = pw.Document(theme: fonts.theme);
|
||||||
);
|
|
||||||
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 g = _geometryOf(format);
|
final g = _geometryOf(format);
|
||||||
final labelWidth = (PdfPageFormat.a4.width - _margin * 2) / g.columns;
|
final labelWidth = (PdfPageFormat.a4.width - _margin * 2) / g.columns;
|
||||||
|
|
@ -150,7 +144,7 @@ class LabelSheetService {
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: pw.TextOverflow.clip,
|
overflow: pw.TextOverflow.clip,
|
||||||
style: pw.TextStyle(
|
style: pw.TextStyle(
|
||||||
font: bold,
|
font: fonts.bold,
|
||||||
fontSize: g.common,
|
fontSize: g.common,
|
||||||
color: PdfColors.grey800,
|
color: PdfColors.grey800,
|
||||||
),
|
),
|
||||||
|
|
@ -159,7 +153,7 @@ class LabelSheetService {
|
||||||
label.varietyLabel,
|
label.varietyLabel,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: pw.TextOverflow.clip,
|
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)
|
if (label.scientificName != null)
|
||||||
pw.Text(
|
pw.Text(
|
||||||
|
|
|
||||||
38
apps/app_seeds/lib/services/pdf_fonts.dart
Normal file
38
apps/app_seeds/lib/services/pdf_fonts.dart
Normal file
|
|
@ -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<PdfFonts> 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);
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/services.dart' show rootBundle;
|
|
||||||
import 'package:pdf/pdf.dart';
|
import 'package:pdf/pdf.dart';
|
||||||
import 'package:pdf/widgets.dart' as pw;
|
import 'package:pdf/widgets.dart' as pw;
|
||||||
|
|
||||||
import 'file_service.dart';
|
import 'file_service.dart';
|
||||||
|
import 'pdf_fonts.dart';
|
||||||
|
|
||||||
/// Renders the printable recovery sheet — a QR plus the typed code and short
|
/// Renders the printable recovery sheet — a QR plus the typed code and short
|
||||||
/// instructions ("keep two paper copies, like your best seed"). All strings
|
/// instructions ("keep two paper copies, like your best seed"). All strings
|
||||||
|
|
@ -19,22 +19,15 @@ class RecoverySheetService {
|
||||||
required String intro,
|
required String intro,
|
||||||
required String code,
|
required String code,
|
||||||
}) async {
|
}) async {
|
||||||
final base = pw.Font.ttf(
|
final fonts = await loadPdfFonts();
|
||||||
await rootBundle.load('assets/fonts/DejaVuSans.ttf'),
|
final doc = pw.Document(theme: fonts.theme);
|
||||||
);
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
doc.addPage(
|
doc.addPage(
|
||||||
pw.Page(
|
pw.Page(
|
||||||
pageFormat: PdfPageFormat.a4,
|
pageFormat: PdfPageFormat.a4,
|
||||||
build: (context) => pw.Column(
|
build: (context) => pw.Column(
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
children: [
|
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.SizedBox(height: 12),
|
||||||
pw.Text(intro, style: const pw.TextStyle(fontSize: 11)),
|
pw.Text(intro, style: const pw.TextStyle(fontSize: 11)),
|
||||||
pw.SizedBox(height: 24),
|
pw.SizedBox(height: 24),
|
||||||
|
|
@ -54,7 +47,7 @@ class RecoverySheetService {
|
||||||
child: pw.Text(
|
child: pw.Text(
|
||||||
code,
|
code,
|
||||||
textAlign: pw.TextAlign.center,
|
textAlign: pw.TextAlign.center,
|
||||||
style: pw.TextStyle(font: bold, fontSize: 12),
|
style: pw.TextStyle(font: fonts.bold, fontSize: 12),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/services.dart' show rootBundle;
|
|
||||||
import 'package:pdf/pdf.dart';
|
import 'package:pdf/pdf.dart';
|
||||||
import 'package:pdf/widgets.dart' as pw;
|
import 'package:pdf/widgets.dart' as pw;
|
||||||
|
|
||||||
import 'file_service.dart';
|
import 'file_service.dart';
|
||||||
|
import 'pdf_fonts.dart';
|
||||||
|
|
||||||
/// One printable line of the "what I share" catalog. All strings arrive
|
/// One printable line of the "what I share" catalog. All strings arrive
|
||||||
/// already localized — this service knows nothing about i18n or the domain.
|
/// already localized — this service knows nothing about i18n or the domain.
|
||||||
|
|
@ -47,20 +47,13 @@ class ShareCatalogService {
|
||||||
required String date,
|
required String date,
|
||||||
required List<ShareCatalogRow> rows,
|
required List<ShareCatalogRow> rows,
|
||||||
}) async {
|
}) async {
|
||||||
final base = pw.Font.ttf(
|
final fonts = await loadPdfFonts();
|
||||||
await rootBundle.load('assets/fonts/DejaVuSans.ttf'),
|
final doc = pw.Document(theme: fonts.theme);
|
||||||
);
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
doc.addPage(
|
doc.addPage(
|
||||||
pw.MultiPage(
|
pw.MultiPage(
|
||||||
pageFormat: PdfPageFormat.a4,
|
pageFormat: PdfPageFormat.a4,
|
||||||
build: (context) => [
|
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.SizedBox(height: 2),
|
||||||
pw.Text(
|
pw.Text(
|
||||||
date,
|
date,
|
||||||
|
|
@ -84,7 +77,7 @@ class ShareCatalogService {
|
||||||
children: [
|
children: [
|
||||||
pw.Text(
|
pw.Text(
|
||||||
row.name,
|
row.name,
|
||||||
style: pw.TextStyle(font: bold, fontSize: 12),
|
style: pw.TextStyle(font: fonts.bold, fontSize: 12),
|
||||||
),
|
),
|
||||||
if (row.scientificName != null)
|
if (row.scientificName != null)
|
||||||
pw.Text(
|
pw.Text(
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,10 @@ class InventoryListScreen extends StatelessWidget {
|
||||||
key: const Key('inventory.search'),
|
key: const Key('inventory.search'),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: t.inventory.searchHint,
|
hintText: t.inventory.searchHint,
|
||||||
prefixIcon: const Icon(Icons.search, color: seedMuted),
|
prefixIcon: const Icon(
|
||||||
|
Icons.search,
|
||||||
|
color: seedMuted,
|
||||||
|
),
|
||||||
hintStyle: const TextStyle(color: seedMuted),
|
hintStyle: const TextStyle(color: seedMuted),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Colors.white,
|
fillColor: Colors.white,
|
||||||
|
|
@ -404,15 +407,21 @@ class _FilterBar extends StatelessWidget {
|
||||||
|
|
||||||
// Lay out the groups in order, dropping a hairline divider between any two
|
// Lay out the groups in order, dropping a hairline divider between any two
|
||||||
// that both have chips.
|
// that both have chips.
|
||||||
final groups = [attrChips, formChips, categoryChips].where((g) => g.isNotEmpty);
|
final groups = [
|
||||||
|
attrChips,
|
||||||
|
formChips,
|
||||||
|
categoryChips,
|
||||||
|
].where((g) => g.isNotEmpty);
|
||||||
final row = <Widget>[];
|
final row = <Widget>[];
|
||||||
for (final group in groups) {
|
for (final group in groups) {
|
||||||
if (row.isNotEmpty) row.add(const _FilterGroupDivider());
|
if (row.isNotEmpty) row.add(const _FilterGroupDivider());
|
||||||
for (final chip in group) {
|
for (final chip in group) {
|
||||||
row.add(Padding(
|
row.add(
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsetsDirectional.only(end: 8),
|
padding: const EdgeInsetsDirectional.only(end: 8),
|
||||||
child: chip,
|
child: chip,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final scroller = EdgeFade(
|
final scroller = EdgeFade(
|
||||||
|
|
@ -587,7 +596,10 @@ class _CategoryHeader extends StatelessWidget {
|
||||||
width: 10,
|
width: 10,
|
||||||
height: 10,
|
height: 10,
|
||||||
margin: const EdgeInsetsDirectional.only(end: 8),
|
margin: const EdgeInsetsDirectional.only(end: 8),
|
||||||
decoration: BoxDecoration(color: swatch.ink, shape: BoxShape.circle),
|
decoration: BoxDecoration(
|
||||||
|
color: swatch.ink,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
// Base on the text theme so it honours the system font-scale factor.
|
// 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 trimmed = item.label.trim();
|
||||||
final initial = trimmed.isEmpty
|
final initial = trimmed.isEmpty
|
||||||
? '?'
|
? '?'
|
||||||
: trimmed.substring(0, 1).toUpperCase();
|
: trimmed.characters.first.toUpperCase();
|
||||||
return ExcludeSemantics(
|
return ExcludeSemantics(
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
backgroundColor: seedAvatar,
|
backgroundColor: seedAvatar,
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ List<(AppLocale, String)> _languages(Translations t) => [
|
||||||
(AppLocale.pt, t.settings.langPt),
|
(AppLocale.pt, t.settings.langPt),
|
||||||
(AppLocale.fr, t.settings.langFr),
|
(AppLocale.fr, t.settings.langFr),
|
||||||
(AppLocale.de, t.settings.langDe),
|
(AppLocale.de, t.settings.langDe),
|
||||||
|
(AppLocale.ja, t.settings.langJa),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// A single tile showing the active language; tapping opens a picker with all
|
/// A single tile showing the active language; tapping opens a picker with all
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,11 @@ ThemeData buildTaneTheme() {
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
colorScheme: scheme,
|
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,
|
scaffoldBackgroundColor: seedCanvas,
|
||||||
// Green app bars — aligned with the v2 mockups and the real app.
|
// Green app bars — aligned with the v2 mockups and the real app.
|
||||||
appBarTheme: const AppBarTheme(
|
appBarTheme: const AppBarTheme(
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,8 @@ class _DetailView extends StatelessWidget {
|
||||||
if (SeedSavingCatalog.guideFor(
|
if (SeedSavingCatalog.guideFor(
|
||||||
scientificName: detail.scientificName,
|
scientificName: detail.scientificName,
|
||||||
family: detail.family ?? detail.category,
|
family: detail.family ?? detail.category,
|
||||||
) case final guide? when guide.hasAny) ...[
|
)
|
||||||
|
case final guide? when guide.hasAny) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
// Reference data (facts about the species, not the grower's own
|
// Reference data (facts about the species, not the grower's own
|
||||||
// records) — collapsed by default so it's opt-in depth, not noise.
|
// 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.statusReturned
|
||||||
: t.plantare.statusForgiven,
|
: t.plantare.statusForgiven,
|
||||||
];
|
];
|
||||||
final overdue = !done &&
|
final overdue =
|
||||||
|
!done &&
|
||||||
p.dueBy != null &&
|
p.dueBy != null &&
|
||||||
p.dueBy! < DateTime.now().millisecondsSinceEpoch;
|
p.dueBy! < DateTime.now().millisecondsSinceEpoch;
|
||||||
final returnBy = p.dueBy == null
|
final returnBy = p.dueBy == null
|
||||||
|
|
@ -997,7 +999,9 @@ class _EditVarietySheetState extends State<_EditVarietySheet> {
|
||||||
child: Text(
|
child: Text(
|
||||||
t.cropCalendar.editorHint,
|
t.cropCalendar.editorHint,
|
||||||
style: const TextStyle(
|
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.
|
/// A short (≤3-grapheme) label for a month name, for the compact calendar chips.
|
||||||
String _monthAbbrev(String name) =>
|
/// Counts by grapheme (not UTF-16 units) so CJK/complex scripts don't truncate
|
||||||
name.length <= 3 ? name : name.substring(0, 3);
|
/// 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").
|
/// Read-only crop calendar: one line per recorded phase ("Sow · Mar, Apr, Sep").
|
||||||
/// Renders only the phases that have months set.
|
/// Renders only the phases that have months set.
|
||||||
|
|
@ -1201,14 +1209,24 @@ class _SeedSavingView extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((guide.recommendedPlants ?? guide.minPlants) case final n?) {
|
if ((guide.recommendedPlants ?? guide.minPlants) case final n?) {
|
||||||
rows.add(_line(
|
rows.add(
|
||||||
Icons.groups_outlined, t.seedSaving.plants, t.seedSaving.plantsValue(n: n)));
|
_line(
|
||||||
|
Icons.groups_outlined,
|
||||||
|
t.seedSaving.plants,
|
||||||
|
t.seedSaving.plantsValue(n: n),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (guide.processing case final proc?) {
|
if (guide.processing case final proc?) {
|
||||||
final wet = proc == SeedProcessing.wet;
|
final wet = proc == SeedProcessing.wet;
|
||||||
rows.add(_line(wet ? Icons.water_drop_outlined : Icons.grass,
|
rows.add(
|
||||||
t.seedSaving.processing, wet ? t.seedSaving.procWet : t.seedSaving.procDry));
|
_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);
|
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
|
// Advisory + a light source credit — the figures are general, not local
|
||||||
// gospel, and drawn from the seed-saving literature.
|
// gospel, and drawn from the seed-saving literature.
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text(t.seedSaving.advisory,
|
Text(
|
||||||
|
t.seedSaving.advisory,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: seedMuted, fontSize: 12, fontStyle: FontStyle.italic)),
|
color: seedMuted,
|
||||||
if (SeedSavingCatalog.sources case final sources when sources.isNotEmpty)
|
fontSize: 12,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (SeedSavingCatalog.sources case final sources
|
||||||
|
when sources.isNotEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 2),
|
padding: const EdgeInsets.only(top: 2),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -1252,12 +1276,17 @@ class _SeedSavingView extends StatelessWidget {
|
||||||
Icon(icon, size: 18, color: seedGreen),
|
Icon(icon, size: 18, color: seedGreen),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text.rich(TextSpan(children: [
|
child: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: '$label · ',
|
text: '$label · ',
|
||||||
style: const TextStyle(fontWeight: FontWeight.w600)),
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
TextSpan(text: value),
|
TextSpan(text: value),
|
||||||
])),
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -1274,24 +1303,30 @@ class _DifficultyChip extends StatelessWidget {
|
||||||
final t = context.t;
|
final t = context.t;
|
||||||
final (label, color) = switch (difficulty) {
|
final (label, color) = switch (difficulty) {
|
||||||
SavingDifficulty.easy => (t.seedSaving.diffEasy, const Color(0xFF3B6D11)),
|
SavingDifficulty.easy => (t.seedSaving.diffEasy, const Color(0xFF3B6D11)),
|
||||||
SavingDifficulty.medium =>
|
SavingDifficulty.medium => (
|
||||||
(t.seedSaving.diffMedium, const Color(0xFF8A6D1E)),
|
t.seedSaving.diffMedium,
|
||||||
|
const Color(0xFF8A6D1E),
|
||||||
|
),
|
||||||
SavingDifficulty.hard => (t.seedSaving.diffHard, const Color(0xFFA14234)),
|
SavingDifficulty.hard => (t.seedSaving.diffHard, const Color(0xFFA14234)),
|
||||||
};
|
};
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.insights, size: 18, color: seedGreen),
|
Icon(Icons.insights, size: 18, color: seedGreen),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text('${t.seedSaving.difficulty} · ',
|
Text(
|
||||||
style: const TextStyle(fontWeight: FontWeight.w600)),
|
'${t.seedSaving.difficulty} · ',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: color.withValues(alpha: 0.14),
|
color: color.withValues(alpha: 0.14),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: Text(label,
|
child: Text(
|
||||||
style: TextStyle(color: color, fontWeight: FontWeight.w600)),
|
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,
|
DesiccantState.fresh => t.desiccant.fresh,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/// Single-select chips for the share terms. Gift and swap come first and sale
|
/// 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
|
/// 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).
|
/// (sharing-model §4.1). There is always a selection ("just for me" default).
|
||||||
|
|
@ -1401,7 +1435,7 @@ class _ConditionChecksBlock extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: AlignmentDirectional.centerStart,
|
||||||
child: Text(
|
child: Text(
|
||||||
t.conditionCheck.title,
|
t.conditionCheck.title,
|
||||||
style: Theme.of(context).textTheme.labelLarge,
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
|
@ -1428,7 +1462,7 @@ class _ConditionChecksBlock extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: AlignmentDirectional.centerStart,
|
||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
key: const Key('conditionCheck.open'),
|
key: const Key('conditionCheck.open'),
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
|
|
@ -1778,7 +1812,7 @@ Future<void> _showLotSheet(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: AlignmentDirectional.centerStart,
|
||||||
child: Text(
|
child: Text(
|
||||||
t.preservation.title,
|
t.preservation.title,
|
||||||
style: Theme.of(
|
style: Theme.of(
|
||||||
|
|
|
||||||
|
|
@ -146,13 +146,26 @@ flutter:
|
||||||
# Remove before shipping if you don't want them in the bundle.
|
# Remove before shipping if you don't want them in the bundle.
|
||||||
- assets/ocr_fixtures/
|
- assets/ocr_fixtures/
|
||||||
# Embedded in generated PDFs (printable catalog). DejaVu covers Latin
|
# Embedded in generated PDFs (printable catalog). DejaVu covers Latin
|
||||||
# (extended), Cyrillic and Greek; like the OCR packs, PDF fonts are a
|
# (extended), Cyrillic and Greek; Noto Arabic + Noto CJK ride alongside as
|
||||||
# per-locale resource — swap in wider coverage (Arabic, CJK) as locales
|
# per-glyph fallbacks so RTL and CJK text render instead of tofu. Fonts are
|
||||||
# land. Free license (Bitstream Vera / public domain additions).
|
# 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.ttf
|
||||||
- assets/fonts/DejaVuSans-Bold.ttf
|
- assets/fonts/DejaVuSans-Bold.ttf
|
||||||
|
- assets/fonts/NotoSansArabic.ttf
|
||||||
|
- assets/fonts/NotoSansJP.ttf
|
||||||
fonts:
|
fonts:
|
||||||
# Custom seed-themed icon glyphs (chars a–k). See docs/mockups/seedks-glyphs.png.
|
# Custom seed-themed icon glyphs (chars a–k). See docs/mockups/seedks-glyphs.png.
|
||||||
- family: seedks
|
- family: seedks
|
||||||
fonts:
|
fonts:
|
||||||
- asset: fonts/seedks.ttf
|
- 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
|
||||||
|
|
|
||||||
31
apps/app_seeds/test/services/i18n_ja_locale_test.dart
Normal file
31
apps/app_seeds/test/services/i18n_ja_locale_test.dart
Normal file
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -76,6 +76,27 @@ void main() {
|
||||||
expect(String.fromCharCodes(bytes.take(5)), '%PDF-');
|
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 {
|
test('saveLabels hands the PDF to the save dialog', () async {
|
||||||
final files = _RecordingFileService();
|
final files = _RecordingFileService();
|
||||||
final service = LabelSheetService(files: files);
|
final service = LabelSheetService(files: files);
|
||||||
|
|
|
||||||
18
docs/i18n.md
18
docs/i18n.md
|
|
@ -10,11 +10,15 @@ speaks nested JSON) handles natively.
|
||||||
```
|
```
|
||||||
apps/app_seeds/lib/i18n/
|
apps/app_seeds/lib/i18n/
|
||||||
en.i18n.json ← base locale (source of truth for keys)
|
en.i18n.json ← base locale (source of truth for keys)
|
||||||
es.i18n.json
|
es.i18n.json ast, pt, fr, de, ja …
|
||||||
pt.i18n.json
|
|
||||||
strings*.g.dart ← generated, do not edit
|
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.
|
- Placeholders use braces: `"{count} entries added"`. Keep them verbatim.
|
||||||
- Keys are code and never change per locale; only values are translated.
|
- Keys are code and never change per locale; only values are translated.
|
||||||
- Missing keys fall back to English at runtime (`fallback_strategy:
|
- 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
|
Right-to-left locales (Arabic, Hebrew, Persian) are first-class: layouts use
|
||||||
directional insets and are exercised by
|
directional insets and are exercised by
|
||||||
[`rtl_smoke_test.dart`](../apps/app_seeds/test/ui/rtl_smoke_test.dart).
|
[`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
|
Locale-specific resources ship with their locale, never hardcoded: OCR
|
||||||
language packs (`assets/tessdata/`), PDF fonts (`assets/fonts/`), and species
|
language packs (`assets/tessdata/`), PDF fonts (`assets/fonts/`), and species
|
||||||
common names (`assets/catalog/species.json`, a locale-keyed map).
|
common names (`assets/catalog/species.json`, a locale-keyed map).
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue