Skip to content

Commit 28e2263

Browse files
committed
settings: Add language setting
Since there is no Figma design for the settings page yet, the design is kept simple while matching zulip-mobile: we show both selfname (in its own locale) and name (in the ambient locale) 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 435db38 commit 28e2263

12 files changed

+199
-1
lines changed

assets/l10n/app_en.arb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,10 @@
835835
"@openLinksWithInAppBrowser": {
836836
"description": "Label for toggling setting to open links with in-app browser"
837837
},
838+
"languageSettingTitle": "Language",
839+
"@languageSettingTitle": {
840+
"description": "Title for language setting."
841+
},
838842
"languageEn": "English",
839843
"@languageEn": {
840844
"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
@@ -1226,6 +1226,12 @@ abstract class ZulipLocalizations {
12261226
/// **'Open links with in-app browser'**
12271227
String get openLinksWithInAppBrowser;
12281228

1229+
/// Title for language setting.
1230+
///
1231+
/// In en, this message translates to:
1232+
/// **'Language'**
1233+
String get languageSettingTitle;
1234+
12291235
/// Label for the English language.
12301236
///
12311237
/// 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
@@ -679,6 +679,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
679679
@override
680680
String get openLinksWithInAppBrowser => 'Open links with in-app browser';
681681

682+
@override
683+
String get languageSettingTitle => 'Language';
684+
682685
@override
683686
String get languageEn => 'English';
684687

lib/generated/l10n/zulip_localizations_en.dart

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

682+
@override
683+
String get languageSettingTitle => 'Language';
684+
682685
@override
683686
String get languageEn => 'English';
684687

lib/generated/l10n/zulip_localizations_ja.dart

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

682+
@override
683+
String get languageSettingTitle => 'Language';
684+
682685
@override
683686
String get languageEn => 'English';
684687

lib/generated/l10n/zulip_localizations_nb.dart

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

682+
@override
683+
String get languageSettingTitle => 'Language';
684+
682685
@override
683686
String get languageEn => 'English';
684687

lib/generated/l10n/zulip_localizations_pl.dart

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

691+
@override
692+
String get languageSettingTitle => 'Language';
693+
691694
@override
692695
String get languageEn => 'English';
693696

lib/generated/l10n/zulip_localizations_ru.dart

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

695+
@override
696+
String get languageSettingTitle => 'Language';
697+
695698
@override
696699
String get languageEn => 'English';
697700

lib/generated/l10n/zulip_localizations_sk.dart

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

684+
@override
685+
String get languageSettingTitle => 'Language';
686+
684687
@override
685688
String get languageEn => 'English';
686689

lib/generated/l10n/zulip_localizations_uk.dart

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

695+
@override
696+
String get languageSettingTitle => 'Language';
697+
695698
@override
696699
String get languageEn => 'English';
697700

lib/widgets/settings.dart

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import 'package:flutter/material.dart';
22

33
import '../generated/l10n/zulip_localizations.dart';
4+
import '../model/localizations.dart';
45
import '../model/settings.dart';
56
import 'app_bar.dart';
7+
import 'icons.dart';
68
import 'page.dart';
79
import 'store.dart';
810

@@ -17,17 +19,28 @@ class SettingsPage extends StatelessWidget {
1719
@override
1820
Widget build(BuildContext context) {
1921
final zulipLocalizations = ZulipLocalizations.of(context);
22+
23+
Widget? subtitle;
24+
final language = GlobalStoreWidget.settingsOf(context).language;
25+
if (language != null && kSelfnamesByLocale.containsKey(language)) {
26+
subtitle = Text(kSelfnamesByLocale[language]!);
27+
}
28+
2029
return Scaffold(
2130
appBar: ZulipAppBar(
2231
title: Text(zulipLocalizations.settingsPageTitle)),
2332
body: Column(children: [
2433
const _ThemeSetting(),
2534
const _BrowserPreferenceSetting(),
35+
ListTile(
36+
title: Text(zulipLocalizations.languageSettingTitle),
37+
subtitle: subtitle,
38+
onTap: () => Navigator.push(context, _LanguagePage.buildRoute())),
2639
if (GlobalSettingsStore.experimentalFeatureFlags.isNotEmpty)
2740
ListTile(
2841
title: Text(zulipLocalizations.experimentalFeatureSettingsPageTitle),
2942
onTap: () => Navigator.push(context,
30-
ExperimentalFeaturesPage.buildRoute()))
43+
ExperimentalFeaturesPage.buildRoute())),
3144
]));
3245
}
3346
}
@@ -82,6 +95,66 @@ class _BrowserPreferenceSetting extends StatelessWidget {
8295
}
8396
}
8497

98+
class _LanguagePage extends StatelessWidget {
99+
const _LanguagePage();
100+
101+
static WidgetRoute<void> buildRoute() {
102+
return MaterialWidgetRoute(page: const _LanguagePage());
103+
}
104+
105+
@override
106+
Widget build(BuildContext context) {
107+
final zulipLocalizations = ZulipLocalizations.of(context);
108+
return Scaffold(
109+
appBar: AppBar(
110+
title: Text(zulipLocalizations.languageSettingTitle)),
111+
body: SingleChildScrollView(
112+
child: Column(children: [
113+
for (final MapEntry(:key, :value) in kSelfnamesByLocale.entries)
114+
_LanguageItem(locale: key, selfname: value),
115+
])));
116+
}
117+
}
118+
119+
class _LanguageItem extends StatelessWidget {
120+
const _LanguageItem({required this.locale, required this.selfname});
121+
122+
final Locale locale;
123+
final String selfname;
124+
125+
@override
126+
Widget build(BuildContext context) {
127+
final globalSettings = GlobalStoreWidget.settingsOf(context);
128+
129+
final Widget subtitle;
130+
final Widget? trailing;
131+
if (locale == globalSettings.language) {
132+
assert(Localizations.localeOf(context) == locale);
133+
// For `subtitle`, the ambient locale is the same as selfname's
134+
// corresponding locale — there is no need to override [Localization].
135+
subtitle = Text(selfname);
136+
trailing = Icon(ZulipIcons.check);
137+
} else {
138+
final zulipLocalizations = ZulipLocalizations.of(context);
139+
subtitle = Text(zulipLocalizations.localeDisplayName(locale));
140+
trailing = null;
141+
}
142+
143+
return ListTile(
144+
title: Localizations.override(
145+
context: context,
146+
// Different from other text in the app, `selfname` is localized with
147+
// its corresponding locale, which is not always the ambient one.
148+
locale: locale,
149+
child: Text(selfname)),
150+
subtitle: subtitle,
151+
trailing: trailing,
152+
onTap: () {
153+
globalSettings.setLanguage(locale);
154+
});
155+
}
156+
}
157+
85158
class ExperimentalFeaturesPage extends StatelessWidget {
86159
const ExperimentalFeaturesPage({super.key});
87160

test/widgets/settings_test.dart

Lines changed: 91 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,95 @@ 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+
Locale ambientLocale(WidgetTester tester, [Finder? finder]) {
137+
final element = tester.element(finder ?? find.byType(SettingsPage));
138+
return Localizations.localeOf(element);
139+
}
140+
141+
testWidgets('on SettingsPage, when no language is set', (tester) async {
142+
await prepare(tester);
143+
check(ambientLocale(tester)).equals(const Locale('en'));
144+
145+
assert(testBinding.globalStore.settings.language == null);
146+
await tester.pump();
147+
check(languageListTileFinder).findsOne();
148+
check(find.text('English')).findsNothing();
149+
});
150+
151+
testWidgets('on SettingsPage, when a language is set', (tester) async {
152+
await prepare(tester);
153+
check(ambientLocale(tester)).equals(const Locale('en'));
154+
155+
await testBinding.globalStore.settings.setLanguage(const Locale('en'));
156+
await tester.pump();
157+
check(find.descendant(
158+
of: languageListTileFinder, matching: find.text('English'))).findsOne();
159+
});
160+
161+
testWidgets('LanguagePage smoke', (tester) async {
162+
await prepare(tester);
163+
await tester.tap(languageListTileFinder);
164+
await tester.pump();
165+
await tester.pump();
166+
check(find.text('Polski').hitTestable()).findsOne();
167+
check(find.text('Polish')).findsOne();
168+
check(find.byIcon(ZulipIcons.check)).findsNothing();
169+
check(ambientLocale(tester)).equals(const Locale('en'));
170+
check(testBinding.globalStore).settings.language.isNull();
171+
172+
await tester.tap(find.text('Polish'));
173+
await tester.pump();
174+
check(find.text('Polski').hitTestable()).findsExactly(2);
175+
check(find.text('Polish')).findsNothing();
176+
check(find.descendant(
177+
of: find.widgetWithText(ListTile, 'Polski'),
178+
matching: find.byIcon(ZulipIcons.check)),
179+
).findsOne();
180+
check(ambientLocale(tester)).equals(const Locale('pl'));
181+
check(testBinding.globalStore).settings.language.equals(const Locale('pl'));
182+
});
183+
184+
testWidgets('show selfname in its appropriate (different) locale', (tester) async {
185+
await prepare(tester);
186+
await tester.tap(languageListTileFinder);
187+
await tester.pump();
188+
await tester.pump();
189+
190+
Finder findInPolishListTile(String label) => find.descendant(
191+
of: find.ancestor(
192+
of: find.text('Polish'),
193+
matching: find.byType(ListTile)),
194+
matching: find.text(label)).hitTestable();
195+
196+
check(ambientLocale(tester, findInPolishListTile('Polish')))
197+
.equals(const Locale('en'));
198+
check(ambientLocale(tester, findInPolishListTile('Polski')))
199+
.equals(const Locale('pl'));
200+
});
201+
202+
testWidgets('handle unsupported (but valid) locale stored in database', (tester) async {
203+
await prepare(tester);
204+
// https://www.loc.gov/standards/iso639-2/php/code_list.php
205+
await testBinding.globalStore.settings.setLanguage(const Locale('zxx'));
206+
await tester.pumpAndSettle(); // expect no errors
207+
check(ambientLocale(tester)).equals(const Locale('en'));
208+
209+
await tester.tap(languageListTileFinder);
210+
await tester.pump();
211+
await tester.pump();
212+
check(find.byIcon(ZulipIcons.check)).findsNothing();
213+
214+
await tester.tap(find.text('Polish'));
215+
await tester.pump();
216+
check(ambientLocale(tester)).equals(const Locale('pl'));
217+
check(testBinding.globalStore).settings.language.equals(const Locale('pl'));
218+
});
219+
});
220+
130221
// TODO maybe test GlobalSettingType.experimentalFeatureFlag settings
131222
// Or maybe not; after all, it's a developer-facing feature, so
132223
// should be low risk.

0 commit comments

Comments
 (0)