Skip to content

Commit 9175db3

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: #1139
1 parent eb18e3b commit 9175db3

13 files changed

+183
-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: 75 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? subtitle;
26+
final language = GlobalStoreWidget.settingsOf(context).language;
27+
if (language != null && kSelfnamesByLocale.containsKey(language)) {
28+
subtitle = 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: subtitle,
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,65 @@ 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 MapEntry(:key, :value) in kSelfnamesByLocale.entries)
116+
_LanguageItem(locale: key, selfname: value),
117+
])));
118+
}
119+
}
120+
121+
class _LanguageItem extends StatelessWidget {
122+
const _LanguageItem({required this.locale, required this.selfname});
123+
124+
/// The [Locale] this corresponds to, from [kSelfnamesByLocale].
125+
final Locale locale;
126+
127+
/// The display name of the language in its own [locale], from
128+
/// [kSelfnamesByLocale].
129+
final String selfname;
130+
131+
@override
132+
Widget build(BuildContext context) {
133+
final globalSettings = GlobalStoreWidget.settingsOf(context);
134+
135+
final Widget subtitle;
136+
final Widget? trailing;
137+
if (locale == globalSettings.language) {
138+
// Make sure the subtitle text is consistent to selfname — especially if
139+
// zulipLocalizations.localeDisplayName(locale) is different from our
140+
// hard-coded [kSelfnamesByLocale] value.
141+
subtitle = Text(selfname);
142+
trailing = Icon(ZulipIcons.check);
143+
} else {
144+
final zulipLocalizations = ZulipLocalizations.of(context);
145+
subtitle = Text(zulipLocalizations.localeDisplayName(locale));
146+
trailing = null;
147+
}
148+
149+
return ListTile(
150+
title: Text(selfname),
151+
subtitle: subtitle,
152+
trailing: trailing,
153+
onTap: () {
154+
unawaited(globalSettings.setLanguage(locale));
155+
});
156+
}
157+
}
158+
85159
class ExperimentalFeaturesPage extends StatelessWidget {
86160
const ExperimentalFeaturesPage({super.key});
87161

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)