Skip to content

Commit bdd7d9f

Browse files
committed
settings: Add language setting
Since there is no Figma design for the settings page yet, the design is kept simple while mostly matching zulip-mobile: we show both selfname and name of each available language option, and leave out the search funtionality. We don't allow unsetting the language once it is set, but that can easily change. Fixes: zulip#1139
1 parent 7987ae0 commit bdd7d9f

13 files changed

+173
-1
lines changed

assets/l10n/app_en.arb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,10 @@
891891
"@openLinksWithInAppBrowser": {
892892
"description": "Label for toggling setting to open links with in-app browser"
893893
},
894+
"languageSettingTitle": "Language",
895+
"@languageSettingTitle": {
896+
"description": "Title for language setting."
897+
},
894898
"languageEn": "English",
895899
"@languageEn": {
896900
"description": "Label for the English language."

lib/generated/l10n/zulip_localizations.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,6 +1313,12 @@ abstract class ZulipLocalizations {
13131313
/// **'Open links with in-app browser'**
13141314
String get openLinksWithInAppBrowser;
13151315

1316+
/// Title for language setting.
1317+
///
1318+
/// In en, this message translates to:
1319+
/// **'Language'**
1320+
String get languageSettingTitle;
1321+
13161322
/// Label for the English language.
13171323
///
13181324
/// In en, this message translates to:

lib/generated/l10n/zulip_localizations_ar.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
724724
@override
725725
String get openLinksWithInAppBrowser => 'Open links with in-app browser';
726726

727+
@override
728+
String get languageSettingTitle => 'Language';
729+
727730
@override
728731
String get languageEn => 'English';
729732

lib/generated/l10n/zulip_localizations_de.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,9 @@ class ZulipLocalizationsDe extends ZulipLocalizations {
724724
@override
725725
String get openLinksWithInAppBrowser => 'Open links with in-app browser';
726726

727+
@override
728+
String get languageSettingTitle => 'Language';
729+
727730
@override
728731
String get languageEn => 'English';
729732

lib/generated/l10n/zulip_localizations_en.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
724724
@override
725725
String get openLinksWithInAppBrowser => 'Open links with in-app browser';
726726

727+
@override
728+
String get languageSettingTitle => 'Language';
729+
727730
@override
728731
String get languageEn => 'English';
729732

lib/generated/l10n/zulip_localizations_ja.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
724724
@override
725725
String get openLinksWithInAppBrowser => 'Open links with in-app browser';
726726

727+
@override
728+
String get languageSettingTitle => 'Language';
729+
727730
@override
728731
String get languageEn => 'English';
729732

lib/generated/l10n/zulip_localizations_nb.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,9 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
724724
@override
725725
String get openLinksWithInAppBrowser => 'Open links with in-app browser';
726726

727+
@override
728+
String get languageSettingTitle => 'Language';
729+
727730
@override
728731
String get languageEn => 'English';
729732

lib/generated/l10n/zulip_localizations_pl.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
733733
@override
734734
String get openLinksWithInAppBrowser => 'Otwieraj odnośniki w aplikacji';
735735

736+
@override
737+
String get languageSettingTitle => 'Language';
738+
736739
@override
737740
String get languageEn => 'English';
738741

lib/generated/l10n/zulip_localizations_ru.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
737737
@override
738738
String get openLinksWithInAppBrowser => 'Открывать ссылки внутри приложения';
739739

740+
@override
741+
String get languageSettingTitle => 'Language';
742+
740743
@override
741744
String get languageEn => 'English';
742745

lib/generated/l10n/zulip_localizations_sk.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,9 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
726726
@override
727727
String get openLinksWithInAppBrowser => 'Open links with in-app browser';
728728

729+
@override
730+
String get languageSettingTitle => 'Language';
731+
729732
@override
730733
String get languageEn => 'English';
731734

lib/generated/l10n/zulip_localizations_uk.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,9 @@ class ZulipLocalizationsUk extends ZulipLocalizations {
737737
String get openLinksWithInAppBrowser =>
738738
'Відкривати посилання за допомогою браузера додатку';
739739

740+
@override
741+
String get languageSettingTitle => 'Language';
742+
740743
@override
741744
String get languageEn => 'English';
742745

lib/widgets/settings.dart

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import 'dart:async';
2+
13
import 'package:flutter/material.dart';
24

35
import '../generated/l10n/zulip_localizations.dart';
6+
import '../model/localizations.dart';
47
import '../model/settings.dart';
58
import 'app_bar.dart';
9+
import 'icons.dart';
610
import 'page.dart';
711
import 'store.dart';
812

@@ -17,17 +21,28 @@ class SettingsPage extends StatelessWidget {
1721
@override
1822
Widget build(BuildContext context) {
1923
final zulipLocalizations = ZulipLocalizations.of(context);
24+
25+
Widget? languageSettingSubtitle;
26+
final language = GlobalStoreWidget.settingsOf(context).language;
27+
if (language != null && kSelfnamesByLocale.containsKey(language)) {
28+
languageSettingSubtitle = Text(kSelfnamesByLocale[language]!);
29+
}
30+
2031
return Scaffold(
2132
appBar: ZulipAppBar(
2233
title: Text(zulipLocalizations.settingsPageTitle)),
2334
body: Column(children: [
2435
const _ThemeSetting(),
2536
const _BrowserPreferenceSetting(),
37+
ListTile(
38+
title: Text(zulipLocalizations.languageSettingTitle),
39+
subtitle: languageSettingSubtitle,
40+
onTap: () => Navigator.push(context, _LanguagePage.buildRoute())),
2641
if (GlobalSettingsStore.experimentalFeatureFlags.isNotEmpty)
2742
ListTile(
2843
title: Text(zulipLocalizations.experimentalFeatureSettingsPageTitle),
2944
onTap: () => Navigator.push(context,
30-
ExperimentalFeaturesPage.buildRoute()))
45+
ExperimentalFeaturesPage.buildRoute())),
3146
]));
3247
}
3348
}
@@ -82,6 +97,55 @@ class _BrowserPreferenceSetting extends StatelessWidget {
8297
}
8398
}
8499

100+
class _LanguagePage extends StatelessWidget {
101+
const _LanguagePage();
102+
103+
static WidgetRoute<void> buildRoute() {
104+
return MaterialWidgetRoute(page: const _LanguagePage());
105+
}
106+
107+
@override
108+
Widget build(BuildContext context) {
109+
final zulipLocalizations = ZulipLocalizations.of(context);
110+
return Scaffold(
111+
appBar: AppBar(
112+
title: Text(zulipLocalizations.languageSettingTitle)),
113+
body: SingleChildScrollView(
114+
child: Column(children: [
115+
for (final language in zulipLocalizations.languages())
116+
_LanguageItem(language: language),
117+
])));
118+
}
119+
}
120+
121+
class _LanguageItem extends StatelessWidget {
122+
const _LanguageItem({required this.language});
123+
124+
/// The [Language] this corresponds to, from [ZulipLocalizations.languages].
125+
final Language language;
126+
127+
@override
128+
Widget build(BuildContext context) {
129+
final (locale, selfname, displayName) = language;
130+
final isCurrentLanguageInSettings =
131+
locale == GlobalStoreWidget.settingsOf(context).language;
132+
133+
return ListTile(
134+
title: Text(selfname),
135+
subtitle: Text(
136+
isCurrentLanguageInSettings
137+
? // Make sure the subtitle text is consistent to the title — since
138+
// displayName (decided by translators) can be different from our
139+
// hard-coded selfname when isCurrentLanguage is true.
140+
selfname
141+
: displayName),
142+
trailing: isCurrentLanguageInSettings ? Icon(ZulipIcons.check) : null,
143+
onTap: () {
144+
unawaited(GlobalStoreWidget.settingsOf(context).setLanguage(locale));
145+
});
146+
}
147+
}
148+
85149
class ExperimentalFeaturesPage extends StatelessWidget {
86150
const ExperimentalFeaturesPage({super.key});
87151

test/widgets/settings_test.dart

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import 'package:checks/checks.dart';
22
import 'package:flutter/foundation.dart';
33
import 'package:flutter/material.dart';
4+
import 'package:flutter_checks/flutter_checks.dart';
45
import 'package:flutter_test/flutter_test.dart';
56
import 'package:zulip/model/settings.dart';
7+
import 'package:zulip/widgets/icons.dart';
68
import 'package:zulip/widgets/settings.dart';
79

810
import '../flutter_checks.dart';
@@ -127,6 +129,75 @@ void main() {
127129
}, variant: TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
128130
});
129131

132+
group('language setting', () {
133+
Finder languageListTileFinder = find.ancestor(
134+
of: find.text('Language'), matching: find.byType(ListTile));
135+
136+
Subject<Locale> checkAmbientLocale(WidgetTester tester) =>
137+
check(Localizations.localeOf(tester.element(find.byType(SettingsPage))));
138+
139+
testWidgets('on SettingsPage, when no language is set', (tester) async {
140+
await prepare(tester);
141+
checkAmbientLocale(tester).equals(const Locale('en'));
142+
143+
assert(testBinding.globalStore.settings.language == null);
144+
await tester.pump();
145+
check(languageListTileFinder).findsOne();
146+
check(find.text('English')).findsNothing();
147+
});
148+
149+
testWidgets('on SettingsPage, when a language is set', (tester) async {
150+
await prepare(tester);
151+
checkAmbientLocale(tester).equals(const Locale('en'));
152+
153+
await testBinding.globalStore.settings.setLanguage(const Locale('en'));
154+
await tester.pump();
155+
check(find.descendant(
156+
of: languageListTileFinder, matching: find.text('English'))).findsOne();
157+
});
158+
159+
testWidgets('LanguagePage smoke', (tester) async {
160+
await prepare(tester);
161+
await tester.tap(languageListTileFinder);
162+
await tester.pump();
163+
await tester.pump();
164+
check(find.text('Polski').hitTestable()).findsOne();
165+
check(find.text('Polish')).findsOne();
166+
check(find.byIcon(ZulipIcons.check)).findsNothing();
167+
checkAmbientLocale(tester).equals(const Locale('en'));
168+
check(testBinding.globalStore).settings.language.isNull();
169+
170+
await tester.tap(find.text('Polish'));
171+
await tester.pump();
172+
check(find.text('Polski').hitTestable()).findsExactly(2);
173+
check(find.text('Polish')).findsNothing();
174+
check(find.descendant(
175+
of: find.widgetWithText(ListTile, 'Polski'),
176+
matching: find.byIcon(ZulipIcons.check)),
177+
).findsOne();
178+
checkAmbientLocale(tester).equals(const Locale('pl'));
179+
check(testBinding.globalStore).settings.language.equals(const Locale('pl'));
180+
});
181+
182+
testWidgets('handle unsupported (but valid) locale stored in database', (tester) async {
183+
await prepare(tester);
184+
// https://www.loc.gov/standards/iso639-2/php/code_list.php
185+
await testBinding.globalStore.settings.setLanguage(const Locale('zxx'));
186+
await tester.pumpAndSettle(); // expect no errors
187+
checkAmbientLocale(tester).equals(const Locale('en'));
188+
189+
await tester.tap(languageListTileFinder);
190+
await tester.pump();
191+
await tester.pump();
192+
check(find.byIcon(ZulipIcons.check)).findsNothing();
193+
194+
await tester.tap(find.text('Polish'));
195+
await tester.pump();
196+
checkAmbientLocale(tester).equals(const Locale('pl'));
197+
check(testBinding.globalStore).settings.language.equals(const Locale('pl'));
198+
});
199+
});
200+
130201
// TODO maybe test GlobalSettingType.experimentalFeatureFlag settings
131202
// Or maybe not; after all, it's a developer-facing feature, so
132203
// should be low risk.

0 commit comments

Comments
 (0)