Skip to content

Refactor improve localizations #30

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 71 additions & 30 deletions lib/app_configuration/view/app_configuration_page.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:ht_dashboard/app_configuration/bloc/app_configuration_bloc.dart';
import 'package:ht_dashboard/l10n/app_localizations.dart';
import 'package:ht_dashboard/l10n/l10n.dart';
import 'package:ht_dashboard/shared/constants/app_spacing.dart';
import 'package:ht_dashboard/shared/widgets/widgets.dart';
Expand Down Expand Up @@ -766,15 +767,14 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> {
@override
Widget build(BuildContext context) {
final userPreferenceConfig = widget.remoteConfig.userPreferenceConfig;
final l10n = context.l10n;

return Column(
children: [
widget.buildIntField(
context,
label: 'Followed Items Limit',
description:
'Maximum number of countries, news sources, or categories this '
'user role can follow (each type has its own limit).',
label: _getFollowedItemsLimitLabel(l10n),
description: _getFollowedItemsLimitDescription(l10n),
value: _getFollowedItemsLimit(userPreferenceConfig),
onChanged: (value) {
widget.onConfigChanged(
Expand All @@ -790,8 +790,8 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> {
),
widget.buildIntField(
context,
label: 'Saved Headlines Limit',
description: 'Maximum number of headlines this user role can save.',
label: _getSavedHeadlinesLimitLabel(l10n),
description: _getSavedHeadlinesLimitDescription(l10n),
value: _getSavedHeadlinesLimit(userPreferenceConfig),
onChanged: (value) {
widget.onConfigChanged(
Expand All @@ -809,6 +809,50 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> {
);
}

String _getFollowedItemsLimitLabel(AppLocalizations l10n) {
switch (widget.userRole) {
case AppUserRole.guestUser:
return l10n.guestFollowedItemsLimitLabel;
case AppUserRole.standardUser:
return l10n.standardUserFollowedItemsLimitLabel;
case AppUserRole.premiumUser:
return l10n.premiumFollowedItemsLimitLabel;
}
}

String _getFollowedItemsLimitDescription(AppLocalizations l10n) {
switch (widget.userRole) {
case AppUserRole.guestUser:
return l10n.guestFollowedItemsLimitDescription;
case AppUserRole.standardUser:
return l10n.standardUserFollowedItemsLimitDescription;
case AppUserRole.premiumUser:
return l10n.premiumFollowedItemsLimitDescription;
}
}

String _getSavedHeadlinesLimitLabel(AppLocalizations l10n) {
switch (widget.userRole) {
case AppUserRole.guestUser:
return l10n.guestSavedHeadlinesLimitLabel;
case AppUserRole.standardUser:
return l10n.standardUserSavedHeadlinesLimitLabel;
case AppUserRole.premiumUser:
return l10n.premiumSavedHeadlinesLimitLabel;
}
}

String _getSavedHeadlinesLimitDescription(AppLocalizations l10n) {
switch (widget.userRole) {
case AppUserRole.guestUser:
return l10n.guestSavedHeadlinesLimitDescription;
case AppUserRole.standardUser:
return l10n.standardUserSavedHeadlinesLimitDescription;
case AppUserRole.premiumUser:
return l10n.premiumSavedHeadlinesLimitDescription;
}
}
Comment on lines +812 to +854

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These four methods are very repetitive, each containing a similar switch statement. This leads to code duplication and makes maintenance harder, especially if new user roles are added in the future.

A more declarative and maintainable approach would be to use Maps to associate user roles with their corresponding localization functions. You could replace these four methods with static maps inside the _UserPreferenceLimitsFormState class.

For example:

  static final _followedItemsLimitLabels = <AppUserRole, String Function(AppLocalizations)>{
    AppUserRole.guestUser: (l10n) => l10n.guestFollowedItemsLimitLabel,
    AppUserRole.standardUser: (l10n) => l10n.standardUserFollowedItemsLimitLabel,
    AppUserRole.premiumUser: (l10n) => l10n.premiumFollowedItemsLimitLabel,
  };

  // ... and similar maps for descriptions and saved headlines.

Then, in your build method, you would access the localized strings like this:

// For followed items limit
label: _followedItemsLimitLabels[widget.userRole]!(l10n),
description: _followedItemsLimitDescriptions[widget.userRole]!(l10n),

// For saved headlines limit
label: _savedHeadlinesLimitLabels[widget.userRole]!(l10n),
description: _savedHeadlinesLimitDescriptions[widget.userRole]!(l10n),

This refactoring would significantly reduce code duplication and improve the code's readability and maintainability.


int _getFollowedItemsLimit(UserPreferenceConfig config) {
switch (widget.userRole) {
case AppUserRole.guestUser:
Expand Down Expand Up @@ -990,15 +1034,14 @@ class _AdConfigFormState extends State<_AdConfigForm> {
@override
Widget build(BuildContext context) {
final adConfig = widget.remoteConfig.adConfig;
final l10n = context.l10n;

return Column(
children: [
widget.buildIntField(
context,
label: 'Ad Frequency',
description:
'How often an ad can appear for this user role (e.g., a value '
'of 5 means an ad could be placed after every 5 news items).',
label: l10n.adFrequencyLabel,
description: l10n.adFrequencyDescription,
value: _getAdFrequency(adConfig),
onChanged: (value) {
widget.onConfigChanged(
Expand All @@ -1011,10 +1054,8 @@ class _AdConfigFormState extends State<_AdConfigForm> {
),
widget.buildIntField(
context,
label: 'Ad Placement Interval',
description:
'Minimum number of news items that must be shown before the '
'very first ad appears for this user role.',
label: l10n.adPlacementIntervalLabel,
description: l10n.adPlacementIntervalDescription,
value: _getAdPlacementInterval(adConfig),
onChanged: (value) {
widget.onConfigChanged(
Expand All @@ -1027,10 +1068,8 @@ class _AdConfigFormState extends State<_AdConfigForm> {
),
widget.buildIntField(
context,
label: 'Articles Before Interstitial Ads',
description:
'Number of articles this user role needs to read before a '
'full-screen interstitial ad is shown.',
label: l10n.articlesBeforeInterstitialAdsLabel,
description: l10n.articlesBeforeInterstitialAdsDescription,
value: _getArticlesBeforeInterstitial(adConfig),
onChanged: (value) {
widget.onConfigChanged(
Expand Down Expand Up @@ -1198,27 +1237,29 @@ class _AccountActionConfigFormState extends State<_AccountActionConfigForm> {
super.dispose();
}

String _formatLabel(String enumName) {
// Converts camelCase to Title Case
final spaced = enumName.replaceAllMapped(
RegExp('([A-Z])'),
(match) => ' ${match.group(1)}',
);
return '${spaced[0].toUpperCase()}${spaced.substring(1)} Days';
}

@override
Widget build(BuildContext context) {
final accountActionConfig = widget.remoteConfig.accountActionConfig;
final relevantActionTypes = _getDaysMap(accountActionConfig).keys.toList();
final l10n = context.l10n;

return Column(
children: relevantActionTypes.map((actionType) {
final localizedActionType = switch (actionType) {
FeedActionType.linkAccount => l10n.feedActionTypeLinkAccount,
FeedActionType.rateApp => l10n.feedActionTypeRateApp,
FeedActionType.followTopics => l10n.feedActionTypeFollowTopics,
FeedActionType.followSources => l10n.feedActionTypeFollowSources,
FeedActionType.upgrade => l10n.feedActionTypeUpgrade,
FeedActionType.enableNotifications =>
l10n.feedActionTypeEnableNotifications,
};
return widget.buildIntField(
context,
label: _formatLabel(actionType.name),
description:
'Minimum number of days before showing the ${actionType.name} prompt.',
label: '$localizedActionType ${l10n.daysSuffix}',
description: l10n.daysBetweenPromptDescription(
localizedActionType,
),
value: _getDaysMap(accountActionConfig)[actionType] ?? 0,
onChanged: (value) {
final currentMap = _getDaysMap(accountActionConfig);
Expand Down
118 changes: 113 additions & 5 deletions lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -716,11 +716,11 @@ abstract class AppLocalizations {
/// **'Android Store URL'**
String get androidStoreUrlLabel;

/// Description for Android Store URL
/// Description for Android Update URL
///
/// In en, this message translates to:
/// **'URL to the app on the Google Play Store.'**
String get androidStoreUrlDescription;
String get androidUpdateUrlDescription;

/// Label for Guest Days Between In-App Prompts
///
Expand Down Expand Up @@ -1496,11 +1496,119 @@ abstract class AppLocalizations {
/// **'Android Update URL'**
String get androidUpdateUrlLabel;

/// Description for Android Update URL
/// Label for Followed Items Limit
///
/// In en, this message translates to:
/// **'URL for Android app updates.'**
String get androidUpdateUrlDescription;
/// **'Followed Items Limit'**
String get followedItemsLimitLabel;

/// Description for Followed Items Limit
///
/// In en, this message translates to:
/// **'Maximum number of countries, news sources, or categories this user role can follow (each type has its own limit).'**
String get followedItemsLimitDescription;

/// Label for Saved Headlines Limit
///
/// In en, this message translates to:
/// **'Saved Headlines Limit'**
String get savedHeadlinesLimitLabel;

/// Description for Saved Headlines Limit
///
/// In en, this message translates to:
/// **'Maximum number of headlines this user role can save.'**
String get savedHeadlinesLimitDescription;

/// Label for Ad Frequency
///
/// In en, this message translates to:
/// **'Ad Frequency'**
String get adFrequencyLabel;

/// Description for Ad Frequency
///
/// In en, this message translates to:
/// **'How often an ad can appear for this user role (e.g., a value of 5 means an ad could be placed after every 5 news items).'**
String get adFrequencyDescription;

/// Label for Ad Placement Interval
///
/// In en, this message translates to:
/// **'Ad Placement Interval'**
String get adPlacementIntervalLabel;

/// Description for Ad Placement Interval
///
/// In en, this message translates to:
/// **'Minimum number of news items that must be shown before the very first ad appears for this user role.'**
String get adPlacementIntervalDescription;

/// Label for Articles Before Interstitial Ads
///
/// In en, this message translates to:
/// **'Articles Before Interstitial Ads'**
String get articlesBeforeInterstitialAdsLabel;

/// Description for Articles Before Interstitial Ads
///
/// In en, this message translates to:
/// **'Number of articles this user role needs to read before a full-screen interstitial ad is shown.'**
String get articlesBeforeInterstitialAdsDescription;

/// Suffix for number of days in prompt descriptions
///
/// In en, this message translates to:
/// **'Days'**
String get daysSuffix;

/// Description for days between in-app prompts
///
/// In en, this message translates to:
/// **'Minimum number of days before showing the {actionType} prompt.'**
String daysBetweenPromptDescription(String actionType);

/// Text for the retry button
///
/// In en, this message translates to:
/// **'Retry'**
String get retryButtonText;

/// Feed action type for linking an account
///
/// In en, this message translates to:
/// **'Link Account'**
String get feedActionTypeLinkAccount;

/// Feed action type for rating the app
///
/// In en, this message translates to:
/// **'Rate App'**
String get feedActionTypeRateApp;

/// Feed action type for following topics
///
/// In en, this message translates to:
/// **'Follow Topics'**
String get feedActionTypeFollowTopics;

/// Feed action type for following sources
///
/// In en, this message translates to:
/// **'Follow Sources'**
String get feedActionTypeFollowSources;

/// Feed action type for upgrading
///
/// In en, this message translates to:
/// **'Upgrade'**
String get feedActionTypeUpgrade;

/// Feed action type for enabling notifications
///
/// In en, this message translates to:
/// **'Enable Notifications'**
String get feedActionTypeEnableNotifications;
}

class _AppLocalizationsDelegate
Expand Down
67 changes: 64 additions & 3 deletions lib/l10n/app_localizations_ar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -371,8 +371,7 @@ class AppLocalizationsAr extends AppLocalizations {
String get androidStoreUrlLabel => 'رابط متجر Android';

@override
String get androidStoreUrlDescription =>
'رابط التطبيق على متجر Google Play Store.';
String get androidUpdateUrlDescription => 'رابط تحديثات تطبيق Android.';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There's an inconsistency between the English and Arabic localizations for androidUpdateUrlDescription. The English version was updated to use the description for the Google Play Store URL ("URL to the app on the Google Play Store."), but the Arabic version still uses the old text ("رابط تحديثات تطبيق Android.", which translates to "URL for Android app updates.").

To ensure consistency, the Arabic translation should also refer to the Google Play Store URL.

  String get androidUpdateUrlDescription => 'رابط التطبيق على متجر Google Play Store.';


@override
String get guestDaysBetweenInAppPromptsLabel =>
Expand Down Expand Up @@ -785,5 +784,67 @@ class AppLocalizationsAr extends AppLocalizations {
String get androidUpdateUrlLabel => 'رابط تحديث Android';

@override
String get androidUpdateUrlDescription => 'رابط تحديثات تطبيق Android.';
String get followedItemsLimitLabel => 'حد العناصر المتابعة';

@override
String get followedItemsLimitDescription =>
'الحد الأقصى لعدد البلدان أو مصادر الأخبار أو المواضيع التي يمكن لهذا الدور المستخدم متابعتها (لكل نوع حد خاص به).';

@override
String get savedHeadlinesLimitLabel => 'حد العناوين المحفوظة';

@override
String get savedHeadlinesLimitDescription =>
'الحد الأقصى لعدد العناوين الرئيسية التي يمكن لهذا الدور المستخدم حفظها.';

@override
String get adFrequencyLabel => 'تكرار الإعلان';

@override
String get adFrequencyDescription =>
'عدد مرات ظهور الإعلان لهذا الدور المستخدم (على سبيل المثال، قيمة 5 تعني أنه يمكن وضع إعلان بعد كل 5 عناصر إخبارية).';

@override
String get adPlacementIntervalLabel => 'فترة وضع الإعلان';

@override
String get adPlacementIntervalDescription =>
'الحد الأدنى لعدد عناصر الأخبار التي يجب عرضها قبل ظهور الإعلان الأول لهذا الدور المستخدم.';

@override
String get articlesBeforeInterstitialAdsLabel =>
'مقالات قبل الإعلانات البينية';

@override
String get articlesBeforeInterstitialAdsDescription =>
'عدد المقالات التي يحتاج هذا الدور المستخدم لقراءتها قبل عرض إعلان بيني بملء الشاشة.';

@override
String get daysSuffix => 'أيام';

@override
String daysBetweenPromptDescription(String actionType) {
return 'الحد الأدنى لعدد الأيام قبل عرض تنبيه $actionType.';
}

@override
String get retryButtonText => 'إعادة المحاولة';

@override
String get feedActionTypeLinkAccount => 'ربط الحساب';

@override
String get feedActionTypeRateApp => 'تقييم التطبيق';

@override
String get feedActionTypeFollowTopics => 'متابعة المواضيع';

@override
String get feedActionTypeFollowSources => 'متابعة المصادر';

@override
String get feedActionTypeUpgrade => 'ترقية';

@override
String get feedActionTypeEnableNotifications => 'تفعيل الإشعارات';
}
Loading
Loading