Skip to content

Commit

Permalink
fix: add asynchronous translations [CLERK_SDK #74]
Browse files Browse the repository at this point in the history
  • Loading branch information
shinyford committed Dec 20, 2024
1 parent 8cb13ec commit 708a8b4
Show file tree
Hide file tree
Showing 12 changed files with 134 additions and 46 deletions.
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ class ClerkErrorMessage extends StatelessWidget {
const ClerkErrorMessage({super.key});

Future<void> _showError(BuildContext context, clerk.AuthError error) async {
await null;
final translator = ClerkAuth.translatorOf(context);
final message = await translator.translateAsync(
error.message,
substitutions: error.messageSubstitutions,
);
if (context.mounted) {
final translator = ClerkAuth.translatorOf(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
shape: const RoundedRectangleBorder(
Expand All @@ -25,10 +28,7 @@ class ClerkErrorMessage extends StatelessWidget {
),
),
content: Text(
translator.translate(
error.message,
substitution: error.substitution,
),
message,
style: ClerkTextStyle.subtitle.copyWith(color: ClerkColors.white),
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ class StrategyButton extends StatelessWidget {
child: MaterialButton(
onPressed: onClick,
elevation: 2.0,
shape: RoundedRectangleBorder(
shape: const RoundedRectangleBorder(
borderRadius: borderRadius4,
side: const BorderSide(color: ClerkColors.dawnPink),
side: BorderSide(color: ClerkColors.dawnPink),
),
child: Padding(
padding: verticalPadding4,
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 @@ -102,11 +102,13 @@ class ClerkUserProfile extends StatelessWidget {
if (submitted) {
if (_validate(identifier, type)) {
await auth.addIdentifyingData(identifier, type);
if (context.mounted) _verifyIdentifyingData(context, auth, identifier);
if (context.mounted) {
await _verifyIdentifyingData(context, auth, identifier);
}
} else {
throw clerk.AuthError(
message: "$title '###' is invalid",
substitution: identifier,
messageSubstitutions: [identifier],
);
}
}
Expand Down

0 comments on commit 708a8b4

Please sign in to comment.