Skip to content

Commit 4c2509a

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 c979279 commit 4c2509a

16 files changed

+192
-0
lines changed

assets/l10n/app_en.arb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,10 @@
943943
"@openLinksWithInAppBrowser": {
944944
"description": "Label for toggling setting to open links with in-app browser"
945945
},
946+
"languageSettingTitle": "Language",
947+
"@languageSettingTitle": {
948+
"description": "Title for language setting."
949+
},
946950
"languageEn": "English",
947951
"@languageEn": {
948952
"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
@@ -1407,6 +1407,12 @@ abstract class ZulipLocalizations {
14071407
/// **'Open links with in-app browser'**
14081408
String get openLinksWithInAppBrowser;
14091409

1410+
/// Title for language setting.
1411+
///
1412+
/// In en, this message translates to:
1413+
/// **'Language'**
1414+
String get languageSettingTitle;
1415+
14101416
/// Label for the English language.
14111417
///
14121418
/// 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
@@ -767,6 +767,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
767767
@override
768768
String get openLinksWithInAppBrowser => 'Open links with in-app browser';
769769

770+
@override
771+
String get languageSettingTitle => 'Language';
772+
770773
@override
771774
String get languageEn => 'English';
772775

lib/generated/l10n/zulip_localizations_de.dart

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

770+
@override
771+
String get languageSettingTitle => 'Language';
772+
770773
@override
771774
String get languageEn => 'English';
772775

lib/generated/l10n/zulip_localizations_en.dart

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

770+
@override
771+
String get languageSettingTitle => 'Language';
772+
770773
@override
771774
String get languageEn => 'English';
772775

lib/generated/l10n/zulip_localizations_it.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,9 @@ class ZulipLocalizationsIt extends ZulipLocalizations {
773773
@override
774774
String get openLinksWithInAppBrowser => 'Open links with in-app browser';
775775

776+
@override
777+
String get languageSettingTitle => 'Language';
778+
776779
@override
777780
String get languageEn => 'English';
778781

lib/generated/l10n/zulip_localizations_ja.dart

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

770+
@override
771+
String get languageSettingTitle => 'Language';
772+
770773
@override
771774
String get languageEn => 'English';
772775

lib/generated/l10n/zulip_localizations_nb.dart

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

770+
@override
771+
String get languageSettingTitle => 'Language';
772+
770773
@override
771774
String get languageEn => 'English';
772775

lib/generated/l10n/zulip_localizations_pl.dart

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

780+
@override
781+
String get languageSettingTitle => 'Language';
782+
780783
@override
781784
String get languageEn => 'English';
782785

lib/generated/l10n/zulip_localizations_ru.dart

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

784+
@override
785+
String get languageSettingTitle => 'Language';
786+
784787
@override
785788
String get languageEn => 'English';
786789

lib/generated/l10n/zulip_localizations_sk.dart

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

772+
@override
773+
String get languageSettingTitle => 'Language';
774+
772775
@override
773776
String get languageEn => 'English';
774777

lib/generated/l10n/zulip_localizations_sl.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,9 @@ class ZulipLocalizationsSl extends ZulipLocalizations {
789789
String get openLinksWithInAppBrowser =>
790790
'Odpri povezave v brskalniku znotraj aplikacije';
791791

792+
@override
793+
String get languageSettingTitle => 'Language';
794+
792795
@override
793796
String get languageEn => 'English';
794797

lib/generated/l10n/zulip_localizations_uk.dart

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

784+
@override
785+
String get languageSettingTitle => 'Language';
786+
784787
@override
785788
String get languageEn => 'English';
786789

lib/generated/l10n/zulip_localizations_zh.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,9 @@ class ZulipLocalizationsZh extends ZulipLocalizations {
767767
@override
768768
String get openLinksWithInAppBrowser => 'Open links with in-app browser';
769769

770+
@override
771+
String get languageSettingTitle => 'Language';
772+
770773
@override
771774
String get languageEn => 'English';
772775

lib/widgets/settings.dart

Lines changed: 75 additions & 0 deletions
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

@@ -25,6 +29,7 @@ class SettingsPage extends StatelessWidget {
2529
const _BrowserPreferenceSetting(),
2630
const _VisitFirstUnreadSetting(),
2731
const _MarkReadOnScrollSetting(),
32+
const _LanguageSetting(),
2833
if (GlobalSettingsStore.experimentalFeatureFlags.isNotEmpty)
2934
ListTile(
3035
title: Text(zulipLocalizations.experimentalFeatureSettingsPageTitle),
@@ -231,6 +236,76 @@ class MarkReadOnScrollSettingPage extends StatelessWidget {
231236
}
232237
}
233238

239+
class _LanguageSetting extends StatelessWidget {
240+
const _LanguageSetting();
241+
242+
@override
243+
Widget build(BuildContext context) {
244+
final zulipLocalizations = ZulipLocalizations.of(context);
245+
246+
Widget? subtitle;
247+
final currentLanguageSelfname = kSelfnamesByLocale[
248+
GlobalStoreWidget.settingsOf(context).language];
249+
if (currentLanguageSelfname != null) {
250+
subtitle = Text(currentLanguageSelfname);
251+
}
252+
253+
return ListTile(
254+
title: Text(zulipLocalizations.languageSettingTitle),
255+
subtitle: subtitle,
256+
onTap: () => Navigator.push(context, _LanguagePage.buildRoute()));
257+
}
258+
}
259+
260+
class _LanguagePage extends StatelessWidget {
261+
const _LanguagePage();
262+
263+
static WidgetRoute<void> buildRoute() {
264+
return MaterialWidgetRoute(page: const _LanguagePage());
265+
}
266+
267+
@override
268+
Widget build(BuildContext context) {
269+
final zulipLocalizations = ZulipLocalizations.of(context);
270+
return Scaffold(
271+
appBar: AppBar(
272+
title: Text(zulipLocalizations.languageSettingTitle)),
273+
body: SingleChildScrollView(
274+
child: Column(children: [
275+
for (final language in zulipLocalizations.languages())
276+
_LanguageItem(language: language),
277+
])));
278+
}
279+
}
280+
281+
class _LanguageItem extends StatelessWidget {
282+
const _LanguageItem({required this.language});
283+
284+
/// The [Language] this corresponds to, from [ZulipLocalizations.languages].
285+
final Language language;
286+
287+
@override
288+
Widget build(BuildContext context) {
289+
final (locale, selfname, displayName) = language;
290+
final isCurrentLanguageInSettings =
291+
locale == GlobalStoreWidget.settingsOf(context).language;
292+
293+
return ListTile(
294+
title: Text(selfname),
295+
subtitle: Text(
296+
isCurrentLanguageInSettings
297+
? // Make sure the subtitle text is consistent to the title — since
298+
// displayName (decided by translators) can be different from our
299+
// hard-coded selfname when isCurrentLanguage is true.
300+
selfname
301+
: displayName),
302+
trailing: isCurrentLanguageInSettings ? Icon(ZulipIcons.check) : null,
303+
onTap: () {
304+
unawaited(GlobalStoreWidget.settingsOf(context).setLanguage(locale));
305+
});
306+
}
307+
}
308+
234309
class ExperimentalFeaturesPage extends StatelessWidget {
235310
const ExperimentalFeaturesPage({super.key});
236311

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';
@@ -129,6 +131,75 @@ void main() {
129131

130132
// TODO(#1571): test visitFirstUnread setting UI
131133

134+
group('language setting', () {
135+
Finder languageListTileFinder = find.ancestor(
136+
of: find.text('Language'), matching: find.byType(ListTile));
137+
138+
Subject<Locale> checkAmbientLocale(WidgetTester tester) =>
139+
check(Localizations.localeOf(tester.element(find.byType(SettingsPage))));
140+
141+
testWidgets('on SettingsPage, when no language is set', (tester) async {
142+
await prepare(tester);
143+
checkAmbientLocale(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+
checkAmbientLocale(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+
checkAmbientLocale(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+
checkAmbientLocale(tester).equals(const Locale('pl'));
181+
check(testBinding.globalStore).settings.language.equals(const Locale('pl'));
182+
});
183+
184+
testWidgets('handle unsupported (but valid) locale stored in database', (tester) async {
185+
await prepare(tester);
186+
// https://www.loc.gov/standards/iso639-2/php/code_list.php
187+
await testBinding.globalStore.settings.setLanguage(const Locale('zxx'));
188+
await tester.pumpAndSettle(); // expect no errors
189+
checkAmbientLocale(tester).equals(const Locale('en'));
190+
191+
await tester.tap(languageListTileFinder);
192+
await tester.pump();
193+
await tester.pump();
194+
check(find.byIcon(ZulipIcons.check)).findsNothing();
195+
196+
await tester.tap(find.text('Polish'));
197+
await tester.pump();
198+
checkAmbientLocale(tester).equals(const Locale('pl'));
199+
check(testBinding.globalStore).settings.language.equals(const Locale('pl'));
200+
});
201+
});
202+
132203
// TODO maybe test GlobalSettingType.experimentalFeatureFlag settings
133204
// Or maybe not; after all, it's a developer-facing feature, so
134205
// should be low risk.

0 commit comments

Comments
 (0)