Skip to content

Commit

Permalink
feat: sanitize invalid identifiers as keys
Browse files Browse the repository at this point in the history
  • Loading branch information
Tienisto committed Nov 15, 2024
1 parent 9f890e6 commit b0d8995
Show file tree
Hide file tree
Showing 34 changed files with 412 additions and 68 deletions.
45 changes: 45 additions & 0 deletions slang/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,10 @@ translation_class_visibility: private
key_case: snake
key_map_case: camel
param_case: pascal
sanitization:
enabled: true
prefix: k
case: camel
string_interpolation: double_braces
flat_map: false
translation_overrides: false
Expand Down Expand Up @@ -371,6 +375,10 @@ targets:
key_case: snake
key_map_case: camel
param_case: pascal
sanitization:
enabled: true
prefix: k
case: camel
string_interpolation: double_braces
flat_map: false
translation_overrides: false
Expand Down Expand Up @@ -431,6 +439,9 @@ targets:
| `key_case` | `null`, `camel`, `pascal`, `snake` | transform keys (optional) [(i)](#-recasing) | `null` |
| `key_map_case` | `null`, `camel`, `pascal`, `snake` | transform keys for maps (optional) [(i)](#-recasing) | `null` |
| `param_case` | `null`, `camel`, `pascal`, `snake` | transform parameters (optional) [(i)](#-recasing) | `null` |
| `sanitization`/`enabled` | `Boolean` | enable sanitization [(i)](#-sanitization) | `true` |
| `sanitization`/`prefix` | `String` | prefix for sanitization [(i)](#-sanitization) | `k` |
| `sanitization`/`case` | `null`, `camel`, `pascal`, `snake` | case style for sanitization [(i)](#-sanitization) | `camel` |
| `string_interpolation` | `dart`, `braces`, `double_braces` | string interpolation mode [(i)](#-string-interpolation) | `dart` |
| `flat_map` | `Boolean` | generate flat map [(i)](#-dynamic-keys--flat-map) | `true` |
| `translation_overrides` | `Boolean` | enable translation overrides [(i)](#-translation-overrides) | `false` |
Expand Down Expand Up @@ -1420,6 +1431,40 @@ maps:
- myMap # all paths must be cased accordingly
```

### ➤ Sanitization

All keys must be valid Dart identifiers. Slang will automatically sanitize them.

By default, the prefix `k` is added if the key is one of the [reserved words](https://dart.dev/language/keywords) or starts with a number.

As always, you can configure this behavior.

```yaml
# Config
sanitization:
enabled: true
prefix: k
case: camel
```

Now the following key:

```json
{
"continue": "Continue"
}
```

will be sanitized to:

```dart
String get kContinue => 'Continue';
```

**Note:**
Sanitization is happening before resolving [Linked Translations](#-linked-translations).
Therefore, you need to use the sanitized key (e.g. `@:kContinue`).

### ➤ Obfuscation

Obfuscate the translation strings to make reverse engineering harder.
Expand Down
1 change: 1 addition & 0 deletions slang/lib/overrides.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export 'package:slang/src/api/translation_overrides.dart';
export 'package:slang/src/builder/model/build_model_config.dart';
export 'package:slang/src/builder/model/context_type.dart';
export 'package:slang/src/builder/model/enums.dart';
export 'package:slang/src/builder/model/sanitization_config.dart';
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ extension BuildModelConfigBuilder on RawConfig {
keyCase: keyCase,
keyMapCase: keyMapCase,
paramCase: paramCase,
sanitization: sanitization,
stringInterpolation: stringInterpolation,
maps: maps,
pluralAuto: pluralAuto,
Expand Down
28 changes: 26 additions & 2 deletions slang/lib/src/builder/builder/raw_config_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:slang/src/builder/model/i18n_locale.dart';
import 'package:slang/src/builder/model/interface.dart';
import 'package:slang/src/builder/model/obfuscation_config.dart';
import 'package:slang/src/builder/model/raw_config.dart';
import 'package:slang/src/builder/model/sanitization_config.dart';
import 'package:slang/src/builder/utils/map_utils.dart';
import 'package:slang/src/builder/utils/regex_utils.dart';
import 'package:slang/src/builder/utils/string_extensions.dart';
Expand Down Expand Up @@ -54,6 +55,9 @@ class RawConfigBuilder {
);
}

final keyCase =
(map['key_case'] as String?)?.toCaseStyle() ?? RawConfig.defaultKeyCase;

return RawConfig(
baseLocale: I18nLocale.fromString(
map['base_locale'] ?? RawConfig.defaultBaseLocale),
Expand Down Expand Up @@ -82,12 +86,19 @@ class RawConfigBuilder {
(map['translation_class_visibility'] as String?)
?.toTranslationClassVisibility() ??
RawConfig.defaultTranslationClassVisibility,
keyCase: (map['key_case'] as String?)?.toCaseStyle() ??
RawConfig.defaultKeyCase,
keyCase: keyCase,
keyMapCase: (map['key_map_case'] as String?)?.toCaseStyle() ??
RawConfig.defaultKeyMapCase,
paramCase: (map['param_case'] as String?)?.toCaseStyle() ??
RawConfig.defaultParamCase,
sanitization: (map['sanitization'] as Map<String, dynamic>?)
?.toSanitizationConfig(
keyCase ?? SanitizationConfig.defaultCaseStyle) ??
SanitizationConfig(
enabled: SanitizationConfig.defaultEnabled,
prefix: SanitizationConfig.defaultPrefix,
caseStyle: keyCase ?? SanitizationConfig.defaultCaseStyle,
),
stringInterpolation:
(map['string_interpolation'] as String?)?.toStringInterpolation() ??
RawConfig.defaultStringInterpolation,
Expand Down Expand Up @@ -229,6 +240,19 @@ extension on Map<String, dynamic> {
width: this['width'],
);
}

/// Parses the 'sanitization' config
SanitizationConfig toSanitizationConfig(CaseStyle fallbackCase) {
return SanitizationConfig(
enabled: this['enabled'] ?? SanitizationConfig.defaultEnabled,
prefix: this['prefix'] ?? SanitizationConfig.defaultPrefix,
caseStyle: switch (this['case'] as String?) {
String s => s.toCaseStyle() ?? fallbackCase,
// explicit null or not present
null => containsKey('case') ? null : fallbackCase,
},
);
}
}

extension on String {
Expand Down
15 changes: 13 additions & 2 deletions slang/lib/src/builder/builder/translation_model_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import 'package:slang/src/builder/model/node.dart';
import 'package:slang/src/builder/model/pluralization.dart';
import 'package:slang/src/builder/utils/node_utils.dart';
import 'package:slang/src/builder/utils/regex_utils.dart';
import 'package:slang/src/builder/utils/string_extensions.dart';
import 'package:slang/src/builder/utils/reserved_keyword_sanitizer.dart';

class BuildModelResult {
final ObjectNode root; // the actual strings
Expand Down Expand Up @@ -122,6 +122,7 @@ class TranslationModelBuilder {
baseContexts: baseContexts,
shouldEscapeText: shouldEscapeText,
handleTypes: handleTypes,
sanitizeKey: true,
);

// 2nd iteration: Handle parameterized linked translations
Expand Down Expand Up @@ -271,6 +272,7 @@ Map<String, Node> _parseMapNode({
required Map<String, PopulatedContextType>? baseContexts,
required bool shouldEscapeText,
required bool handleTypes,
required bool sanitizeKey,
}) {
final Map<String, Node> resultNodeTree = {};

Expand All @@ -283,7 +285,13 @@ Map<String, Node> _parseMapNode({
final originalKey = key;

final nodePathInfo = NodeUtils.parseModifiers(originalKey);
key = nodePathInfo.path.toCase(keyCase);
key = sanitizeReservedKeyword(
name: nodePathInfo.path,
prefix: config.sanitization.prefix,
sanitizeCaseStyle: config.sanitization.caseStyle,
defaultCaseStyle: keyCase,
sanitize: sanitizeKey && config.sanitization.enabled,
);
final modifiers = nodePathInfo.modifiers;
final currPath = parentPath.isNotEmpty ? '$parentPath.$key' : key;
final currRawPath =
Expand Down Expand Up @@ -352,6 +360,7 @@ Map<String, Node> _parseMapNode({
baseContexts: baseContexts,
shouldEscapeText: shouldEscapeText,
handleTypes: handleTypes,
sanitizeKey: false,
);

// finally only take their values, ignoring keys
Expand Down Expand Up @@ -384,6 +393,7 @@ Map<String, Node> _parseMapNode({
baseContexts: baseContexts,
shouldEscapeText: shouldEscapeText,
handleTypes: handleTypes,
sanitizeKey: true,
);

final Node finalNode;
Expand Down Expand Up @@ -441,6 +451,7 @@ Map<String, Node> _parseMapNode({
baseContexts: baseContexts,
shouldEscapeText: shouldEscapeText,
handleTypes: handleTypes,
sanitizeKey: false,
).cast<String, RichTextNode>();
}

Expand Down
2 changes: 2 additions & 0 deletions slang/lib/src/builder/generator/generate_header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ void _generateBuildConfig({
'\tkeyMapCase: ${config.keyMapCase != null ? 'CaseStyle.${config.keyMapCase!.name}' : 'null'},');
buffer.writeln(
'\tparamCase: ${config.paramCase != null ? 'CaseStyle.${config.paramCase!.name}' : 'null'},');
buffer.writeln(
'\tsanitization: SanitizationConfig(enabled: ${config.sanitization.enabled}, prefix: \'${config.sanitization.prefix}\', caseStyle: ${config.sanitization.caseStyle}),');
buffer.writeln(
'\tstringInterpolation: StringInterpolation.${config.stringInterpolation.name},');
buffer.writeln('\tmaps: [${config.maps.map((m) => "'$m'").join(', ')}],');
Expand Down
3 changes: 3 additions & 0 deletions slang/lib/src/builder/model/build_model_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:slang/src/builder/model/context_type.dart';
import 'package:slang/src/builder/model/enums.dart';
import 'package:slang/src/builder/model/interface.dart';
import 'package:slang/src/builder/model/raw_config.dart';
import 'package:slang/src/builder/model/sanitization_config.dart';

/// Config to generate the model.
/// A subset of [RawConfig].
Expand All @@ -10,6 +11,7 @@ class BuildModelConfig {
final CaseStyle? keyCase;
final CaseStyle? keyMapCase;
final CaseStyle? paramCase;
final SanitizationConfig sanitization;
final StringInterpolation stringInterpolation;
final List<String> maps;
final PluralAuto pluralAuto;
Expand All @@ -24,6 +26,7 @@ class BuildModelConfig {
required this.keyCase,
required this.keyMapCase,
required this.paramCase,
required this.sanitization,
required this.stringInterpolation,
required this.maps,
required this.pluralAuto,
Expand Down
15 changes: 14 additions & 1 deletion slang/lib/src/builder/model/raw_config.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:slang/overrides.dart';
import 'package:slang/src/builder/model/context_type.dart';
import 'package:slang/src/builder/model/enums.dart';
import 'package:slang/src/builder/model/format_config.dart';
Expand Down Expand Up @@ -25,6 +26,11 @@ class RawConfig {
static const CaseStyle? defaultKeyCase = null;
static const CaseStyle? defaultKeyMapCase = null;
static const CaseStyle? defaultParamCase = null;
static const SanitizationConfig defaultSanitization = SanitizationConfig(
enabled: SanitizationConfig.defaultEnabled,
prefix: SanitizationConfig.defaultPrefix,
caseStyle: SanitizationConfig.defaultCaseStyle,
);
static const StringInterpolation defaultStringInterpolation =
StringInterpolation.dart;
static const bool defaultRenderFlatMap = true;
Expand Down Expand Up @@ -64,6 +70,7 @@ class RawConfig {
final CaseStyle? keyCase;
final CaseStyle? keyMapCase;
final CaseStyle? paramCase;
final SanitizationConfig sanitization;
final StringInterpolation stringInterpolation;
final bool renderFlatMap;
final bool translationOverrides;
Expand Down Expand Up @@ -101,6 +108,7 @@ class RawConfig {
required this.keyCase,
required this.keyMapCase,
required this.paramCase,
required this.sanitization,
required StringInterpolation stringInterpolation,
required this.renderFlatMap,
required this.translationOverrides,
Expand Down Expand Up @@ -135,6 +143,7 @@ class RawConfig {
TranslationClassVisibility? translationClassVisibility,
CaseStyle? keyCase,
CaseStyle? keyMapCase,
CaseStyle? paramCase,
bool? renderFlatMap,
bool? translationOverrides,
bool? renderTimestamp,
Expand Down Expand Up @@ -166,7 +175,8 @@ class RawConfig {
translationClassVisibility ?? this.translationClassVisibility,
keyCase: keyCase ?? this.keyCase,
keyMapCase: keyMapCase ?? this.keyMapCase,
paramCase: paramCase,
paramCase: paramCase ?? this.paramCase,
sanitization: sanitization,
stringInterpolation: stringInterpolation,
renderFlatMap: renderFlatMap ?? this.renderFlatMap,
translationOverrides: translationOverrides ?? this.translationOverrides,
Expand Down Expand Up @@ -228,6 +238,8 @@ class RawConfig {
' -> keyCase (for maps): ${keyMapCase != null ? keyMapCase?.name : 'null (no change)'}');
print(
' -> paramCase: ${paramCase != null ? paramCase?.name : 'null (no change)'}');
print(
' -> sanitization: ${sanitization.enabled ? 'enabled' : 'disabled'} / \'${sanitization.prefix}\' / caseStyle: ${sanitization.caseStyle},');
print(' -> stringInterpolation: ${stringInterpolation.name}');
print(' -> renderFlatMap: $renderFlatMap');
print(' -> translationOverrides: $translationOverrides');
Expand Down Expand Up @@ -283,6 +295,7 @@ class RawConfig {
keyCase: RawConfig.defaultKeyCase,
keyMapCase: RawConfig.defaultKeyMapCase,
paramCase: RawConfig.defaultParamCase,
sanitization: RawConfig.defaultSanitization,
stringInterpolation: RawConfig.defaultStringInterpolation,
renderFlatMap: RawConfig.defaultRenderFlatMap,
translationOverrides: RawConfig.defaultTranslationOverrides,
Expand Down
17 changes: 17 additions & 0 deletions slang/lib/src/builder/model/sanitization_config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import 'package:slang/src/builder/model/enums.dart';

class SanitizationConfig {
static const bool defaultEnabled = true;
static const String defaultPrefix = 'k';
static const CaseStyle defaultCaseStyle = CaseStyle.camel;

final bool enabled;
final String prefix;
final CaseStyle? caseStyle;

const SanitizationConfig({
required this.enabled,
required this.prefix,
required this.caseStyle,
});
}
4 changes: 4 additions & 0 deletions slang/lib/src/builder/utils/regex_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,8 @@ class RegexUtils {
/// 3 - json
static final RegExp analysisFileRegex = RegExp(
r'^_(missing_translations|unused_translations)(?:_(.*))?\.(json|yaml|csv)$');

/// Matches if the string starts with a number.
/// Example: 1hello, 2world
static final RegExp startsWithNumber = RegExp(r'^\d');
}
Loading

0 comments on commit b0d8995

Please sign in to comment.