Skip to content

Commit

Permalink
Add scaffold for localizations (l10n) in Sharezone (#1798)
Browse files Browse the repository at this point in the history
This pull request introduces a new package, sharezone_localizations,
which provides a streamlined way of managing and accessing localized
strings in the Sharezone-App using Flutter’s internationalization
features. Below is an overview of the core additions:
	1.	New Package: sharezone_localizations
• README Documentation: Explains how to add, generate, and use
translatable strings.
• Internationalization Setup: Based on Flutter’s gen-l10n tooling,
enabling multiple locales and seamless code generation.
	2.	Locale Management
	•	`AppLocaleProvider`: Facilitates dynamic switching of app locales.
	3.	Usage Guidelines
• String Access: Via the context.sl extension, which simplifies
referencing keys from the .arb files.
• Adding/Updating Strings: Detailed steps on modifying .arb files,
including placeholder usage and recommended auto-translation tools.
	4.	Generating Localizations
• Flutter gen-l10n Command: Simple CLI approach to regenerate
localizations after .arb updates.
• VS Code Task: Option to run a dedicated VS Code Task (“Generate l10n
for sharezone_localizations”) for developers who prefer an IDE-based
workflow.

Overall, this package consolidates translation logic, enhances
maintainability by centralizing locale management, and simplifies how
developers interact with localized strings.

<img width="1552" alt="image"
src="https://github.com/user-attachments/assets/39c625b4-1f5d-437f-b1fb-3b6d2f81511a"
/>

Things, we need to do in future pull requests:
* #1799
* Write tests for language page
* Store locale in user document (the backend have to know the language
to send push notifications in the user's language)
* Replace hard-coded strings with l10n strings
* Remove old l10n files in `/app/l10n/`
* Document how to add new languages (copy `.arb` file, use
[`arb_translate`](https://pub.dev/packages/arb_translate), makes native
changes - `Info.plist` needs changed)
* Make sure that when system language is French, English is selected as
default


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

- **New Features**
	- Introduced a language selection feature in the settings.
	- Added localization support for German and English languages.
- Implemented a `LanguagePage` for users to choose their preferred
language.
	- Enhanced localization management with a new `FeatureFlagl10n` class.
	- Integrated dynamic locale adjustment based on user preferences.
- Added a new `AppLocaleProvider` for managing application locale
settings.
- Established a structured localization framework with
`SharezoneLocalizations`.
- **Bug Fixes**
	- Enhanced state management for localization features.
- **Documentation**
- Added a comprehensive README for the `sharezone_localizations`
package.
- **Chores**
	- Updated `.gitignore` to manage ignored files more effectively.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Part of #346

---------

Co-authored-by: nilsreichardt <me@nils.re>
  • Loading branch information
flowhorn and nilsreichardt authored Dec 29, 2024
1 parent 01368dc commit 1b8aa06
Show file tree
Hide file tree
Showing 32 changed files with 1,158 additions and 23 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ coverage/
.vscode/*
!.vscode/launch.json
!.vscode/settings.json
!.vscode/tasks.json

# FVM will create a relative symlink in your project from .fvm/versions/ to
# the cache of the selected version. We should add this to our .gitignore.
Expand Down
22 changes: 22 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
// This task generates the l10n files for the
// sharezone_localizations.
//
// FVM is required to run this task.
"label": "Generate l10n for sharezone_localizations",
"type": "shell",
// Additionally, we add the license header again (the "flutter
// gen-l10n" always removes the license header).
"command": "fvm flutter gen-l10n && addlicense -c \"Sharezone UG (haftungsbeschränkt)\" -f ../../header_template.txt .",
"options": {
"cwd": "${workspaceFolder}/lib/sharezone_localizations"
},
"problemMatcher": []
}
]
}
20 changes: 20 additions & 0 deletions app/lib/activation_code/src/bloc/enter_activation_code_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import 'package:flutter/material.dart';
import 'package:key_value_store/key_value_store.dart';
import 'package:rxdart/rxdart.dart';
import 'package:helper_functions/helper_functions.dart';
import 'package:sharezone/l10n/feature_flag_l10n.dart';

import '../models/enter_activation_code_result.dart';
import 'enter_activation_code_activator.dart';
Expand All @@ -26,6 +27,7 @@ class EnterActivationCodeBloc extends BlocBase {
final _enterActivationCodeSubject =
BehaviorSubject<EnterActivationCodeResult>();
final KeyValueStore keyValueStore;
final FeatureFlagl10n featureFlagl10n;

String? _lastEnteredValue;

Expand All @@ -34,6 +36,7 @@ class EnterActivationCodeBloc extends BlocBase {
this.crashAnalytics,
this.appFunctions,
this.keyValueStore,
this.featureFlagl10n,
) {
_changeEnterActivationCodeResult(NoDataEnterActivationCodeResult());
}
Expand Down Expand Up @@ -89,6 +92,11 @@ class EnterActivationCodeBloc extends BlocBase {
return;
}

if (_lastEnteredValue?.trim().toLowerCase() == 'l10n') {
_togglel10nFeatureFlag();
return;
}

_changeEnterActivationCodeResult(LoadingEnterActivationCodeResult());

final enterActivationCodeResult = await _runAppFunction(enteredValue);
Expand All @@ -107,6 +115,18 @@ class EnterActivationCodeBloc extends BlocBase {
);
}

void _togglel10nFeatureFlag() {
final currentValue = featureFlagl10n.isl10nEnabled;
featureFlagl10n.toggle();

_changeEnterActivationCodeResult(
SuccessfulEnterActivationCodeResult(
'l10n',
'l10n wurde ${!currentValue ? 'aktiviert' : 'deaktiviert'}. Starte die App neu, um die Änderungen zu sehen.',
),
);
}

Future<void> _clearCache(BuildContext context) async {
await Future.wait([
keyValueStore.clear(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,21 @@ import 'package:bloc_base/bloc_base.dart';
import 'package:crash_analytics/crash_analytics.dart';
import 'package:key_value_store/key_value_store.dart';
import 'package:sharezone/activation_code/src/bloc/enter_activation_code_bloc.dart';
import 'package:sharezone/l10n/feature_flag_l10n.dart';

class EnterActivationCodeBlocFactory extends BlocBase {
final CrashAnalytics crashAnalytics;
final Analytics analytics;
final SharezoneAppFunctions appFunctions;
final KeyValueStore keyValueStore;
final FeatureFlagl10n featureFlagl10n;

EnterActivationCodeBlocFactory({
required this.analytics,
required this.crashAnalytics,
required this.appFunctions,
required this.keyValueStore,
required this.featureFlagl10n,
});

EnterActivationCodeBloc createBloc() {
Expand All @@ -32,6 +35,7 @@ class EnterActivationCodeBlocFactory extends BlocBase {
crashAnalytics,
appFunctions,
keyValueStore,
featureFlagl10n,
);
}

Expand Down
42 changes: 42 additions & 0 deletions app/lib/l10n/feature_flag_l10n.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) 2024 Sharezone UG (haftungsbeschränkt)
// Licensed under the EUPL-1.2-or-later.
//
// You may obtain a copy of the Licence at:
// https://joinup.ec.europa.eu/software/page/eupl
//
// SPDX-License-Identifier: EUPL-1.2

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:sharezone/util/cache/streaming_key_value_store.dart';

class FeatureFlagl10n extends ChangeNotifier {
FeatureFlagl10n(this.keyValueStore) {
_subscription = keyValueStore
.getBool('l10n_enabled', defaultValue: false)
.listen((event) {
final newValue = event == true;
if (isl10nEnabled != newValue) {
isl10nEnabled = newValue;
notifyListeners();
}
});
}

final StreamingKeyValueStore keyValueStore;
late StreamSubscription<bool> _subscription;
bool isl10nEnabled = false;

void toggle() {
isl10nEnabled = !isl10nEnabled;
keyValueStore.setBool('l10n_enabled', isl10nEnabled);
notifyListeners();
}

@override
void dispose() {
_subscription.cancel();
super.dispose();
}
}
42 changes: 42 additions & 0 deletions app/lib/l10n/flutter_app_local_gateway.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) 2024 Sharezone UG (haftungsbeschränkt)
// Licensed under the EUPL-1.2-or-later.
//
// You may obtain a copy of the Licence at:
// https://joinup.ec.europa.eu/software/page/eupl
//
// SPDX-License-Identifier: EUPL-1.2

import 'dart:convert';

import 'package:sharezone/l10n/feature_flag_l10n.dart';
import 'package:sharezone/util/cache/streaming_key_value_store.dart';
import 'package:sharezone_localizations/sharezone_localizations.dart';

class FlutterAppLocaleProviderGateway extends AppLocaleProviderGateway {
const FlutterAppLocaleProviderGateway({
required this.keyValueStore,
required this.featureFlagl10n,
});

final FeatureFlagl10n featureFlagl10n;
final StreamingKeyValueStore keyValueStore;

@override
Stream<AppLocale> getLocale() {
final defaultValue = jsonEncode(featureFlagl10n.isl10nEnabled
? AppLocale.system.toMap()
: AppLocale.en.toMap());
return keyValueStore
.getString('locale', defaultValue: defaultValue)
.map((event) => AppLocale.fromMap(jsonDecode(event)));
}

@override
Future<void> setLocale(AppLocale locale) async {
final value = jsonEncode(locale.toMap());
keyValueStore.setString(
'locale',
value,
);
}
}
20 changes: 20 additions & 0 deletions app/lib/main/sharezone.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,20 @@ import 'package:provider/provider.dart';
import 'package:sharezone/dynamic_links/beitrittsversuch.dart';
import 'package:sharezone/dynamic_links/dynamic_link_bloc.dart';
import 'package:sharezone/dynamic_links/dynamic_links.dart';
import 'package:sharezone/l10n/feature_flag_l10n.dart';
import 'package:sharezone/l10n/flutter_app_local_gateway.dart';
import 'package:sharezone/main/auth_app.dart';
import 'package:sharezone/main/bloc_dependencies.dart';
import 'package:sharezone/main/sharezone_app.dart';
import 'package:sharezone/main/sharezone_bloc_providers.dart';
import 'package:sharezone/navigation/logic/navigation_bloc.dart';
import 'package:sharezone/notifications/notifications_permission.dart';
import 'package:sharezone/onboarding/group_onboarding/logic/signed_up_bloc.dart';
import 'package:sharezone/util/cache/streaming_key_value_store.dart';
import 'package:sharezone/util/flavor.dart';
import 'package:sharezone/widgets/animation/color_fade_in.dart';
import 'package:sharezone/widgets/development_stage_banner.dart';
import 'package:sharezone_localizations/sharezone_localizations.dart';
import 'package:sharezone_utils/device_information_manager.dart';
import 'package:sharezone_widgets/sharezone_widgets.dart';

Expand Down Expand Up @@ -72,6 +76,8 @@ class Sharezone extends StatefulWidget {
class _SharezoneState extends State<Sharezone> with WidgetsBindingObserver {
late SignUpBloc signUpBloc;
late StreamSubscription<AuthUser?> authSubscription;
late StreamingKeyValueStore streamingKeyValueStore;
late FeatureFlagl10n featureFlagl10n;

@override
void initState() {
Expand All @@ -93,6 +99,11 @@ class _SharezoneState extends State<Sharezone> with WidgetsBindingObserver {
authSubscription = listenToAuthStateChanged().listen((user) {
authUserSubject.sink.add(user);
});

streamingKeyValueStore = FlutterStreamingKeyValueStore(
widget.blocDependencies.streamingSharedPreferences,
);
featureFlagl10n = FeatureFlagl10n(streamingKeyValueStore);
}

void logAppOpen() {
Expand Down Expand Up @@ -129,6 +140,15 @@ class _SharezoneState extends State<Sharezone> with WidgetsBindingObserver {
MobileDeviceInformationRetriever(),
),
),
ChangeNotifierProvider.value(value: featureFlagl10n),
ChangeNotifierProvider(
create: (context) => AppLocaleProvider(
gateway: FlutterAppLocaleProviderGateway(
keyValueStore: streamingKeyValueStore,
featureFlagl10n: featureFlagl10n,
),
),
),
],
child: MultiBlocProvider(
blocProviders: [
Expand Down
2 changes: 2 additions & 0 deletions app/lib/main/sharezone_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import 'package:sharezone/settings/settings_page.dart';
import 'package:sharezone/settings/src/subpages/about/about_page.dart';
import 'package:sharezone/settings/src/subpages/changelog_page.dart';
import 'package:sharezone/settings/src/subpages/imprint/page/imprint_page.dart';
import 'package:sharezone/settings/src/subpages/language/language_page.dart';
import 'package:sharezone/settings/src/subpages/my_profile/change_email.dart';
import 'package:sharezone/settings/src/subpages/my_profile/change_password.dart';
import 'package:sharezone/settings/src/subpages/my_profile/change_state.dart';
Expand Down Expand Up @@ -204,6 +205,7 @@ class _SharezoneAppState extends State<SharezoneApp>
ICalLinksDialog.tag: (context) => const ICalLinksDialog(),
CreateTermPage.tag: (context) => const CreateTermPage(),
GradesDialog.tag: (context) => const GradesDialog(),
LanguagePage.tag: (context) => const LanguagePage(),
},
navigatorKey: navigationService.navigatorKey,
),
Expand Down
4 changes: 3 additions & 1 deletion app/lib/main/sharezone_bloc_providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import 'package:sharezone/ical_links/dialog/ical_links_dialog_controller_factory
import 'package:sharezone/ical_links/list/ical_links_page_controller.dart';
import 'package:sharezone/ical_links/shared/ical_link_analytics.dart';
import 'package:sharezone/ical_links/shared/ical_links_gateway.dart';
import 'package:sharezone/l10n/feature_flag_l10n.dart';
import 'package:sharezone/main/application_bloc.dart';
import 'package:sharezone/main/bloc_dependencies.dart';
import 'package:sharezone/main/onboarding/onboarding_navigator.dart';
Expand Down Expand Up @@ -497,7 +498,7 @@ class _SharezoneBlocProvidersState extends State<SharezoneBlocProviders> {
),
lazy: false,
),
Provider<KeyValueStore>.value(value: keyValueStore)
Provider<KeyValueStore>.value(value: keyValueStore),
];

mainBlocProviders = <BlocProvider>[
Expand Down Expand Up @@ -593,6 +594,7 @@ class _SharezoneBlocProvidersState extends State<SharezoneBlocProviders> {
analytics: analytics,
appFunctions: api.references.functions,
keyValueStore: widget.blocDependencies.keyValueStore,
featureFlagl10n: context.read<FeatureFlagl10n>(),
),
),
BlocProvider<NotificationsBlocFactory>(
Expand Down
16 changes: 5 additions & 11 deletions app/lib/main/sharezone_material_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
import 'package:analytics/analytics.dart';
import 'package:analytics/observer.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:platform_check/platform_check.dart';
import 'package:provider/provider.dart';
import 'package:sharezone/main/bloc_dependencies.dart';
import 'package:sharezone_localizations/sharezone_localizations.dart';
import 'package:sharezone_widgets/sharezone_widgets.dart';

class SharezoneMaterialApp extends StatelessWidget {
Expand All @@ -35,7 +35,7 @@ class SharezoneMaterialApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final themeSettings = context.watch<ThemeSettings>();

final localProvider = context.watch<AppLocaleProvider>();
return MaterialApp(
debugShowCheckedModeBanner: false,
title: PlatformCheck.isWeb ? "Sharezone Web-App" : "Sharezone",
Expand All @@ -45,15 +45,9 @@ class SharezoneMaterialApp extends StatelessWidget {
theme: getLightTheme().copyWith(
visualDensity: themeSettings.visualDensitySetting.visualDensity),
themeMode: _getThemeMode(themeSettings.themeBrightness),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('en', 'US'),
Locale('de', 'DE'),
],
localizationsDelegates: SharezoneLocalizations.localizationsDelegates,
supportedLocales: SharezoneLocalizations.supportedLocales,
locale: localProvider.locale.toLocale(),
navigatorObservers: <NavigatorObserver>[
AnalyticsNavigationObserver(analytics: analytics)
],
Expand Down
Loading

0 comments on commit 1b8aa06

Please sign in to comment.