From dd74506d7f868f097802a7aac217c25bf5887eb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johnny=20S=C3=B8rensen?= Date: Wed, 8 Sep 2021 13:50:21 +0200 Subject: [PATCH] feat: made language switch work via InheritedWidget --- example/lib/main.dart | 44 ++++++------ example/lib/nstack.dart | 71 ++++++++++++------- example/pubspec.lock | 2 +- lib/nstack.dart | 126 +++++++++++++++++++-------------- lib/src/nstack_builder.dart | 73 ++++++++++++------- lib/src/nstack_repository.dart | 9 +++ lib/src/repository.dart | 13 ++-- pubspec.lock | 12 ++-- 8 files changed, 208 insertions(+), 142 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 72b61d2..5d51c59 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -10,32 +10,28 @@ void main() { class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - Consumer>( - builder: (context, nstack, child) { - return Text(context.localization.defaultSection.title); - } + return MaterialApp( + home: MainScreen(), ); + } +} - return MaterialApp( - home: NStackInitWidget( - child: Scaffold( - appBar: AppBar( - title: Consumer>( - builder: (context, nstack, child) { - return Text(context.localization.defaultSection.test); - } - ), - ), - body: Center( - child: MaterialButton(onPressed: () => { - NStackWidget.of(context).changeLocalizationOffline(Locale("de-DE")) - }, child: Consumer>( - builder: (context, nstack, child) { - return Text("Selected locale: ${NStackWidget.of(context).activeLanguage.name}"); - } - ),), - ), - ), +class MainScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + // App open! + NStackScope.of(context).nstack.appOpen(Localizations.localeOf(context)); + + return Scaffold( + appBar: AppBar( + title: Text(context.localization.test.testDollarSign), + ), + body: Center( + child: MaterialButton(onPressed: () async => { + NStackScope.of(context).changeLanguage(Locale("de-DE")) + }, + child: Text("Selected locale: ${NStackScope.of(context).nstack.activeLanguage.name}") + ,), ), ); } diff --git a/example/lib/nstack.dart b/example/lib/nstack.dart index 249432e..7195e67 100644 --- a/example/lib/nstack.dart +++ b/example/lib/nstack.dart @@ -1,11 +1,12 @@ /// Generated by NStack, do not modify this file. +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:nstack/models/language.dart'; +import 'package:nstack/models/localize_index.dart'; import 'package:nstack/models/nstack_config.dart'; import 'package:nstack/nstack.dart'; import 'package:nstack/partial/section_key_delegate.dart'; -import 'package:provider/provider.dart'; // Update this file by running: // - `flutter pub run build_runner build`, if your package depends on Flutter @@ -36,9 +37,9 @@ class _Test extends SectionKeyDelegate { const _config = NStackConfig(projectId: 'h6wJremI2TGFM88gbLkdyljWQuwf2hxhxvCH', apiKey: 'zp2S18H32b67eYAbRQh94tVw76ZzaKKXlHjd'); -const _languages = [ - Language(id: 56, name: 'English', locale: 'en-EN', direction: 'LRM', isDefault: true, isBestFit: true), - Language(id: 7, name: 'German (Austria)', locale: 'de-AT', direction: 'LRM', isDefault: false, isBestFit: false), +final _languages = [ +LocalizeIndex(id: 1216, url: null, lastUpdatedAt: null, shouldUpdate: false, language: Language(id: 56, name: 'English', locale: 'en-EN', direction: 'LRM', isDefault: true, isBestFit: true)), +LocalizeIndex(id: 1270, url: null, lastUpdatedAt: null, shouldUpdate: false, language: Language(id: 7, name: 'German (Austria)', locale: 'de-AT', direction: 'LRM', isDefault: false, isBestFit: false)), ]; const _bundledTranslations = { @@ -52,55 +53,71 @@ final _nstack = NStack( availableLanguages: _languages, bundledTranslations: _bundledTranslations, pickedLanguageLocale: '', + debug: kDebugMode ); -class NStackWidget extends InheritedWidget { - final NStack nstack = _nstack; +class NStackScope extends InheritedWidget { + final NStack nstack; + final NStackState state; + final String checksum; - NStackWidget({Key? key, required Widget child}) + NStackScope({Key? key, required Widget child, required this.state, required this.nstack, required this.checksum}) : super(key: key, child: child); - static NStack of(BuildContext context) => - context.dependOnInheritedWidgetOfExactType()!.nstack; + static NStackState of(BuildContext context) => + context.dependOnInheritedWidgetOfExactType()!.state; @override - bool updateShouldNotify(NStackWidget oldWidget) => - nstack != oldWidget.nstack; + bool updateShouldNotify(NStackScope oldWidget) => + checksum != oldWidget.checksum; } -class NStackInitWidget extends StatefulWidget { +class NStackWidget extends StatefulWidget { final Widget child; - const NStackInitWidget({Key? key, required Widget child}) + const NStackWidget({Key? key, required Widget child}) : child = child, super(key: key); @override - _NStackInitState createState() => _NStackInitState(); + NStackState createState() => NStackState(); } -class _NStackInitState extends State { - static bool _initialized = false; - - void setupNStack(BuildContext context) { - //final locale = Localizations.localeOf(context); - final nstack = NStackWidget.of(context); - nstack.appOpen(Locale("en-UK")); +class NStackState extends State { + final NStack nstack = _nstack; + + changeLanguage(Locale locale) async { + print("Starting language switch..."); + await _nstack.changeLocalization(locale); + print("Language switch done!"); + setState(() {}); + } + + _attemptAppOpen() { + try { + nstack.appOpen(Localizations.localeOf(context)); + } catch(s,e) { + print("NStack could not call appOpen() as the NStackWidget is too far up the widget tree."); + print("Consider calling NStackScope.of(context).nstack.appOpen(Localizations.localeOf(context)) in a splashscreen or later."); + //throw e; + } + } + + @override + void initState() { + super.initState(); + _attemptAppOpen(); } @override Widget build(BuildContext context) { - if (!_initialized) { - setupNStack(context); - _initialized = true; - } - return ChangeNotifierProvider(create: (_) => _nstack, child: widget.child,); + return NStackScope(child: widget.child, state: this, nstack: this.nstack, checksum: nstack.checksum,); } } /// Allows to access the Nstack Localization using the BuildContext extension NStackWidgetExtension on BuildContext { - Localization get localization => NStackWidget.of(this).localization; + Localization get localization => NStackScope.of(this).nstack.localization; } /// Allows to access the Nstack Localization from StatefulWidget's State diff --git a/example/pubspec.lock b/example/pubspec.lock index 8c35171..c98979f 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -309,7 +309,7 @@ packages: path: ".." relative: true source: path - version: "0.2.5" + version: "0.3.0" package_config: dependency: transitive description: diff --git a/lib/nstack.dart b/lib/nstack.dart index d5f8764..8e8bb3e 100644 --- a/lib/nstack.dart +++ b/lib/nstack.dart @@ -6,6 +6,7 @@ import 'package:flutter/widgets.dart'; import 'package:nstack/models/app_open.dart'; import 'package:nstack/models/language.dart'; import 'package:nstack/models/language_response.dart'; +import 'package:nstack/models/localize_index.dart'; import 'package:nstack/models/nstack_appopen_data.dart'; import 'package:nstack/models/nstack_config.dart'; import 'package:nstack/src/nstack_repository.dart'; @@ -14,7 +15,7 @@ import 'package:package_info/package_info.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:uuid/uuid.dart'; -class NStack with ChangeNotifier { +class NStack { final NStackConfig config; final T localization; @@ -24,18 +25,23 @@ class NStack with ChangeNotifier { final NStackRepository _repository; late List supportedLocales; late NStackAppOpenData _appOpenData; + final bool debug; Language get activeLanguage => LocalizationRepository().pickedLanguage; Locale get activeLocale => Locale(LocalizationRepository().pickedLanguage.locale!); + String get checksum => LocalizationRepository().checksum; + + var _appOpenCalled = false; NStack({ required this.config, required this.localization, - required List availableLanguages, + required List availableLanguages, required Map bundledTranslations, required String pickedLanguageLocale, + required this.debug }) : _repository = NStackRepository(config) { - supportedLocales = availableLanguages.map((e) => Locale(e.locale?.split("-")[0] ?? "en", e.locale?.split("-")[1].toUpperCase() ?? "US" )).toList(); + supportedLocales = availableLanguages.map((e) => Locale(e.language?.locale?.split("-")[0] ?? "en", e.language?.locale?.split("-")[1].toUpperCase() ?? "US" )).toList(); LocalizationRepository().setupLocalization( bundledTranslations, @@ -91,65 +97,73 @@ class NStack with ChangeNotifier { ); } - Future changeLocalizationOffline(Locale locale) async { + /// + /// Change the localization in the internal map + /// 1. Find the best match in the language list that the project was built with + /// 2. Look if we have a cached localization in preferences and use that + /// 3. Query NStack for the localization, cache it and use that + /// 4. Fallback to bundled localizations from last build + Future changeLocalization(Locale locale) async { try { // Direct locale match - var localLanguage = LocalizationRepository().availableLanguages.firstWhere( - (element) => locale.toLanguageTag().toLowerCase() == (element.locale?.toLowerCase() ?? ""), + var localLanguage = LocalizationRepository().localizeIndexes.firstWhere( + (element) => locale.toLanguageTag().toLowerCase() == (element.language?.locale?.toLowerCase() ?? ""), // Match language part of Locale - orElse: () => LocalizationRepository().availableLanguages.firstWhere( - (element) => locale.toLanguageTag().toLowerCase().split("-")[0] == (element.locale?.toLowerCase().split("-")[0] ?? ""), - // Fallback to default from NStack - orElse: () => LocalizationRepository().availableLanguages.firstWhere((element) => element.isDefault) + orElse: () => LocalizationRepository().localizeIndexes.firstWhere( + (element) => locale.toLanguageTag().toLowerCase().split("-")[0] == (element.language?.locale?.toLowerCase().split("-")[0] ?? ""), ) ); - LocalizationRepository().switchBundledLocalization(localLanguage.locale!); - print("switched!"); - notifyListeners(); - } catch (e, s) { - print(e); - print(s); - } - } - Future changeLocalization(Locale locale) async { - try { final prefs = await SharedPreferences.getInstance(); - var availableLanguages = await _repository.fetchAvailableLanguages(); - var languageFromAPI = availableLanguages.firstWhere( - (element) => locale.toLanguageTag().toLowerCase() == (element.language?.locale?.toLowerCase() ?? ""), - orElse: () => availableLanguages.firstWhere((element) => element.language?.isDefault ?? false) - ); - print('Language from API: ${languageFromAPI.language?.locale}'); - var localizationResponse = await _repository.fetchLocalizationForLanguage(languageFromAPI); - - final translationJson = LocalizationData.fromJson( - jsonDecode(localizationResponse), - ); - LocalizationRepository().updateLocalization( - translationJson.data!, - languageFromAPI.language!.locale!, - ); - print('Updated language in repo!'); + final prefsKey = 'nstack_lang_${localLanguage.language!.locale}'; + var hasCachedLocalization = prefs.containsKey(prefsKey); - // Update cache for key - final nstackKey = 'nstack_lang_${languageFromAPI.language?.locale}'; - prefs.setString(nstackKey, localizationResponse); - - notifyListeners(); - - // Update last_updated for next app open call - //prefs.setString(prefsKeyLastUpdated, DateTime.now().toUtc().toIso8601String()); + if(hasCachedLocalization) { + final cachedResponse = json.decode(prefs.getString(prefsKey)!); + final languageResponse = LocalizationData.fromJson(cachedResponse); + LocalizationRepository().updateLocalization( + languageResponse.data!, + localLanguage.language!.locale!, + ); + _log("Switched cached localization..."); + } else { + try { + var localizationResponse = await _repository.fetchLocalizationForLanguageId(localLanguage.id!); + // Save in cache + prefs.setString(prefsKey, localizationResponse); + + // Switch to the language + final translationJson = LocalizationData.fromJson( + jsonDecode(localizationResponse), + ); + LocalizationRepository().updateLocalization( + translationJson.data!, + localLanguage.language!.locale!, + ); + _log("Switched API localization..."); + } catch (e, s) { + // Use bundled localization as fallback + _log(e.toString()); + _log("Switched to bundled localization since we failed updating from API..."); + LocalizationRepository().switchBundledLocalization(localLanguage.language!.locale!); + } + } } catch (e, s) { - print(s); + _log(e.toString()); + _log(s.toString()); } } Future appOpen(Locale locale) async { try { + if(_appOpenCalled) { + _log("NStack.appOpen() has already been called, returning early..."); + return AppOpenResult.success; + } + await _setupAppOpenData(); - print("NStack --> Calling App Open..."); + _log("NStack --> Calling App Open..."); final Map result = await _repository.postAppOpen( acceptHeader: locale.toLanguageTag(), appOpenData: _appOpenData, @@ -158,7 +172,6 @@ class NStack with ChangeNotifier { ); final appOpen = AppOpen.fromJson(result); - final prefs = await SharedPreferences.getInstance(); // Find best fit @@ -171,7 +184,7 @@ class NStack with ChangeNotifier { // Fetch from the server or use the cache? if (bestFitLanguage?.shouldUpdate == true) { // Fetch best fit language from the server - print( + _log( 'NStack --> Fetching best fit language: ${bestFitLanguage!.language!.locale}'); final String bestFitLanguageResponse = await _repository.fetchLocalizationForLanguage( @@ -192,7 +205,7 @@ class NStack with ChangeNotifier { } else { // Using best fit language from the cache if (prefs.containsKey(nstackKey)) { - print( + _log( 'NStack --> Using cache for best fit language: ${bestFitLanguage?.language?.locale}'); final cachedResponse = json.decode(prefs.getString(nstackKey)!); final languageResponse = LocalizationData.fromJson(cachedResponse); @@ -203,19 +216,26 @@ class NStack with ChangeNotifier { ); // No cache, default values (this shouldn't happen, should_update should be true) } else { - print( + _log( 'NStack --> WARNING: No cache found for best fit language: ${bestFitLanguage?.language?.locale}'); } } - print('NStack --> Updated localization.'); + _log('NStack --> Updated localization.'); + _appOpenCalled = true; return AppOpenResult.success; } catch (e, s) { - print('NStack --> App Open failed because of: ${e.toString()}'); - print(s); + _log('NStack --> App Open failed because of: ${e.toString()}'); + _log(s.toString()); return AppOpenResult.failed; } } + + _log(String message) { + if(debug) { + print(message); + } + } } enum AppOpenResult { success, failed } diff --git a/lib/src/nstack_builder.dart b/lib/src/nstack_builder.dart index 3622555..f6efa80 100644 --- a/lib/src/nstack_builder.dart +++ b/lib/src/nstack_builder.dart @@ -88,12 +88,13 @@ class NstackBuilder implements Builder { output.writeln(''' /// Generated by NStack, do not modify this file. +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:nstack/models/language.dart'; +import 'package:nstack/models/localize_index.dart'; import 'package:nstack/models/nstack_config.dart'; import 'package:nstack/nstack.dart'; import 'package:nstack/partial/section_key_delegate.dart'; -import 'package:provider/provider.dart'; // Update this file by running: // - `flutter pub run build_runner build`, if your package depends on Flutter @@ -176,13 +177,15 @@ const _config = NStackConfig(projectId: '$projectId', apiKey: '$apiKey'); } void _writeLanguageList(List languages, StringBuffer output) { - output.writeln('const _languages = ['); + output.writeln('final _languages = ['); languages.forEach((localizeIndex) { + output.write("LocalizeIndex(id: ${localizeIndex.id}, url: null, lastUpdatedAt: null, shouldUpdate: false, language: "); Language language = localizeIndex.language!; - output.writeln( - '\tLanguage(id: ${language.id}, name: \'${language.name}\', locale: \'${language.locale}\', direction: \'${language.direction}\', isDefault: ${language.isDefault}, isBestFit: ${language.isBestFit}),', + output.write( + '\tLanguage(id: ${language.id}, name: \'${language.name}\', locale: \'${language.locale}\', direction: \'${language.direction}\', isDefault: ${language.isDefault}, isBestFit: ${language.isBestFit})', ); + output.writeln('),'); }); output.writeln(''' @@ -218,59 +221,75 @@ final _nstack = NStack( availableLanguages: _languages, bundledTranslations: _bundledTranslations, pickedLanguageLocale: '', + debug: kDebugMode ); '''); } void _writeNStackWidget(StringBuffer output) async { output.writeln(''' -class NStackWidget extends InheritedWidget { - final NStack nstack = _nstack; +class NStackScope extends InheritedWidget { + final NStack nstack; + final NStackState state; + final String checksum; - NStackWidget({Key? key, required Widget child}) + NStackScope({Key? key, required Widget child, required this.state, required this.nstack, required this.checksum}) : super(key: key, child: child); - static NStack of(BuildContext context) => - context.dependOnInheritedWidgetOfExactType()!.nstack; + static NStackState of(BuildContext context) => + context.dependOnInheritedWidgetOfExactType()!.state; @override - bool updateShouldNotify(NStackWidget oldWidget) => - nstack != oldWidget.nstack; + bool updateShouldNotify(NStackScope oldWidget) => + checksum != oldWidget.checksum; } -class NStackInitWidget extends StatefulWidget { +class NStackWidget extends StatefulWidget { final Widget child; - const NStackInitWidget({Key? key, required Widget child}) + const NStackWidget({Key? key, required Widget child}) : child = child, super(key: key); @override - _NStackInitState createState() => _NStackInitState(); + NStackState createState() => NStackState(); } -class _NStackInitState extends State { - static bool _initialized = false; - - void setupNStack(BuildContext context) { - final locale = Localizations.localeOf(context); - final nstack = NStackWidget.of(context); - nstack.appOpen(locale); +class NStackState extends State { + final NStack nstack = _nstack; + + changeLanguage(Locale locale) async { + print("Starting language switch..."); + await _nstack.changeLocalization(locale); + print("Language switch done!"); + setState(() {}); + } + + _attemptAppOpen() { + try { + nstack.appOpen(Localizations.localeOf(context)); + } catch(s,e) { + print("NStack could not call appOpen() as the NStackWidget is too far up the widget tree."); + print("Consider calling NStackScope.of(context).nstack.appOpen(Localizations.localeOf(context)) in a splashscreen or later."); + //throw e; + } + } + + @override + void initState() { + super.initState(); + _attemptAppOpen(); } @override Widget build(BuildContext context) { - if (!_initialized) { - setupNStack(context); - _initialized = true; - } - return ChangeNotifierProvider(create: (_) => _nstack, child: widget.child,); + return NStackScope(child: widget.child, state: this, nstack: this.nstack, checksum: nstack.checksum,); } } /// Allows to access the Nstack Localization using the BuildContext extension NStackWidgetExtension on BuildContext { - Localization get localization => NStackWidget.of(this).localization; + Localization get localization => NStackScope.of(this).nstack.localization; } /// Allows to access the Nstack Localization from StatefulWidget's State diff --git a/lib/src/nstack_repository.dart b/lib/src/nstack_repository.dart index 1d512d6..ebb9a33 100644 --- a/lib/src/nstack_repository.dart +++ b/lib/src/nstack_repository.dart @@ -78,4 +78,13 @@ class NStackRepository { .get(Uri.parse(language.url!), headers: _headers) .then((value) => value.body); } + + Future fetchLocalizationForLanguageId(int languageId) async { + var response = await http.get( + Uri.parse( + '$_baseUrl/content/localize/resources/$languageId'), + headers: _headers, + ); + return response.body; + } } diff --git a/lib/src/repository.dart b/lib/src/repository.dart index b977af5..5dfa957 100644 --- a/lib/src/repository.dart +++ b/lib/src/repository.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:nstack/models/language.dart'; +import 'package:nstack/models/localize_index.dart'; class LocalizationRepository { // Factory @@ -15,21 +16,25 @@ class LocalizationRepository { Map? _sectionsMap; late Map _bundledTranslations; late List _availableLanguages; + late List _availableLocalizeIndexes; late Language _pickedLanguage; Language get pickedLanguage => _pickedLanguage; List get availableLanguages => _availableLanguages; + List get localizeIndexes => _availableLocalizeIndexes; + String get checksum => _sectionsMap.hashCode.toString() + this.pickedLanguage.id.toString(); void setupLocalization( Map bundledTranslations, - List availableLanguages, + List availableLanguages, String pickedLanguageLocale, ) { this._bundledTranslations = bundledTranslations; - this._availableLanguages = availableLanguages; - this._pickedLanguage = availableLanguages.firstWhere( + this._availableLocalizeIndexes = availableLanguages; + this._availableLanguages = availableLanguages.map((e) => e.language!).toList(); + this._pickedLanguage = this._availableLanguages.firstWhere( (language) => language.locale == pickedLanguageLocale, - orElse: () => availableLanguages.firstWhere( + orElse: () => this._availableLanguages.firstWhere( (language) => language.isDefault, ), ); diff --git a/pubspec.lock b/pubspec.lock index 1ce0de5..3b3618e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "25.0.0" + version: "22.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "1.7.1" args: dependency: transitive description: @@ -28,7 +28,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.1" + version: "2.6.1" boolean_selector: dependency: transitive description: @@ -63,7 +63,7 @@ packages: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.3.1" + version: "1.2.0" checked_yaml: dependency: transitive description: @@ -183,7 +183,7 @@ packages: name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "4.1.0" + version: "4.0.1" logging: dependency: transitive description: @@ -204,7 +204,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.3.0" package_config: dependency: transitive description: