Skip to content

Commit

Permalink
fix: should not escape strings when parsed via the "Translation Overr…
Browse files Browse the repository at this point in the history
…ides" feature
  • Loading branch information
Tienisto committed Dec 7, 2023
1 parent 031a1aa commit 5b8c707
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 9 deletions.
4 changes: 4 additions & 0 deletions slang/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 3.26.2

- fix: should not escape strings when parsed via the "Translation Overrides" feature (#177)

## 3.26.1

- fix: generate correct compatibility typedef for `Translations` class (#176)
Expand Down
1 change: 1 addition & 0 deletions slang/lib/api/singleton.dart
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ extension AppLocaleUtilsExt<E extends BaseAppLocale<E, T>,
buildConfig: buildConfig!,
map: digestedMap,
handleLinks: false,
shouldEscapeText: false,
localeDebug: locale.languageTag,
);

Expand Down
16 changes: 15 additions & 1 deletion slang/lib/builder/builder/translation_model_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,19 @@ class TranslationModelBuilder {
/// The map must be of type Map<String, dynamic> and all children may of type
/// String, num, List<dynamic> or Map<String, dynamic>.
///
/// [handleLinks] can be set false to ignore links at leave them as is
/// [handleLinks] can be set false to ignore links and leave them as is
/// e.g. ${_root.greet(name: name} will be ${_root.greet}
/// This is used for "Translation Overrides" where the links are resolved
/// on invocation.
///
/// [shouldEscapeText] can be set false to ignore escaping of text nodes
/// e.g. "Let's go" will be "Let's go" instead of "Let\'s go".
/// Similar to [handleLinks], this is used for "Translation Overrides".
static BuildModelResult build({
required BuildModelConfig buildConfig,
required Map<String, dynamic> map,
bool handleLinks = true,
bool shouldEscapeText = true,
required String localeDebug,
}) {
// flat map for leaves (TextNode, PluralNode, ContextNode)
Expand All @@ -59,6 +66,7 @@ class TranslationModelBuilder {
keyCase: buildConfig.keyCase,
leavesMap: leavesMap,
contextCollection: contextCollection,
shouldEscapeText: shouldEscapeText,
);

// 2nd iteration: Handle parameterized linked translations
Expand Down Expand Up @@ -199,6 +207,7 @@ Map<String, Node> _parseMapNode({
required CaseStyle? keyCase,
required Map<String, LeafNode> leavesMap,
required Map<String, ContextType> contextCollection,
required bool shouldEscapeText,
}) {
final Map<String, Node> resultNodeTree = {};

Expand Down Expand Up @@ -228,6 +237,7 @@ Map<String, Node> _parseMapNode({
modifiers: modifiers,
raw: value.toString(),
comment: comment,
shouldEscape: shouldEscapeText,
interpolation: config.stringInterpolation,
paramCase: config.paramCase,
)
Expand All @@ -237,6 +247,7 @@ Map<String, Node> _parseMapNode({
modifiers: modifiers,
raw: value.toString(),
comment: comment,
shouldEscape: shouldEscapeText,
interpolation: config.stringInterpolation,
paramCase: config.paramCase,
);
Expand All @@ -260,6 +271,7 @@ Map<String, Node> _parseMapNode({
keyCase: config.keyCase,
leavesMap: leavesMap,
contextCollection: contextCollection,
shouldEscapeText: shouldEscapeText,
);

// finally only take their values, ignoring keys
Expand Down Expand Up @@ -287,6 +299,7 @@ Map<String, Node> _parseMapNode({
: config.keyCase,
leavesMap: leavesMap,
contextCollection: contextCollection,
shouldEscapeText: shouldEscapeText,
);

final Node finalNode;
Expand Down Expand Up @@ -338,6 +351,7 @@ Map<String, Node> _parseMapNode({
keyCase: config.keyCase,
leavesMap: leavesMap,
contextCollection: contextCollection,
shouldEscapeText: shouldEscapeText,
).cast<String, RichTextNode>();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ class TranslationModelListBuilder {
/// After this method call, information about the namespace is lost.
/// It will be just a normal parent.
static List<I18nData> build(
RawConfig rawConfig, TranslationMap translationMap) {
RawConfig rawConfig,
TranslationMap translationMap,
) {
final buildConfig = rawConfig.toBuildModelConfig();

return translationMap.getInternalMap().entries.map((localeEntry) {
Expand Down
10 changes: 8 additions & 2 deletions slang/lib/builder/model/node.dart
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ abstract class TextNode extends Node implements LeafNode {

/// Several configs, persisted into node to make it easier to copy
/// See [updateWithLinkParams]
final bool shouldEscape;
final StringInterpolation interpolation;
final CaseStyle? paramCase;

Expand All @@ -238,6 +239,7 @@ abstract class TextNode extends Node implements LeafNode {
required super.modifiers,
required super.comment,
required this.raw,
required this.shouldEscape,
required this.interpolation,
required this.paramCase,
});
Expand Down Expand Up @@ -278,12 +280,13 @@ class StringTextNode extends TextNode {
required super.modifiers,
required super.raw,
required super.comment,
required super.shouldEscape,
required super.interpolation,
required super.paramCase,
Map<String, Set<String>>? linkParamMap,
}) {
final parsedResult = _parseInterpolation(
raw: _escapeContent(raw, interpolation),
raw: shouldEscape ? _escapeContent(raw, interpolation) : raw,
interpolation: interpolation,
paramCase: paramCase,
);
Expand Down Expand Up @@ -317,6 +320,7 @@ class StringTextNode extends TextNode {
modifiers: modifiers,
raw: raw,
comment: comment,
shouldEscape: shouldEscape,
interpolation: interpolation,
paramCase: paramCase,
linkParamMap: linkParamMap,
Expand Down Expand Up @@ -361,12 +365,13 @@ class RichTextNode extends TextNode {
required super.modifiers,
required super.raw,
required super.comment,
required super.shouldEscape,
required super.interpolation,
required super.paramCase,
Map<String, Set<String>>? linkParamMap,
}) {
final rawParsedResult = _parseInterpolation(
raw: _escapeContent(raw, interpolation),
raw: shouldEscape ? _escapeContent(raw, interpolation) : raw,
interpolation: interpolation,
paramCase: null, // param case will be applied later
);
Expand Down Expand Up @@ -437,6 +442,7 @@ class RichTextNode extends TextNode {
modifiers: modifiers,
raw: raw,
comment: comment,
shouldEscape: shouldEscape,
interpolation: interpolation,
paramCase: paramCase,
linkParamMap: linkParamMap,
Expand Down
20 changes: 15 additions & 5 deletions slang/lib/builder/utils/string_interpolation_extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,38 @@ extension StringInterpolationExtensions on String {
int startCharacterLength = 1;
int startIndex = curr.indexOf(_DOLLAR);
if (startIndex == -1) {
// no more matches
buffer.write(curr);
break;
}

// Check if the $ is escaped with a preceding \
if (startIndex >= 1 && curr[startIndex - 1] == '\\') {
// ignore because of preceding \
buffer.write(curr.substring(0, startIndex)); // *do* include \
buffer.write(_DOLLAR);
if (startIndex + 1 < curr.length) {
curr = curr.substring(startIndex + startCharacterLength);
curr = curr.substring(startIndex + 1);
continue;
} else {
break;
}
}

if (startIndex != 0) {
// add prefix
// Add everything before the $ to the buffer
buffer.write(curr.substring(0, startIndex));
}

if (startIndex + 1 < curr.length && curr[startIndex + 1] == '{') {
startCharacterLength = 2; // it is now "${"
if (startIndex + 1 < curr.length) {
final nextCharacter = curr[startIndex + 1];
if (nextCharacter == '{') {
startCharacterLength = 2; // it is now "${"
} else if (nextCharacter.contains(_nonWordRegex)) {
// $ stands alone
buffer.write(_DOLLAR);
curr = curr.substring(startIndex + 1);
continue;
}
}

final endRegex = startCharacterLength == 1 ? _nonWordRegex : '}';
Expand Down
70 changes: 70 additions & 0 deletions slang/test/unit/api/translation_overrides_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import 'package:slang/api/locale.dart';
import 'package:slang/api/singleton.dart';
import 'package:slang/api/translation_overrides.dart';
import 'package:slang/builder/builder/build_model_config_builder.dart';
import 'package:slang/builder/model/raw_config.dart';
import 'package:test/test.dart';

void main() {
group('string', () {
test('Should return a plain string', () {
final meta = _buildMetaWithOverrides({
'aboutPage.title': 'About',
});
final parsed = TranslationOverrides.string(meta, 'aboutPage.title', {});
expect(parsed, 'About');
});

test('Should return a plain string without escaping', () {
final meta = _buildMetaWithOverrides({
'aboutPage.title': 'About \' \$ {arg}',
});
final parsed = TranslationOverrides.string(meta, 'aboutPage.title', {});
expect(parsed, 'About \' \$ {arg}');
});

test('Should return an interpolated string', () {
final meta = _buildMetaWithOverrides({
'aboutPage.title': r"About ${arg}",
});
final parsed = TranslationOverrides.string(meta, 'aboutPage.title', {
'arg': 'Page',
});
expect(parsed, 'About Page');
});

test('Should return an interpolated string with dollar only', () {
final meta = _buildMetaWithOverrides({
'aboutPage.title': r"About $arg",
});
final parsed = TranslationOverrides.string(meta, 'aboutPage.title', {
'arg': 'Page',
});
expect(parsed, 'About Page');
});
});
}

TranslationMetadata<FakeAppLocale, FakeTranslations> _buildMetaWithOverrides(
Map<String, dynamic> overrides,
) {
final utils = _Utils();
return utils
.buildWithOverridesFromMap(
locale: FakeAppLocale(languageCode: 'und'),
isFlatMap: false,
map: overrides,
)
.$meta;
}

class _Utils extends BaseAppLocaleUtils<FakeAppLocale, FakeTranslations> {
_Utils()
: super(
baseLocale: FakeAppLocale(languageCode: 'und'),
locales: [FakeAppLocale(languageCode: 'und')],
buildConfig: _defaultConfig,
);
}

final _defaultConfig = RawConfig.defaultConfig.toBuildModelConfig();
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ void main() {
expect(input.dart(), '} X');
});

test('dollar in the middle', () {
final input = r'$a $ ';
expect(input.dart(), r'X $ ');
});

test('ends with dollar', () {
final input = r'$a $';
expect(input.dart(), r'X $');
Expand Down
2 changes: 2 additions & 0 deletions slang/test/util/text_node_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ StringTextNode textNode(
comment: null,
interpolation: interpolation,
paramCase: paramCase,
shouldEscape: true,
);
}

Expand All @@ -30,5 +31,6 @@ RichTextNode richTextNode(
raw: raw,
interpolation: interpolation,
paramCase: paramCase,
shouldEscape: true,
);
}

0 comments on commit 5b8c707

Please sign in to comment.