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

feat: add support for rich text #724

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
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
50 changes: 48 additions & 2 deletions lib/src/localization.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class Localization {
late Locale _locale;

final RegExp _replaceArgRegex = RegExp('{}');
final RegExp _namedArgMatcher = RegExp('({([a-zA-Z0-9_]+)})');
final RegExp _linkKeyMatcher =
RegExp(r'(?:@(?:\.[a-z]+)?:(?:[\w\-_|.]+|\([\w\-_|.]+\)))');
final RegExp _linkKeyPrefixMatcher = RegExp(r'^@(?:\.([a-z]+))?:');
Expand Down Expand Up @@ -68,6 +69,22 @@ class Localization {
return _replaceArgs(res, args);
}

TextSpan trSpan(
String key, {
Map<String, TextSpan>? namedArgs,
}) {
late String res;
late TextSpan span;

res = _resolve(key);

res = _replaceLinks(res);

span = _replaceSpanNamedArgs(res, namedArgs);

return span;
}
Comment on lines +72 to +86
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add documentation and consider feature parity with tr method.

The new trSpan method lacks documentation and doesn't support all features available in the tr method. Consider:

  1. Adding documentation with usage examples
  2. Supporting positional arguments and gender like the tr method
  3. Adding error handling for invalid keys

Add documentation and consider implementing feature parity:

+  /// Translates a string key into a [TextSpan] with support for named arguments.
+  /// 
+  /// Example:
+  /// ```dart
+  /// Text.rich(trSpan('msg_named', namedArgs: {'lang': TextSpan(text: 'Dart')}))
+  /// ```
+  /// 
+  /// The translation string should contain named placeholders in curly braces:
+  /// ```json
+  /// {
+  ///   "msg_named": "I love coding in {lang}"
+  /// }
+  /// ```
   TextSpan trSpan(
     String key, {
+    List<TextSpan>? args,
     Map<String, TextSpan>? namedArgs,
+    String? gender,
   }) {
     late String res;
     late TextSpan span;
 
+    if (gender != null) {
+      res = _gender(key, gender: gender);
+    } else {
+      res = _resolve(key);
+    }
-    res = _resolve(key);
 
     res = _replaceLinks(res);
 
     span = _replaceSpanNamedArgs(res, namedArgs);
+    
+    if (args != null) {
+      // TODO: Implement positional args support
+    }
 
     return span;
   }


String _replaceLinks(String res, {bool logging = true}) {
// TODO: add recursion detection and a resolve stack.
final matches = _linkKeyMatcher.allMatches(res);
Expand Down Expand Up @@ -103,6 +120,24 @@ class Localization {
return result;
}

List<String> _splitTextWithNamedArg(String text) {
final matches = _namedArgMatcher.allMatches(text);
var lastIndex = 0;
final result = <String>[];

for (final match in matches) {
result
..add(text.substring(lastIndex, match.start))
..add(match.group(0) ?? '');
lastIndex = match.end;
}
if (lastIndex < text.length) {
result.add(text.substring(lastIndex));
}

return result;
}

String _replaceArgs(String res, List<String>? args) {
if (args == null || args.isEmpty) return res;
for (var str in args) {
Expand All @@ -118,6 +153,15 @@ class Localization {
return res;
}

TextSpan _replaceSpanNamedArgs(String res, Map<String, TextSpan>? args) {
if (args == null || args.isEmpty) return TextSpan(text: res);
final spans = _splitTextWithNamedArg(res).map((part) {
final key = part.replaceAll(RegExp(r'^\{|\}$'), '');
return args[key] ?? TextSpan(text: part);
}).toList();
return TextSpan(children: spans);
}
Comment on lines +156 to +163
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add validation and optimize performance.

The method could benefit from validation and performance improvements:

  1. Validate that all required named arguments are provided
  2. Pre-check if text contains any placeholders before processing

Consider these improvements:

   TextSpan _replaceSpanNamedArgs(String res, Map<String, TextSpan>? args) {
     if (args == null || args.isEmpty) return TextSpan(text: res);
+    
+    // Quick check if text contains any placeholders
+    if (!res.contains('{')) return TextSpan(text: res);
+    
+    // Extract all required argument names
+    final required = _namedArgMatcher
+        .allMatches(res)
+        .map((m) => m.group(2))
+        .where((name) => name != null)
+        .toSet();
+    
+    // Validate all required arguments are provided
+    final missing = required.where((name) => !args.containsKey(name));
+    if (missing.isNotEmpty) {
+      throw ArgumentError('Missing named arguments: ${missing.join(', ')}');
+    }
+    
     final spans = _splitTextWithNamedArg(res).map((part) {
       final key = part.replaceAll(RegExp(r'^\{|\}$'), '');
       return args[key] ?? TextSpan(text: part);
     }).toList();
     return TextSpan(children: spans);
   }

Committable suggestion skipped: line range outside the PR's diff.


static PluralRule? _pluralRule(String? locale, num howMany) {
if (instance._ignorePluralRules) {
return () => _pluralCaseFallback(howMany);
Expand Down Expand Up @@ -150,7 +194,8 @@ class Localization {
late String res;

final pluralRule = _pluralRule(_locale.languageCode, value);
final pluralCase = pluralRule != null ? pluralRule() : _pluralCaseFallback(value);
final pluralCase =
pluralRule != null ? pluralRule() : _pluralCaseFallback(value);

switch (pluralCase) {
case PluralCase.ZERO:
Expand Down Expand Up @@ -193,7 +238,8 @@ class Localization {
if (subKey == 'other') return _resolve('$key.other');

final tag = '$key.$subKey';
var resource = _resolve(tag, logging: false, fallback: _fallbackTranslations != null);
var resource =
_resolve(tag, logging: false, fallback: _fallbackTranslations != null);
if (resource == tag) {
resource = _resolve('$key.other');
}
Expand Down
31 changes: 29 additions & 2 deletions lib/src/public.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,36 @@ String tr(
.tr(key, args: args, namedArgs: namedArgs, gender: gender);
}

/// {@template trSpan}
/// function for translate your language keys
/// [key] Localization key
/// [BuildContext] The location in the tree where this widget builds
/// [namedArgs] Map of localized strings. Replaces the name keys {key_name} with [TextSpan]
///
/// Example:
///
/// ```json
/// {
/// "msg_named":"Easy localization is written in the {lang} language"
/// }
/// ```
/// ```dart
/// Text.rich(trSpan('msg_named',namedArgs: {'lang': TextSpan(text: 'Dart')})),
/// ```
/// {@endtemplate}

TextSpan trSpan(
String key, {
BuildContext? context,
Map<String, TextSpan>? namedArgs,
}) {
return context != null
? Localization.of(context)!.trSpan(key, namedArgs: namedArgs)
: Localization.instance.trSpan(key, namedArgs: namedArgs);
}

bool trExists(String key) {
return Localization.instance
.exists(key);
return Localization.instance.exists(key);
}

/// {@template plural}
Expand Down
16 changes: 16 additions & 0 deletions lib/src/public_ext.dart
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,22 @@ extension BuildContextEasyLocalizationExtension on BuildContext {
);
}

TextSpan trSpan(
String key, {
Map<String, TextSpan>? namedArgs,
}) {
final localization = Localization.of(this);

if (localization == null) {
throw const LocalizationNotFoundException();
}

return localization.trSpan(
key,
namedArgs: namedArgs,
);
}

String plural(
String key,
num number, {
Expand Down