Skip to content

Commit 004c55a

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 4d760e1 commit 004c55a

16 files changed

+181
-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
@@ -767,6 +767,9 @@ class ZulipLocalizationsIt 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_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
@@ -778,6 +778,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
778778
@override
779779
String get openLinksWithInAppBrowser => 'Otwieraj odnośniki w aplikacji';
780780

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

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: 64 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

@@ -17,13 +21,24 @@ 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(),
2637
const _VisitFirstUnreadSetting(),
38+
ListTile(
39+
title: Text(zulipLocalizations.languageSettingTitle),
40+
subtitle: languageSettingSubtitle,
41+
onTap: () => Navigator.push(context, _LanguagePage.buildRoute())),
2742
if (GlobalSettingsStore.experimentalFeatureFlags.isNotEmpty)
2843
ListTile(
2944
title: Text(zulipLocalizations.experimentalFeatureSettingsPageTitle),
@@ -150,6 +165,55 @@ class VisitFirstUnreadSettingPage extends StatelessWidget {
150165
}
151166
}
152167

168+
class _LanguagePage extends StatelessWidget {
169+
const _LanguagePage();
170+
171+
static WidgetRoute<void> buildRoute() {
172+
return MaterialWidgetRoute(page: const _LanguagePage());
173+
}
174+
175+
@override
176+
Widget build(BuildContext context) {
177+
final zulipLocalizations = ZulipLocalizations.of(context);
178+
return Scaffold(
179+
appBar: AppBar(
180+
title: Text(zulipLocalizations.languageSettingTitle)),
181+
body: SingleChildScrollView(
182+
child: Column(children: [
183+
for (final language in zulipLocalizations.languages())
184+
_LanguageItem(language: language),
185+
])));
186+
}
187+
}
188+
189+
class _LanguageItem extends StatelessWidget {
190+
const _LanguageItem({required this.language});
191+
192+
/// The [Language] this corresponds to, from [ZulipLocalizations.languages].
193+
final Language language;
194+
195+
@override
196+
Widget build(BuildContext context) {
197+
final (locale, selfname, displayName) = language;
198+
final isCurrentLanguageInSettings =
199+
locale == GlobalStoreWidget.settingsOf(context).language;
200+
201+
return ListTile(
202+
title: Text(selfname),
203+
subtitle: Text(
204+
isCurrentLanguageInSettings
205+
? // Make sure the subtitle text is consistent to the title — since
206+
// displayName (decided by translators) can be different from our
207+
// hard-coded selfname when isCurrentLanguage is true.
208+
selfname
209+
: displayName),
210+
trailing: isCurrentLanguageInSettings ? Icon(ZulipIcons.check) : null,
211+
onTap: () {
212+
unawaited(GlobalStoreWidget.settingsOf(context).setLanguage(locale));
213+
});
214+
}
215+
}
216+
153217
class ExperimentalFeaturesPage extends StatelessWidget {
154218
const ExperimentalFeaturesPage({super.key});
155219

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)