Skip to content

Commit

Permalink
fix: context enums should use fallback
Browse files Browse the repository at this point in the history
  • Loading branch information
Tienisto committed Jan 21, 2024
1 parent a1cbe1e commit 3414630
Show file tree
Hide file tree
Showing 9 changed files with 427 additions and 16 deletions.
78 changes: 77 additions & 1 deletion slang/lib/builder/builder/translation_model_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ class BuildModelResult {
class TranslationModelBuilder {
/// Builds the i18n model for ONE locale
///
/// The map must be of type Map<String, dynamic> and all children may of type
/// The [map] must be of type Map<String, dynamic> and all children may of type
/// String, num, List<dynamic> or Map<String, dynamic>.
///
/// If [baseData] is set and [BuildModelConfig.fallbackStrategy] is [FallbackStrategy.baseLocale],
/// then the base translations will be added to contexts where the translation is missing.
///
/// [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
Expand All @@ -40,13 +43,28 @@ class TranslationModelBuilder {
static BuildModelResult build({
required BuildModelConfig buildConfig,
required Map<String, dynamic> map,
BuildModelResult? baseData,
bool handleLinks = true,
bool shouldEscapeText = true,
required String localeDebug,
}) {
// flat map for leaves (TextNode, PluralNode, ContextNode)
final Map<String, LeafNode> leavesMap = {};

// base contexts to be used for fallback
final Map<String, PopulatedContextType>? baseContexts = baseData == null ||
baseData.contexts.isEmpty ||
buildConfig.fallbackStrategy == FallbackStrategy.none
? null
: {
for (final c in baseData.contexts)
c.enumName: PopulatedContextType(
enumName: c.enumName,
enumValues: c.enumValues,
generateEnum: c.generateEnum,
),
};

final contextCollection = {
for (final context in buildConfig.contexts) context.enumName: context,
};
Expand All @@ -66,6 +84,8 @@ class TranslationModelBuilder {
keyCase: buildConfig.keyCase,
leavesMap: leavesMap,
contextCollection: contextCollection,
baseData: baseData,
baseContexts: baseContexts,
shouldEscapeText: shouldEscapeText,
);

Expand Down Expand Up @@ -213,6 +233,8 @@ Map<String, Node> _parseMapNode({
required CaseStyle? keyCase,
required Map<String, LeafNode> leavesMap,
required Map<String, ContextType> contextCollection,
required BuildModelResult? baseData,
required Map<String, PopulatedContextType>? baseContexts,
required bool shouldEscapeText,
}) {
final Map<String, Node> resultNodeTree = {};
Expand Down Expand Up @@ -284,6 +306,8 @@ Map<String, Node> _parseMapNode({
keyCase: config.keyCase,
leavesMap: leavesMap,
contextCollection: contextCollection,
baseData: baseData,
baseContexts: baseContexts,
shouldEscapeText: shouldEscapeText,
);

Expand Down Expand Up @@ -312,6 +336,8 @@ Map<String, Node> _parseMapNode({
: config.keyCase,
leavesMap: leavesMap,
contextCollection: contextCollection,
baseData: baseData,
baseContexts: baseContexts,
shouldEscapeText: shouldEscapeText,
);

Expand Down Expand Up @@ -365,6 +391,8 @@ Map<String, Node> _parseMapNode({
keyCase: config.keyCase,
leavesMap: leavesMap,
contextCollection: contextCollection,
baseData: baseData,
baseContexts: baseContexts,
shouldEscapeText: shouldEscapeText,
).cast<String, RichTextNode>();
}
Expand All @@ -385,6 +413,21 @@ Map<String, Node> _parseMapNode({
contextCollection[context.enumName] = context;
}

if (config.fallbackStrategy == FallbackStrategy.baseLocale ||
config.fallbackStrategy ==
FallbackStrategy.baseLocaleEmptyString) {
// add base context values if necessary
final baseContext = baseContexts?[context.enumName];
if (baseContext != null) {
digestedMap = _digestContextEntries(
baseTranslation: baseData!.root,
baseContext: baseContext,
path: '$currPath',
entries: digestedMap,
);
}
}

finalNode = ContextNode(
path: currPath,
rawPath: currRawPath,
Expand Down Expand Up @@ -780,6 +823,39 @@ void _fixEmptyLists({
});
}

/// Makes sure that every enum value in [baseContext] is also present in [entries].
/// If a value is missing, the base translation is used.
Map<String, TextNode> _digestContextEntries({
required ObjectNode baseTranslation,
required PopulatedContextType baseContext,
required String path,
required Map<String, TextNode> entries,
}) {
// Using "late" keyword because we are optimistic that all values are present
late ContextNode baseContextNode =
_findContextNode(baseTranslation, path.split('.'));
return {
for (final value in baseContext.enumValues)
value: entries[value] ?? baseContextNode.entries[value]!,
};
}

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

enum _DetectionType {
classType,
map,
Expand Down
51 changes: 39 additions & 12 deletions slang/lib/builder/builder/translation_model_list_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,49 @@ class TranslationModelListBuilder {
) {
final buildConfig = rawConfig.toBuildModelConfig();

final baseEntry = translationMap.getInternalMap().entries.firstWhere(
(entry) => entry.key == rawConfig.baseLocale,
orElse: () => throw Exception('Base locale not found'),
);

// Create the base data first.
final namespaces = baseEntry.value;
final baseResult = TranslationModelBuilder.build(
buildConfig: buildConfig,
map: rawConfig.namespaces ? namespaces : namespaces.values.first,
localeDebug: baseEntry.key.languageTag,
);

return translationMap.getInternalMap().entries.map((localeEntry) {
final locale = localeEntry.key;
final namespaces = localeEntry.value;
final result = TranslationModelBuilder.build(
buildConfig: buildConfig,
map: rawConfig.namespaces ? namespaces : namespaces.values.first,
localeDebug: locale.languageTag,
);
final base = locale == rawConfig.baseLocale;

if (base) {
// Use the already computed base data
return I18nData(
base: true,
locale: locale,
root: baseResult.root,
contexts: baseResult.contexts,
interfaces: baseResult.interfaces,
);
} else {
final result = TranslationModelBuilder.build(
buildConfig: buildConfig,
map: rawConfig.namespaces ? namespaces : namespaces.values.first,
baseData: baseResult,
localeDebug: locale.languageTag,
);

return I18nData(
base: rawConfig.baseLocale == locale,
locale: locale,
root: result.root,
contexts: result.contexts,
interfaces: result.interfaces,
);
return I18nData(
base: rawConfig.baseLocale == locale,
locale: locale,
root: result.root,
contexts: result.contexts,
interfaces: result.interfaces,
);
}
}).toList()
..sort(I18nData.generationComparator);
}
Expand Down
2 changes: 1 addition & 1 deletion slang/lib/builder/model/raw_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'package:slang/builder/model/i18n_locale.dart';
import 'package:slang/builder/model/interface.dart';
import 'package:slang/builder/model/obfuscation_config.dart';

/// represents a build.yaml
/// represents a build.yaml or a slang.yaml file
class RawConfig {
static const String defaultBaseLocale = 'en';
static const FallbackStrategy defaultFallbackStrategy = FallbackStrategy.none;
Expand Down
34 changes: 33 additions & 1 deletion slang/test/integration/main/fallback_base_locale_test.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:slang/builder/builder/raw_config_builder.dart';
import 'package:slang/builder/decoder/csv_decoder.dart';
import 'package:slang/builder/decoder/json_decoder.dart';
import 'package:slang/builder/generator_facade.dart';
import 'package:slang/builder/model/enums.dart';
import 'package:slang/builder/model/i18n_locale.dart';
Expand All @@ -13,15 +14,25 @@ void main() {
late String buildYaml;
late String expectedOutput;

late String specialEnInput;
late String specialDeInput;
late String specialExpectedOutput;

setUp(() {
compactInput = loadResource('main/csv_compact.csv');
buildYaml = loadResource('main/build_config.yaml');
expectedOutput = loadResource(
'main/_expected_fallback_base_locale.output',
);

specialEnInput = loadResource('main/fallback_en.json');
specialDeInput = loadResource('main/fallback_de.json');
specialExpectedOutput = loadResource(
'main/_expected_fallback_base_locale_special.output',
);
});

test('translation overrides', () {
test('fallback with generic integration data', () {
final parsed = CsvDecoder().decode(compactInput);

final result = GeneratorFacade.generate(
Expand All @@ -43,4 +54,25 @@ void main() {

expect(result.joinAsSingleOutput(), expectedOutput);
});

test('fallback with special integration data', () {
final result = GeneratorFacade.generate(
rawConfig: RawConfigBuilder.fromYaml(buildYaml)!.copyWith(
fallbackStrategy: FallbackStrategy.baseLocale,
),
baseName: 'translations',
translationMap: TranslationMap()
..addTranslations(
locale: I18nLocale.fromString('en'),
translations: JsonDecoder().decode(specialEnInput),
)
..addTranslations(
locale: I18nLocale.fromString('de'),
translations: JsonDecoder().decode(specialDeInput),
),
inputDirectoryHint: 'fake/path/integration',
);

expect(result.joinAsSingleOutput(), specialExpectedOutput);
});
}
Loading

0 comments on commit 3414630

Please sign in to comment.