Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add asynchronous translations #74 #83

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 3 additions & 11 deletions packages/clerk_auth/lib/src/clerk_auth/auth_error.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,14 @@
///
class AuthError extends Error {
/// Construct an [AuthError]
AuthError({this.code, required this.message, this.substitution});
AuthError({this.code, required this.message, this.messageSubstitutions});

/// An error [code], likely to be an http status code
final int? code;

/// The associated [message]
final String message;

/// A possible [substitution] within the message
final String? substitution;

@override
String toString() {
if (substitution case String substitution) {
return message.replaceAll('###', substitution);
}
return message;
}
/// A possible set of substitutions within the message
final List<String>? messageSubstitutions;
}
2 changes: 1 addition & 1 deletion packages/clerk_auth/lib/src/clerk_auth/http_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ class DefaultHttpClient implements HttpClient {
String ext when _imageExts.contains(ext) => MediaType('image', ext),
String ext => throw AuthError(
message: 'Unknown media type for upload: ###',
substitution: ext,
messageSubstitutions: [ext],
),
};
}
Expand Down
2 changes: 1 addition & 1 deletion packages/clerk_auth/lib/src/models/enums.dart
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ enum Stage {
Status.needsSecondFactor => second,
_ => throw AuthError(
message: 'No Stage for ###',
substitution: status.toString(),
messageSubstitutions: [status.toString()],
),
};
}
Expand Down
2 changes: 1 addition & 1 deletion packages/clerk_auth/lib/src/models/strategy.dart
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ class Strategy {
Field.emailAddress => Strategy.emailCode,
_ => throw AuthError(
message: 'No way to verify ###',
substitution: field.name,
messageSubstitutions: [field.name],
),
};
}
Expand Down
63 changes: 63 additions & 0 deletions packages/clerk_flutter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,69 @@ class _ExampleAppState extends State<ExampleApp> {
}
```

## Translation

The default language in use for alert and UI messaging within the Clerk Flutter SDK is English. For other languages,
you will need to override the `ClerkTranslator` passed in to the `ClerkAuth` or `ClerkAuthProvider` widgets via the
optional `translator` parameter. Translations will need to be provided for the following (hopefully self-explanatory)
words and phrases:
```text
Add account
Add email address
Add phone number
Already have an account?
Authenticator app
Backup code
Cancel
Click on the link that‘s been sent to ### and then check back here
Continue
Don’t have an account?
Email address
Email address '###' is invalid
Email address verification
Enter the code sent to ###
Enter the code sent to you
First name
Last name
Manage account
OK
Optional
or
Passkey
Password
Password and password confirmation must match
Password confirmation
Phone number
Phone number '###' is invalid
Phone number verification
Phone numbers
PRIMARY
Profile
Profile details
Sign in
Sign in by entering a code sent to you by email
Sign in by entering a code sent to you by text message
Sign in to ###
Sign out
Sign out of all accounts
Sign up
Sign up to ###
Type '###' invalid
UNVERIFIED
Username
Verify your email address
Verify your phone number
Web3 wallet
Welcome back! Please sign in to continue
Welcome! Please fill in the details to get started
```


### Asynchronous translation
Some translations cannot be known ahead of time e.g. arbitrary error messages from the Clerk server. An
asynchronous translation method `translateAsync` is provided to allow for translation generation on the fly in this
instance.

## License

This SDK is licensed under the MIT license found in the [LICENSE](./LICENSE) file.
60 changes: 45 additions & 15 deletions packages/clerk_flutter/lib/src/utils/clerk_translator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,45 @@ abstract class ClerkTranslator {
const ClerkTranslator();

/// the character sequence to replace with [substitution] during
/// a call to [translate]
final substitutionKey = '###';
/// a call to [translate]. By default this is `###` for the first item
/// (index 0) and `#<index + 1>#` for subsequent indices (index 1 or more)
///
String substitutionKey([int idx = 0]) => idx < 1 ? '###' : '#${idx + 1}#';

/// Translate a [phrase]] into a different language. Identity function by default
/// for English
///
/// A [substitution] or multiple [substitutions] for keys inside the [phrase]:
///
/// if a single [substitution] is present, replace the first instance of
/// [substitutionKey] (default `###`) with it
/// If a single [substitution] is present, replace the first instance of
/// `substitutionKey()` with it
///
/// if multiple [substitutions] are present, replace the first found character sequences `#<n>#`
/// with each substitution where [n] is the 1-based index into the array.
/// If multiple [substitutions] are present, use each substitution to
/// replace the first found appropriate character sequence:
/// `substitutionKey(index)`
///
/// This allows word order to vary across different languages without having to manipulate the
/// [substitutions] array, viz:
/// This allows word order to vary across different languages without having
/// to manipulate the [substitutions] array, viz:
///
/// `translate('#1# bites #2#', substitutions: ['man', 'dog'])`
/// `translate('### bites #2#', substitutions: ['man', 'dog'])`
/// vs
/// `translate('#2# is bitten by #1#', substitutions: ['man', 'dog'])`
/// `translate('#2# is bitten by ###', substitutions: ['man', 'dog'])`
///
String translate(
String phrase, {
String? substitution,
List<String> substitutions = const [],
});

/// An asynchronous version of [translate], for situations in which the
/// translation might not immediately be to hand. Parameters as above.
///
Future<String> translateAsync(
String phrase, {
String? substitution,
List<String>? substitutions,
});

/// A method that takes a list of [items] e.g. \['first', 'second', 'third'\]
/// and returns a textual representation of its contents as alternatives
/// e.g. "first, second or third"
Expand All @@ -55,19 +67,37 @@ class DefaultClerkTranslator extends ClerkTranslator {
const DefaultClerkTranslator();

@override
String translate(String phrase,
{String? substitution, List<String> substitutions = const []}) {
String translate(
String phrase, {
String? substitution,
List<String>? substitutions,
}) {
if (substitution case String sub) {
return phrase.replaceFirst(substitutionKey, sub);
return phrase.replaceFirst(substitutionKey(), sub);
}

for (int i = 0; i < substitutions.length; ++i) {
phrase = phrase.replaceFirst('#${i + 1}#', substitutions[i]);
if (substitutions case List<String> substitutions) {
for (int i = 0; i < substitutions.length; ++i) {
phrase = phrase.replaceFirst(substitutionKey(i), substitutions[i]);
}
}

return phrase;
}

@override
Future<String> translateAsync(
String phrase, {
String? substitution,
List<String>? substitutions,
}) async {
return translate(
phrase,
substitution: substitution,
substitutions: substitutions,
);
}

@override
String alternatives(List<String> items,
{String connector = 'or', String? prefix}) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class ClerkSignOutPanel extends StatelessWidget {
padding: horizontalPadding16,
child: ClerkMaterialButton(
onPressed: () => auth.signOut(),
label: Text(auth.translator.translate('Sign Out')),
label: Text(auth.translator.translate('Sign out')),
),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class _ClerkSignUpPanelState extends State<ClerkSignUpPanel> {
key: const Key('phone_code'),
title: translator.translate('Verify your phone number'),
subtitle: translator.translate(
'Enter code sent to ###',
'Enter the code sent to ###',
substitution: _values[clerk.UserAttribute.phoneNumber],
),
onSubmit: (code) async {
Expand All @@ -96,7 +96,7 @@ class _ClerkSignUpPanelState extends State<ClerkSignUpPanel> {
key: const Key('email_code'),
title: translator.translate('Verify your email address'),
subtitle: translator.translate(
'Enter code sent to ###',
'Enter the code sent to ###',
substitution: _values[clerk.UserAttribute.emailAddress],
),
onSubmit: (code) async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,9 @@ class ClerkAuthProvider extends clerk.Auth with ChangeNotifier {
/// but may still not be acceptable to the back end
String? checkPassword(String? password, String? confirmation) {
if (password != confirmation) {
return translator
.translate('Password and password confirmation must match');
return translator.translate(
'Password and password confirmation must match',
);
}

if (password case String password when password.isNotEmpty) {
Expand Down
35 changes: 19 additions & 16 deletions packages/clerk_flutter/lib/src/widgets/ui/clerk_error_listener.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,28 @@ class ClerkErrorListener extends StatefulWidget {
static Future<void> defaultErrorHandler(
BuildContext context, AuthError error) async {
final translator = ClerkAuth.translatorOf(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12.0),
topRight: Radius.circular(12.0),
),
),
content: Text(
translator.translate(
error.message,
substitution: error.substitution,
final message = await translator.translateAsync(
error.message,
substitutions: error.messageSubstitutions,
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12.0),
topRight: Radius.circular(12.0),
),
),
style: ClerkTextStyle.subtitle.copyWith(
color: ClerkColors.white,
content: Text(
message,
style: ClerkTextStyle.subtitle.copyWith(
color: ClerkColors.white,
),
),
),
),
);
);
}
}

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class ClerkUserProfile extends StatelessWidget {
default:
throw clerk.AuthError(
message: "Type '###' invalid",
substitution: type.name,
messageSubstitutions: [type.name],
);
}
}
Expand Down Expand Up @@ -92,7 +92,7 @@ class ClerkUserProfile extends StatelessWidget {
),
_ => throw clerk.AuthError(
message: "Type '###' invalid",
substitution: type.name,
messageSubstitutions: [type.name],
),
},
);
Expand All @@ -108,7 +108,7 @@ class ClerkUserProfile extends StatelessWidget {
} else {
throw clerk.AuthError(
message: "$title '###' is invalid",
substitution: identifier,
messageSubstitutions: [identifier],
);
}
}
Expand Down