Skip to content

Commit

Permalink
feat: add outdated workflow
Browse files Browse the repository at this point in the history
  • Loading branch information
Tienisto committed Mar 29, 2023
1 parent babedc7 commit 3958564
Show file tree
Hide file tree
Showing 19 changed files with 694 additions and 138 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@ jobs:
run: flutter analyze
working-directory: slang
- name: Test (core)
run: flutter test
run: dart test
working-directory: slang
6 changes: 6 additions & 0 deletions slang/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 3.15.0

- feat: add `OUTDATED` modifier to flag translations as outdated (`slang analyze` will treat them as missing)
- feat: run `flutter pub run slang outdated my.key.path` to flag translations as outdated
- feat: `slang apply` prefers modifiers from base locale over secondary locales

## 3.14.0

- feat: `LocaleSettings.useDeviceLocale` listens to device locale changes
Expand Down
35 changes: 33 additions & 2 deletions slang/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ String i = page1.title; // type-safe call
- [Main Command](#-main-command)
- [Analyze Translations](#-analyze-translations)
- [Apply Translations](#-apply-translations)
- [Outdated Translations](#-outdated-translations)
- [Migration](#-migration)
- [ARB](#arb)
- [Statistics](#-statistics)
Expand Down Expand Up @@ -944,8 +945,14 @@ Available Modifiers:
| `(param=<Param Name>)` | This has the parameter `<Param Name>` | Maps (Plural / Context) |
| `(interface=<I>)` | Container of interfaces of type `I` | Map/List containing Maps |
| `(singleInterface=<I>)` | This is an interface of type `I` | Maps |
| `(ignoreMissing)` | Ignore missing translations during analysis | All nodes |
| `(ignoreUnused)` | Ignore unused translations during analysis | All nodes |

Analysis Modifiers (only used for the analysis tool):

| Modifier | Meaning | Applicable for |
|-------------------|---------------------------------------------|----------------|
| `(ignoreMissing)` | Ignore missing translations during analysis | All nodes |
| `(ignoreUnused)` | Ignore unused translations during analysis | All nodes |
| `(OUTDATED)` | Flagged as outdated for secondary locales | All nodes |

### ➤ Locale Enum

Expand Down Expand Up @@ -1351,6 +1358,30 @@ flutter pub run slang apply [--locale=fr-FR] [--outdir=assets/i18n]
| `--locale=<locale>` | Apply only one specific locale |
| `--outdir=<dir>` | Path of analysis output (`input_directory` by default) |

### ➤ Outdated Translations

You want to update an existing string, but you want to keep the old translations for other locales?

Here, you can run a simple command to flag translations as `OUTDATED`. They will show up in `_missing_translations` when running `analyze`.

```sh
flutter pub run slang outdated a.b.c
```

This will add an `(OUTDATED)` modifier to all secondary locales.

```json5
{
"a": {
"b": {
"c(OUTDATED)": "This translation is outdated"
}
}
}
```

You can also add these flags manually!

### ➤ Migration

There are some tools to make migration from other i18n solutions easier.
Expand Down
5 changes: 5 additions & 0 deletions slang/bin/outdated.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import 'slang.dart' as mainRunner;

void main(List<String> arguments) async {
mainRunner.main(['outdated', ...arguments]);
}
22 changes: 19 additions & 3 deletions slang/bin/slang.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:slang/builder/model/translation_file.dart';
import 'package:slang/runner/analyze.dart';
import 'package:slang/runner/apply.dart';
import 'package:slang/runner/migrate.dart';
import 'package:slang/runner/outdated.dart';
import 'package:slang/runner/stats.dart';
import 'package:slang/builder/utils/file_utils.dart';
import 'package:slang/builder/utils/path_utils.dart';
Expand All @@ -22,6 +23,7 @@ enum RunnerMode {
analyze, // generate missing translations
apply, // apply translations from analyze
migrate, // migration tool
outdated, // add 'OUTDATED' modifier to secondary locales
}

/// To run this:
Expand Down Expand Up @@ -49,6 +51,9 @@ void main(List<String> arguments) async {
case 'migrate':
mode = RunnerMode.migrate;
break;
case 'outdated':
mode = RunnerMode.outdated;
break;
default:
mode = RunnerMode.generate;
}
Expand All @@ -75,6 +80,9 @@ void main(List<String> arguments) async {
break;
case RunnerMode.migrate:
break;
case RunnerMode.outdated:
print('Adding "OUTDATED" flag...');
break;
}

final stopwatch = Stopwatch();
Expand All @@ -86,11 +94,12 @@ void main(List<String> arguments) async {
final fileCollection = await readFileCollection(verbose: verbose);

// the actual runner
final filteredArguments = arguments.skip(1).toList();
switch (mode) {
case RunnerMode.apply:
await runApplyTranslations(
fileCollection: fileCollection,
arguments: arguments,
arguments: filteredArguments,
);
break;
case RunnerMode.watch:
Expand All @@ -105,11 +114,18 @@ void main(List<String> arguments) async {
files: fileCollection.translationFiles,
verbose: verbose,
stopwatch: stopwatch,
arguments: arguments,
arguments: filteredArguments,
);
break;
case RunnerMode.migrate:
await runMigrate(arguments.skip(1).toList());
await runMigrate(filteredArguments);
break;
case RunnerMode.outdated:
await runOutdated(
fileCollection: fileCollection,
arguments: filteredArguments,
);
break;
}
}

Expand Down
55 changes: 5 additions & 50 deletions slang/lib/builder/builder/translation_model_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:slang/builder/model/context_type.dart';
import 'package:slang/builder/model/interface.dart';
import 'package:slang/builder/model/node.dart';
import 'package:slang/builder/model/pluralization.dart';
import 'package:slang/builder/utils/node_utils.dart';
import 'package:slang/builder/utils/regex_utils.dart';
import 'package:slang/builder/utils/string_extensions.dart';

Expand Down Expand Up @@ -196,15 +197,13 @@ Map<String, Node> _parseMapNode({

final originalKey = key;

// transform key if necessary
// the part after '(' is considered as the modifier
key = key.split('(').first.toCase(keyCase);
final nodePathInfo = NodeUtils.parseModifiers(originalKey);
key = nodePathInfo.path.toCase(keyCase);
final modifiers = nodePathInfo.modifiers;
final currPath = parentPath.isNotEmpty ? '$parentPath.$key' : key;
final currRawPath =
parentRawPath.isNotEmpty ? '$parentRawPath.$originalKey' : originalKey;

final comment = _parseCommentNode(curr['@$key']);
final modifiers = _parseModifiers(originalKey);

if (value is String || value is num) {
// leaf
Expand Down Expand Up @@ -318,7 +317,7 @@ Map<String, Node> _parseMapNode({
parentRawPath: currRawPath,
curr: {
for (final cKey in digestedMap.keys)
cKey._withModifier(NodeModifiers.rich): value[cKey],
cKey.withModifier(NodeModifiers.rich): value[cKey],
},
config: config,
keyCase: config.keyCase,
Expand Down Expand Up @@ -727,35 +726,6 @@ void _fixEmptyLists({
});
}

/// Returns a map containing modifiers
/// greet(param: gender, rich)
/// will result in
/// {param: gender, rich: rich)
Map<String, String> _parseModifiers(String originalKey) {
final String? modifierSection =
RegexUtils.modifierRegex.firstMatch(originalKey)?.group(1);
if (modifierSection == null) {
return {};
}

final modifiers = modifierSection.split(',');
final resultMap = <String, String>{};
for (final modifier in modifiers) {
if (modifier.contains('=')) {
final parts = modifier.split('=');
if (parts.length != 2) {
throw 'Hints must be in format "key: value" or "key"';
}

resultMap[parts[0].trim()] = parts[1].trim();
} else {
final modifierDigested = modifier.trim();
resultMap[modifierDigested] = modifierDigested;
}
}
return resultMap;
}

enum _DetectionType {
classType,
map,
Expand Down Expand Up @@ -819,18 +789,3 @@ extension on BuildModelConfig {
);
}
}

extension on String {
/// Add a modifier to a key
/// myKey -> myKey(modifier)
/// myKey(rich) -> myKey(rich,modifier)
String _withModifier(String mKey, [String? mValue]) {
final modifier = '$mKey${mValue != null ? '=$mValue' : ''}';
if (this.contains('(')) {
final trimmed = this.trim();
return '${trimmed.substring(0, trimmed.length - 1)},$modifier)';
} else {
return '$this($modifier)';
}
}
}
1 change: 1 addition & 0 deletions slang/lib/builder/model/node.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class NodeModifiers {
static const singleInterface = 'singleInterface';
static const ignoreMissing = 'ignoreMissing';
static const ignoreUnused = 'ignoreUnused';
static const outdated = 'OUTDATED';
}

/// the super class of every node
Expand Down
52 changes: 52 additions & 0 deletions slang/lib/builder/utils/map_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,58 @@ class MapUtils {
return false;
}
}

/// Updates an existing entry at [path].
/// Modifiers are ignored and should be not included in the [path].
/// The [update] function is called with the key and value of the entry.
/// The return value of the [update] function is used to update the entry.
/// It updates the entry in place.
static void updateEntry({
required Map<String, dynamic> map,
required String path,
required MapEntry<String, dynamic> Function(String key, Object path) update,
}) {
final pathList = path.split('.');

Map<String, dynamic> currMap = map;

for (int i = 0; i < pathList.length; i++) {
final subPath = pathList[i];
final entryList = currMap.entries.toList();
final entryIndex = entryList.indexWhere((entry) {
final key = entry.key;
if (key.contains('(')) {
return key.substring(0, key.indexOf('(')) == subPath;
}
return key == subPath;
});
if (entryIndex == -1) {
throw 'The leaf "$path" cannot be updated because it does not exist.';
}
final MapEntry<String, dynamic> currEntry = entryList[entryIndex];

if (i == pathList.length - 1) {
// destination
final updated = update(currEntry.key, currEntry.value);

if (currEntry.key == updated.key) {
// key did not change
currMap[currEntry.key] = updated.value;
} else {
// key changed, we need to reconstruct the map to keep the order
currMap.clear();
currMap.addEntries(entryList.take(entryIndex));
currMap[updated.key] = updated.value;
currMap.addEntries(entryList.skip(entryIndex + 1));
}
} else {
if (currEntry.value is! Map<String, dynamic>) {
throw 'The leaf "$path" cannot be updated because "$subPath" is not a map.';
}
currMap = currEntry.value;
}
}
}
}

/// Helper function for [deepCast] handling lists
Expand Down
Loading

0 comments on commit 3958564

Please sign in to comment.