Skip to content

Commit

Permalink
feat: override and arb integration for l10n
Browse files Browse the repository at this point in the history
  • Loading branch information
Tienisto committed Oct 20, 2024
1 parent 34f84ab commit 7b7bcbd
Show file tree
Hide file tree
Showing 16 changed files with 629 additions and 103 deletions.
6 changes: 6 additions & 0 deletions slang/lib/src/api/locale.dart
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,13 @@ class FakeAppLocale extends BaseAppLocale<FakeAppLocale, FakeTranslations> {
@override
final String? countryCode;

final Map<String, ValueFormatter>? types;

FakeAppLocale({
required this.languageCode,
this.scriptCode,
this.countryCode,
this.types,
});

@override
Expand Down Expand Up @@ -156,6 +159,7 @@ class FakeAppLocale extends BaseAppLocale<FakeAppLocale, FakeTranslations> {
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
types: types,
);
}
}
Expand All @@ -167,12 +171,14 @@ class FakeTranslations
Map<String, Node>? overrides,
PluralResolver? cardinalResolver,
PluralResolver? ordinalResolver,
Map<String, ValueFormatter>? types,
int? s,
}) : $meta = TranslationMetadata(
locale: locale,
overrides: overrides ?? {},
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
types: types ?? {},
s: s ?? 0,
),
providedNullOverrides = overrides == null;
Expand Down
1 change: 1 addition & 0 deletions slang/lib/src/api/singleton.dart
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ extension AppLocaleUtilsExt<E extends BaseAppLocale<E, T>,
buildConfig: buildConfig!,
map: digestedMap,
handleLinks: false,
handleTypes: false,
shouldEscapeText: false,
locale: I18nLocale(
language: locale.languageCode,
Expand Down
36 changes: 32 additions & 4 deletions slang/lib/src/api/translation_overrides.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:slang/src/api/formatter.dart';
import 'package:slang/src/api/locale.dart';
import 'package:slang/src/api/pluralization.dart';
import 'package:slang/src/builder/builder/text/l10n_override_parser.dart';
import 'package:slang/src/builder/generator/helper.dart';
import 'package:slang/src/builder/model/node.dart';
import 'package:slang/src/builder/model/pluralization.dart';
Expand Down Expand Up @@ -119,14 +121,39 @@ class TranslationOverrides {

extension TranslationOverridesStringExt on String {
/// Replaces every ${param} with the given parameter
String applyParams(Map<String, Object> param) {
String applyParams(
Map<String, ValueFormatter> existingTypes,
String locale,
Map<String, Object> param,
) {
return replaceDartNormalizedInterpolation(replacer: (match) {
final nodeParam = match.substring(2, match.length - 1);
final providedParam = param[nodeParam];

final colonIndex = nodeParam.indexOf(':');
if (colonIndex == -1) {
// parameter without type
final providedParam = param[nodeParam];
if (providedParam == null) {
return match; // do not replace, keep as is
}
return providedParam.toString();
}

final paramName = nodeParam.substring(0, colonIndex).trim();
final paramType = nodeParam.substring(colonIndex + 1).trim();

final providedParam = param[paramName];
if (providedParam == null) {
return match; // do not replace, keep as is
}
return providedParam.toString();

return digestL10nOverride(
locale: locale,
existingTypes: existingTypes,
type: paramType,
value: providedParam,
) ??
providedParam.toString();
});
}

Expand Down Expand Up @@ -171,6 +198,7 @@ extension TranslationOverridesStringExt on String {
/// Shortcut to call both at once.
String applyParamsAndLinks(
TranslationMetadata meta, Map<String, Object> param) {
return applyParams(param).applyLinks(meta, param);
return applyParams(meta.types, meta.locale.underscoreTag, param)
.applyLinks(meta, param);
}
}
142 changes: 142 additions & 0 deletions slang/lib/src/builder/builder/text/l10n_override_parser.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import 'package:intl/intl.dart';
import 'package:slang/src/api/formatter.dart';
import 'package:slang/src/builder/builder/text/l10n_parser.dart';

class L10nOverrideResult {
final String methodName;
final Map<Symbol, dynamic> params;

L10nOverrideResult({
required this.methodName,
required this.params,
});

String format(Object value) {
final Function f = NumberFormat.currency;
final dynamic formatter = Function.apply(f, const [], params);
return formatter.format(value);
}
}

/// Converts a type definition to an actual value.
/// e.g.
/// - currency -> $3.14
/// - currency(symbol: '€') -> €3.14
String? digestL10nOverride({
required Map<String, ValueFormatter> existingTypes,
required String locale,
required String type,
required Object value,
}) {
final existingType = existingTypes[type];
if (existingType != null) {
// Use existing type formatter directly
return existingType.format(value);
}

final parsed = parseL10nIntermediate(type);
if (parsed == null) {
return null;
}

// Let's parse the method name and arguments

if (numberFormatsWithNamedParameters.contains(parsed.methodName)) {
// named arguments
final arguments = switch (parsed.arguments) {
String args => parseArguments(args),
null => const {},
};
final Function formatterBuilder = switch (parsed.methodName) {
'NumberFormat.compact' => NumberFormat.compact,
'NumberFormat.compactCurrency' => NumberFormat.compactCurrency,
'NumberFormat.compactSimpleCurrency' =>
NumberFormat.compactSimpleCurrency,
'NumberFormat.compactLong' => NumberFormat.compactLong,
'NumberFormat.currency' => NumberFormat.currency,
'NumberFormat.decimalPatternDigits' => NumberFormat.decimalPatternDigits,
'NumberFormat.decimalPercentPattern' =>
NumberFormat.decimalPercentPattern,
'NumberFormat.simpleCurrency' => NumberFormat.simpleCurrency,
_ => throw UnimplementedError('Unknown formatter: ${parsed.methodName}'),
};

final formatter = Function.apply(formatterBuilder, [], {
...arguments,
#locale: locale,
});

return formatter.format(value);
} else {
// positional arguments
final argument = switch (parsed.arguments) {
String args => parseSinglePositionalArgument(args),
null => null,
};
final Function formatterBuilder = switch (parsed.methodName) {
'NumberFormat.decimalPattern' => NumberFormat.decimalPattern,
'NumberFormat.percentPattern' => NumberFormat.percentPattern,
'NumberFormat.scientificPattern' => NumberFormat.scientificPattern,
'NumberFormat' => _numberFormatBuilder,
'DateFormat.yM' => DateFormat.yM,
'DateFormat.yMd' => DateFormat.yMd,
'DateFormat.Hm' => DateFormat.Hm,
'DateFormat.Hms' => DateFormat.Hms,
'DateFormat.jm' => DateFormat.jm,
'DateFormat.jms' => DateFormat.jms,
'DateFormat' => _dateFormatBuilder,
_ => throw UnimplementedError('Unknown formatter: ${parsed.methodName}'),
};

final formatter = Function.apply(
formatterBuilder,
[
if (argument != null) argument,
locale,
],
);
return formatter.format(value);
}
}

Map<Symbol, Object> parseArguments(String arguments) {
final result = <Symbol, Object>{};
final parts = arguments.split(',');
for (final part in parts) {
final keyValue = part.split(':');
if (keyValue.length != 2) {
continue;
}
final key = keyValue[0].trim();
final value = keyValue[1].trim();

if ((value.startsWith("'") && value.endsWith("'")) ||
(value.startsWith('"') && value.endsWith('"'))) {
result[Symbol(key)] = value.substring(1, value.length - 1);
} else {
final number = num.tryParse(value);
if (number != null) {
result[Symbol(key)] = number;
}
}
}
return result;
}

Object? parseSinglePositionalArgument(String argument) {
if ((argument.startsWith("'") && argument.endsWith("'") ||
argument.startsWith('"') && argument.endsWith('"'))) {
return argument.substring(1, argument.length - 1);
} else {
final number = num.tryParse(argument);
return number;
}
}

NumberFormat _numberFormatBuilder(String pattern, String locale) {
return NumberFormat(pattern, locale);
}

DateFormat _dateFormatBuilder(String pattern, String locale) {
return DateFormat(pattern, locale);
}
91 changes: 55 additions & 36 deletions slang/lib/src/builder/builder/text/l10n_parser.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:slang/src/builder/model/i18n_locale.dart';
import 'package:slang/src/builder/utils/regex_utils.dart';

class ParseL10nResult {
/// The actual parameter type.
Expand All @@ -14,7 +15,7 @@ class ParseL10nResult {
});
}

const _numberFormats = {
const numberFormats = {
'compact',
'compactCurrency',
'compactSimpleCurrency',
Expand All @@ -28,17 +29,19 @@ const _numberFormats = {
'simpleCurrency',
};

const _numberFormatsWithNamedParameters = {
const numberFormatsWithNamedParameters = {
'NumberFormat.compact',
'NumberFormat.compactCurrency',
'NumberFormat.compactSimpleCurrency',
'NumberFormat.compactLong',
'NumberFormat.currency',
'NumberFormat.decimalPatternDigits',
'NumberFormat.decimalPercentPattern',
'NumberFormat.simpleCurrency',
};

final _numberFormatsWithClass = {
for (final format in _numberFormats) 'NumberFormat.$format',
final numberFormatsWithClass = {
for (final format in numberFormats) 'NumberFormat.$format',
'NumberFormat',
};

Expand All @@ -56,40 +59,36 @@ final _dateFormatsWithClass = {
'DateFormat',
};

// Parses "currency(symbol: '€')"
// -> paramType: num, format: NumberFormat.currency(symbol: '€', locale: locale).format(value)
ParseL10nResult? parseL10n({
required I18nLocale locale,
required String paramName,
required String type,
}) {
final bracketStart = type.indexOf('(');
class L10nIntermediateResult {
final String paramType;
final String methodName;
final String? arguments;

// The type without parameters.
// E.g. currency(symbol: '€') -> currency
final digestedType =
bracketStart == -1 ? type : type.substring(0, bracketStart);
L10nIntermediateResult({
required this.paramType,
required this.methodName,
required this.arguments,
});
}

final String paramType;
if (_numberFormats.contains(digestedType) ||
_numberFormatsWithClass.contains(digestedType)) {
paramType = 'num';
} else if (_dateFormats.contains(digestedType) ||
_dateFormatsWithClass.contains(digestedType)) {
paramType = 'DateTime';
} else {
L10nIntermediateResult? parseL10nIntermediate(String type) {
final parsed = RegexUtils.formatTypeRegex.firstMatch(type);
if (parsed == null) {
return null;
}

String methodName;
String arguments;
String methodName = parsed.group(1)!;
final arguments = parsed.group(2);

if (bracketStart != -1 && type.endsWith(')')) {
methodName = type.substring(0, bracketStart);
arguments = type.substring(bracketStart + 1, type.length - 1);
final String paramType;
if (numberFormats.contains(methodName) ||
numberFormatsWithClass.contains(methodName)) {
paramType = 'num';
} else if (_dateFormats.contains(methodName) ||
_dateFormatsWithClass.contains(methodName)) {
paramType = 'DateTime';
} else {
methodName = type;
arguments = '';
return null;
}

// Prepend class if necessary
Expand All @@ -104,11 +103,31 @@ ParseL10nResult? parseL10n({
}
}

return L10nIntermediateResult(
paramType: paramType,
methodName: methodName,
arguments: arguments?.trim(),
);
}

// Parses "currency(symbol: '€')"
// -> paramType: num, format: NumberFormat.currency(symbol: '€', locale: locale).format(value)
ParseL10nResult? parseL10n({
required I18nLocale locale,
required String paramName,
required String type,
}) {
final parsed = parseL10nIntermediate(type);
if (parsed == null) {
return null;
}

// Add locale
if (paramType == 'num' &&
_numberFormatsWithNamedParameters.contains(methodName)) {
String arguments = parsed.arguments ?? '';
if (parsed.paramType == 'num' &&
numberFormatsWithNamedParameters.contains(parsed.methodName)) {
// add locale as named parameter
if (arguments.isEmpty) {
if (parsed.arguments == null) {
arguments = "locale: '${locale.underscoreTag}'";
} else {
arguments = "$arguments, locale: '${locale.underscoreTag}'";
Expand All @@ -125,7 +144,7 @@ ParseL10nResult? parseL10n({
}

return ParseL10nResult(
paramType: paramType,
format: '$methodName($arguments).format($paramName)',
paramType: parsed.paramType,
format: '${parsed.methodName}($arguments).format($paramName)',
);
}
Loading

0 comments on commit 7b7bcbd

Please sign in to comment.