Skip to content

Commit

Permalink
feat: add fallback modifier for maps (#269)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tienisto authored Dec 1, 2024
1 parent 3c4d8af commit e5f934b
Show file tree
Hide file tree
Showing 5 changed files with 488 additions and 73 deletions.
4 changes: 4 additions & 0 deletions slang/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 4.4.0

- feat: add `(fallback)` modifier to fallback entries within a map (#268)

## 4.3.0

- feat: simplify file names without namespaces (#267)
Expand Down
22 changes: 21 additions & 1 deletion slang/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1088,6 +1088,7 @@ Available Modifiers:
|----------------------------|-----------------------------------------------|---------------------------------|
| `(rich)` | This is a rich text. | Leaves, Maps (Plural / Context) |
| `(map)` | This is a map / dictionary (and not a class). | Maps |
| `(fallback)` | Should fallback. `(map)` required. | Maps |
| `(plural)` | This is a plural (type: cardinal) | Maps |
| `(cardinal)` | This is a plural (type: cardinal) | Maps |
| `(ordinal)` | This is a plural (type: ordinal) | Maps |
Expand Down Expand Up @@ -1300,6 +1301,14 @@ By default, you must provide all translations for all locales. Otherwise, you ca

In case of rapid development, you can turn off this feature. Missing translations will fall back to base locale.

The following configurations are available:

| Fallback Strategy | Description |
|----------------------------|-------------------------------------------------------------------|
| `none` | Don't fallback (default). |
| `base_locale` | Fallback to the base locale. |
| `base_locale_empty_string` | Fallback to the base locale. Also treat empty strings as missing. |

```yaml
# Config
base_locale: en
Expand All @@ -1322,7 +1331,18 @@ fallback_strategy: base_locale # add this
}
```

To also treat empty strings as missing translations, set `fallback_strategy: base_locale_empty_string`.
By default, entries inside `(map)` are not affected by the fallback strategy.
This allows you to provide different map entries for each locale.
To still apply the fallback strategy to maps, add the `(fallback)` modifier.

```json5
{
"myMap(map, fallback)": {
"someKey": "Some value",
// missing keys will fallback to the base locale
}
}
```

### ➤ Lazy Loading

Expand Down
53 changes: 44 additions & 9 deletions slang/lib/src/builder/builder/translation_model_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class BuildModelResult {
}

class TranslationModelBuilder {
TranslationModelBuilder._();

/// Builds the i18n model for ONE locale
///
/// The [map] must be of type Map<String, dynamic> and all children may of type
Expand Down Expand Up @@ -381,7 +383,7 @@ Map<String, Node> _parseMapNode({
: null;

// key: { ...value }
children = _parseMapNode(
final tempChildren = _parseMapNode(
locale: locale,
types: types,
parentPath: currPath,
Expand All @@ -402,6 +404,19 @@ Map<String, Node> _parseMapNode({
sanitizeKey: detectedType == null,
);

if (detectedType?.nodeType == _DetectionType.map &&
baseData != null &&
modifiers.containsKey(NodeModifiers.fallback)) {
children = _digestMapEntries(
locale: locale,
baseTranslation: baseData.root,
path: currPath,
entries: tempChildren,
);
} else {
children = tempChildren;
}

detectedType ??=
_determineNodeType(config, currPath, modifiers, children);

Expand Down Expand Up @@ -902,34 +917,54 @@ Map<String, TextNode> _digestContextEntries({
}) {
// Using "late" keyword because we are optimistic that all values are present
late ContextNode baseContextNode =
_findContextNode(baseTranslation, path.split('.'));
_findNode<ContextNode>(baseTranslation, path.split('.'));
return {
for (final value in baseContext.enumValues)
value: entries[value] ??
baseContextNode.entries[value] ??
baseContextNode.entries[value]
?.clone(keepParent: false, locale: locale) ??
_throwError(
'In <${locale.languageTag}>, the value for $value in $path is missing (required by ${baseContext.enumName})',
),
};
}

/// Makes sure that every map entry in [baseTranslation] is also present in [entries].
/// If a value is missing, the base translation is used.
Map<String, Node> _digestMapEntries({
required I18nLocale locale,
required ObjectNode baseTranslation,
required String path,
required Map<String, Node> entries,
}) {
// Using "late" keyword because we are optimistic that all values are present
late ObjectNode baseMapNode =
_findNode<ObjectNode>(baseTranslation, path.split('.'));
return {
for (final entry in baseMapNode.entries.entries)
entry.key: entries[entry.key] ??
baseMapNode.entries[entry.key]!
.clone(keepParent: false, locale: locale),
};
}

Never _throwError(String message) {
throw message;
}

/// Recursively find the [ContextNode] using the given [path].
ContextNode _findContextNode(ObjectNode node, List<String> path) {
/// Recursively find the [Node] using the given [path].
T _findNode<T extends Node>(ObjectNode node, List<String> path) {
final child = node.entries[path[0]];
if (path.length == 1) {
if (child is ContextNode) {
if (child is T) {
return child;
} else {
throw 'Parent node is not a ContextNode but a ${node.runtimeType} at path $path';
throw 'Parent node is not a $T but a ${node.runtimeType} at path $path';
}
} else if (child is ObjectNode) {
return _findContextNode(child, path.sublist(1));
return _findNode(child, path.sublist(1));
} else {
throw 'Cannot find base ContextNode';
throw 'Cannot find base $T';
}
}

Expand Down
Loading

0 comments on commit e5f934b

Please sign in to comment.