From e006e896c5584b35784936929102b53def9a350c Mon Sep 17 00:00:00 2001 From: ken-morel Date: Wed, 25 Jun 2025 13:51:29 +0100 Subject: [PATCH 1/7] added required packages --- pubspec.lock | 13 +++++++++++++ pubspec.yaml | 3 +++ 2 files changed, 16 insertions(+) diff --git a/pubspec.lock b/pubspec.lock index 74f3395..32795d3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -182,6 +182,11 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -224,6 +229,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.4" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" intl_phone_number_input: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index b0f6256..c32f5b6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,9 @@ dependencies: cached_network_image: ^3.4.1 http: ^1.4.0 intl_phone_number_input: ^0.7.4 + flutter_localizations: + sdk: flutter + intl: any dev_dependencies: flutter_test: From 6aaba3d2ce9db425c59731ee9a4f8813265aafde Mon Sep 17 00:00:00 2001 From: ken-morel Date: Wed, 25 Jun 2025 13:54:13 +0100 Subject: [PATCH 2/7] Added automatic language switching for flutter widgets --- lib/app.dart | 7 +++++++ pubspec.yaml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/app.dart b/lib/app.dart index 2f08692..9b075a7 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:ins/theme.dart'; import 'package:ins/pages/home.dart' as home; +import 'package:flutter_localizations/flutter_localizations.dart'; class ISApp extends StatelessWidget { const ISApp({super.key}); @@ -12,6 +13,12 @@ class ISApp extends StatelessWidget { builder: (_, _) { return MaterialApp( title: 'IS', + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: [Locale('en'), Locale('fr')], theme: themeManager.getTheme(), home: home.getPage(), ); diff --git a/pubspec.yaml b/pubspec.yaml index c32f5b6..9735268 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: ins -description: "A new Flutter project." +description: "The intranet of schools." # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: "none" # Remove this line if you wish to publish to pub.dev From 157915e8f9d3f2b8755ba3bbe3d52b6f6626063b Mon Sep 17 00:00:00 2001 From: ken-morel Date: Wed, 25 Jun 2025 14:02:16 +0100 Subject: [PATCH 3/7] Added app locale manager --- lib/locale.dart | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 lib/locale.dart diff --git a/lib/locale.dart b/lib/locale.dart new file mode 100644 index 0000000..f06ca01 --- /dev/null +++ b/lib/locale.dart @@ -0,0 +1,27 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +const localeKey = "is-locale"; +String? locale; + +Future init() async { + final prefs = await SharedPreferences.getInstance(); + locale = prefs.getString(localeKey); +} + +Future set(String? val) async { + locale = val; + final prefs = await SharedPreferences.getInstance(); + if (val != null) { + prefs.setString(localeKey, val); + } else { + prefs.remove(localeKey); + } +} + +String? get() { + return locale; +} + +bool isSet() { + return locale != null; +} From 28200c7dde5c27102aa2712e5d7bcd3004350e38 Mon Sep 17 00:00:00 2001 From: ken-morel Date: Wed, 25 Jun 2025 14:16:27 +0100 Subject: [PATCH 4/7] Added localization support --- l10n.yaml | 3 + lib/app.dart | 11 +-- lib/l10n/app_en.arb | 6 ++ lib/l10n/app_fr.arb | 3 + lib/l10n/app_localizations.dart | 140 +++++++++++++++++++++++++++++ lib/l10n/app_localizations_en.dart | 13 +++ lib/l10n/app_localizations_fr.dart | 13 +++ lib/main.dart | 12 ++- lib/theme.dart | 4 + pubspec.yaml | 1 + 10 files changed, 196 insertions(+), 10 deletions(-) create mode 100644 l10n.yaml create mode 100644 lib/l10n/app_en.arb create mode 100644 lib/l10n/app_fr.arb create mode 100644 lib/l10n/app_localizations.dart create mode 100644 lib/l10n/app_localizations_en.dart create mode 100644 lib/l10n/app_localizations_fr.dart diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..15338f2 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,3 @@ +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart diff --git a/lib/app.dart b/lib/app.dart index 9b075a7..9d1898e 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:ins/theme.dart'; import 'package:ins/pages/home.dart' as home; -import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:ins/l10n/app_localizations.dart'; +//import 'package:ins/locale.dart' as locale; class ISApp extends StatelessWidget { const ISApp({super.key}); @@ -13,12 +14,8 @@ class ISApp extends StatelessWidget { builder: (_, _) { return MaterialApp( title: 'IS', - localizationsDelegates: [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: [Locale('en'), Locale('fr')], + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, theme: themeManager.getTheme(), home: home.getPage(), ); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..c025192 --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,6 @@ +{ + "helloWorld": "Hello World!", + "@helloWorld": { + "description": "The conventional newborn programmer greeting" + } +} diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb new file mode 100644 index 0000000..a32120f --- /dev/null +++ b/lib/l10n/app_fr.arb @@ -0,0 +1,3 @@ +{ + "helloWorld": "bonjour le monde" +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart new file mode 100644 index 0000000..c68a943 --- /dev/null +++ b/lib/l10n/app_localizations.dart @@ -0,0 +1,140 @@ +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_fr.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 'l10n/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('fr'), + ]; + + /// The conventional newborn programmer greeting + /// + /// In en, this message translates to: + /// **'Hello World!'** + String get helloWorld; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['en', 'fr'].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 'fr': + return AppLocalizationsFr(); + } + + 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/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart new file mode 100644 index 0000000..2c753f4 --- /dev/null +++ b/lib/l10n/app_localizations_en.dart @@ -0,0 +1,13 @@ +// 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 helloWorld => 'Hello World!'; +} diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart new file mode 100644 index 0000000..0f2dcfc --- /dev/null +++ b/lib/l10n/app_localizations_fr.dart @@ -0,0 +1,13 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for French (`fr`). +class AppLocalizationsFr extends AppLocalizations { + AppLocalizationsFr([String locale = 'fr']) : super(locale); + + @override + String get helloWorld => 'bonjour le monde'; +} diff --git a/lib/main.dart b/lib/main.dart index 8cd4b7e..70e4944 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,14 @@ import 'package:flutter/material.dart'; import 'package:ins/app.dart'; -import 'package:ins/theme.dart'; +import 'package:ins/theme.dart' as theme; +import 'package:ins/locale.dart' as locale; -void main() { - themeManager.initialize(); +void main() async { + try { + await theme.init(); + await locale.init(); + } catch (e) { + // nothing + } runApp(const ISApp()); } diff --git a/lib/theme.dart b/lib/theme.dart index 148893c..9fe6393 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -384,3 +384,7 @@ class ThemeManager with ChangeNotifier { } final themeManager = ThemeManager(); + +Future init() async { + themeManager.initialize(); +} diff --git a/pubspec.yaml b/pubspec.yaml index 9735268..3adfb2b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,6 +59,7 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: + generate: true # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. From a1fa9084a58c654a6bbd33a09182812f3e85568e Mon Sep 17 00:00:00 2001 From: ken-morel Date: Wed, 25 Jun 2025 15:02:50 +0100 Subject: [PATCH 5/7] Added localization watching script --- translationWatcher.jl | 52 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 translationWatcher.jl diff --git a/translationWatcher.jl b/translationWatcher.jl new file mode 100644 index 0000000..8a1e897 --- /dev/null +++ b/translationWatcher.jl @@ -0,0 +1,52 @@ +function getwatcheablefile()::Channel{String} + Channel{String}() do channel + for file ∈ collect(walkdir("lib/l10n/"))[1][3] + if startswith(file, "app") && endswith(file, ".arb") + put!(channel, "./lib/l10n/" * file) + end + end + end +end +function checktimer()::Channel{Nothing} + Channel{Nothing}() do channel + while true + sleep(5) + put!(channel, nothing) + end + end +end +function rebuildLocalizations() + run(`flutter pub get`) +end +function watchchanges()::Channel{String} + println("Watching for changes...") + mtimes = Dict{String,Float64}() + + for _ ∈ checktimer() # controlled checks seperation + shouldRefresh = false + for file ∈ getwatcheablefile() + if file ∉ keys(mtimes) + println(" - Adding file $file") + mtimes[file] = mtime(file) + shouldRefresh = true + else + modified = mtime(file) + if modified > mtimes[file] + println("File modified", last(splitdir(file))) + mtimes[file] = modified + shouldRefresh = true + end + end + end + shouldRefresh && rebuildLocalizations() + end +end +function (@main)(::Vector{String})::Cint + try + watchchanges() + catch e + showerror(Core.stdout, e) + finally + return 0 + end +end From 9b6316355133c4809884fe102aa914c697d46e3b Mon Sep 17 00:00:00 2001 From: ken-morel Date: Wed, 25 Jun 2025 15:53:28 +0100 Subject: [PATCH 6/7] Moved all strings to translations --- lib/l10n/app_en.arb | 31 ++++- lib/l10n/app_fr.arb | 28 +++- lib/l10n/app_localizations.dart | 162 +++++++++++++++++++++++- lib/l10n/app_localizations_en.dart | 86 ++++++++++++- lib/l10n/app_localizations_fr.dart | 89 ++++++++++++- lib/pages/home.dart | 14 +- lib/pages/sign/signup/base.dart | 5 +- lib/pages/sign/signup/extlinked.dart | 13 +- lib/pages/sign/signup/namepassword.dart | 24 ++-- lib/pages/sign/signup/submit.dart | 24 ++-- lib/pages/welcomepage.dart | 11 +- translationWatcher.jl | 4 +- 12 files changed, 434 insertions(+), 57 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c025192..9432658 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,6 +1,29 @@ { - "helloWorld": "Hello World!", - "@helloWorld": { - "description": "The conventional newborn programmer greeting" - } + "homeLoadingMessages": "Loading app state...|Verifying user details...|Getting permissions from your schools..|Verifying the age on your birth certificate...|Please wait...|Almost there...", + "welcomeExcl": "Welcome!", + "welcomeToIS": "Welcome to IS", + "welcomeConnectOrCreateAccount": "Connect an existing account or create a new one to embark on your journey with us", + "connectAccount": "Connect account", + "createAccount": "Create an account", + "sloganShort": "Empowering connections, empowering futures", + "optionalInformations": "Optional information", + "emailAddress": "Email address", + "phoneNumber": "Phone number", + "signupAssistant_phoneNumberError": "Phone number should be 9 digits long (without country code)", + "addEmailaoPhoneNumber": "Add email and/or phone number", + "fullName": "Full name", + "username": "Username", + "usernameDesc": "A short public name visible by others", + "password": "Password", + "reenterPassword": "Reenter password", + "loginInformations": "Login information", + "passwordsDoNotMatch": "Password do not match", + "passwordShouldHaveAtleast8Characters": "Password should have atleast 8 characters length", + "usernameMustHaveBetween4And20Characters": "Username must have between 3 and 20 characters", + "thisUsernameIsAlreadyTaken": "This username is already taken", + "creatingYourAccount": "Creating you account", + "retry": "Retry", + "accountCreatedSuccesfuly": "Account created succesfuly", + "openDashboard": "Open dashboard", + "waitingMessages": "We're getting this done...|Please wait a little more...|Things are going as expected...|...|Wait once more...|A few moments..." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index a32120f..e998d46 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1,3 +1,29 @@ { - "helloWorld": "bonjour le monde" + "homeLoadingMessages": "Verification des details d'utilisateur...|Acquisition de permission de votre ecole..|Veuillez patienter...|Nous y sommes presque...", + "welcomeExcl": "Bienvenue!", + "welcomeToIS": "Bienvenue sur IS", + "welcomeConnectOrCreateAccount": "Connectez un compte existant ou creez en un nouveau pour utiliser IS", + "connectAccount": "Connectez votre compte", + "createAccount": "Creez un compte", + "sloganShort": "Construire des liens puissants, bâtir des avenirs prometteurs", + "optionalInformations": "Informations optionelles", + "emailAddress": "Addresse email", + "phoneNumber": "Numero de telephone", + "signupAssistant_phoneNumberError": "Le numero devrais faire 9 chiffres de long (Sans code iso)", + "addEmailaoPhoneNumber": "Renseigner une addresse email et/ou numero de telephone", + "fullName": "Nom complet", + "username": "Nom d'utilisateur", + "usernameDesc": "Un surnom court, et visible des autres", + "password": "Mot de passe", + "reenterPassword": "Re-entrez votre mot de passe", + "loginInformations": "Informations de connection", + "passwordsDoNotMatch": "Les mot-de-passe ne coincident pas", + "passwordShouldHaveAtleast8Characters": "Le mot-de-passe doit faire au moins 8 characteres de long", + "usernameMustHaveBetween4And20Characters": "Le nom d'utilisateur doit faire entre 3 et 20 characteres de long", + "thisUsernameIsAlreadyTaken": "Ce nom d'utilisateur est deja prit", + "creatingYourAccount": "Nous creons votre compte", + "retry": "Reessayer", + "accountCreatedSuccesfuly": "Compte cree avec succes", + "openDashboard": "Ouvrir le tableaux de bord", + "waitingMessages": "Nous ourons bientot fini...|Attendez encore un peu...|Tout ce passe bien...|...|Un peu plus de temps...|Quelques instants d'attente..." } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index c68a943..7a25071 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -98,11 +98,167 @@ abstract class AppLocalizations { Locale('fr'), ]; - /// The conventional newborn programmer greeting + /// No description provided for @homeLoadingMessages. /// /// In en, this message translates to: - /// **'Hello World!'** - String get helloWorld; + /// **'Loading app state...|Verifying user details...|Getting permissions from your schools..|Verifying the age on your birth certificate...|Please wait...|Almost there...'** + String get homeLoadingMessages; + + /// No description provided for @welcomeExcl. + /// + /// In en, this message translates to: + /// **'Welcome!'** + String get welcomeExcl; + + /// No description provided for @welcomeToIS. + /// + /// In en, this message translates to: + /// **'Welcome to IS'** + String get welcomeToIS; + + /// No description provided for @welcomeConnectOrCreateAccount. + /// + /// In en, this message translates to: + /// **'Connect an existing account or create a new one to embark on your journey with us'** + String get welcomeConnectOrCreateAccount; + + /// No description provided for @connectAccount. + /// + /// In en, this message translates to: + /// **'Connect account'** + String get connectAccount; + + /// No description provided for @createAccount. + /// + /// In en, this message translates to: + /// **'Create an account'** + String get createAccount; + + /// No description provided for @sloganShort. + /// + /// In en, this message translates to: + /// **'Empowering connections, empowering futures'** + String get sloganShort; + + /// No description provided for @optionalInformations. + /// + /// In en, this message translates to: + /// **'Optional information'** + String get optionalInformations; + + /// No description provided for @emailAddress. + /// + /// In en, this message translates to: + /// **'Email address'** + String get emailAddress; + + /// No description provided for @phoneNumber. + /// + /// In en, this message translates to: + /// **'Phone number'** + String get phoneNumber; + + /// No description provided for @signupAssistant_phoneNumberError. + /// + /// In en, this message translates to: + /// **'Phone number should be 9 digits long (without country code)'** + String get signupAssistant_phoneNumberError; + + /// No description provided for @addEmailaoPhoneNumber. + /// + /// In en, this message translates to: + /// **'Add email and/or phone number'** + String get addEmailaoPhoneNumber; + + /// No description provided for @fullName. + /// + /// In en, this message translates to: + /// **'Full name'** + String get fullName; + + /// No description provided for @username. + /// + /// In en, this message translates to: + /// **'Username'** + String get username; + + /// No description provided for @usernameDesc. + /// + /// In en, this message translates to: + /// **'A short public name visible by others'** + String get usernameDesc; + + /// No description provided for @password. + /// + /// In en, this message translates to: + /// **'Password'** + String get password; + + /// No description provided for @reenterPassword. + /// + /// In en, this message translates to: + /// **'Reenter password'** + String get reenterPassword; + + /// No description provided for @loginInformations. + /// + /// In en, this message translates to: + /// **'Login information'** + String get loginInformations; + + /// No description provided for @passwordsDoNotMatch. + /// + /// In en, this message translates to: + /// **'Password do not match'** + String get passwordsDoNotMatch; + + /// No description provided for @passwordShouldHaveAtleast8Characters. + /// + /// In en, this message translates to: + /// **'Password should have atleast 8 characters length'** + String get passwordShouldHaveAtleast8Characters; + + /// No description provided for @usernameMustHaveBetween4And20Characters. + /// + /// In en, this message translates to: + /// **'Username must have between 3 and 20 characters'** + String get usernameMustHaveBetween4And20Characters; + + /// No description provided for @thisUsernameIsAlreadyTaken. + /// + /// In en, this message translates to: + /// **'This username is already taken'** + String get thisUsernameIsAlreadyTaken; + + /// No description provided for @creatingYourAccount. + /// + /// In en, this message translates to: + /// **'Creating you account'** + String get creatingYourAccount; + + /// No description provided for @retry. + /// + /// In en, this message translates to: + /// **'Retry'** + String get retry; + + /// No description provided for @accountCreatedSuccesfuly. + /// + /// In en, this message translates to: + /// **'Account created succesfuly'** + String get accountCreatedSuccesfuly; + + /// No description provided for @openDashboard. + /// + /// In en, this message translates to: + /// **'Open dashboard'** + String get openDashboard; + + /// No description provided for @waitingMessages. + /// + /// In en, this message translates to: + /// **'We\'re getting this done...|Please wait a little more...|Things are going as expected...|...|Wait once more...|A few moments...'** + String get waitingMessages; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 2c753f4..93b3f00 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -9,5 +9,89 @@ class AppLocalizationsEn extends AppLocalizations { AppLocalizationsEn([String locale = 'en']) : super(locale); @override - String get helloWorld => 'Hello World!'; + String get homeLoadingMessages => + 'Loading app state...|Verifying user details...|Getting permissions from your schools..|Verifying the age on your birth certificate...|Please wait...|Almost there...'; + + @override + String get welcomeExcl => 'Welcome!'; + + @override + String get welcomeToIS => 'Welcome to IS'; + + @override + String get welcomeConnectOrCreateAccount => + 'Connect an existing account or create a new one to embark on your journey with us'; + + @override + String get connectAccount => 'Connect account'; + + @override + String get createAccount => 'Create an account'; + + @override + String get sloganShort => 'Empowering connections, empowering futures'; + + @override + String get optionalInformations => 'Optional information'; + + @override + String get emailAddress => 'Email address'; + + @override + String get phoneNumber => 'Phone number'; + + @override + String get signupAssistant_phoneNumberError => + 'Phone number should be 9 digits long (without country code)'; + + @override + String get addEmailaoPhoneNumber => 'Add email and/or phone number'; + + @override + String get fullName => 'Full name'; + + @override + String get username => 'Username'; + + @override + String get usernameDesc => 'A short public name visible by others'; + + @override + String get password => 'Password'; + + @override + String get reenterPassword => 'Reenter password'; + + @override + String get loginInformations => 'Login information'; + + @override + String get passwordsDoNotMatch => 'Password do not match'; + + @override + String get passwordShouldHaveAtleast8Characters => + 'Password should have atleast 8 characters length'; + + @override + String get usernameMustHaveBetween4And20Characters => + 'Username must have between 3 and 20 characters'; + + @override + String get thisUsernameIsAlreadyTaken => 'This username is already taken'; + + @override + String get creatingYourAccount => 'Creating you account'; + + @override + String get retry => 'Retry'; + + @override + String get accountCreatedSuccesfuly => 'Account created succesfuly'; + + @override + String get openDashboard => 'Open dashboard'; + + @override + String get waitingMessages => + 'We\'re getting this done...|Please wait a little more...|Things are going as expected...|...|Wait once more...|A few moments...'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 0f2dcfc..382d6aa 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -9,5 +9,92 @@ class AppLocalizationsFr extends AppLocalizations { AppLocalizationsFr([String locale = 'fr']) : super(locale); @override - String get helloWorld => 'bonjour le monde'; + String get homeLoadingMessages => + 'Verification des details d\'utilisateur...|Acquisition de permission de votre ecole..|Veuillez patienter...|Nous y sommes presque...'; + + @override + String get welcomeExcl => 'Bienvenue!'; + + @override + String get welcomeToIS => 'Bienvenue sur IS'; + + @override + String get welcomeConnectOrCreateAccount => + 'Connectez un compte existant ou creez en un nouveau pour utiliser IS'; + + @override + String get connectAccount => 'Connectez votre compte'; + + @override + String get createAccount => 'Creez un compte'; + + @override + String get sloganShort => + 'Construire des liens puissants, bâtir des avenirs prometteurs'; + + @override + String get optionalInformations => 'Informations optionelles'; + + @override + String get emailAddress => 'Addresse email'; + + @override + String get phoneNumber => 'Numero de telephone'; + + @override + String get signupAssistant_phoneNumberError => + 'Le numero devrais faire 9 chiffres de long (Sans code iso)'; + + @override + String get addEmailaoPhoneNumber => + 'Renseigner une addresse email et/ou numero de telephone'; + + @override + String get fullName => 'Nom complet'; + + @override + String get username => 'Nom d\'utilisateur'; + + @override + String get usernameDesc => 'Un surnom court, et visible des autres'; + + @override + String get password => 'Mot de passe'; + + @override + String get reenterPassword => 'Re-entrez votre mot de passe'; + + @override + String get loginInformations => 'Informations de connection'; + + @override + String get passwordsDoNotMatch => 'Les mot-de-passe ne coincident pas'; + + @override + String get passwordShouldHaveAtleast8Characters => + 'Le mot-de-passe doit faire au moins 8 characteres de long'; + + @override + String get usernameMustHaveBetween4And20Characters => + 'Le nom d\'utilisateur doit faire entre 3 et 20 characteres de long'; + + @override + String get thisUsernameIsAlreadyTaken => + 'Ce nom d\'utilisateur est deja prit'; + + @override + String get creatingYourAccount => 'Nous creons votre compte'; + + @override + String get retry => 'Reessayer'; + + @override + String get accountCreatedSuccesfuly => 'Compte cree avec succes'; + + @override + String get openDashboard => 'Ouvrir le tableaux de bord'; + + @override + String get waitingMessages => + 'Nous ourons bientot fini...|Attendez encore un peu...|Tout ce passe bien...|...|Un peu plus de temps...|Quelques instants d\'attente...'; } diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 3476dae..7dcf6d6 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -2,22 +2,18 @@ import 'package:flutter/material.dart'; import 'package:ins/appstate.dart'; import 'package:ins/widgets/loading.dart'; import 'welcomepage.dart'; +import 'package:ins/l10n/app_localizations.dart'; Widget getPage() { return FutureBuilder( future: AppState.load(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { - return const Scaffold( + return Scaffold( body: LoadingWidget( - messages: [ - "Loading app state...", - "Verifying user details...", - "Getting permissions from your schools..", - "Verifying the age on your birth certificate...", - "Please wait...", - "Almost there...", - ], + messages: AppLocalizations.of( + context, + )!.homeLoadingMessages.split("|"), switchInterval: Duration(seconds: 1), ), ); diff --git a/lib/pages/sign/signup/base.dart b/lib/pages/sign/signup/base.dart index 7b39b24..f1d82d3 100644 --- a/lib/pages/sign/signup/base.dart +++ b/lib/pages/sign/signup/base.dart @@ -1,5 +1,6 @@ import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:ins/l10n/app_localizations.dart'; class SignupAssistantBase extends StatelessWidget { final Widget title; @@ -159,12 +160,12 @@ class SignupAssistantBase extends StatelessWidget { child: Column( children: [ Text( - "Welcome to IS", + AppLocalizations.of(context)!.welcomeToIS, style: Theme.of(context).textTheme.displayLarge, ), const SizedBox(height: 30), Text( - " Empowering connections, empowering futures ", + AppLocalizations.of(context)!.sloganShort, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: Theme.of( diff --git a/lib/pages/sign/signup/extlinked.dart b/lib/pages/sign/signup/extlinked.dart index b8db7c5..96e4ccb 100644 --- a/lib/pages/sign/signup/extlinked.dart +++ b/lib/pages/sign/signup/extlinked.dart @@ -5,6 +5,7 @@ import 'package:ins/utils/email.dart'; import 'package:intl_phone_number_input/intl_phone_number_input.dart'; import 'submit.dart'; import 'package:ins/animations/page/slide.dart'; +import 'package:ins/l10n/app_localizations.dart'; class ExtraLinkedPage extends StatefulWidget { final SignupForm form; @@ -33,7 +34,7 @@ class _ExtraLinkedPageState extends State { children: [ const SizedBox(height: 30), Text( - "Optional information.", + AppLocalizations.of(context)!.optionalInformations, style: Theme.of(context).textTheme.headlineLarge, ), const SizedBox(height: 30), @@ -41,7 +42,7 @@ class _ExtraLinkedPageState extends State { controller: _emailController, decoration: InputDecoration( prefixIcon: Icon(Icons.person_outline), - labelText: "Email Address", + labelText: AppLocalizations.of(context)!.emailAddress, hintText: 'temexvironie12@ama.co', border: OutlineInputBorder(), suffixIcon: _emailError == null @@ -87,12 +88,14 @@ class _ExtraLinkedPageState extends State { decimal: true, ), inputDecoration: InputDecoration( - labelText: 'Phone Number', + labelText: AppLocalizations.of(context)!.phoneNumber, border: OutlineInputBorder(), error: _phoneValid ? null : Text( - "Phone number should be 9 digits long (without country code)", + AppLocalizations.of( + context, + )!.signupAssistant_phoneNumberError, style: Theme.of( context, ).textTheme.bodySmall?.copyWith(color: Colors.red), @@ -104,7 +107,7 @@ class _ExtraLinkedPageState extends State { ), ), ), - title: const Text("Add email and/or phone number"), + title: Text(AppLocalizations.of(context)!.addEmailaoPhoneNumber), showNextButton: true, next: (_emailError == null || _emailController.text.isEmpty) && diff --git a/lib/pages/sign/signup/namepassword.dart b/lib/pages/sign/signup/namepassword.dart index b975b56..03555dc 100644 --- a/lib/pages/sign/signup/namepassword.dart +++ b/lib/pages/sign/signup/namepassword.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:ins/animations/page/slide.dart'; +import 'package:ins/l10n/app_localizations.dart'; import 'package:ins/models.dart' as models; import 'package:ins/utils/username.dart'; @@ -46,12 +47,11 @@ class _NamePasswordPageState extends State { children: [ TextField( controller: _nameController, - decoration: const InputDecoration( + decoration: InputDecoration( prefixIcon: Icon(Icons.person), - labelText: "Full Name", + labelText: AppLocalizations.of(context)!.fullName, hintText: 'Temex Vironie', border: OutlineInputBorder(), - helperText: "Your private name, not shown to others", ), autofocus: true, keyboardType: TextInputType.name, @@ -66,10 +66,10 @@ class _NamePasswordPageState extends State { controller: _usernameController, decoration: InputDecoration( prefixIcon: Icon(Icons.person_outline), - labelText: "Username", + labelText: AppLocalizations.of(context)!.username, hintText: 'temexvironie12', border: OutlineInputBorder(), - helperText: "A short, public username for your profile", + helperText: AppLocalizations.of(context)!.usernameDesc, suffixIcon: _isCheckingUsername ? const Padding( padding: EdgeInsets.all(8.0), @@ -108,7 +108,7 @@ class _NamePasswordPageState extends State { controller: _passwordController, decoration: InputDecoration( prefixIcon: Icon(Icons.lock), - labelText: "Password", + labelText: AppLocalizations.of(context)!.password, border: const OutlineInputBorder(), error: _buildPasswordError(), suffixIcon: Row( @@ -137,7 +137,7 @@ class _NamePasswordPageState extends State { controller: _password2Controller, decoration: InputDecoration( prefixIcon: Icon(Icons.lock), - labelText: "Reenter password", + labelText: AppLocalizations.of(context)!.reenterPassword, border: const OutlineInputBorder(), error: _password2Controller.text.isNotEmpty ? _buildPassword2Error() @@ -162,7 +162,7 @@ class _NamePasswordPageState extends State { ), ), ), - title: const Text("Profile Setup"), + title: Text(AppLocalizations.of(context)!.loginInformations), showNextButton: true, next: !(_isNameValid && _isPasswordValid && _arePasswordMatching) ? null @@ -231,7 +231,7 @@ class _NamePasswordPageState extends State { if (_password2Controller.text.isEmpty) return null; if (!_arePasswordMatching) { return Text( - "Password do not match", + AppLocalizations.of(context)!.passwordsDoNotMatch, style: Theme.of( context, ).textTheme.bodySmall?.copyWith(color: Colors.red), @@ -244,7 +244,7 @@ class _NamePasswordPageState extends State { if (_passwordController.text.isEmpty) return null; if (!_isPasswordValid) { return Text( - "Password must have atleast 8 characters", + AppLocalizations.of(context)!.passwordShouldHaveAtleast8Characters, style: Theme.of( context, ).textTheme.bodySmall?.copyWith(color: Colors.red), @@ -267,7 +267,7 @@ class _NamePasswordPageState extends State { if (_wasUsernameEdited) { if (username.length < 3 || username.length > 20) { return Text( - "Username must be between 3 and 20 characters", + AppLocalizations.of(context)!.usernameMustHaveBetween4And20Characters, style: Theme.of( context, ).textTheme.bodySmall?.copyWith(color: Colors.red), @@ -276,7 +276,7 @@ class _NamePasswordPageState extends State { } if (!_isUserNameAvailable) { return Text( - "Username is already taken", + AppLocalizations.of(context)!.thisUsernameIsAlreadyTaken, style: Theme.of( context, ).textTheme.bodySmall?.copyWith(color: Colors.red), diff --git a/lib/pages/sign/signup/submit.dart b/lib/pages/sign/signup/submit.dart index 5620bfd..872e814 100644 --- a/lib/pages/sign/signup/submit.dart +++ b/lib/pages/sign/signup/submit.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:ins/l10n/app_localizations.dart'; import 'package:ins/widgets/imsg.dart'; import 'form.dart'; import 'package:ins/widgets/loading.dart'; @@ -9,7 +10,9 @@ class SubmitingPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text("Creating your account")), + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.creatingYourAccount), + ), body: FutureBuilder( future: form.submit(), builder: (context, snapshot) { @@ -32,7 +35,7 @@ class SubmitingPage extends StatelessWidget { onPressed: () { Navigator.of(context).pop(); }, - child: Text("Retry"), + child: Text(AppLocalizations.of(context)!.retry), ), ), ), @@ -49,24 +52,21 @@ class SubmitingPage extends StatelessWidget { size: 200, color: Colors.greenAccent, ), - message: Text("Account created succesfully"), + message: Text( + AppLocalizations.of(context)!.accountCreatedSuccesfuly, + ), actions: OutlinedButton( onPressed: () => _openDashboard(context), - child: const Text("Open dashboard"), + child: Text(AppLocalizations.of(context)!.openDashboard), ), ), ), ); } else { return LoadingWidget( - messages: [ - "Creating your account..", - "Adding your preferences..", - "Updating our database...", - "Putting in your contact information..", - "Looking for potential reelatives...", - "Generating your dashboard", - ], + messages: AppLocalizations.of( + context, + )!.waitingMessages.split("|"), ); } }, diff --git a/lib/pages/welcomepage.dart b/lib/pages/welcomepage.dart index 49f4eef..d8f1e71 100644 --- a/lib/pages/welcomepage.dart +++ b/lib/pages/welcomepage.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:ins/l10n/app_localizations.dart'; import 'package:ins/pages/sign/signup/launcher.dart'; class WelcomePage extends StatelessWidget { @@ -16,7 +17,7 @@ class WelcomePage extends StatelessWidget { return Scaffold( appBar: AppBar( - title: const Text('Welcome'), + title: Text(AppLocalizations.of(context)!.welcomeExcl), backgroundColor: Colors.transparent, elevation: 0, ), @@ -44,7 +45,7 @@ class WelcomePage extends StatelessWidget { const SizedBox(height: 32.0), Text( - "Welcome to IS!", + AppLocalizations.of(context)!.welcomeToIS, style: textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, color: colorScheme.primary, @@ -57,7 +58,7 @@ class WelcomePage extends StatelessWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Text( - "Connect an existing account or create a new one to embark on your journey with us.", + AppLocalizations.of(context)!.welcomeConnectOrCreateAccount, style: textTheme.bodyLarge?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -69,7 +70,7 @@ class WelcomePage extends StatelessWidget { _buildWelcomeButton( context: context, - text: "Connect account", + text: AppLocalizations.of(context)!.connectAccount, onPressed: () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -86,7 +87,7 @@ class WelcomePage extends StatelessWidget { _buildWelcomeButton( context: context, - text: "Create account", + text: AppLocalizations.of(context)!.createAccount, onPressed: () { launchSignupAssistant(context); }, diff --git a/translationWatcher.jl b/translationWatcher.jl index 8a1e897..20cf592 100644 --- a/translationWatcher.jl +++ b/translationWatcher.jl @@ -10,13 +10,13 @@ end function checktimer()::Channel{Nothing} Channel{Nothing}() do channel while true - sleep(5) + sleep(10) put!(channel, nothing) end end end function rebuildLocalizations() - run(`flutter pub get`) + run(`flutter gen-l10n`) end function watchchanges()::Channel{String} println("Watching for changes...") From 350551c3b91528c2e3b887b9d095fe4c188c8f5d Mon Sep 17 00:00:00 2001 From: ken-morel Date: Wed, 25 Jun 2025 23:16:55 +0100 Subject: [PATCH 7/7] Added french support --- lib/animations/page/slide.dart | 2 +- lib/app.dart | 60 +++++++++++--- lib/appstate.dart | 1 - lib/l10n/app_en.arb | 3 +- lib/l10n/app_fr.arb | 3 +- lib/l10n/app_localizations.dart | 6 ++ lib/l10n/app_localizations_en.dart | 3 + lib/l10n/app_localizations_fr.dart | 3 + lib/locale.dart | 126 +++++++++++++++++++++++++---- lib/main.dart | 3 +- lib/pages/sign/signup/base.dart | 10 ++- lib/pages/welcomepage.dart | 4 +- lib/widgets/locale_chooser.dart | 79 ++++++++++++++++++ pubspec.lock | 16 ++++ pubspec.yaml | 1 + 15 files changed, 280 insertions(+), 40 deletions(-) create mode 100644 lib/widgets/locale_chooser.dart diff --git a/lib/animations/page/slide.dart b/lib/animations/page/slide.dart index 38f48f3..2fea928 100644 --- a/lib/animations/page/slide.dart +++ b/lib/animations/page/slide.dart @@ -8,7 +8,7 @@ class SlidePageRoute extends PageRouteBuilder { SlidePageRoute({ super.settings, required this.child, - this.duration = const Duration(milliseconds: 400), + this.duration = const Duration(milliseconds: 100), this.direction = AxisDirection.left, }) : super( pageBuilder: diff --git a/lib/app.dart b/lib/app.dart index 9d1898e..b862e6f 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,25 +1,59 @@ import 'package:flutter/material.dart'; import 'package:ins/theme.dart'; -import 'package:ins/pages/home.dart' as home; +import 'package:ins/pages/home.dart' + as home; // Ensure home.getPage() returns a Widget import 'package:ins/l10n/app_localizations.dart'; -//import 'package:ins/locale.dart' as locale; +import 'package:ins/locale.dart' as locale_manager; // Aliased +import 'package:provider/provider.dart'; +import 'package:ins/widgets/loading.dart'; class ISApp extends StatelessWidget { const ISApp({super.key}); @override Widget build(BuildContext context) { - return AnimatedBuilder( - animation: themeManager, - builder: (_, _) { - return MaterialApp( - title: 'IS', - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - theme: themeManager.getTheme(), - home: home.getPage(), - ); - }, + return ChangeNotifierProvider( + create: (_) => locale_manager.getProvider(), + child: Consumer( + builder: (context, localeProvider, child) { + return AnimatedBuilder( + animation: themeManager, + builder: (context, _) { + if (localeProvider.isLoading) { + return MaterialApp( + home: Scaffold( + body: LoadingWidget(messages: ["Loading your locale..."]), + ), + ); + } + return MaterialApp( + title: 'IS', + locale: localeProvider.locale, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + localeResolutionCallback: (deviceLocale, supportedLocales) { + if (localeProvider.locale != null) { + return localeProvider.locale; + } + if (deviceLocale != null) { + for (var supportedLocale in supportedLocales) { + if (supportedLocale.languageCode == + deviceLocale.languageCode) { + // You might want to check countryCode as well if it's important: + // && (deviceLocale.countryCode == null || supportedLocale.countryCode == deviceLocale.countryCode) + return supportedLocale; + } + } + } + return supportedLocales.first; + }, + theme: themeManager.getTheme(), + home: home.getPage(), + ); + }, + ); + }, + ), ); } } diff --git a/lib/appstate.dart b/lib/appstate.dart index c98cca7..79be41d 100644 --- a/lib/appstate.dart +++ b/lib/appstate.dart @@ -9,7 +9,6 @@ class AppState { models.User? user; AppState({this.session, this.user}); static Future load() async { - await Future.delayed(Duration(seconds: 5)); final prefs = await SharedPreferences.getInstance(); final appStateJson = prefs.getString(appStateKey); if (appStateJson != null) { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 9432658..484cf7a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -25,5 +25,6 @@ "retry": "Retry", "accountCreatedSuccesfuly": "Account created succesfuly", "openDashboard": "Open dashboard", - "waitingMessages": "We're getting this done...|Please wait a little more...|Things are going as expected...|...|Wait once more...|A few moments..." + "waitingMessages": "We're getting this done...|Please wait a little more...|Things are going as expected...|...|Wait once more...|A few moments...", + "continueGt": "Continue >" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index e998d46..1882cbe 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -25,5 +25,6 @@ "retry": "Reessayer", "accountCreatedSuccesfuly": "Compte cree avec succes", "openDashboard": "Ouvrir le tableaux de bord", - "waitingMessages": "Nous ourons bientot fini...|Attendez encore un peu...|Tout ce passe bien...|...|Un peu plus de temps...|Quelques instants d'attente..." + "waitingMessages": "Nous ourons bientot fini...|Attendez encore un peu...|Tout ce passe bien...|...|Un peu plus de temps...|Quelques instants d'attente...", + "continueGt": "Continuer >" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 7a25071..fcfe2a3 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -259,6 +259,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'We\'re getting this done...|Please wait a little more...|Things are going as expected...|...|Wait once more...|A few moments...'** String get waitingMessages; + + /// No description provided for @continueGt. + /// + /// In en, this message translates to: + /// **'Continue >'** + String get continueGt; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 93b3f00..90b6263 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -94,4 +94,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get waitingMessages => 'We\'re getting this done...|Please wait a little more...|Things are going as expected...|...|Wait once more...|A few moments...'; + + @override + String get continueGt => 'Continue >'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 382d6aa..8a0d0a2 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -97,4 +97,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get waitingMessages => 'Nous ourons bientot fini...|Attendez encore un peu...|Tout ce passe bien...|...|Un peu plus de temps...|Quelques instants d\'attente...'; + + @override + String get continueGt => 'Continuer >'; } diff --git a/lib/locale.dart b/lib/locale.dart index f06ca01..874c771 100644 --- a/lib/locale.dart +++ b/lib/locale.dart @@ -1,27 +1,121 @@ +// locale.dart +import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ins/l10n/app_localizations.dart'; // Assuming this defines supportedLocales -const localeKey = "is-locale"; -String? locale; +const Map localeNames = { + "en": "English", + "fr": "Français", + // Add other language names here +}; -Future init() async { - final prefs = await SharedPreferences.getInstance(); - locale = prefs.getString(localeKey); +String? getLocaleFullName(Locale? locale) { + if (locale == null) return null; + return localeNames[locale.languageCode]; } -Future set(String? val) async { - locale = val; - final prefs = await SharedPreferences.getInstance(); - if (val != null) { - prefs.setString(localeKey, val); - } else { - prefs.remove(localeKey); +class LocaleProvider with ChangeNotifier { + Locale? _locale; + static const String _selectedLocaleKey = 'selected_locale_language_code'; + bool _isLoading = true; // To track initial loading + + Locale? get locale => _locale; + bool get isLoading => _isLoading; // Expose loading state if needed elsewhere + + LocaleProvider() { + _loadSavedLocale(); + } + + Future _loadSavedLocale() async { + _isLoading = true; + notifyListeners(); // Notify UI that loading has started + + final prefs = await SharedPreferences.getInstance(); + final languageCode = prefs.getString(_selectedLocaleKey); + + if (languageCode != null) { + final foundLocale = AppLocalizations.supportedLocales.firstWhere( + (sl) => sl.languageCode == languageCode, + orElse: () { + // If saved locale is no longer supported, clear it and use null (device default) + // or fallback to the first supported. Let's try null first. + // _removeSavedLocale(); // Optionally remove invalid preference + // return AppLocalizations.supportedLocales.first; + print( + "Saved locale '$languageCode' not in supported list, using device default or first supported.", + ); + return AppLocalizations + .supportedLocales + .first; // Or null to let MaterialApp decide + }, + ); + _locale = foundLocale; + } else { + // No saved locale, _locale remains null, MaterialApp will use device locale or first supported. + _locale = null; + } + _isLoading = false; + notifyListeners(); } + + Future setLocale(Locale newLocale) async { + if (!AppLocalizations.supportedLocales.any( + (sl) => sl.languageCode == newLocale.languageCode, + )) { + debugPrint("Locale ${newLocale.languageCode} is not supported."); + return; + } + + if (_locale?.languageCode != newLocale.languageCode) { + _locale = newLocale; + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_selectedLocaleKey, newLocale.languageCode); + notifyListeners(); + } + } + + Future clearLocale() async { + if (_locale != null) { + _locale = null; // Let MaterialApp use device locale / resolution + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_selectedLocaleKey); + notifyListeners(); + } + } +} + +// --- Global Singleton Access (Your current pattern) --- +// This part can remain if you prefer it, but Provider.of is often cleaner. +LocaleProvider? _globalLocaleProvider; + +void init() { + _globalLocaleProvider ??= LocaleProvider(); +} + +LocaleProvider getProvider() { + if (_globalLocaleProvider == null) { + // This case should ideally not happen if init() is called in main.dart + debugPrint( + "Warning: LocaleProvider accessed before init(). Initializing now.", + ); + init(); + } + return _globalLocaleProvider!; +} + +// Helper functions (could also be methods on provider accessed via Provider.of) +// These are less necessary if you use Provider.of directly in widgets. +void setCurrentLocale(BuildContext context, String languageCode) { + // Provider.of(context, listen: false).setLocale(Locale(languageCode)); + getProvider().setLocale(Locale(languageCode)); // Using your singleton } -String? get() { - return locale; +void clearCurrentLocale(BuildContext context) { + // Provider.of(context, listen: false).clearLocale(); + getProvider().clearLocale(); // Using your singleton } -bool isSet() { - return locale != null; +Locale? getCurrentLocale() { + // return Provider.of(context, listen: false).locale; + return getProvider().locale; // Using your singleton } diff --git a/lib/main.dart b/lib/main.dart index 70e4944..65339cb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,9 +4,10 @@ import 'package:ins/theme.dart' as theme; import 'package:ins/locale.dart' as locale; void main() async { + WidgetsFlutterBinding.ensureInitialized(); try { await theme.init(); - await locale.init(); + locale.init(); } catch (e) { // nothing } diff --git a/lib/pages/sign/signup/base.dart b/lib/pages/sign/signup/base.dart index f1d82d3..eec5227 100644 --- a/lib/pages/sign/signup/base.dart +++ b/lib/pages/sign/signup/base.dart @@ -1,13 +1,14 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:ins/l10n/app_localizations.dart'; +import 'package:ins/widgets/locale_chooser.dart'; class SignupAssistantBase extends StatelessWidget { final Widget title; final Widget body; final Function()? next; final bool showNextButton; - final String nextText; + final String? nextText; const SignupAssistantBase({ super.key, @@ -15,7 +16,7 @@ class SignupAssistantBase extends StatelessWidget { required this.body, this.showNextButton = false, this.next, - this.nextText = "Continue >", + this.nextText, }); // Define a breakpoint for wide screens @@ -29,6 +30,7 @@ class SignupAssistantBase extends StatelessWidget { leading: const BackButton(), backgroundColor: Colors.transparent, // Make AppBar transparent elevation: 0, // No shadow for AppBar + actions: const [LocaleChooserWidget(), SizedBox(width: 30)], ), extendBodyBehindAppBar: true, // Allow body to extend behind AppBar body: Stack( @@ -105,7 +107,7 @@ class SignupAssistantBase extends StatelessWidget { elevation: 8.0, borderRadius: BorderRadius.circular(16.0), // Make card background slightly transparent to blend with blurred background - color: Theme.of(context).colorScheme.surface.withOpacity(0.85), + color: Theme.of(context).colorScheme.surface.withAlpha(200), child: SingleChildScrollView( // Allows content to scroll if it's too long padding: const EdgeInsets.all(24.0), @@ -128,7 +130,7 @@ class SignupAssistantBase extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 16.0), textStyle: Theme.of(context).textTheme.labelLarge, ), - child: Text(nextText), + child: Text(AppLocalizations.of(context)!.continueGt), ), ], ], diff --git a/lib/pages/welcomepage.dart b/lib/pages/welcomepage.dart index d8f1e71..79add93 100644 --- a/lib/pages/welcomepage.dart +++ b/lib/pages/welcomepage.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:ins/l10n/app_localizations.dart'; import 'package:ins/pages/sign/signup/launcher.dart'; +import 'package:ins/widgets/locale_chooser.dart'; class WelcomePage extends StatelessWidget { const WelcomePage({super.key}); @@ -11,8 +12,6 @@ class WelcomePage extends StatelessWidget { final ColorScheme colorScheme = Theme.of(context).colorScheme; final screenWidth = MediaQuery.of(context).size.width; - // Responsive radius for the CircleAvatar - // Clamped between a min and max radius for better control and appearance. final double avatarRadius = (screenWidth * 0.22).clamp(70.0, 110.0); return Scaffold( @@ -20,6 +19,7 @@ class WelcomePage extends StatelessWidget { title: Text(AppLocalizations.of(context)!.welcomeExcl), backgroundColor: Colors.transparent, elevation: 0, + actions: const [LocaleChooserWidget(), SizedBox(width: 20)], ), body: SafeArea( child: Center( diff --git a/lib/widgets/locale_chooser.dart b/lib/widgets/locale_chooser.dart new file mode 100644 index 0000000..10c0fb4 --- /dev/null +++ b/lib/widgets/locale_chooser.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:ins/l10n/app_localizations.dart'; +import 'package:ins/locale.dart' as locale_manager; // Aliased +import 'package:provider/provider.dart'; + +class LocaleChooserWidget extends StatelessWidget { + const LocaleChooserWidget({super.key}); + + @override + Widget build(BuildContext context) { + // Get the LocaleProvider instance + final localeProvider = Provider.of(context); + + // Determine the currently selected locale for the DropdownButton + // It should be the locale from our provider if set, + // otherwise, it's what Flutter has resolved (e.g., device locale). + Locale? currentLocaleInDropdown; + + if (localeProvider.locale != null) { + // Ensure the provider's locale is one of the supported ones for the dropdown + currentLocaleInDropdown = AppLocalizations.supportedLocales.firstWhere( + (l) => l.languageCode == localeProvider.locale!.languageCode, + orElse: () => Localizations.localeOf(context), // Fallback + ); + } else { + // If provider has no locale (e.g. fresh install, or locale cleared), + // use the locale Flutter is currently using. + currentLocaleInDropdown = Localizations.localeOf(context); + // Ensure this resolved locale is one of our explicit supported ones for the dropdown + // This can happen if Localizations.localeOf(context) picks a device locale + // that has a country code but our supportedLocales only has language code. + currentLocaleInDropdown = AppLocalizations.supportedLocales.firstWhere( + (l) => l.languageCode == currentLocaleInDropdown!.languageCode, + orElse: () => + AppLocalizations.supportedLocales.first, // Ultimate fallback + ); + } + + return DropdownButtonHideUnderline( + // Often looks better in AppBar + child: DropdownButton( + value: currentLocaleInDropdown, + icon: const Icon(Icons.language), + style: TextStyle( + color: Theme.of(context).appBarTheme.iconTheme?.color ?? Colors.white, + ), // Match AppBar icon color + dropdownColor: + Theme.of(context).appBarTheme.backgroundColor ?? + Theme.of(context).primaryColor, // Match AppBar background + items: AppLocalizations.supportedLocales.map>(( + Locale loc, + ) { + return DropdownMenuItem( + value: loc, + child: Padding( + padding: const EdgeInsets.only(right: 5), + child: Text( + locale_manager.getLocaleFullName(loc) ?? loc.languageCode, + style: TextStyle( + // Ensure text color is visible on dropdown background + color: + Theme.of(context).textTheme.bodyLarge?.color ?? + (Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Colors.black), + ), + ), + ), + ); + }).toList(), + onChanged: (Locale? newValue) { + if (newValue != null) { + locale_manager.setCurrentLocale(context, newValue.languageCode); + } + }, + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 32795d3..bca33d1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -341,6 +341,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" octo_image: dependency: transitive description: @@ -437,6 +445,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" + url: "https://pub.dev" + source: hosted + version: "6.1.5" rxdart: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3adfb2b..e361d11 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,7 @@ dependencies: flutter_localizations: sdk: flutter intl: any + provider: ^6.1.5 dev_dependencies: flutter_test: