diff --git a/app/lib/core/routing/routes.dart b/app/lib/core/routing/routes.dart index 217220d6..c36d15c7 100644 --- a/app/lib/core/routing/routes.dart +++ b/app/lib/core/routing/routes.dart @@ -45,6 +45,7 @@ import 'package:weblibre/features/settings/presentation/screens/developer_settin import 'package:weblibre/features/settings/presentation/screens/doh_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/fingerprint_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/general_settings.dart'; +import 'package:weblibre/features/settings/presentation/screens/locale_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/settings.dart'; import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening.dart'; import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening_group.dart'; diff --git a/app/lib/core/routing/routes.g.dart b/app/lib/core/routing/routes.g.dart index fe7fcb53..c397fa32 100644 --- a/app/lib/core/routing/routes.g.dart +++ b/app/lib/core/routing/routes.g.dart @@ -116,6 +116,11 @@ RouteBase get $settingsRoute => GoRouteData.$route( name: 'FingerprintSettingsRoute', factory: $FingerprintSettingsRoute._fromState, ), + GoRouteData.$route( + path: 'locales', + name: 'LocaleSettingsRoute', + factory: $LocaleSettingsRoute._fromState, + ), ], ), GoRouteData.$route( @@ -306,6 +311,27 @@ mixin $FingerprintSettingsRoute on GoRouteData { void replace(BuildContext context) => context.replace(location); } +mixin $LocaleSettingsRoute on GoRouteData { + static LocaleSettingsRoute _fromState(GoRouterState state) => + LocaleSettingsRoute(); + + @override + String get location => GoRouteData.$location('/settings/web_engine/locales'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + mixin $DeveloperSettingsRoute on GoRouteData { static DeveloperSettingsRoute _fromState(GoRouterState state) => DeveloperSettingsRoute(); diff --git a/app/lib/core/routing/routes.settings.dart b/app/lib/core/routing/routes.settings.dart index 2726101b..3bac52aa 100644 --- a/app/lib/core/routing/routes.settings.dart +++ b/app/lib/core/routing/routes.settings.dart @@ -47,6 +47,10 @@ part of 'routes.dart'; name: 'FingerprintSettingsRoute', path: 'fingerprint', ), + TypedGoRoute( + name: 'LocaleSettingsRoute', + path: 'locales', + ), ], ), TypedGoRoute( @@ -97,6 +101,13 @@ class FingerprintSettingsRoute extends GoRouteData } } +class LocaleSettingsRoute extends GoRouteData with $LocaleSettingsRoute { + @override + Widget build(BuildContext context, GoRouterState state) { + return const LocaleSettingsScreen(); + } +} + class WebEngineSettingsRoute extends GoRouteData with $WebEngineSettingsRoute { @override Widget build(BuildContext context, GoRouterState state) { diff --git a/app/lib/domain/repositories/locale_resolver.dart b/app/lib/domain/repositories/locale_resolver.dart new file mode 100644 index 00000000..d5f39f0f --- /dev/null +++ b/app/lib/domain/repositories/locale_resolver.dart @@ -0,0 +1,39 @@ +import 'package:flutter/widgets.dart' hide Locale; +import 'package:intl/locale.dart' as intl; +import 'package:locale_resolver/locale_resolver.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weblibre/extensions/locale.dart'; + +part 'locale_resolver.g.dart'; + +@Riverpod(keepAlive: true) +class LocaleResolverRepository extends _$LocaleResolverRepository { + final _service = LocaleResolver(); + final _cache = {}; + + Future resolve(intl.Locale locale) async { + final cached = _cache[locale]; + if (cached != null) { + return cached; + } + + return _cache[locale] = await _service.resolve( + locale.toLanguageTag(), + targetLocale.toLanguageTag(), + ); + } + + @override + void build(intl.Locale targetLocale) {} +} + +@Riverpod() +Future resolveLocale(Ref ref, intl.Locale locale) { + return ref + .read( + localeResolverRepositoryProvider( + WidgetsBinding.instance.platformDispatcher.locale.toIntlLocale(), + ).notifier, + ) + .resolve(locale); +} diff --git a/app/lib/domain/repositories/locale_resolver.g.dart b/app/lib/domain/repositories/locale_resolver.g.dart new file mode 100644 index 00000000..c499a29d --- /dev/null +++ b/app/lib/domain/repositories/locale_resolver.g.dart @@ -0,0 +1,185 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'locale_resolver.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(LocaleResolverRepository) +const localeResolverRepositoryProvider = LocaleResolverRepositoryFamily._(); + +final class LocaleResolverRepositoryProvider + extends $NotifierProvider { + const LocaleResolverRepositoryProvider._({ + required LocaleResolverRepositoryFamily super.from, + required intl.Locale super.argument, + }) : super( + retry: null, + name: r'localeResolverRepositoryProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$localeResolverRepositoryHash(); + + @override + String toString() { + return r'localeResolverRepositoryProvider' + '' + '($argument)'; + } + + @$internal + @override + LocaleResolverRepository create() => LocaleResolverRepository(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(void value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } + + @override + bool operator ==(Object other) { + return other is LocaleResolverRepositoryProvider && + other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$localeResolverRepositoryHash() => + r'dc74908d6ac1f5cc851e2a10dcf33b0dc68b5b15'; + +final class LocaleResolverRepositoryFamily extends $Family + with + $ClassFamilyOverride< + LocaleResolverRepository, + void, + void, + void, + intl.Locale + > { + const LocaleResolverRepositoryFamily._() + : super( + retry: null, + name: r'localeResolverRepositoryProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: false, + ); + + LocaleResolverRepositoryProvider call(intl.Locale targetLocale) => + LocaleResolverRepositoryProvider._(argument: targetLocale, from: this); + + @override + String toString() => r'localeResolverRepositoryProvider'; +} + +abstract class _$LocaleResolverRepository extends $Notifier { + late final _$args = ref.$arg as intl.Locale; + intl.Locale get targetLocale => _$args; + + void build(intl.Locale targetLocale); + @$mustCallSuper + @override + void runBuild() { + build(_$args); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + void, + Object?, + Object? + >; + element.handleValue(ref, null); + } +} + +@ProviderFor(resolveLocale) +const resolveLocaleProvider = ResolveLocaleFamily._(); + +final class ResolveLocaleProvider + extends + $FunctionalProvider< + AsyncValue, + LocalizedResult, + FutureOr + > + with $FutureModifier, $FutureProvider { + const ResolveLocaleProvider._({ + required ResolveLocaleFamily super.from, + required intl.Locale super.argument, + }) : super( + retry: null, + name: r'resolveLocaleProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$resolveLocaleHash(); + + @override + String toString() { + return r'resolveLocaleProvider' + '' + '($argument)'; + } + + @$internal + @override + $FutureProviderElement $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + final argument = this.argument as intl.Locale; + return resolveLocale(ref, argument); + } + + @override + bool operator ==(Object other) { + return other is ResolveLocaleProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$resolveLocaleHash() => r'94d7a9b307a81de372b8b40b06f046acc76e01c8'; + +final class ResolveLocaleFamily extends $Family + with $FunctionalFamilyOverride, intl.Locale> { + const ResolveLocaleFamily._() + : super( + retry: null, + name: r'resolveLocaleProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + ResolveLocaleProvider call(intl.Locale locale) => + ResolveLocaleProvider._(argument: locale, from: this); + + @override + String toString() => r'resolveLocaleProvider'; +} diff --git a/app/lib/extensions/locale.dart b/app/lib/extensions/locale.dart new file mode 100644 index 00000000..e85cff1e --- /dev/null +++ b/app/lib/extensions/locale.dart @@ -0,0 +1,27 @@ +import 'dart:ui' as ui; + +import 'package:intl/locale.dart' as intl; + +extension LocaleFormat on intl.Locale { + String rawToString(String separator) { + final StringBuffer out = StringBuffer(languageCode); + if (scriptCode != null && scriptCode!.isNotEmpty) { + out.write('$separator$scriptCode'); + } + final String? countryCode = this.countryCode; + if (countryCode != null && countryCode.isNotEmpty) { + out.write('$separator${this.countryCode}'); + } + return out.toString(); + } +} + +extension LocaleConverter on ui.Locale { + intl.Locale toIntlLocale() { + return intl.Locale.fromSubtags( + languageCode: languageCode, + countryCode: countryCode, + scriptCode: scriptCode, + ); + } +} diff --git a/app/lib/features/geckoview/features/browser/domain/services/engine_settings_replication.dart b/app/lib/features/geckoview/features/browser/domain/services/engine_settings_replication.dart index b6434a04..50ce78cf 100644 --- a/app/lib/features/geckoview/features/browser/domain/services/engine_settings_replication.dart +++ b/app/lib/features/geckoview/features/browser/domain/services/engine_settings_replication.dart @@ -17,6 +17,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import 'package:collection/collection.dart'; import 'package:flutter/material.dart' show ThemeMode; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:riverpod/riverpod.dart'; @@ -141,6 +142,18 @@ class EngineSettingsReplicationService .read(preferenceFixatorProvider.notifier) .register('pdfjs.disabled', !settings.enablePdfJs); } + if (!const DeepCollectionEquality.unordered().equals( + previous.value?.locales, + settings.locales, + )) { + // ignore: only_use_keep_alive_inside_keep_alive + await ref + .read(preferenceFixatorProvider.notifier) + .register( + 'intl.accept_languages', + settings.locales.join(','), + ); + } } else { await _service.setDefaultSettings(settings); @@ -149,6 +162,11 @@ class EngineSettingsReplicationService .read(preferenceFixatorProvider.notifier) .register('pdfjs.disabled', !settings.enablePdfJs); + // ignore: only_use_keep_alive_inside_keep_alive + await ref + .read(preferenceFixatorProvider.notifier) + .register('intl.accept_languages', settings.locales.join(',')); + initialSettingsSent = true; } } diff --git a/app/lib/features/geckoview/features/browser/domain/services/engine_settings_replication.g.dart b/app/lib/features/geckoview/features/browser/domain/services/engine_settings_replication.g.dart index 8d4443d4..ecc15882 100644 --- a/app/lib/features/geckoview/features/browser/domain/services/engine_settings_replication.g.dart +++ b/app/lib/features/geckoview/features/browser/domain/services/engine_settings_replication.g.dart @@ -44,7 +44,7 @@ final class EngineSettingsReplicationServiceProvider } String _$engineSettingsReplicationServiceHash() => - r'd28cc58a94f45008a478bf52a9c40d61811b591d'; + r'7583ebc8ca9c11377486ff5a3e763493dcdc903a'; abstract class _$EngineSettingsReplicationService extends $Notifier { void build(); diff --git a/app/lib/features/geckoview/features/preferences/data/repositories/preference_observer.dart b/app/lib/features/geckoview/features/preferences/data/repositories/preference_observer.dart index f48ade50..143ef136 100644 --- a/app/lib/features/geckoview/features/preferences/data/repositories/preference_observer.dart +++ b/app/lib/features/geckoview/features/preferences/data/repositories/preference_observer.dart @@ -25,8 +25,6 @@ class PreferenceChangeListener extends _$PreferenceChangeListener { @Riverpod() class PreferenceFixator extends _$PreferenceFixator { Future register(String name, Object value) async { - await GeckoPrefService().applyPrefs({name: value}); - state = {...state} ..update( name, @@ -36,6 +34,8 @@ class PreferenceFixator extends _$PreferenceFixator { return value; }, ); + + await GeckoPrefService().applyPrefs({name: value}); } Future unregister(String name) async { diff --git a/app/lib/features/geckoview/features/preferences/data/repositories/preference_observer.g.dart b/app/lib/features/geckoview/features/preferences/data/repositories/preference_observer.g.dart index 24eed1a8..1e07ae28 100644 --- a/app/lib/features/geckoview/features/preferences/data/repositories/preference_observer.g.dart +++ b/app/lib/features/geckoview/features/preferences/data/repositories/preference_observer.g.dart @@ -87,7 +87,7 @@ final class PreferenceFixatorProvider } } -String _$preferenceFixatorHash() => r'dfa323859c660d3db74dba7297e2a33b758ed78a'; +String _$preferenceFixatorHash() => r'c76245d5c420e2e3f6c026f23c017c939b69ef59'; abstract class _$PreferenceFixator extends $Notifier> { Map build(); diff --git a/app/lib/features/settings/presentation/screens/locale_settings.dart b/app/lib/features/settings/presentation/screens/locale_settings.dart new file mode 100644 index 00000000..49894850 --- /dev/null +++ b/app/lib/features/settings/presentation/screens/locale_settings.dart @@ -0,0 +1,148 @@ +import 'package:country_flags/country_flags.dart'; +import 'package:fast_equatable/fast_equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:intl/locale.dart'; +import 'package:nullability/nullability.dart'; +import 'package:weblibre/domain/repositories/locale_resolver.dart'; +import 'package:weblibre/extensions/locale.dart'; +import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart'; +import 'package:weblibre/features/user/data/models/engine_settings.dart'; +import 'package:weblibre/features/user/domain/repositories/engine_settings.dart'; + +class LocaleSettingsScreen extends HookConsumerWidget { + const LocaleSettingsScreen(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final formKey = useMemoized(() => GlobalKey()); + + final systemLocales = useMemoized( + () => WidgetsBinding.instance.platformDispatcher.locales + .map((locale) => locale.toIntlLocale()) + .toList(), + ); + + final userLocales = ref.watch( + engineSettingsWithDefaultsProvider.select( + (settings) => EquatableValue( + settings.locales.map(Locale.tryParse).nonNulls.toSet(), + ), + ), + ); + + final availableLocales = { + ...systemLocales, + ...userLocales.value, + Locale.fromSubtags(languageCode: 'en', countryCode: 'US'), + }; + + final customLocaleController = useTextEditingController(); + + return Scaffold( + appBar: AppBar(title: const Text('Browser Languages')), + body: SafeArea( + child: ListView( + children: [ + ...availableLocales.map((locale) { + return CheckboxListTile.adaptive( + value: userLocales.value.contains(locale), + onChanged: (value) async { + if (value != null) { + await ref + .read(saveEngineSettingsControllerProvider.notifier) + .save( + (currentSettings) => currentSettings.copyWith.locales( + value + ? { + ...currentSettings.locales, + locale.toLanguageTag(), + }.toList() + : ([ + ...currentSettings.locales, + ]..remove(locale.toLanguageTag())).toList(), + ), + ); + } + }, + title: Consumer( + builder: (context, ref, child) { + final resolvedAsync = ref.watch( + resolveLocaleProvider(locale), + ); + + return Text( + resolvedAsync.maybeWhen( + data: (data) => + data.mapNotNull( + (data) => + '${data.languageName} ${data.countryName.mapNotNull((country) => '($country)') ?? ''}' + .trim(), + ) ?? + locale.toLanguageTag(), + orElse: () => locale.toLanguageTag(), + ), + ); + }, + ), + subtitle: Text(locale.toLanguageTag()), + secondary: CountryFlag.fromLanguageCode( + locale.languageCode, + theme: const ImageTheme(shape: RoundedRectangle(8.0)), + ), + ); + }), + const Divider(), + Padding( + padding: const EdgeInsets.only(left: 16.0, right: 20), + child: Form( + key: formKey, + child: TextFormField( + controller: customLocaleController, + decoration: InputDecoration( + label: const Text('Custom Locale'), + hint: const Text('en-US'), + floatingLabelBehavior: FloatingLabelBehavior.always, + suffixIcon: IconButton( + onPressed: () { + if (formKey.currentState?.validate() == true) { + formKey.currentState?.save(); + } + }, + icon: const Icon(Icons.add), + ), + ), + validator: (value) { + if (value != null && Locale.tryParse(value) == null) { + return 'Invalid locale identifier'; + } + + return null; + }, + onSaved: (newValue) async { + if (newValue != null) { + await ref + .read(saveEngineSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.locales( + { + ...currentSettings.locales, + Locale.parse(newValue).toLanguageTag(), + }.toList(), + ), + ); + + customLocaleController.clear(); + } + }, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/app/lib/features/settings/presentation/screens/web_engine_settings.dart b/app/lib/features/settings/presentation/screens/web_engine_settings.dart index ed10f401..5ea41eca 100644 --- a/app/lib/features/settings/presentation/screens/web_engine_settings.dart +++ b/app/lib/features/settings/presentation/screens/web_engine_settings.dart @@ -203,6 +203,18 @@ class WebEngineSettingsScreen extends HookConsumerWidget { ], ), ), + ListTile( + title: const Text('Browser Languages'), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + leading: const Icon(MdiIcons.translate), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await LocaleSettingsRoute().push(context); + }, + ), SwitchListTile.adaptive( title: const Text('Global Privacy Control (GPC)'), secondary: const Icon(MdiIcons.incognitoCircleOff), diff --git a/app/lib/features/user/data/models/engine_settings.dart b/app/lib/features/user/data/models/engine_settings.dart index 22f12c73..cf6f27ba 100644 --- a/app/lib/features/user/data/models/engine_settings.dart +++ b/app/lib/features/user/data/models/engine_settings.dart @@ -21,6 +21,7 @@ import 'dart:convert'; import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:fast_equatable/fast_equatable.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:nullability/nullability.dart'; @@ -75,6 +76,9 @@ class EngineSettings extends GeckoEngineSettings with FastEquatable { @override bool get enterpriseRootsEnabled => super.enterpriseRootsEnabled!; + @override + List get locales => super.locales!; + final QueryParameterStripping queryParameterStripping; final BounceTrackingProtectionMode bounceTrackingProtectionMode; @@ -130,6 +134,7 @@ class EngineSettings extends GeckoEngineSettings with FastEquatable { required this.dohExceptionsList, required super.fingerprintingProtectionOverrides, required this.enablePdfJs, + required super.locales, }); EngineSettings.withDefaults({ @@ -154,6 +159,7 @@ class EngineSettings extends GeckoEngineSettings with FastEquatable { List? dohExceptionsList, String? fingerprintingProtectionOverrides, bool? enablePdfJs, + List? locales, }) : queryParameterStripping = queryParameterStripping ?? QueryParameterStripping.disabled, bounceTrackingProtectionMode = @@ -188,6 +194,11 @@ class EngineSettings extends GeckoEngineSettings with FastEquatable { fingerprintingProtectionOverrides: fingerprintingProtectionOverrides ?? FingerprintOverrides.defaults().toString(), + locales: + locales ?? + WidgetsBinding.instance.platformDispatcher.locales + .map((x) => x.toLanguageTag()) + .toList(), ); static AddonCollection? _addonCollectionFromJson(String? json) => @@ -226,5 +237,6 @@ class EngineSettings extends GeckoEngineSettings with FastEquatable { dohExceptionsList, fingerprintingProtectionOverrides, enablePdfJs, + locales, ]; } diff --git a/app/lib/features/user/data/models/engine_settings.g.dart b/app/lib/features/user/data/models/engine_settings.g.dart index c7be94e6..1fb3bdad 100644 --- a/app/lib/features/user/data/models/engine_settings.g.dart +++ b/app/lib/features/user/data/models/engine_settings.g.dart @@ -67,6 +67,8 @@ abstract class _$EngineSettingsCWProxy { EngineSettings enablePdfJs(bool enablePdfJs); + EngineSettings locales(List? locales); + /// Creates a new instance with the provided field values. /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `EngineSettings(...).copyWith.fieldName(value)`. /// @@ -96,6 +98,7 @@ abstract class _$EngineSettingsCWProxy { List dohExceptionsList, String? fingerprintingProtectionOverrides, bool enablePdfJs, + List? locales, }); } @@ -207,6 +210,9 @@ class _$EngineSettingsCWProxyImpl implements _$EngineSettingsCWProxy { EngineSettings enablePdfJs(bool enablePdfJs) => call(enablePdfJs: enablePdfJs); + @override + EngineSettings locales(List? locales) => call(locales: locales); + @override /// Creates a new instance with the provided field values. /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `EngineSettings(...).copyWith.fieldName(value)`. @@ -239,6 +245,7 @@ class _$EngineSettingsCWProxyImpl implements _$EngineSettingsCWProxy { Object? dohExceptionsList = const $CopyWithPlaceholder(), Object? fingerprintingProtectionOverrides = const $CopyWithPlaceholder(), Object? enablePdfJs = const $CopyWithPlaceholder(), + Object? locales = const $CopyWithPlaceholder(), }) { return EngineSettings( javascriptEnabled: javascriptEnabled == const $CopyWithPlaceholder() @@ -350,6 +357,10 @@ class _$EngineSettingsCWProxyImpl implements _$EngineSettingsCWProxy { ? _value.enablePdfJs // ignore: cast_nullable_to_non_nullable : enablePdfJs as bool, + locales: locales == const $CopyWithPlaceholder() + ? _value.locales + // ignore: cast_nullable_to_non_nullable + : locales as List?, ); } } @@ -422,6 +433,9 @@ EngineSettings _$EngineSettingsFromJson(Map json) => fingerprintingProtectionOverrides: json['fingerprintingProtectionOverrides'] as String?, enablePdfJs: json['enablePdfJs'] as bool?, + locales: (json['locales'] as List?) + ?.map((e) => e as String) + .toList(), ); Map _$EngineSettingsToJson( @@ -448,6 +462,7 @@ Map _$EngineSettingsToJson( _$WebContentIsolationStrategyEnumMap[instance .webContentIsolationStrategy]!, 'enterpriseRootsEnabled': instance.enterpriseRootsEnabled, + 'locales': instance.locales, 'queryParameterStripping': _$QueryParameterStrippingEnumMap[instance.queryParameterStripping]!, 'bounceTrackingProtectionMode': diff --git a/app/lib/features/user/domain/repositories/engine_settings.dart b/app/lib/features/user/domain/repositories/engine_settings.dart index 11422211..51778d4d 100644 --- a/app/lib/features/user/domain/repositories/engine_settings.dart +++ b/app/lib/features/user/domain/repositories/engine_settings.dart @@ -124,6 +124,9 @@ class EngineSettingsRepository extends _$EngineSettingsRepository { DriftSqlType.bool, db.typeMapping, ), + 'locales': settings['locales'] + ?.readAs(DriftSqlType.string, db.typeMapping) + .mapNotNull(jsonDecode), }); } diff --git a/app/lib/features/user/domain/repositories/engine_settings.g.dart b/app/lib/features/user/domain/repositories/engine_settings.g.dart index ffeb433e..536b63fb 100644 --- a/app/lib/features/user/domain/repositories/engine_settings.g.dart +++ b/app/lib/features/user/domain/repositories/engine_settings.g.dart @@ -34,7 +34,7 @@ final class EngineSettingsRepositoryProvider } String _$engineSettingsRepositoryHash() => - r'cd087c5631faaddf1fced31569b0fd577d6030a7'; + r'916abcd932fc3b3b1e060bc93a46c1a3a38bb984'; abstract class _$EngineSettingsRepository extends $StreamNotifier { diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 5ea6a827..41a99f1f 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -11,6 +11,7 @@ dependencies: background_fetch: ^1.4.0 collection: ^1.19.1 copy_with_extension: ^9.1.1 + country_flags: ^4.1.0 drift: ^2.29.0 dynamic_color: ^1.8.1 exceptions: ^0.6.1 @@ -42,6 +43,8 @@ dependencies: lexo_rank: git: url: https://github.com/FaFre/lexo_rank.git + locale_resolver: + path: ../packages/locale_resolver local_auth: ^3.0.0 logger: ^2.6.2 markdown: ^7.3.0 diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/EngineProvider.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/EngineProvider.kt index 4de96e51..242c3d0b 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/EngineProvider.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/EngineProvider.kt @@ -73,6 +73,7 @@ object EngineProvider { //builder.debugLogging(components.logLevel == Log.Priority.DEBUG) builder.consoleOutput(components.logLevel == Log.Priority.DEBUG) builder.contentBlocking(contentBlocking.build()) + builder.locales(arrayOf("en-US", "en")) // Will be overridden later runtime = GeckoRuntime.create(context, builder.build()) } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt index be9da4ff..7fbf6f67 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt @@ -6,6 +6,7 @@ package eu.weblibre.flutter_mozilla_components.api +import eu.weblibre.flutter_mozilla_components.EngineProvider import eu.weblibre.flutter_mozilla_components.GlobalComponents import eu.weblibre.flutter_mozilla_components.pigeons.ColorScheme import eu.weblibre.flutter_mozilla_components.pigeons.CookieBannerHandlingMode @@ -147,6 +148,10 @@ class GeckoEngineSettingsApiImpl : GeckoEngineSettingsApi { if(settings.fingerprintingProtectionOverrides != null) { components.core.engineSettings.fingerprintingProtectionOverrides = settings.fingerprintingProtectionOverrides } + if(settings.locales != null) { +// components.core.engineSettings.automaticLanguageAdjustment = false + components.core.runtime.settings.locales = settings.locales.toTypedArray() + } } override fun updateRuntimeSettings(settings: GeckoEngineSettings) { diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Core.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Core.kt index 8a4d0358..eef01939 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Core.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Core.kt @@ -14,6 +14,7 @@ import eu.weblibre.flutter_mozilla_components.Components import eu.weblibre.flutter_mozilla_components.interceptor.AppRequestInterceptor import eu.weblibre.flutter_mozilla_components.services.DownloadService import eu.weblibre.flutter_mozilla_components.EngineProvider +import eu.weblibre.flutter_mozilla_components.EngineProvider.getOrCreateRuntime import eu.weblibre.flutter_mozilla_components.PermissionStorage import eu.weblibre.flutter_mozilla_components.services.MediaSessionService import eu.weblibre.flutter_mozilla_components.activities.NotificationActivity @@ -58,6 +59,7 @@ import mozilla.components.feature.session.middleware.undo.UndoMiddleware import mozilla.components.feature.sitepermissions.OnDiskSitePermissionsStorage import mozilla.components.feature.webnotifications.WebNotificationFeature import mozilla.components.support.base.worker.Frequency +import org.mozilla.geckoview.GeckoRuntime import java.util.concurrent.TimeUnit private const val AMO_COLLECTION_MAX_CACHE_AGE = 24 * 60L @@ -107,6 +109,10 @@ class Core( ) } + val runtime: GeckoRuntime by lazy { + getOrCreateRuntime(context) + } + val engine: Engine by lazy { EngineProvider.createEngine(context, engineSettings, extensionEvents) } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt index 692fa172..aedce966 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt @@ -1678,7 +1678,8 @@ data class GeckoEngineSettings ( val contentBlocking: ContentBlocking? = null, val enterpriseRootsEnabled: Boolean? = null, val dohSettings: DohSettings? = null, - val fingerprintingProtectionOverrides: String? = null + val fingerprintingProtectionOverrides: String? = null, + val locales: List? = null ) { companion object { @@ -1698,7 +1699,8 @@ data class GeckoEngineSettings ( val enterpriseRootsEnabled = pigeonVar_list[12] as Boolean? val dohSettings = pigeonVar_list[13] as DohSettings? val fingerprintingProtectionOverrides = pigeonVar_list[14] as String? - return GeckoEngineSettings(javascriptEnabled, trackingProtectionPolicy, httpsOnlyMode, globalPrivacyControlEnabled, preferredColorScheme, cookieBannerHandlingMode, cookieBannerHandlingModePrivateBrowsing, cookieBannerHandlingGlobalRules, cookieBannerHandlingGlobalRulesSubFrames, webContentIsolationStrategy, userAgent, contentBlocking, enterpriseRootsEnabled, dohSettings, fingerprintingProtectionOverrides) + val locales = pigeonVar_list[15] as List? + return GeckoEngineSettings(javascriptEnabled, trackingProtectionPolicy, httpsOnlyMode, globalPrivacyControlEnabled, preferredColorScheme, cookieBannerHandlingMode, cookieBannerHandlingModePrivateBrowsing, cookieBannerHandlingGlobalRules, cookieBannerHandlingGlobalRulesSubFrames, webContentIsolationStrategy, userAgent, contentBlocking, enterpriseRootsEnabled, dohSettings, fingerprintingProtectionOverrides, locales) } } fun toList(): List { @@ -1718,6 +1720,7 @@ data class GeckoEngineSettings ( enterpriseRootsEnabled, dohSettings, fingerprintingProtectionOverrides, + locales, ) } override fun equals(other: Any?): Boolean { diff --git a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart index 3f3e8c99..3a99ee69 100644 --- a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart +++ b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart @@ -2044,6 +2044,7 @@ class GeckoEngineSettings { this.enterpriseRootsEnabled, this.dohSettings, this.fingerprintingProtectionOverrides, + this.locales, }); bool? javascriptEnabled; @@ -2076,6 +2077,8 @@ class GeckoEngineSettings { String? fingerprintingProtectionOverrides; + List? locales; + List _toList() { return [ javascriptEnabled, @@ -2093,6 +2096,7 @@ class GeckoEngineSettings { enterpriseRootsEnabled, dohSettings, fingerprintingProtectionOverrides, + locales, ]; } @@ -2117,6 +2121,7 @@ class GeckoEngineSettings { enterpriseRootsEnabled: result[12] as bool?, dohSettings: result[13] as DohSettings?, fingerprintingProtectionOverrides: result[14] as String?, + locales: (result[15] as List?)?.cast(), ); } diff --git a/packages/flutter_mozilla_components/pigeons/gecko.dart b/packages/flutter_mozilla_components/pigeons/gecko.dart index 351b44fa..816f4d3c 100644 --- a/packages/flutter_mozilla_components/pigeons/gecko.dart +++ b/packages/flutter_mozilla_components/pigeons/gecko.dart @@ -667,6 +667,7 @@ class GeckoEngineSettings { final bool? enterpriseRootsEnabled; final DohSettings? dohSettings; final String? fingerprintingProtectionOverrides; + final List? locales; GeckoEngineSettings( this.javascriptEnabled, @@ -684,6 +685,7 @@ class GeckoEngineSettings { this.enterpriseRootsEnabled, this.dohSettings, this.fingerprintingProtectionOverrides, + this.locales, ); } diff --git a/packages/locale_resolver/.gitignore b/packages/locale_resolver/.gitignore new file mode 100644 index 00000000..b9d7f25b --- /dev/null +++ b/packages/locale_resolver/.gitignore @@ -0,0 +1,33 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.flutter-plugins-dependencies +/build/ +/coverage/ diff --git a/packages/locale_resolver/.metadata b/packages/locale_resolver/.metadata new file mode 100644 index 00000000..d9114f3b --- /dev/null +++ b/packages/locale_resolver/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "9f455d2486bcb28cad87b062475f42edc959f636" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + - platform: android + create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/locale_resolver/CHANGELOG.md b/packages/locale_resolver/CHANGELOG.md new file mode 100644 index 00000000..41cc7d81 --- /dev/null +++ b/packages/locale_resolver/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/locale_resolver/LICENSE b/packages/locale_resolver/LICENSE new file mode 100644 index 00000000..ba75c69f --- /dev/null +++ b/packages/locale_resolver/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/locale_resolver/README.md b/packages/locale_resolver/README.md new file mode 100644 index 00000000..40c0d0dd --- /dev/null +++ b/packages/locale_resolver/README.md @@ -0,0 +1,15 @@ +# locale_resolver + +A new Flutter plugin project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/to/develop-plugins), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/packages/locale_resolver/analysis_options.yaml b/packages/locale_resolver/analysis_options.yaml new file mode 100644 index 00000000..9357549d --- /dev/null +++ b/packages/locale_resolver/analysis_options.yaml @@ -0,0 +1,35 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lint/package.yaml +# Uncomment the following section to specify additional rules. + +linter: + rules: + unawaited_futures: true + discarded_futures: true + collection_methods_unrelated_type: true + +analyzer: + plugins: + - custom_lint + exclude: + - "**.g.dart" + - "**.swagger.dart" + - "**.freezed.dart" + - "**.chopper.dart" +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/packages/locale_resolver/android/.gitignore b/packages/locale_resolver/android/.gitignore new file mode 100644 index 00000000..161bdcda --- /dev/null +++ b/packages/locale_resolver/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/packages/locale_resolver/android/build.gradle b/packages/locale_resolver/android/build.gradle new file mode 100644 index 00000000..cbac013c --- /dev/null +++ b/packages/locale_resolver/android/build.gradle @@ -0,0 +1,66 @@ +group = "eu.weblibre.locale_resolver" +version = "1.0-SNAPSHOT" + +buildscript { + ext.kotlin_version = "2.1.20" + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:8.11.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" + +android { + namespace = "eu.weblibre.locale_resolver" + + compileSdk = 36 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11 + } + + sourceSets { + main.java.srcDirs += "src/main/kotlin" + test.java.srcDirs += "src/test/kotlin" + } + + defaultConfig { + minSdk = 24 + } + + dependencies { + testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("org.mockito:mockito-core:5.0.0") + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/packages/locale_resolver/android/settings.gradle b/packages/locale_resolver/android/settings.gradle new file mode 100644 index 00000000..850a6caf --- /dev/null +++ b/packages/locale_resolver/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'locale_resolver' diff --git a/packages/locale_resolver/android/src/main/AndroidManifest.xml b/packages/locale_resolver/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8a813492 --- /dev/null +++ b/packages/locale_resolver/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/packages/locale_resolver/android/src/main/kotlin/eu/weblibre/locale_resolver/LocaleResolver.kt b/packages/locale_resolver/android/src/main/kotlin/eu/weblibre/locale_resolver/LocaleResolver.kt new file mode 100644 index 00000000..8471e9b2 --- /dev/null +++ b/packages/locale_resolver/android/src/main/kotlin/eu/weblibre/locale_resolver/LocaleResolver.kt @@ -0,0 +1,20 @@ +package eu.weblibre.locale_resolver + +import eu.weblibre.locale_resolver.pigeons.LocaleResolver +import eu.weblibre.locale_resolver.pigeons.LocalizedResult +import java.util.Locale + +class LocaleResolverImpl : LocaleResolver { + override fun resolve(languageTag: String, targetLangouageTag: String): LocalizedResult { + val deviceLocale = Locale.forLanguageTag(targetLangouageTag) + val targetLocale = Locale.forLanguageTag(languageTag) + + val languageName = targetLocale.getDisplayLanguage(deviceLocale) + val countryName = targetLocale.getDisplayCountry(deviceLocale) + + return LocalizedResult( + languageName, + countryName.ifBlank { null } + ) + } +} \ No newline at end of file diff --git a/packages/locale_resolver/android/src/main/kotlin/eu/weblibre/locale_resolver/LocaleResolverPlugin.kt b/packages/locale_resolver/android/src/main/kotlin/eu/weblibre/locale_resolver/LocaleResolverPlugin.kt new file mode 100644 index 00000000..52325081 --- /dev/null +++ b/packages/locale_resolver/android/src/main/kotlin/eu/weblibre/locale_resolver/LocaleResolverPlugin.kt @@ -0,0 +1,19 @@ +package eu.weblibre.locale_resolver + +import eu.weblibre.locale_resolver.pigeons.LocaleResolver +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +/** LocaleResolverPlugin */ +class LocaleResolverPlugin : FlutterPlugin { + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + LocaleResolver.setUp(flutterPluginBinding.binaryMessenger, LocaleResolverImpl()) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + + } +} diff --git a/packages/locale_resolver/android/src/main/kotlin/eu/weblibre/locale_resolver/pigeons/Locales.g.kt b/packages/locale_resolver/android/src/main/kotlin/eu/weblibre/locale_resolver/pigeons/Locales.g.kt new file mode 100644 index 00000000..1e94597a --- /dev/null +++ b/packages/locale_resolver/android/src/main/kotlin/eu/weblibre/locale_resolver/pigeons/Locales.g.kt @@ -0,0 +1,166 @@ +// Autogenerated from Pigeon (v26.0.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package eu.weblibre.locale_resolver.pigeons + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMethodCodec +import io.flutter.plugin.common.StandardMessageCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +private object LocalesPigeonUtils { + + fun wrapResult(result: Any?): List { + return listOf(result) + } + + fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf( + exception.code, + exception.message, + exception.details + ) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) + } + } + fun deepEquals(a: Any?, b: Any?): Boolean { + if (a is ByteArray && b is ByteArray) { + return a.contentEquals(b) + } + if (a is IntArray && b is IntArray) { + return a.contentEquals(b) + } + if (a is LongArray && b is LongArray) { + return a.contentEquals(b) + } + if (a is DoubleArray && b is DoubleArray) { + return a.contentEquals(b) + } + if (a is Array<*> && b is Array<*>) { + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } + } + if (a is List<*> && b is List<*>) { + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } + } + if (a is Map<*, *> && b is Map<*, *>) { + return a.size == b.size && a.all { + (b as Map).containsKey(it.key) && + deepEquals(it.value, b[it.key]) + } + } + return a == b + } + +} + +/** + * Error class for passing custom error details to Flutter via a thrown PlatformException. + * @property code The error code. + * @property message The error message. + * @property details The error details. Must be a datatype supported by the api codec. + */ +class FlutterError ( + val code: String, + override val message: String? = null, + val details: Any? = null +) : Throwable() + +/** Generated class from Pigeon that represents data sent in messages. */ +data class LocalizedResult ( + val languageName: String, + val countryName: String? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): LocalizedResult { + val languageName = pigeonVar_list[0] as String + val countryName = pigeonVar_list[1] as String? + return LocalizedResult(languageName, countryName) + } + } + fun toList(): List { + return listOf( + languageName, + countryName, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is LocalizedResult) { + return false + } + if (this === other) { + return true + } + return LocalesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} +private open class LocalesPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as? List)?.let { + LocalizedResult.fromList(it) + } + } + else -> super.readValueOfType(type, buffer) + } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is LocalizedResult -> { + stream.write(129) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } +} + +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface LocaleResolver { + fun resolve(languageTag: String, targetLangouageTag: String): LocalizedResult + + companion object { + /** The codec used by LocaleResolver. */ + val codec: MessageCodec by lazy { + LocalesPigeonCodec() + } + /** Sets up an instance of `LocaleResolver` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: LocaleResolver?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.locale_resolver.LocaleResolver.resolve$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val languageTagArg = args[0] as String + val targetLangouageTagArg = args[1] as String + val wrapped: List = try { + listOf(api.resolve(languageTagArg, targetLangouageTagArg)) + } catch (exception: Throwable) { + LocalesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} diff --git a/packages/locale_resolver/android/src/test/kotlin/eu/weblibre/locale_resolver/LocaleResolverPluginTest.kt b/packages/locale_resolver/android/src/test/kotlin/eu/weblibre/locale_resolver/LocaleResolverPluginTest.kt new file mode 100644 index 00000000..a29a7e33 --- /dev/null +++ b/packages/locale_resolver/android/src/test/kotlin/eu/weblibre/locale_resolver/LocaleResolverPluginTest.kt @@ -0,0 +1,27 @@ +package eu.weblibre.locale_resolver + +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import org.mockito.Mockito +import kotlin.test.Test + +/* + * This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation. + * + * Once you have built the plugin's example app, you can run these tests from the command + * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or + * you can run them directly from IDEs that support JUnit such as Android Studio. + */ + +internal class LocaleResolverPluginTest { + @Test + fun onMethodCall_getPlatformVersion_returnsExpectedValue() { + val plugin = LocaleResolverPlugin() + + val call = MethodCall("getPlatformVersion", null) + val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java) + plugin.onMethodCall(call, mockResult) + + Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE) + } +} diff --git a/packages/locale_resolver/example/.gitignore b/packages/locale_resolver/example/.gitignore new file mode 100644 index 00000000..3820a95c --- /dev/null +++ b/packages/locale_resolver/example/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/locale_resolver/example/README.md b/packages/locale_resolver/example/README.md new file mode 100644 index 00000000..80f8a73b --- /dev/null +++ b/packages/locale_resolver/example/README.md @@ -0,0 +1,16 @@ +# locale_resolver_example + +Demonstrates how to use the locale_resolver plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/locale_resolver/example/analysis_options.yaml b/packages/locale_resolver/example/analysis_options.yaml new file mode 100644 index 00000000..0d290213 --- /dev/null +++ b/packages/locale_resolver/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/locale_resolver/example/android/.gitignore b/packages/locale_resolver/example/android/.gitignore new file mode 100644 index 00000000..be3943c9 --- /dev/null +++ b/packages/locale_resolver/example/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/packages/locale_resolver/example/android/app/build.gradle.kts b/packages/locale_resolver/example/android/app/build.gradle.kts new file mode 100644 index 00000000..0908c76e --- /dev/null +++ b/packages/locale_resolver/example/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "eu.weblibre.locale_resolver_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "eu.weblibre.locale_resolver_example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/packages/locale_resolver/example/android/app/src/debug/AndroidManifest.xml b/packages/locale_resolver/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/packages/locale_resolver/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/locale_resolver/example/android/app/src/main/AndroidManifest.xml b/packages/locale_resolver/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..47e54513 --- /dev/null +++ b/packages/locale_resolver/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/locale_resolver/example/android/app/src/main/kotlin/eu/weblibre/locale_resolver_example/MainActivity.kt b/packages/locale_resolver/example/android/app/src/main/kotlin/eu/weblibre/locale_resolver_example/MainActivity.kt new file mode 100644 index 00000000..bb5cb26e --- /dev/null +++ b/packages/locale_resolver/example/android/app/src/main/kotlin/eu/weblibre/locale_resolver_example/MainActivity.kt @@ -0,0 +1,5 @@ +package eu.weblibre.locale_resolver_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/packages/locale_resolver/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/locale_resolver/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/packages/locale_resolver/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/locale_resolver/example/android/app/src/main/res/drawable/launch_background.xml b/packages/locale_resolver/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/packages/locale_resolver/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/locale_resolver/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/locale_resolver/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..db77bb4b Binary files /dev/null and b/packages/locale_resolver/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/locale_resolver/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/locale_resolver/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..17987b79 Binary files /dev/null and b/packages/locale_resolver/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/locale_resolver/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/locale_resolver/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..09d43914 Binary files /dev/null and b/packages/locale_resolver/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/locale_resolver/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/locale_resolver/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..d5f1c8d3 Binary files /dev/null and b/packages/locale_resolver/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/locale_resolver/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/locale_resolver/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..4d6372ee Binary files /dev/null and b/packages/locale_resolver/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/locale_resolver/example/android/app/src/main/res/values-night/styles.xml b/packages/locale_resolver/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..06952be7 --- /dev/null +++ b/packages/locale_resolver/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/locale_resolver/example/android/app/src/main/res/values/styles.xml b/packages/locale_resolver/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..cb1ef880 --- /dev/null +++ b/packages/locale_resolver/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/locale_resolver/example/android/app/src/profile/AndroidManifest.xml b/packages/locale_resolver/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/packages/locale_resolver/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/locale_resolver/example/android/build.gradle.kts b/packages/locale_resolver/example/android/build.gradle.kts new file mode 100644 index 00000000..dbee657b --- /dev/null +++ b/packages/locale_resolver/example/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/packages/locale_resolver/example/android/gradle.properties b/packages/locale_resolver/example/android/gradle.properties new file mode 100644 index 00000000..f018a618 --- /dev/null +++ b/packages/locale_resolver/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/packages/locale_resolver/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/locale_resolver/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..c6a2952d --- /dev/null +++ b/packages/locale_resolver/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip diff --git a/packages/locale_resolver/example/android/settings.gradle.kts b/packages/locale_resolver/example/android/settings.gradle.kts new file mode 100644 index 00000000..fb605bc8 --- /dev/null +++ b/packages/locale_resolver/example/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.9.1" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/packages/locale_resolver/example/lib/main.dart b/packages/locale_resolver/example/lib/main.dart new file mode 100644 index 00000000..62f11714 --- /dev/null +++ b/packages/locale_resolver/example/lib/main.dart @@ -0,0 +1,63 @@ +// import 'package:flutter/material.dart'; +// import 'dart:async'; + +// import 'package:flutter/services.dart'; +// import 'package:locale_resolver/locale_resolver.dart'; + +// void main() { +// runApp(const MyApp()); +// } + +// class MyApp extends StatefulWidget { +// const MyApp({super.key}); + +// @override +// State createState() => _MyAppState(); +// } + +// class _MyAppState extends State { +// String _platformVersion = 'Unknown'; +// final _localeResolverPlugin = LocaleResolver(); + +// @override +// void initState() { +// super.initState(); +// initPlatformState(); +// } + +// // Platform messages are asynchronous, so we initialize in an async method. +// Future initPlatformState() async { +// String platformVersion; +// // Platform messages may fail, so we use a try/catch PlatformException. +// // We also handle the message potentially returning null. +// try { +// platformVersion = +// await _localeResolverPlugin.getPlatformVersion() ?? 'Unknown platform version'; +// } on PlatformException { +// platformVersion = 'Failed to get platform version.'; +// } + +// // If the widget was removed from the tree while the asynchronous platform +// // message was in flight, we want to discard the reply rather than calling +// // setState to update our non-existent appearance. +// if (!mounted) return; + +// setState(() { +// _platformVersion = platformVersion; +// }); +// } + +// @override +// Widget build(BuildContext context) { +// return MaterialApp( +// home: Scaffold( +// appBar: AppBar( +// title: const Text('Plugin example app'), +// ), +// body: Center( +// child: Text('Running on: $_platformVersion\n'), +// ), +// ), +// ); +// } +// } diff --git a/packages/locale_resolver/example/pubspec.yaml b/packages/locale_resolver/example/pubspec.yaml new file mode 100644 index 00000000..ee6ce6a0 --- /dev/null +++ b/packages/locale_resolver/example/pubspec.yaml @@ -0,0 +1,86 @@ +name: locale_resolver_example +description: "Demonstrates how to use the locale_resolver plugin." +# The following line prevents the package from being accidentally published to +# 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 +resolution: workspace + +environment: + sdk: ^3.9.2 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + locale_resolver: + # When depending on this package from a real application you should use: + # locale_resolver: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + +dev_dependencies: + integration_test: + sdk: flutter + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^6.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # 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. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/packages/locale_resolver/lib/locale_resolver.dart b/packages/locale_resolver/lib/locale_resolver.dart new file mode 100644 index 00000000..a54e018d --- /dev/null +++ b/packages/locale_resolver/lib/locale_resolver.dart @@ -0,0 +1 @@ +export 'src/pigeons/locales.g.dart' show LocaleResolver, LocalizedResult; diff --git a/packages/locale_resolver/lib/src/pigeons/locales.g.dart b/packages/locale_resolver/lib/src/pigeons/locales.g.dart new file mode 100644 index 00000000..81f76238 --- /dev/null +++ b/packages/locale_resolver/lib/src/pigeons/locales.g.dart @@ -0,0 +1,145 @@ +// Autogenerated from Pigeon (v26.0.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} +bool _deepEquals(Object? a, Object? b) { + if (a is List && b is List) { + return a.length == b.length && + a.indexed + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + } + if (a is Map && b is Map) { + return a.length == b.length && a.entries.every((MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key])); + } + return a == b; +} + + +class LocalizedResult { + LocalizedResult({ + required this.languageName, + this.countryName, + }); + + String languageName; + + String? countryName; + + List _toList() { + return [ + languageName, + countryName, + ]; + } + + Object encode() { + return _toList(); } + + static LocalizedResult decode(Object result) { + result as List; + return LocalizedResult( + languageName: result[0]! as String, + countryName: result[1] as String?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! LocalizedResult || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is LocalizedResult) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + return LocalizedResult.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class LocaleResolver { + /// Constructor for [LocaleResolver]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + LocaleResolver({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future resolve(String languageTag, String targetLangouageTag) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.locale_resolver.LocaleResolver.resolve$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([languageTag, targetLangouageTag]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as LocalizedResult?)!; + } + } +} diff --git a/packages/locale_resolver/pigeons/locales.dart b/packages/locale_resolver/pigeons/locales.dart new file mode 100644 index 00000000..a7924f21 --- /dev/null +++ b/packages/locale_resolver/pigeons/locales.dart @@ -0,0 +1,25 @@ +import 'package:pigeon/pigeon.dart'; + +class LocalizedResult { + final String languageName; + final String? countryName; + + LocalizedResult(this.languageName, this.countryName); +} + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/src/pigeons/locales.g.dart', + dartOptions: DartOptions(), + kotlinOut: + 'android/src/main/kotlin/eu/weblibre/locale_resolver/pigeons/Locales.g.kt', + kotlinOptions: KotlinOptions( + package: 'eu.weblibre.locale_resolver.pigeons', + ), + dartPackageName: 'locale_resolver', + ), +) +@HostApi() +abstract class LocaleResolver { + LocalizedResult resolve(String languageTag, String targetLangouageTag); +} diff --git a/packages/locale_resolver/pubspec.yaml b/packages/locale_resolver/pubspec.yaml new file mode 100644 index 00000000..909c32b9 --- /dev/null +++ b/packages/locale_resolver/pubspec.yaml @@ -0,0 +1,71 @@ +name: locale_resolver +description: "A new Flutter plugin project." +version: 0.0.1 +publish_to: 'none' +resolution: workspace + +environment: + sdk: ^3.9.2 + flutter: '>=3.3.0' + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + lint: ^2.8.0 + pigeon: ^26.0.2 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: eu.weblibre.locale_resolver + pluginClass: LocaleResolverPlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/to/asset-from-package + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/to/font-from-package diff --git a/pubspec.yaml b/pubspec.yaml index 0143895e..c0d2949f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,8 @@ workspace: - packages/simple_intent_receiver - packages/simple_intent_receiver/example - packages/tor + - packages/locale_resolver + - packages/locale_resolver/example dev_dependencies: melos: ^7.2.0