From bb7b1cf6b2fa95fd5b0e6cff419b80d5d44e94f4 Mon Sep 17 00:00:00 2001 From: Mikkeli Matlock Date: Sun, 24 Aug 2025 20:47:16 +0900 Subject: [PATCH] Implement comprehensive localization framework with English and Finnish support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add flutter_localizations dependency and l10n configuration - Create ARB files for English (en) and Finnish (fi) translations - Localize UI strings across all major components: * Navigation drawer and page titles * Scoring interface (arrows per end, ruleset names) * Scorecard headers (End, Arrows, Sum of 3/6, Accumulative) * Keypad functional buttons (CLEAR/TYHJENNÄ, CLOSE/SULJE) * Settings page placeholder content * Dialog messages and confirmation prompts - Preserve universal archery scoring notation (X, 10, 9, M remain unlocalised) - Use authentic Finnish archery terminology throughout - Configure main app with proper localization delegates and supported locales - Generate localization classes automatically via flutter_localizations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- l10n.yaml | 6 + lib/config/localization/app_en.arb | 64 +++ lib/config/localization/app_fi.arb | 64 +++ .../generated/app_localizations.dart | 392 ++++++++++++++++++ .../generated/app_localizations_en.dart | 144 +++++++ .../generated/app_localizations_fi.dart | 145 +++++++ lib/main.dart | 15 + .../pages/main_navigation_page.dart | 36 +- lib/presentation/pages/scoring_page.dart | 8 +- lib/presentation/pages/settings_page.dart | 37 +- .../widgets/common/toggle_switches.dart | 19 +- .../widgets/scoring_grid/scoring_grid.dart | 20 +- .../widgets/scoring_grid/scoring_keypad.dart | 24 +- pubspec.lock | 13 + pubspec.yaml | 6 + 15 files changed, 938 insertions(+), 55 deletions(-) create mode 100644 l10n.yaml create mode 100644 lib/config/localization/app_en.arb create mode 100644 lib/config/localization/app_fi.arb create mode 100644 lib/config/localization/generated/app_localizations.dart create mode 100644 lib/config/localization/generated/app_localizations_en.dart create mode 100644 lib/config/localization/generated/app_localizations_fi.dart diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..c7baa70 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,6 @@ +# Flutter localization configuration +arb-dir: lib/config/localization +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart +output-class: AppLocalizations +output-dir: lib/config/localization/generated \ No newline at end of file diff --git a/lib/config/localization/app_en.arb b/lib/config/localization/app_en.arb new file mode 100644 index 0000000..125732b --- /dev/null +++ b/lib/config/localization/app_en.arb @@ -0,0 +1,64 @@ +{ + "@@locale": "en", + + "appTitle": "Archery Bookkeeper", + "appVersion": "Alpha 0.1", + + "navMainScorer": "Main (Scorer)", + "navSettings": "Settings", + + "arrowsPerEnd": "Arrows per end", + "ruleset": "Ruleset", + "rulesetIndoor": "Indoor", + "rulesetOutdoor": "Outdoor", + "endsCount": "{count} ends", + "@endsCount": { + "placeholders": { + "count": { + "type": "int", + "example": "10" + } + } + }, + + "headerEnd": "End", + "headerArrows": "Arrows", + "headerSumOf3": "Sum of 3", + "headerSumOf6": "Sum of 6", + "headerAccumulative": "Accumulative", + "headerXCount": "X", + "header10Count": "10", + "header9Count": "9", + "scoreTotal": "Total", + + "keypadClear": "CLEAR", + "keypadClose": "CLOSE", + + "dialogClearScoresTitle": "Clear scores", + "dialogClearScoresMessage": "This will clear all scores for the current session. Continue?", + "dialogArrowChangeTitle": "Change arrow count", + "dialogArrowChangeMessage": "Changing arrow count will clear existing scores. Continue?", + "dialogPreserveScores": "Preserve existing scores when possible?", + "dialogCancel": "Cancel", + "dialogContinue": "Continue", + "dialogYes": "Yes", + "dialogNo": "No", + + "settingsTitle": "Settings", + "settingsAppearance": "Appearance", + "settingsScoring": "Scoring", + "settingsExport": "Export", + "settingsAbout": "About", + + "settingsPlaceholder": "This is a placeholder settings page.", + "settingsFutureWillInclude": "Future settings will include:", + "settingsColorScheme": "• Color scheme preferences", + "settingsDefaultArrows": "• Default arrow counts", + "settingsScoreInput": "• Score input preferences", + "settingsExportOptions": "• Export settings", + "settingsSessionManagement": "• Session management", + + "dialogKeepScoresTitle": "Keep scores?", + + "devTodoLocalization": "REPLACE ALL THESE TEXTS WITH LOCALISATION MARKERS!!" +} \ No newline at end of file diff --git a/lib/config/localization/app_fi.arb b/lib/config/localization/app_fi.arb new file mode 100644 index 0000000..46754b2 --- /dev/null +++ b/lib/config/localization/app_fi.arb @@ -0,0 +1,64 @@ +{ + "@@locale": "fi", + + "appTitle": "Jousiammunnan tuloskirja", + "appVersion": "Alpha 0.1", + + "navMainScorer": "Pääsivu (Tuloslaskuri)", + "navSettings": "Asetukset", + + "arrowsPerEnd": "Nuolia per päätös", + "ruleset": "Sääntökokoelma", + "rulesetIndoor": "Sisärata", + "rulesetOutdoor": "Ulkorata", + "endsCount": "{count} päätöstä", + "@endsCount": { + "placeholders": { + "count": { + "type": "int", + "example": "10" + } + } + }, + + "headerEnd": "Päätös", + "headerArrows": "Nuolet", + "headerSumOf3": "Summa 3", + "headerSumOf6": "Summa 6", + "headerAccumulative": "Yhteensä", + "headerXCount": "X", + "header10Count": "10", + "header9Count": "9", + "scoreTotal": "Yhteensä", + + "keypadClear": "TYHJENNÄ", + "keypadClose": "SULJE", + + "dialogClearScoresTitle": "Tyhjennä pisteet", + "dialogClearScoresMessage": "Tämä tyhjentää kaikki pisteet nykyiseltä kierrokselta. Jatka?", + "dialogArrowChangeTitle": "Muuta nuolien määrää", + "dialogArrowChangeMessage": "Nuolien määrän muuttaminen tyhjentää olemassa olevat pisteet. Jatka?", + "dialogPreserveScores": "Säilytetäänkö olemassa olevat pisteet mahdollisuuksien mukaan?", + "dialogCancel": "Peruuta", + "dialogContinue": "Jatka", + "dialogYes": "Kyllä", + "dialogNo": "Ei", + + "settingsTitle": "Asetukset", + "settingsAppearance": "Ulkoasu", + "settingsScoring": "Pisteidenlasku", + "settingsExport": "Vienti", + "settingsAbout": "Tietoja", + + "settingsPlaceholder": "Tämä on väliaikainen asetukset-sivu.", + "settingsFutureWillInclude": "Tulevat asetukset sisältävät:", + "settingsColorScheme": "• Väriteeman asetukset", + "settingsDefaultArrows": "• Nuolien oletusmäärät", + "settingsScoreInput": "• Pisteidenluonnin asetukset", + "settingsExportOptions": "• Vientiasetukset", + "settingsSessionManagement": "• Istunnon hallinta", + + "dialogKeepScoresTitle": "Säilytetäänkö pisteet?", + + "devTodoLocalization": "KORVAA KAIKKI NÄMÄ TEKSTIT LOKALISOINTIMERKINNÖILLÄ!!" +} \ No newline at end of file diff --git a/lib/config/localization/generated/app_localizations.dart b/lib/config/localization/generated/app_localizations.dart new file mode 100644 index 0000000..fe58d92 --- /dev/null +++ b/lib/config/localization/generated/app_localizations.dart @@ -0,0 +1,392 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_en.dart'; +import 'app_localizations_fi.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'generated/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations? of(BuildContext context) { + return Localizations.of(context, AppLocalizations); + } + + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('en'), + Locale('fi'), + ]; + + /// No description provided for @appTitle. + /// + /// In en, this message translates to: + /// **'Archery Bookkeeper'** + String get appTitle; + + /// No description provided for @appVersion. + /// + /// In en, this message translates to: + /// **'Alpha 0.1'** + String get appVersion; + + /// No description provided for @navMainScorer. + /// + /// In en, this message translates to: + /// **'Main (Scorer)'** + String get navMainScorer; + + /// No description provided for @navSettings. + /// + /// In en, this message translates to: + /// **'Settings'** + String get navSettings; + + /// No description provided for @arrowsPerEnd. + /// + /// In en, this message translates to: + /// **'Arrows per end'** + String get arrowsPerEnd; + + /// No description provided for @ruleset. + /// + /// In en, this message translates to: + /// **'Ruleset'** + String get ruleset; + + /// No description provided for @rulesetIndoor. + /// + /// In en, this message translates to: + /// **'Indoor'** + String get rulesetIndoor; + + /// No description provided for @rulesetOutdoor. + /// + /// In en, this message translates to: + /// **'Outdoor'** + String get rulesetOutdoor; + + /// No description provided for @endsCount. + /// + /// In en, this message translates to: + /// **'{count} ends'** + String endsCount(int count); + + /// No description provided for @headerEnd. + /// + /// In en, this message translates to: + /// **'End'** + String get headerEnd; + + /// No description provided for @headerArrows. + /// + /// In en, this message translates to: + /// **'Arrows'** + String get headerArrows; + + /// No description provided for @headerSumOf3. + /// + /// In en, this message translates to: + /// **'Sum of 3'** + String get headerSumOf3; + + /// No description provided for @headerSumOf6. + /// + /// In en, this message translates to: + /// **'Sum of 6'** + String get headerSumOf6; + + /// No description provided for @headerAccumulative. + /// + /// In en, this message translates to: + /// **'Accumulative'** + String get headerAccumulative; + + /// No description provided for @headerXCount. + /// + /// In en, this message translates to: + /// **'X'** + String get headerXCount; + + /// No description provided for @header10Count. + /// + /// In en, this message translates to: + /// **'10'** + String get header10Count; + + /// No description provided for @header9Count. + /// + /// In en, this message translates to: + /// **'9'** + String get header9Count; + + /// No description provided for @scoreTotal. + /// + /// In en, this message translates to: + /// **'Total'** + String get scoreTotal; + + /// No description provided for @keypadClear. + /// + /// In en, this message translates to: + /// **'CLEAR'** + String get keypadClear; + + /// No description provided for @keypadClose. + /// + /// In en, this message translates to: + /// **'CLOSE'** + String get keypadClose; + + /// No description provided for @dialogClearScoresTitle. + /// + /// In en, this message translates to: + /// **'Clear scores'** + String get dialogClearScoresTitle; + + /// No description provided for @dialogClearScoresMessage. + /// + /// In en, this message translates to: + /// **'This will clear all scores for the current session. Continue?'** + String get dialogClearScoresMessage; + + /// No description provided for @dialogArrowChangeTitle. + /// + /// In en, this message translates to: + /// **'Change arrow count'** + String get dialogArrowChangeTitle; + + /// No description provided for @dialogArrowChangeMessage. + /// + /// In en, this message translates to: + /// **'Changing arrow count will clear existing scores. Continue?'** + String get dialogArrowChangeMessage; + + /// No description provided for @dialogPreserveScores. + /// + /// In en, this message translates to: + /// **'Preserve existing scores when possible?'** + String get dialogPreserveScores; + + /// No description provided for @dialogCancel. + /// + /// In en, this message translates to: + /// **'Cancel'** + String get dialogCancel; + + /// No description provided for @dialogContinue. + /// + /// In en, this message translates to: + /// **'Continue'** + String get dialogContinue; + + /// No description provided for @dialogYes. + /// + /// In en, this message translates to: + /// **'Yes'** + String get dialogYes; + + /// No description provided for @dialogNo. + /// + /// In en, this message translates to: + /// **'No'** + String get dialogNo; + + /// No description provided for @settingsTitle. + /// + /// In en, this message translates to: + /// **'Settings'** + String get settingsTitle; + + /// No description provided for @settingsAppearance. + /// + /// In en, this message translates to: + /// **'Appearance'** + String get settingsAppearance; + + /// No description provided for @settingsScoring. + /// + /// In en, this message translates to: + /// **'Scoring'** + String get settingsScoring; + + /// No description provided for @settingsExport. + /// + /// In en, this message translates to: + /// **'Export'** + String get settingsExport; + + /// No description provided for @settingsAbout. + /// + /// In en, this message translates to: + /// **'About'** + String get settingsAbout; + + /// No description provided for @settingsPlaceholder. + /// + /// In en, this message translates to: + /// **'This is a placeholder settings page.'** + String get settingsPlaceholder; + + /// No description provided for @settingsFutureWillInclude. + /// + /// In en, this message translates to: + /// **'Future settings will include:'** + String get settingsFutureWillInclude; + + /// No description provided for @settingsColorScheme. + /// + /// In en, this message translates to: + /// **'• Color scheme preferences'** + String get settingsColorScheme; + + /// No description provided for @settingsDefaultArrows. + /// + /// In en, this message translates to: + /// **'• Default arrow counts'** + String get settingsDefaultArrows; + + /// No description provided for @settingsScoreInput. + /// + /// In en, this message translates to: + /// **'• Score input preferences'** + String get settingsScoreInput; + + /// No description provided for @settingsExportOptions. + /// + /// In en, this message translates to: + /// **'• Export settings'** + String get settingsExportOptions; + + /// No description provided for @settingsSessionManagement. + /// + /// In en, this message translates to: + /// **'• Session management'** + String get settingsSessionManagement; + + /// No description provided for @dialogKeepScoresTitle. + /// + /// In en, this message translates to: + /// **'Keep scores?'** + String get dialogKeepScoresTitle; + + /// No description provided for @devTodoLocalization. + /// + /// In en, this message translates to: + /// **'REPLACE ALL THESE TEXTS WITH LOCALISATION MARKERS!!'** + String get devTodoLocalization; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['en', 'fi'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': + return AppLocalizationsEn(); + case 'fi': + return AppLocalizationsFi(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); +} diff --git a/lib/config/localization/generated/app_localizations_en.dart b/lib/config/localization/generated/app_localizations_en.dart new file mode 100644 index 0000000..762e994 --- /dev/null +++ b/lib/config/localization/generated/app_localizations_en.dart @@ -0,0 +1,144 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get appTitle => 'Archery Bookkeeper'; + + @override + String get appVersion => 'Alpha 0.1'; + + @override + String get navMainScorer => 'Main (Scorer)'; + + @override + String get navSettings => 'Settings'; + + @override + String get arrowsPerEnd => 'Arrows per end'; + + @override + String get ruleset => 'Ruleset'; + + @override + String get rulesetIndoor => 'Indoor'; + + @override + String get rulesetOutdoor => 'Outdoor'; + + @override + String endsCount(int count) { + return '$count ends'; + } + + @override + String get headerEnd => 'End'; + + @override + String get headerArrows => 'Arrows'; + + @override + String get headerSumOf3 => 'Sum of 3'; + + @override + String get headerSumOf6 => 'Sum of 6'; + + @override + String get headerAccumulative => 'Accumulative'; + + @override + String get headerXCount => 'X'; + + @override + String get header10Count => '10'; + + @override + String get header9Count => '9'; + + @override + String get scoreTotal => 'Total'; + + @override + String get keypadClear => 'CLEAR'; + + @override + String get keypadClose => 'CLOSE'; + + @override + String get dialogClearScoresTitle => 'Clear scores'; + + @override + String get dialogClearScoresMessage => + 'This will clear all scores for the current session. Continue?'; + + @override + String get dialogArrowChangeTitle => 'Change arrow count'; + + @override + String get dialogArrowChangeMessage => + 'Changing arrow count will clear existing scores. Continue?'; + + @override + String get dialogPreserveScores => 'Preserve existing scores when possible?'; + + @override + String get dialogCancel => 'Cancel'; + + @override + String get dialogContinue => 'Continue'; + + @override + String get dialogYes => 'Yes'; + + @override + String get dialogNo => 'No'; + + @override + String get settingsTitle => 'Settings'; + + @override + String get settingsAppearance => 'Appearance'; + + @override + String get settingsScoring => 'Scoring'; + + @override + String get settingsExport => 'Export'; + + @override + String get settingsAbout => 'About'; + + @override + String get settingsPlaceholder => 'This is a placeholder settings page.'; + + @override + String get settingsFutureWillInclude => 'Future settings will include:'; + + @override + String get settingsColorScheme => '• Color scheme preferences'; + + @override + String get settingsDefaultArrows => '• Default arrow counts'; + + @override + String get settingsScoreInput => '• Score input preferences'; + + @override + String get settingsExportOptions => '• Export settings'; + + @override + String get settingsSessionManagement => '• Session management'; + + @override + String get dialogKeepScoresTitle => 'Keep scores?'; + + @override + String get devTodoLocalization => + 'REPLACE ALL THESE TEXTS WITH LOCALISATION MARKERS!!'; +} diff --git a/lib/config/localization/generated/app_localizations_fi.dart b/lib/config/localization/generated/app_localizations_fi.dart new file mode 100644 index 0000000..24bdde4 --- /dev/null +++ b/lib/config/localization/generated/app_localizations_fi.dart @@ -0,0 +1,145 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Finnish (`fi`). +class AppLocalizationsFi extends AppLocalizations { + AppLocalizationsFi([String locale = 'fi']) : super(locale); + + @override + String get appTitle => 'Jousiammunnan tuloskirja'; + + @override + String get appVersion => 'Alpha 0.1'; + + @override + String get navMainScorer => 'Pääsivu (Tuloslaskuri)'; + + @override + String get navSettings => 'Asetukset'; + + @override + String get arrowsPerEnd => 'Nuolia per päätös'; + + @override + String get ruleset => 'Sääntökokoelma'; + + @override + String get rulesetIndoor => 'Sisärata'; + + @override + String get rulesetOutdoor => 'Ulkorata'; + + @override + String endsCount(int count) { + return '$count päätöstä'; + } + + @override + String get headerEnd => 'Päätös'; + + @override + String get headerArrows => 'Nuolet'; + + @override + String get headerSumOf3 => 'Summa 3'; + + @override + String get headerSumOf6 => 'Summa 6'; + + @override + String get headerAccumulative => 'Yhteensä'; + + @override + String get headerXCount => 'X'; + + @override + String get header10Count => '10'; + + @override + String get header9Count => '9'; + + @override + String get scoreTotal => 'Yhteensä'; + + @override + String get keypadClear => 'TYHJENNÄ'; + + @override + String get keypadClose => 'SULJE'; + + @override + String get dialogClearScoresTitle => 'Tyhjennä pisteet'; + + @override + String get dialogClearScoresMessage => + 'Tämä tyhjentää kaikki pisteet nykyiseltä kierrokselta. Jatka?'; + + @override + String get dialogArrowChangeTitle => 'Muuta nuolien määrää'; + + @override + String get dialogArrowChangeMessage => + 'Nuolien määrän muuttaminen tyhjentää olemassa olevat pisteet. Jatka?'; + + @override + String get dialogPreserveScores => + 'Säilytetäänkö olemassa olevat pisteet mahdollisuuksien mukaan?'; + + @override + String get dialogCancel => 'Peruuta'; + + @override + String get dialogContinue => 'Jatka'; + + @override + String get dialogYes => 'Kyllä'; + + @override + String get dialogNo => 'Ei'; + + @override + String get settingsTitle => 'Asetukset'; + + @override + String get settingsAppearance => 'Ulkoasu'; + + @override + String get settingsScoring => 'Pisteidenlasku'; + + @override + String get settingsExport => 'Vienti'; + + @override + String get settingsAbout => 'Tietoja'; + + @override + String get settingsPlaceholder => 'Tämä on väliaikainen asetukset-sivu.'; + + @override + String get settingsFutureWillInclude => 'Tulevat asetukset sisältävät:'; + + @override + String get settingsColorScheme => '• Väriteeman asetukset'; + + @override + String get settingsDefaultArrows => '• Nuolien oletusmäärät'; + + @override + String get settingsScoreInput => '• Pisteidenluonnin asetukset'; + + @override + String get settingsExportOptions => '• Vientiasetukset'; + + @override + String get settingsSessionManagement => '• Istunnon hallinta'; + + @override + String get dialogKeepScoresTitle => 'Säilytetäänkö pisteet?'; + + @override + String get devTodoLocalization => + 'KORVAA KAIKKI NÄMÄ TEKSTIT LOKALISOINTIMERKINNÖILLÄ!!'; +} diff --git a/lib/main.dart b/lib/main.dart index 6413c64..4f05186 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'config/localization/generated/app_localizations.dart'; import 'presentation/pages/main_navigation_page.dart'; void main() { @@ -12,6 +14,19 @@ class ArcheryScorerApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Archery scorer', + + // Localization configuration + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [ + Locale('en', ''), + Locale('fi', ''), + ], + theme: ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: Colors.green, diff --git a/lib/presentation/pages/main_navigation_page.dart b/lib/presentation/pages/main_navigation_page.dart index 069be2d..e006545 100644 --- a/lib/presentation/pages/main_navigation_page.dart +++ b/lib/presentation/pages/main_navigation_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../config/localization/generated/app_localizations.dart'; import 'scoring_page.dart'; import 'settings_page.dart'; @@ -56,9 +57,9 @@ class _MainNavigationPageState extends State { const SettingsPage(), ]; - final List _pageTitles = [ - 'Main (Scorer)', - 'Settings', + List _getPageTitles(BuildContext context) => [ + AppLocalizations.of(context)!.navMainScorer, + AppLocalizations.of(context)!.navSettings, ]; void _onNavigationItemSelected(int index) { @@ -70,9 +71,12 @@ class _MainNavigationPageState extends State { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final pageTitles = _getPageTitles(context); + return Scaffold( appBar: AppBar( - title: Text(_pageTitles[_selectedIndex]), + title: Text(pageTitles[_selectedIndex]), backgroundColor: Theme.of(context).colorScheme.inversePrimary, ), body: _pages[_selectedIndex], @@ -84,22 +88,22 @@ class _MainNavigationPageState extends State { decoration: BoxDecoration( color: Theme.of(context).colorScheme.inversePrimary, ), - child: const Column( + child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.end, children: [ Text( - 'Archery Bookkeeper', - style: TextStyle( + l10n.appTitle, + style: const TextStyle( color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold, ), ), - SizedBox(height: 8), + const SizedBox(height: 8), Text( - 'Alpha 0.1', - style: TextStyle( + l10n.appVersion, + style: const TextStyle( color: Colors.white70, fontSize: 16, ), @@ -109,22 +113,22 @@ class _MainNavigationPageState extends State { ), ListTile( leading: const Icon(Icons.sports), - title: const Text('Main (Scorer)'), + title: Text(l10n.navMainScorer), selected: _selectedIndex == 0, onTap: () => _onNavigationItemSelected(0), ), ListTile( leading: const Icon(Icons.settings), - title: const Text('Settings'), + title: Text(l10n.navSettings), selected: _selectedIndex == 1, onTap: () => _onNavigationItemSelected(1), ), const Divider(), - const Padding( - padding: EdgeInsets.all(16.0), + Padding( + padding: const EdgeInsets.all(16.0), child: Text( - 'REPLACE ALL THESE TEXTS WITH LOCALISATION MARKERS!!', - style: TextStyle( + l10n.devTodoLocalization, + style: const TextStyle( fontSize: 12, color: Colors.grey, ), diff --git a/lib/presentation/pages/scoring_page.dart b/lib/presentation/pages/scoring_page.dart index b9ca494..7557456 100644 --- a/lib/presentation/pages/scoring_page.dart +++ b/lib/presentation/pages/scoring_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../config/localization/generated/app_localizations.dart'; import '../widgets/scoring_grid/scoring_grid.dart'; import '../widgets/scoring_grid/scoring_keypad.dart'; import '../widgets/common/toggle_switches.dart'; @@ -87,16 +88,17 @@ class _ScoringPageState extends State with SingleTickerProviderStat final bool? keepScores = await showDialog( context: context, builder: (BuildContext context) { + final l10n = AppLocalizations.of(context)!; return AlertDialog( - title: const Text('Keep scores?'), + title: Text(l10n.dialogKeepScoresTitle), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), - child: const Text('No'), + child: Text(l10n.dialogNo), ), TextButton( onPressed: () => Navigator.of(context).pop(true), - child: const Text('Yes'), + child: Text(l10n.dialogYes), ), ], ); diff --git a/lib/presentation/pages/settings_page.dart b/lib/presentation/pages/settings_page.dart index 1206984..c2e7223 100644 --- a/lib/presentation/pages/settings_page.dart +++ b/lib/presentation/pages/settings_page.dart @@ -1,49 +1,52 @@ import 'package:flutter/material.dart'; +import '../../config/localization/generated/app_localizations.dart'; class SettingsPage extends StatelessWidget { const SettingsPage({super.key}); @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return Scaffold( appBar: AppBar( - title: const Text('Settings'), + title: Text(l10n.settingsTitle), backgroundColor: Theme.of(context).colorScheme.inversePrimary, ), - body: const Padding( - padding: EdgeInsets.all(16.0), + body: Padding( + padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Settings', - style: TextStyle( + l10n.settingsTitle, + style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), - SizedBox(height: 24), + const SizedBox(height: 24), Text( - 'This is a placeholder settings page.', - style: TextStyle( + l10n.settingsPlaceholder, + style: const TextStyle( fontSize: 16, color: Colors.grey, ), ), - SizedBox(height: 16), + const SizedBox(height: 16), Text( - 'Future settings will include:', - style: TextStyle( + l10n.settingsFutureWillInclude, + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, ), ), - SizedBox(height: 8), - Text('• Color scheme preferences'), - Text('• Default arrow counts'), - Text('• Score input preferences'), - Text('• Export settings'), - Text('• Session management'), + const SizedBox(height: 8), + Text(l10n.settingsColorScheme), + Text(l10n.settingsDefaultArrows), + Text(l10n.settingsScoreInput), + Text(l10n.settingsExportOptions), + Text(l10n.settingsSessionManagement), ], ), ), diff --git a/lib/presentation/widgets/common/toggle_switches.dart b/lib/presentation/widgets/common/toggle_switches.dart index 047e135..ab9eafd 100644 --- a/lib/presentation/widgets/common/toggle_switches.dart +++ b/lib/presentation/widgets/common/toggle_switches.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../../config/localization/generated/app_localizations.dart'; class ToggleSwitches extends StatelessWidget { final int arrowsPerEnd; @@ -15,18 +16,21 @@ class ToggleSwitches extends StatelessWidget { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildToggleGroup( - 'Arrows per end', + context, + l10n.arrowsPerEnd, [3, 6], arrowsPerEnd, onArrowsPerEndChanged, ), - _buildRulesetInfo(), + _buildRulesetInfo(context), ], ), ], @@ -34,6 +38,7 @@ class ToggleSwitches extends StatelessWidget { } Widget _buildToggleGroup( + BuildContext context, String label, List options, int currentValue, @@ -66,12 +71,14 @@ class ToggleSwitches extends StatelessWidget { ); } - Widget _buildRulesetInfo() { - final rulesetName = arrowsPerEnd == 3 ? 'Indoor' : 'Outdoor'; + Widget _buildRulesetInfo(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final rulesetName = arrowsPerEnd == 3 ? l10n.rulesetIndoor : l10n.rulesetOutdoor; + return Column( children: [ Text( - 'Ruleset', + l10n.ruleset, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, @@ -95,7 +102,7 @@ class ToggleSwitches extends StatelessWidget { ), ), Text( - '$endsPerSet ends', + l10n.endsCount(endsPerSet), style: TextStyle( fontSize: 12, color: Colors.blue.shade600, diff --git a/lib/presentation/widgets/scoring_grid/scoring_grid.dart b/lib/presentation/widgets/scoring_grid/scoring_grid.dart index 6b7e4aa..79b5315 100644 --- a/lib/presentation/widgets/scoring_grid/scoring_grid.dart +++ b/lib/presentation/widgets/scoring_grid/scoring_grid.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'scoring_cell.dart'; import '../../../config/constants/scoring_colors.dart'; +import '../../../config/localization/generated/app_localizations.dart'; class ScoringGrid extends StatelessWidget { final List> scores; @@ -139,6 +140,8 @@ class ScoringGrid extends StatelessWidget { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade400), @@ -161,7 +164,7 @@ class ScoringGrid extends StatelessWidget { // End column: ~13% (50/395) Expanded( flex: 13, - child: Center(child: Text('End\\Shot', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))), + child: Center(child: Text(l10n.headerEnd, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12))), ), // Arrow columns: ~42% (165/395) Expanded( @@ -173,7 +176,7 @@ class ScoringGrid extends StatelessWidget { color: Colors.blue.shade100, borderRadius: BorderRadius.circular(4), ), - child: const Text('Arrows', style: TextStyle(fontWeight: FontWeight.bold)), + child: Text(l10n.headerArrows, style: const TextStyle(fontWeight: FontWeight.bold)), ), ), ), @@ -182,7 +185,7 @@ class ScoringGrid extends StatelessWidget { flex: 14, child: Center( child: Text( - 'Sum of 3', + l10n.headerSumOf3, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 12, @@ -196,7 +199,7 @@ class ScoringGrid extends StatelessWidget { flex: 14, child: Center( child: Text( - 'Sum of 6', + l10n.headerSumOf6, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 12, @@ -207,7 +210,7 @@ class ScoringGrid extends StatelessWidget { // Accumulative: ~17% (70/395) Expanded( flex: 17, - child: Center(child: Text('Accumulative', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))), + child: Center(child: Text(l10n.headerAccumulative, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12))), ), ], ), @@ -410,14 +413,15 @@ class ScoringGrid extends StatelessWidget { top: BorderSide(color: Colors.grey.shade400, width: 2), ), ), - child: _buildCountingSection(), + child: _buildCountingSection(context), ), ], ), ); } - Widget _buildCountingSection() { + Widget _buildCountingSection(BuildContext context) { + final l10n = AppLocalizations.of(context)!; final counts = _calculateXAndTenCounts(); final finalScore = _calculateAccumulative(scores.length - 1); @@ -494,7 +498,7 @@ class ScoringGrid extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text('Final score', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + Text(l10n.scoreTotal, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), Text( finalScore.toString(), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24), diff --git a/lib/presentation/widgets/scoring_grid/scoring_keypad.dart b/lib/presentation/widgets/scoring_grid/scoring_keypad.dart index e455a59..db4bb01 100644 --- a/lib/presentation/widgets/scoring_grid/scoring_keypad.dart +++ b/lib/presentation/widgets/scoring_grid/scoring_keypad.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import '../../../config/constants/scoring_colors.dart'; +import '../../../config/localization/generated/app_localizations.dart'; class ScoringKeypad extends StatelessWidget { final Function(String) onScoreInput; @@ -11,6 +12,8 @@ class ScoringKeypad extends StatelessWidget { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + // Score buttons: 4 rows × 3 columns final List> scoreLayout = [ ['X', '10', '9'], @@ -21,8 +24,8 @@ class ScoringKeypad extends StatelessWidget { // Functional buttons: 4 rows × 1 column final List functionalButtons = [ - 'CLEAR', - 'CLOSE', + l10n.keypadClear, + l10n.keypadClose, '', // Reserved for future functions '', // Reserved for future functions ]; @@ -83,15 +86,25 @@ class ScoringKeypad extends StatelessWidget { } Widget _buildKeypadButton(BuildContext context, String score, {bool isFunctional = false}) { + final l10n = AppLocalizations.of(context)!; + // Use different styling for functional buttons Color buttonColor; Color textColor; + // Map display text to internal values for callback + String internalValue = score; + if (score == l10n.keypadClear) { + internalValue = 'CLEAR'; + } else if (score == l10n.keypadClose) { + internalValue = 'CLOSE'; + } + if (isFunctional) { - if (score == 'CLEAR') { + if (internalValue == 'CLEAR') { buttonColor = Colors.red.shade400; textColor = Colors.white; - } else if (score == 'CLOSE') { + } else if (internalValue == 'CLOSE') { buttonColor = Colors.blue.shade400; textColor = Colors.white; } else { @@ -99,12 +112,13 @@ class ScoringKeypad extends StatelessWidget { textColor = Colors.grey.shade600; } } else { + // Use score directly for color mapping since scores are not localized buttonColor = ScoringColors.getScoreBackgroundColor(score); textColor = ScoringColors.getScoreTextColor(score); } return ElevatedButton( - onPressed: () => onScoreInput(score), + onPressed: () => onScoreInput(internalValue), style: ElevatedButton.styleFrom( backgroundColor: buttonColor, foregroundColor: textColor, diff --git a/pubspec.lock b/pubspec.lock index e62b1b1..789c76b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -70,11 +70,24 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + intl: + dependency: transitive + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" leak_tracker: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2f3616f..c74ec67 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,9 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter + # flutter_localizations should automatically get the correct intl version # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. @@ -63,6 +66,9 @@ flutter: # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true + + # Enable code generation for localization + generate: true # To add assets to your application, add an assets section, like this: # assets: