From 917c1157b8bf5bc0d1f4ea83e48c185099e85840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Ma=C3=B1osa?= Date: Sat, 3 Feb 2024 22:06:10 +0100 Subject: [PATCH] =?UTF-8?q?feat(localization):=20=F0=9F=8C=90=20add=20loca?= =?UTF-8?q?lization=20support=20(#50)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + analysis_options.yaml | 71 +------------------ l10n.yaml | 5 ++ lib/localization/app_ca.arb | 31 ++++++++ lib/src/app.dart | 10 +-- lib/src/pages/home_page.dart | 5 +- lib/src/pages/word_page.dart | 2 +- .../autocomplete_entries_list_view.dart | 46 +++++++----- lib/src/widgets/autocomplete_entry_card.dart | 17 ++++- lib/src/widgets/scope_chip.dart | 5 +- lib/src/widgets/search_bar_results.dart | 9 ++- 11 files changed, 97 insertions(+), 105 deletions(-) create mode 100644 l10n.yaml create mode 100644 lib/localization/app_ca.arb diff --git a/.gitignore b/.gitignore index 469f429..a66db40 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ coverage/ .pub-cache/ .pub/ /build/ +untranslated_messages.txt # Web related lib/generated_plugin_registrant.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index 9d8cdd9..d5fb7bb 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -5,83 +5,14 @@ linter: - avoid_classes_with_only_static_members - avoid_types_on_closure_parameters - cancel_subscriptions - # - discarded_futures - unnecessary_null_aware_operator_on_extension_on_nullable -dart_code_metrics: - metrics: - cyclomatic-complexity: 20 - maximum-nesting-level: 5 - number-of-arguments: 4 - metrics-exclude: - - test/** - rules: - - always-remove-listener - - arguments-ordering: - child-last: true - - avoid-border-all - - avoid-cascade-after-if-null - - avoid-collection-methods-with-unrelated-types - - avoid-double-slash-imports - - avoid-duplicate-exports - - avoid-global-state - - avoid-missing-enum-constant-in-map - - avoid-nested-conditional-expressions: - acceptable-level: 2 - - avoid-redundant-async - - avoid-returning-widgets - - avoid-shrink-wrap-in-lists - - avoid-throw-in-catch-block - - avoid-unnecessary-conditionals - - avoid-unnecessary-setstate - - avoid-unnecessary-type-assertions - - avoid-unnecessary-type-casts - - avoid-unrelated-type-assertions - - avoid-expanded-as-spacer - - avoid-wrapping-in-padding - - binary-expression-operand-order - - double-literal-format - - format-comment: - ignored-patterns: - - ^coverage.* - # - member-ordering - - missing-test-assertion - - newline-before-return - - no-boolean-literal-compare - - no-equal-then-else - # - no-magic-number - - prefer-conditional-expressions - - prefer-correct-edge-insets-constructor - - prefer-correct-test-file-name: - exclude: - - lib/** - - bin/** - - "**/main.dart" - - prefer-enums-by-name - # - prefer-extracting-callbacks - - prefer-first - - prefer-immediate-return - - prefer-iterable-of - - prefer-last - - prefer-match-file-name: - exclude: - - test/** - # - prefer-moving-to-variable - - prefer-trailing-comma - - prefer-single-widget-per-file: - ignore-private-widgets: true - - use-setstate-synchronously - anti-patterns: - - long-parameter-list - analyzer: - plugins: - - dart_code_metrics - errors: avoid_equals_and_hash_code_on_mutable_classes: ignore public_member_api_docs: ignore unused_element: ignore # See https://github.com/dart-lang/sdk/issues/49025 + no_default_cases: ignore # Style decisions always_put_required_named_parameters_first: ignore diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..8278d50 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,5 @@ +arb-dir: lib/localization +template-arb-file: app_ca.arb +output-localization-file: app_localizations.dart +untranslated-messages-file: lib/localization/untranslated_messages.txt +nullable-getter: false diff --git a/lib/localization/app_ca.arb b/lib/localization/app_ca.arb new file mode 100644 index 0000000..3f1d968 --- /dev/null +++ b/lib/localization/app_ca.arb @@ -0,0 +1,31 @@ +{ + "@@locale": "ca", + "search": "Cerca", + "myCollection": "La meva coŀlecció", + "firstNResultsAreShown": "Es mostren els primers {n} resultats de l’autocompletat", + "@firstNResultsAreShown": { + "placeholders": { + "n": { + "type": "int", + "example": "20" + } + } + }, + "nResults": "{n, plural, =0{Sense resultats} =1{1 resultat} other{{n} resultats}}", + "@nResults": { + "placeholders": { + "n": { + "type": "int", + "example": "7" + } + } + }, + "orMore": "o més", + "addToTheCollection": "Afegeix a la coŀlecció", + "removeFromTheCollection": "Treu de la coŀlecció", + "added": "S’ha afegit", + "removed": "S’ha tret", + "toTheCollection": "a la coŀlecció", + "fromTheCollection": "de la coŀlecció", + "showAbbreviation": "Mostra l’abreviatura" +} diff --git a/lib/src/app.dart b/lib/src/app.dart index c529ff5..5754c32 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -1,7 +1,7 @@ import 'package:el_meu_diec/src/pages/home_page.dart'; import 'package:el_meu_diec/src/theme.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'settings/settings_controller.dart'; @@ -21,12 +21,8 @@ class MyApp extends StatelessWidget { title: 'DIEC', debugShowCheckedModeBanner: false, restorationScopeId: 'el_meu_diec', - localizationsDelegates: const [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [Locale('ca')], + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, theme: lightThemeData, darkTheme: darkThemeData, themeMode: settingsController.themeMode, diff --git a/lib/src/pages/home_page.dart b/lib/src/pages/home_page.dart index 8d49f1a..131bd25 100644 --- a/lib/src/pages/home_page.dart +++ b/lib/src/pages/home_page.dart @@ -5,6 +5,7 @@ import 'package:el_meu_diec/model.dart'; import 'package:el_meu_diec/src/widgets/autocomplete_entry_card.dart'; import 'package:el_meu_diec/src/widgets/search_bar_results.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_svg/svg.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:provider/provider.dart'; @@ -58,9 +59,11 @@ class _BookmarksIconButton extends StatelessWidget { @override Widget build(BuildContext context) { + final appLocalizations = AppLocalizations.of(context); + return IconButton( icon: const Icon(Icons.bookmark), - tooltip: 'La meva coŀlecció', + tooltip: appLocalizations.myCollection, onPressed: () => (Platform.isAndroid ? showBarModalBottomSheet : showCupertinoModalBottomSheet)( diff --git a/lib/src/pages/word_page.dart b/lib/src/pages/word_page.dart index 4e505b8..6abb16e 100644 --- a/lib/src/pages/word_page.dart +++ b/lib/src/pages/word_page.dart @@ -20,7 +20,7 @@ class WordPage extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text( + SelectableText( word.word, style: theme.textTheme.displaySmall, textAlign: TextAlign.start, diff --git a/lib/src/widgets/autocomplete_entries_list_view.dart b/lib/src/widgets/autocomplete_entries_list_view.dart index 9a47a11..a22a8f0 100644 --- a/lib/src/widgets/autocomplete_entries_list_view.dart +++ b/lib/src/widgets/autocomplete_entries_list_view.dart @@ -3,6 +3,7 @@ import 'package:el_meu_diec/src/constants.dart'; import 'package:el_meu_diec/src/widgets/autocomplete_entry_future_card.dart'; import 'package:el_meu_diec/src/widgets/equipped_card.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class AutocompleteEntriesListView extends StatelessWidget { final String query; @@ -81,6 +82,7 @@ class _EntriesList extends StatelessWidget { @override Widget build(BuildContext context) { + final appLocalizations = AppLocalizations.of(context); final theme = Theme.of(context); return ListView.builder( @@ -95,26 +97,32 @@ class _EntriesList extends StatelessWidget { return Padding( padding: const EdgeInsetsDirectional.all(32), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - '${autocompleteEntries.length} resultats' - '${reachedMaxResults ? ' o més' : ''}', - textAlign: TextAlign.center, - style: theme.textTheme.labelLarge, - ), - if (reachedMaxResults) - const IconButton( - onPressed: null, - iconSize: 16, - visualDensity: VisualDensity(horizontal: -4, vertical: -4), - padding: EdgeInsets.all(2), - tooltip: 'Es mostren els primers $maxResults ' - 'resultats de l’autocompletat.', - icon: Icon(Icons.question_mark), + child: TextButton( + onPressed: null, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + appLocalizations.nResults(autocompleteEntries.length) + + (reachedMaxResults + ? ' ${appLocalizations.orMore}' + : ''), + textAlign: TextAlign.center, + style: theme.textTheme.labelLarge, ), - ], + if (reachedMaxResults) + IconButton( + onPressed: null, + iconSize: 16, + visualDensity: + const VisualDensity(horizontal: -4, vertical: -4), + padding: const EdgeInsets.all(2), + tooltip: + appLocalizations.firstNResultsAreShown(maxResults), + icon: const Icon(Icons.question_mark), + ), + ], + ), ), ); } diff --git a/lib/src/widgets/autocomplete_entry_card.dart b/lib/src/widgets/autocomplete_entry_card.dart index 751452a..d6ed73b 100644 --- a/lib/src/widgets/autocomplete_entry_card.dart +++ b/lib/src/widgets/autocomplete_entry_card.dart @@ -6,6 +6,7 @@ import 'package:el_meu_diec/src/theme.dart'; import 'package:el_meu_diec/src/widgets/definition_entry_sense_line.dart'; import 'package:el_meu_diec/src/widgets/equipped_card.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; class AutocompleteEntryCard extends StatelessWidget { @@ -104,6 +105,7 @@ class _BookmarkButton extends StatelessWidget { const _BookmarkButton({super.key, required this.word}); void _onPressed(BuildContext context) { + final appLocalizations = AppLocalizations.of(context); final bookmarked = Provider.of(context, listen: false) .toggleBookmark(word.id); ScaffoldMessenger.of(context).showSnackBar( @@ -112,14 +114,20 @@ class _BookmarkButton extends StatelessWidget { TextSpan( children: [ TextSpan( - text: bookmarked ? 'S’ha afegit ' : 'S’ha tret ', + text: bookmarked + ? appLocalizations.added + : appLocalizations.removed, ), + const TextSpan(text: ' '), TextSpan( text: word.word, style: const TextStyle(fontWeight: FontWeight.bold), ), + const TextSpan(text: ' '), TextSpan( - text: bookmarked ? ' a la coŀlecció.' : ' de la coŀlecció.', + text: bookmarked + ? appLocalizations.toTheCollection + : appLocalizations.fromTheCollection, ), ], ), @@ -131,6 +139,7 @@ class _BookmarkButton extends StatelessWidget { @override Widget build(BuildContext context) { + final appLocalizations = AppLocalizations.of(context); final theme = Theme.of(context); final isBookmarked = Provider.of(context).isBookmarked(word.id); @@ -141,7 +150,9 @@ class _BookmarkButton extends StatelessWidget { isBookmarked ? Icons.bookmark : Icons.bookmark_outline, ), enableFeedback: true, - tooltip: isBookmarked ? 'Treu de la coŀlecció' : 'Afegeix a la coŀlecció', + tooltip: isBookmarked + ? appLocalizations.removeFromTheCollection + : appLocalizations.addToTheCollection, onPressed: () => _onPressed(context), ); } diff --git a/lib/src/widgets/scope_chip.dart b/lib/src/widgets/scope_chip.dart index e1b0e3c..4d69287 100644 --- a/lib/src/widgets/scope_chip.dart +++ b/lib/src/widgets/scope_chip.dart @@ -1,6 +1,7 @@ import 'package:el_meu_diec/model.dart'; import 'package:el_meu_diec/src/widgets/conditional_widget_wrap.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class ScopeChip extends StatelessWidget { final Scope scope; @@ -19,6 +20,8 @@ class ScopeChip extends StatelessWidget { @override Widget build(BuildContext context) { + final appLocalizations = AppLocalizations.of(context); + return ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(4)), child: Material( @@ -31,7 +34,7 @@ class ScopeChip extends StatelessWidget { condition: isInteractive, conditionalBuilder: (child) { return Tooltip( - message: 'Mostra l’abreviatura', + message: appLocalizations.showAbbreviation, child: InkWell( onTap: () => _onTap(context), child: child, diff --git a/lib/src/widgets/search_bar_results.dart b/lib/src/widgets/search_bar_results.dart index 73b7df8..8c6cdd0 100644 --- a/lib/src/widgets/search_bar_results.dart +++ b/lib/src/widgets/search_bar_results.dart @@ -4,6 +4,7 @@ import 'package:el_meu_diec/model.dart'; import 'package:el_meu_diec/src/theme.dart'; import 'package:el_meu_diec/src/widgets/autocomplete_entries_list_view.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class SearchBarResults extends StatefulWidget { const SearchBarResults({super.key}); @@ -34,6 +35,8 @@ class _SearchBarResultsState extends State { @override Widget build(BuildContext context) { + final appLocalizations = AppLocalizations.of(context); + return Stack( children: [ Padding( @@ -105,9 +108,9 @@ class _SearchBarResultsState extends State { child: TextFormField( autocorrect: false, style: const TextStyle(fontSize: 18), - decoration: const InputDecoration( - hintText: 'Cerca', - suffixIcon: Padding( + decoration: InputDecoration( + hintText: appLocalizations.search, + suffixIcon: const Padding( padding: EdgeInsetsDirectional.only(end: 8), child: Icon(Icons.search, color: Colors.grey), ),