Skip to content

Commit 3e20a17

Browse files
committed
core: Add translations for exceptions related to api requests
Also hook up some other test suites that require localization contexts for tests to pass given this change.
1 parent ea9c599 commit 3e20a17

11 files changed

+102
-24
lines changed

assets/l10n/app_en.arb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
"@aboutPageTapToView": {
1616
"description": "Item subtitle in About Zulip page to navigate to Licenses page"
1717
},
18+
"apiConnectionNetworkRequestFailed": "Network request failed",
19+
"@apiConnectionNetworkRequestFailed": {
20+
"description": "Message for error dialog when a network request fails."
21+
},
1822
"chooseAccountPageTitle": "Choose account",
1923
"@chooseAccountPageTitle": {
2024
"description": "Title for ChooseAccountPage"
@@ -49,6 +53,26 @@
4953
}
5054
}
5155
},
56+
"serverExceptionMalformedResponse": "Server gave malformed response; HTTP status {httpStatus}",
57+
"@serverExceptionMalformedResponse": {
58+
"description": "Message for error dialog when an API call fails because we could not parse it.",
59+
"placeholders": {
60+
"httpStatus": {
61+
"type": "int",
62+
"example": "500"
63+
}
64+
}
65+
},
66+
"serverExceptionRequestFailed": "Network request failed: HTTP status {httpStatus}",
67+
"@serverExceptionRequestFailed": {
68+
"description": "Message for error dialog when an API call fails.",
69+
"placeholders": {
70+
"httpStatus": {
71+
"type": "int",
72+
"example": "500"
73+
}
74+
}
75+
},
5276
"userRoleOwner": "Owner",
5377
"@userRoleOwner": {
5478
"description": "Label for UserRole.owner"

lib/api/core.dart

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:convert';
22
import 'dart:io';
33

4+
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
45
import 'package:http/http.dart' as http;
56

67
import '../log.dart';
@@ -84,15 +85,20 @@ class ApiConnection {
8485
try {
8586
response = await _client.send(request);
8687
} catch (e) {
87-
final String message;
88+
final String Function(ZulipLocalizations) message;
8889
if (e is http.ClientException) {
89-
message = e.message;
90+
message = (ZulipLocalizations zulipLocalizations) => e.message;
9091
} else if (e is TlsException) {
91-
message = e.message;
92+
message = (ZulipLocalizations zulipLocalizations) => e.message;
9293
} else {
93-
message = 'Network request failed';
94+
message = (ZulipLocalizations zulipLocalizations) {
95+
return zulipLocalizations.apiConnectionNetworkRequestFailed;
96+
};
9497
}
95-
throw NetworkException(routeName: routeName, cause: e, message: message);
98+
throw NetworkException(
99+
routeName: routeName,
100+
cause: e,
101+
message: message);
96102
}
97103

98104
final int httpStatus = response.statusCode;
@@ -165,14 +171,17 @@ ApiRequestException _makeApiException(String routeName, int httpStatus, Map<Stri
165171
if (json != null && json['result'] == 'error'
166172
&& json['code'] is String? && json['msg'] is String) {
167173
json.remove('result');
174+
final serverMessage = json.remove('msg');
168175
return ZulipApiException( // TODO(log): systematically log these
169176
routeName: routeName,
170177
httpStatus: httpStatus,
171178
// When `code` is missing, we fall back to `BAD_REQUEST`,
172179
// the same value the server uses when nobody's made it more specific.
173180
// TODO(server): `code` should always be present. Get the "Invalid API key" case fixed.
174181
code: json.remove('code') ?? 'BAD_REQUEST',
175-
message: json.remove('msg'),
182+
message: (ZulipLocalizations zulipLocalizations) {
183+
return serverMessage;
184+
},
176185
data: json,
177186
);
178187
}

lib/api/exception.dart

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11

2+
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
3+
24
/// Some kind of error from a Zulip API network request.
35
sealed class ApiRequestException implements Exception {
46
/// The name of the Zulip API route for the request.
@@ -15,9 +17,10 @@ sealed class ApiRequestException implements Exception {
1517
///
1618
/// For [ZulipApiException] this is supplied by the server as the `message`
1719
/// property in the JSON response, and is translated into the user's language.
18-
final String message;
20+
final String Function(ZulipLocalizations) message;
1921

2022
ApiRequestException({required this.routeName, required this.message});
23+
2124
}
2225

2326
/// An error returned through the Zulip server API.
@@ -89,7 +92,10 @@ class Server5xxException extends ServerException {
8992
required super.httpStatus,
9093
required super.data,
9194
}) : assert(500 <= httpStatus && httpStatus <= 599),
92-
super(message: 'Network request failed: HTTP status $httpStatus'); // TODO(i18n)
95+
super(
96+
message: (ZulipLocalizations zulipLocalizations) {
97+
return zulipLocalizations.serverExceptionRequestFailed(httpStatus);
98+
});
9399
}
94100

95101
/// An error where the server's response doesn't match the Zulip API.
@@ -110,5 +116,8 @@ class MalformedServerResponseException extends ServerException {
110116
required super.routeName,
111117
required super.httpStatus,
112118
required super.data,
113-
}) : super(message: 'Server gave malformed response; HTTP status $httpStatus'); // TODO(i18n)
119+
}) : super(
120+
message: (ZulipLocalizations zulipLocalizations) {
121+
return zulipLocalizations.serverExceptionMalformedResponse(httpStatus);
122+
});
114123
}

lib/widgets/action_sheet.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter/services.dart';
3+
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
34
import 'package:share_plus/share_plus.dart';
45

56
import '../api/exception.dart';
@@ -104,6 +105,7 @@ Future<String?> fetchRawContentWithFeedback({
104105
// - If request(s) take(s) a long time, show snackbar with cancel
105106
// button, like "Still working on quote-and-reply…".
106107
// On final failure or success, auto-dismiss the snackbar.
108+
final zulipLocalizations = ZulipLocalizations.of(context);
107109
try {
108110
fetchedMessage = await getMessageCompat(PerAccountStoreWidget.of(context).connection,
109111
messageId: messageId,
@@ -115,7 +117,7 @@ Future<String?> fetchRawContentWithFeedback({
115117
} catch (e) {
116118
switch (e) {
117119
case ZulipApiException():
118-
errorMessage = e.message;
120+
errorMessage = e.message(zulipLocalizations);
119121
// TODO specific messages for common errors, like network errors
120122
// (support with reusable code)
121123
default:

lib/widgets/compose_box.dart

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
55
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
66
import 'package:image_picker/image_picker.dart';
77

8+
import '../api/exception.dart';
89
import '../api/model/model.dart';
910
import '../api/route/messages.dart';
1011
import '../model/compose.dart';
@@ -474,8 +475,12 @@ Future<void> _uploadFiles({
474475
if (!context.mounted) return;
475476
// TODO(#37): Specifically handle `413 Payload Too Large`
476477
// TODO(#37): On API errors, quote `msg` from server, with "The server said:"
478+
final zulipLocalizations = ZulipLocalizations.of(context);
479+
final String message = (e is ApiRequestException)
480+
? e.message(zulipLocalizations)
481+
: e.toString();
477482
showErrorDialog(context: context,
478-
title: 'Failed to upload file: $filename', message: e.toString());
483+
title: 'Failed to upload file: $filename', message: message);
479484
} finally {
480485
contentController.registerUploadEnd(tag, url);
481486
}
@@ -535,6 +540,7 @@ Future<Iterable<_File>> _getFilePickerFiles(BuildContext context, FileType type)
535540
.pickFiles(allowMultiple: true, withReadStream: true, type: type);
536541
} catch (e) {
537542
if (!context.mounted) return [];
543+
final zulipLocalizations = ZulipLocalizations.of(context);
538544
if (e is PlatformException && e.code == 'read_external_storage_denied') {
539545
// Observed on Android. If Android's error message tells us whether the
540546
// user has checked "Don't ask again", it seems the library doesn't pass
@@ -550,8 +556,13 @@ Future<Iterable<_File>> _getFilePickerFiles(BuildContext context, FileType type)
550556
AppSettings.openAppSettings();
551557
});
552558
} else {
553-
// TODO(i18n)
554-
showErrorDialog(context: context, title: 'Error', message: e.toString());
559+
final String message = (e is ApiRequestException)
560+
? e.message(zulipLocalizations)
561+
: e.toString();
562+
showErrorDialog(
563+
context: context,
564+
title: 'Error',
565+
message: message);
555566
}
556567
return [];
557568
}
@@ -629,8 +640,13 @@ class _AttachFromCameraButton extends _AttachUploadsButton {
629640
AppSettings.openAppSettings();
630641
});
631642
} else {
632-
// TODO(i18n)
633-
showErrorDialog(context: context, title: 'Error', message: e.toString());
643+
final String message = (e is ApiRequestException)
644+
? e.message(zulipLocalizations)
645+
: e.toString();
646+
showErrorDialog(
647+
context: context,
648+
title: 'Error',
649+
message: message);
634650
}
635651
return [];
636652
}

lib/widgets/login.dart

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:flutter/material.dart';
2+
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
23

34
import '../api/core.dart';
45
import '../api/exception.dart';
@@ -289,10 +290,10 @@ class _PasswordLoginPageState extends State<PasswordLoginPage> {
289290
// TODO(#105) give more helpful feedback. The RN app is
290291
// unhelpful here; we should at least recognize invalid auth errors, and
291292
// errors for deactivated user or realm (see zulip-mobile#4571).
292-
final message = (e is ZulipApiException)
293-
? 'The server said:\n\n${e.message}'
294-
: e.message;
295-
showErrorDialog(context: context, title: 'Login failed', message: message);
293+
final zulipLocalizations = ZulipLocalizations.of(context);
294+
showErrorDialog(context: context,
295+
title: 'Login failed',
296+
message: e.message(zulipLocalizations));
296297
return;
297298
}
298299

test/api/core_test.dart

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'dart:io';
33

44
import 'package:checks/checks.dart';
55
import 'package:checks/context.dart';
6+
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
67
import 'package:http/http.dart' as http;
78
import 'package:test/scaffolding.dart';
89
import 'package:zulip/api/core.dart';
@@ -153,9 +154,13 @@ void main() {
153154
..which(condition));
154155
}
155156

156-
checkRequest(http.ClientException('Oops'), it()..message.equals('Oops'));
157-
checkRequest(const TlsException('Oops'), it()..message.equals('Oops'));
158-
checkRequest((foo: 'bar'), it()..message.equals('Network request failed'));
157+
final zulipLocalizations = lookupZulipLocalizations(ZulipLocalizations.supportedLocales.first);
158+
checkRequest(http.ClientException('Oops'), it()..message(zulipLocalizations).equals('Oops'));
159+
checkRequest(const TlsException('Oops'), it()..message(zulipLocalizations).equals('Oops'));
160+
final expectedMessage = zulipLocalizations.apiConnectionNetworkRequestFailed;
161+
checkRequest((foo: 'bar'), it()
162+
..message(zulipLocalizations)
163+
.equals(expectedMessage));
159164
});
160165

161166
test('API 4xx errors, well formed', () async {
@@ -166,6 +171,7 @@ void main() {
166171
String message = 'A thing failed',
167172
Map<String, dynamic> data = const {},
168173
}) async {
174+
final zulipLocalizations = lookupZulipLocalizations(ZulipLocalizations.supportedLocales.first);
169175
final json = {
170176
'result': 'error',
171177
if (code != null) 'code': code,
@@ -178,7 +184,7 @@ void main() {
178184
..httpStatus.equals(httpStatus)
179185
..code.equals(expectedCode ?? code!)
180186
..data.deepEquals(data)
181-
..message.equals(message));
187+
..message(zulipLocalizations).equals(message));
182188
}
183189

184190
await checkRequest();

test/api/exception_checks.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import 'package:checks/checks.dart';
2+
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
23
import 'package:zulip/api/exception.dart';
34

45
extension ApiRequestExceptionChecks on Subject<ApiRequestException> {
56
Subject<String> get routeName => has((e) => e.routeName, 'routeName');
6-
Subject<String> get message => has((e) => e.message, 'message');
7+
Subject<String> message (ZulipLocalizations zulipLocalizations) =>
8+
has((e) => e.message(zulipLocalizations), 'message');
79
}
810

911
extension ZulipApiExceptionChecks on Subject<ZulipApiException> {

test/widgets/action_sheet_test.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:checks/checks.dart';
22
import 'package:flutter/material.dart';
33
import 'package:flutter/services.dart';
4+
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
45
import 'package:flutter_test/flutter_test.dart';
56
import 'package:zulip/api/model/model.dart';
67
import 'package:zulip/api/route/messages.dart';
@@ -48,6 +49,8 @@ Future<void> setupToMessageActionSheet(WidgetTester tester, {
4849

4950
await tester.pumpWidget(
5051
MaterialApp(
52+
localizationsDelegates: ZulipLocalizations.localizationsDelegates,
53+
supportedLocales: ZulipLocalizations.supportedLocales,
5154
home: GlobalStoreWidget(
5255
child: PerAccountStoreWidget(
5356
accountId: eg.selfAccount.id,

test/widgets/autocomplete_test.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:checks/checks.dart';
22
import 'package:flutter/material.dart';
3+
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
34
import 'package:flutter_test/flutter_test.dart';
45
import 'package:zulip/api/model/model.dart';
56
import 'package:zulip/api/route/messages.dart';
@@ -40,6 +41,8 @@ Future<Finder> setupToComposeInput(WidgetTester tester, {
4041

4142
await tester.pumpWidget(
4243
MaterialApp(
44+
localizationsDelegates: ZulipLocalizations.localizationsDelegates,
45+
supportedLocales: ZulipLocalizations.supportedLocales,
4346
home: GlobalStoreWidget(
4447
child: PerAccountStoreWidget(
4548
accountId: eg.selfAccount.id,

test/widgets/content_test.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'dart:io';
33
import 'package:checks/checks.dart';
44
import 'package:flutter/foundation.dart';
55
import 'package:flutter/material.dart';
6+
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
67
import 'package:flutter_test/flutter_test.dart';
78
import 'package:url_launcher/url_launcher.dart';
89
import 'package:zulip/api/core.dart';
@@ -68,6 +69,8 @@ void main() {
6869
addTearDown(testBinding.reset);
6970

7071
await tester.pumpWidget(GlobalStoreWidget(child: MaterialApp(
72+
localizationsDelegates: ZulipLocalizations.localizationsDelegates,
73+
supportedLocales: ZulipLocalizations.supportedLocales,
7174
home: PerAccountStoreWidget(accountId: eg.selfAccount.id,
7275
child: BlockContentList(
7376
nodes: parseContent(html).nodes)))));

0 commit comments

Comments
 (0)