Skip to content

Commit

Permalink
feat: user defined types
Browse files Browse the repository at this point in the history
  • Loading branch information
Tienisto committed Oct 20, 2024
1 parent 0ba2142 commit 34f84ab
Show file tree
Hide file tree
Showing 12 changed files with 141 additions and 0 deletions.
21 changes: 21 additions & 0 deletions slang/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,27 @@ Or adjust built-in formats:
}
```

To avoid repetition, you can define custom types via `@@types`.
Please note that the types are locale-specific. If you use [namespaces](#-namespaces), all definitions are merged.

```json
{
"@@types": {
"price": "currency(symbol: 'USD')",
"dateOnly": "DateFormat('MM/dd/yyyy')"
},
"account": "You have {amount: price} in your account",
"today": "Today is {today: dateOnly}",
"tomorrow": "Tomorrow is {tomorrow: dateOnly}"
}
```

```dart
String a = t.account(amount: 1234.56); // You have $1,234.56 in your account
String b = t.today(today: DateTime(2023, 3, 2)); // Today is 03/02/2023
String c = t.tomorrow(tomorrow: DateTime(2023, 3, 5)); // Tomorrow is 03/05/2023
```

### ➤ Interfaces

Often, multiple objects have the same attributes. You can create a common super class for that.
Expand Down
1 change: 1 addition & 0 deletions slang/lib/node.dart
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export 'package:slang/src/api/formatter.dart';
export 'package:slang/src/builder/model/node.dart';
26 changes: 26 additions & 0 deletions slang/lib/src/api/formatter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:intl/intl.dart';

class ValueFormatter {
/// Is either [NumberFormat] or [DateFormat].
/// Unfortunately, there is no super class for both.
final Object Function() _formatter;

/// The actual formatter.
/// We delay the initialization to ensure that intl is already initialized
/// by Flutter before we create the formatter.
late final Object formatter = _formatter();

ValueFormatter(this._formatter);

/// Formats the given [value] with the formatter.
String format(Object value) {
switch (formatter) {
case NumberFormat formatter:
return formatter.format(value as num);
case DateFormat formatter:
return formatter.format(value as DateTime);
default:
throw Exception('Unknown formatter: $formatter');
}
}
}
3 changes: 3 additions & 0 deletions slang/lib/src/api/locale.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:slang/src/api/formatter.dart';
import 'package:slang/src/api/pluralization.dart';
import 'package:slang/src/builder/model/node.dart';

Expand All @@ -17,6 +18,7 @@ class TranslationMetadata<E extends BaseAppLocale<E, T>,
final Map<String, Node> overrides;
final PluralResolver? cardinalResolver;
final PluralResolver? ordinalResolver;
final Map<String, ValueFormatter> types;

/// The secret.
/// Used to decrypt obfuscated translation strings.
Expand All @@ -29,6 +31,7 @@ class TranslationMetadata<E extends BaseAppLocale<E, T>,
required this.overrides,
required this.cardinalResolver,
required this.ordinalResolver,
this.types = const {},
this.s = 0,
});

Expand Down
46 changes: 46 additions & 0 deletions slang/lib/src/builder/builder/translation_model_builder.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:collection';

import 'package:collection/collection.dart';
import 'package:slang/src/builder/builder/text/l10n_parser.dart';
import 'package:slang/src/builder/model/build_model_config.dart';
import 'package:slang/src/builder/model/context_type.dart';
import 'package:slang/src/builder/model/enums.dart';
Expand All @@ -16,11 +17,13 @@ class BuildModelResult {
final ObjectNode root; // the actual strings
final List<Interface> interfaces; // detected interfaces
final List<PopulatedContextType> contexts; // detected context types
final Map<String, String> types; // detected types, values are rendered as is

BuildModelResult({
required this.root,
required this.interfaces,
required this.contexts,
required this.types,
});
}

Expand Down Expand Up @@ -71,6 +74,28 @@ class TranslationModelBuilder {
context.enumName: context.toPending(),
};

final types = <String, FormatTypeInfo>{};
final typesNode = map['@@types'];
if (typesNode != null && typesNode is Map<String, dynamic>) {
for (final entry in typesNode.entries) {
final key = entry.key;
final value = entry.value;
if (value is String) {
final typeInfo = parseL10n(
locale: locale,
paramName: 'value',
type: value,
);
if (typeInfo != null) {
types[key] = FormatTypeInfo(
paramType: typeInfo.paramType,
implementation: typeInfo.format,
);
}
}
}
}

// 1st iteration: Build nodes according to given map
//
// Linked Translations:
Expand All @@ -79,6 +104,7 @@ class TranslationModelBuilder {
// Reason: Not all TextNodes are built, so final parameters are unknown
final resultNodeTree = _parseMapNode(
locale: locale,
types: types,
parentPath: '',
parentRawPath: '',
curr: map,
Expand Down Expand Up @@ -214,6 +240,10 @@ class TranslationModelBuilder {
generateEnum: c.generateEnum,
))
.toList(),
types: {
for (final entry in types.entries)
entry.key: entry.value.implementation,
},
);
}
}
Expand All @@ -222,6 +252,7 @@ class TranslationModelBuilder {
/// and returns the node model.
Map<String, Node> _parseMapNode({
required I18nLocale locale,
required Map<String, FormatTypeInfo> types,
required String parentPath,
required String parentRawPath,
required Map<String, dynamic> curr,
Expand Down Expand Up @@ -267,6 +298,7 @@ Map<String, Node> _parseMapNode({
rawPath: currRawPath,
modifiers: modifiers,
locale: locale,
types: types,
raw: value.toString(),
comment: comment,
shouldEscape: shouldEscapeText,
Expand All @@ -278,6 +310,7 @@ Map<String, Node> _parseMapNode({
rawPath: currRawPath,
modifiers: modifiers,
locale: locale,
types: types,
raw: value.toString(),
comment: comment,
shouldEscape: shouldEscapeText,
Expand All @@ -297,6 +330,7 @@ Map<String, Node> _parseMapNode({
};
children = _parseMapNode(
locale: locale,
types: types,
parentPath: currPath,
parentRawPath: currRawPath,
curr: listAsMap,
Expand All @@ -323,6 +357,7 @@ Map<String, Node> _parseMapNode({
// key: { ...value }
children = _parseMapNode(
locale: locale,
types: types,
parentPath: currPath,
parentRawPath: currRawPath,
curr: value,
Expand Down Expand Up @@ -379,6 +414,7 @@ Map<String, Node> _parseMapNode({
// rebuild children as RichText
digestedMap = _parseMapNode(
locale: locale,
types: types,
parentPath: currPath,
parentRawPath: currRawPath,
curr: {
Expand Down Expand Up @@ -921,3 +957,13 @@ extension on BuildModelConfig {
);
}
}

class FormatTypeInfo {
final String paramType; // num or DateTime
final String implementation; // raw string that will be rendered as is

FormatTypeInfo({
required this.paramType,
required this.implementation,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class TranslationModelListBuilder {
root: baseResult.root,
contexts: baseResult.contexts,
interfaces: baseResult.interfaces,
types: baseResult.types,
);
} else {
final result = TranslationModelBuilder.build(
Expand All @@ -57,6 +58,7 @@ class TranslationModelListBuilder {
root: result.root,
contexts: result.contexts,
interfaces: result.interfaces,
types: result.types,
);
}
}).toList()
Expand Down
9 changes: 9 additions & 0 deletions slang/lib/src/builder/generator/generate_translations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,15 @@ void _generateClass(
buffer.writeln('\t\t overrides: overrides ?? {},');
buffer.writeln('\t\t cardinalResolver: cardinalResolver,');
buffer.writeln('\t\t ordinalResolver: ordinalResolver,');
if (localeData.types.isNotEmpty) {
buffer.writeln('\t\t types: {');
for (final entry in localeData.types.entries) {
buffer.writeln(
'\t\t \'${entry.key}\': ValueFormatter(() => ${entry.value.substring(0, entry.value.length - 14)}),',
);
}
buffer.writeln('\t\t },');
}
if (config.obfuscation.enabled) {
final String method;
final List<int> parts;
Expand Down
2 changes: 2 additions & 0 deletions slang/lib/src/builder/model/i18n_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ class I18nData {
final ObjectNode root; // the actual strings
final List<PopulatedContextType> contexts; // detected context types
final List<Interface> interfaces; // detected interfaces
final Map<String, String> types; // detected types, values are rendered as is

I18nData({
required this.base,
required this.locale,
required this.root,
required this.contexts,
required this.interfaces,
required this.types,
});

/// sorts base locale first, then alphabetically
Expand Down
18 changes: 18 additions & 0 deletions slang/lib/src/builder/model/node.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:slang/src/builder/builder/text/l10n_parser.dart';
import 'package:slang/src/builder/builder/text/param_parser.dart';
import 'package:slang/src/builder/builder/translation_model_builder.dart';
import 'package:slang/src/builder/model/context_type.dart';
import 'package:slang/src/builder/model/enums.dart';
import 'package:slang/src/builder/model/i18n_locale.dart';
Expand Down Expand Up @@ -218,6 +219,9 @@ abstract class TextNode extends Node implements LeafNode {
/// The locale of the text node
final I18nLocale locale;

/// User-defined types for the locale
final Map<String, FormatTypeInfo> types;

/// The original string
final String raw;

Expand Down Expand Up @@ -251,6 +255,7 @@ abstract class TextNode extends Node implements LeafNode {
required super.modifiers,
required super.comment,
required this.locale,
required this.types,
required this.raw,
required this.shouldEscape,
required this.interpolation,
Expand Down Expand Up @@ -297,6 +302,7 @@ class StringTextNode extends TextNode {
required super.rawPath,
required super.modifiers,
required super.locale,
required super.types,
required super.raw,
required super.comment,
required super.shouldEscape,
Expand All @@ -306,6 +312,7 @@ class StringTextNode extends TextNode {
}) {
final parsedResult = _parseInterpolation(
locale: locale,
types: types,
raw: shouldEscape ? _escapeContent(raw, interpolation) : raw,
interpolation: interpolation,
defaultType: 'Object',
Expand Down Expand Up @@ -341,6 +348,7 @@ class StringTextNode extends TextNode {
rawPath: rawPath,
modifiers: modifiers,
locale: locale,
types: types,
raw: raw,
comment: comment,
shouldEscape: shouldEscape,
Expand Down Expand Up @@ -392,6 +400,7 @@ class RichTextNode extends TextNode {
required super.rawPath,
required super.modifiers,
required super.locale,
required super.types,
required super.raw,
required super.comment,
required super.shouldEscape,
Expand All @@ -401,6 +410,7 @@ class RichTextNode extends TextNode {
}) {
final rawParsedResult = _parseInterpolation(
locale: locale,
types: types,
raw: shouldEscape ? _escapeContent(raw, interpolation) : raw,
interpolation: interpolation,
defaultType: 'ignored',
Expand Down Expand Up @@ -477,6 +487,7 @@ class RichTextNode extends TextNode {
rawPath: rawPath,
modifiers: modifiers,
locale: locale,
types: types,
raw: raw,
comment: comment,
shouldEscape: shouldEscape,
Expand Down Expand Up @@ -542,6 +553,7 @@ class _ParseInterpolationResult {

_ParseInterpolationResult _parseInterpolation({
required I18nLocale locale,
required Map<String, FormatTypeInfo> types,
required String raw,
required StringInterpolation interpolation,
required String defaultType,
Expand All @@ -558,6 +570,12 @@ _ParseInterpolationResult _parseInterpolation({
caseStyle: paramCase,
);

final existingType = types[parsedParam.paramType];
if (existingType != null) {
params[parsedParam.paramName] = existingType.paramType;
return '\${_root.\$meta.types[\'${parsedParam.paramType}\']!.format(${parsedParam.paramName})}';
}

final parsedL10n = parseL10n(
locale: locale,
paramName: parsedParam.paramName,
Expand Down
10 changes: 10 additions & 0 deletions slang/lib/src/builder/model/translation_map.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ class TranslationMap {
}

_internalMap[locale]![namespace] = translations;

// Copy types of each namespace to the global types map,
// merging them with existing types.
final typesMap = translations['@@types'] as Map<String, dynamic>?;
if (typesMap != null) {
_internalMap[locale]!['@@types'] = {
...?_internalMap[locale]!['@@types'],
...typesMap,
};
}
}

/// Return all locales specified in this map
Expand Down
1 change: 1 addition & 0 deletions slang/test/unit/model/i18n_data_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ I18nData _i18n(String locale, [bool base = false]) {
),
contexts: [],
interfaces: [],
types: {},
);
}

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 @@ -15,6 +15,7 @@ StringTextNode textNode(
rawPath: '',
modifiers: {},
locale: _locale,
types: {},
raw: raw,
comment: null,
interpolation: interpolation,
Expand All @@ -36,6 +37,7 @@ RichTextNode richTextNode(
modifiers: {},
comment: null,
locale: _locale,
types: {},
raw: raw,
interpolation: interpolation,
paramCase: paramCase,
Expand Down

0 comments on commit 34f84ab

Please sign in to comment.