Skip to content

Commit 8cf55b9

Browse files
committed
feat: add slang_mcp
1 parent b0e6e8e commit 8cf55b9

File tree

20 files changed

+703
-151
lines changed

20 files changed

+703
-151
lines changed

slang/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 4.12.0
2+
3+
- feat: announcing [slang_mcp](https://pub.dev/packages/slang_mcp), a new package to work with LLMs more efficiently
4+
- fix: $wip should handle nested parenthesis
5+
16
## 4.11.2
27

38
- fix: do not throw an error on empty translation files (#335)

slang/bin/slang.dart

Lines changed: 14 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,22 @@
11
import 'dart:io';
22

33
import 'package:slang/src/builder/builder/slang_file_collection_builder.dart';
4-
import 'package:slang/src/builder/builder/translation_map_builder.dart';
5-
import 'package:slang/src/builder/generator_facade.dart';
64
import 'package:slang/src/builder/model/raw_config.dart';
75
import 'package:slang/src/builder/model/slang_file_collection.dart';
8-
import 'package:slang/src/builder/utils/file_utils.dart';
96
import 'package:slang/src/builder/utils/path_utils.dart';
107
import 'package:slang/src/runner/analyze.dart';
118
import 'package:slang/src/runner/apply.dart';
129
import 'package:slang/src/runner/clean.dart';
1310
import 'package:slang/src/runner/configure.dart';
1411
import 'package:slang/src/runner/edit.dart';
12+
import 'package:slang/src/runner/generate.dart';
1513
import 'package:slang/src/runner/help.dart';
1614
import 'package:slang/src/runner/migrate.dart';
1715
import 'package:slang/src/runner/normalize.dart';
1816
import 'package:slang/src/runner/stats.dart';
19-
import 'package:slang/src/runner/utils/format.dart';
2017
import 'package:slang/src/runner/wip.dart';
2118
import 'package:slang/src/utils/log.dart' as log;
19+
import 'package:slang/src/utils/stopwatch.dart';
2220
import 'package:watcher/watcher.dart';
2321

2422
/// Determines what the runner will do
@@ -168,14 +166,23 @@ void main(List<String> arguments) async {
168166
arguments: filteredArguments,
169167
);
170168
break;
171-
case RunnerMode.generate:
172169
case RunnerMode.stats:
170+
await runStats(
171+
fileCollection: fileCollection,
172+
stopwatch: stopwatch,
173+
);
174+
break;
173175
case RunnerMode.analyze:
176+
await runAnalyzeTranslations(
177+
fileCollection: fileCollection,
178+
arguments: filteredArguments,
179+
stopwatch: stopwatch,
180+
);
181+
break;
182+
case RunnerMode.generate:
174183
await generateTranslations(
175-
mode: mode,
176184
fileCollection: fileCollection,
177185
stopwatch: stopwatch,
178-
arguments: filteredArguments,
179186
);
180187
break;
181188
case RunnerMode.migrate:
@@ -282,7 +289,6 @@ Future<void> _generateTranslationsFromWatch({
282289
bool success = true;
283290
try {
284291
await generateTranslations(
285-
mode: RunnerMode.watch,
286292
fileCollection: SlangFileCollectionBuilder.fromFileModel(
287293
config: config,
288294
files: newFiles,
@@ -308,114 +314,6 @@ Future<void> _generateTranslationsFromWatch({
308314
}
309315
}
310316

311-
/// Reads the translations from hard drive and generates the g.dart file
312-
/// The [files] are already filtered (only translation files!).
313-
Future<void> generateTranslations({
314-
required RunnerMode mode,
315-
required SlangFileCollection fileCollection,
316-
Stopwatch? stopwatch,
317-
List<String>? arguments,
318-
}) async {
319-
if (fileCollection.files.isEmpty) {
320-
log.error('No translation file found.');
321-
return;
322-
}
323-
324-
// STEP 1: determine base name and output file name / path
325-
final outputFilePath = fileCollection.determineOutputPath();
326-
327-
// STEP 2: scan translations
328-
log.verbose('Scanning translations...\n');
329-
330-
final translationMap = await TranslationMapBuilder.build(
331-
fileCollection: fileCollection,
332-
);
333-
334-
if (mode == RunnerMode.stats) {
335-
getStats(
336-
rawConfig: fileCollection.config,
337-
translationMap: translationMap,
338-
).printResult();
339-
if (stopwatch != null) {
340-
log.info('\nScan done. (${stopwatch.elapsed})');
341-
}
342-
return; // skip generation
343-
} else if (mode == RunnerMode.analyze) {
344-
runAnalyzeTranslations(
345-
rawConfig: fileCollection.config,
346-
translationMap: translationMap,
347-
arguments: arguments ?? [],
348-
);
349-
if (stopwatch != null) {
350-
log.info('Analysis done. ${stopwatch.elapsedSeconds}');
351-
}
352-
return; // skip generation
353-
}
354-
355-
// STEP 3: generate .g.dart content
356-
final result = GeneratorFacade.generate(
357-
rawConfig: fileCollection.config,
358-
translationMap: translationMap,
359-
inputDirectoryHint: fileCollection.determineInputPath(),
360-
);
361-
362-
// STEP 4: write output to hard drive
363-
FileUtils.createMissingFolders(filePath: outputFilePath);
364-
365-
FileUtils.writeFile(
366-
path: BuildResultPaths.mainPath(outputFilePath),
367-
content: result.main,
368-
);
369-
for (final entry in result.translations.entries) {
370-
final locale = entry.key;
371-
final localeTranslations = entry.value;
372-
FileUtils.writeFile(
373-
path: BuildResultPaths.localePath(
374-
outputPath: outputFilePath,
375-
locale: locale,
376-
),
377-
content: localeTranslations,
378-
);
379-
}
380-
381-
if (log.level == log.Level.verbose) {
382-
log.verbose('\nOutput:');
383-
log.verbose(' -> $outputFilePath');
384-
for (final locale in result.translations.keys) {
385-
log.verbose(' -> ${BuildResultPaths.localePath(
386-
outputPath: outputFilePath,
387-
locale: locale,
388-
)}');
389-
}
390-
}
391-
392-
if (fileCollection.config.format.enabled) {
393-
final formatDir = PathUtils.getParentPath(outputFilePath)!;
394-
Stopwatch? formatStopwatch;
395-
if (log.level == log.Level.verbose) {
396-
log.verbose('\nFormatting "$formatDir" ...');
397-
if (stopwatch != null) {
398-
formatStopwatch = Stopwatch()..start();
399-
}
400-
}
401-
await runDartFormat(
402-
dir: formatDir,
403-
width: fileCollection.config.format.width,
404-
);
405-
if (formatStopwatch != null) {
406-
log.verbose('Format done. ${formatStopwatch.elapsedSeconds}');
407-
}
408-
}
409-
410-
if (stopwatch != null) {
411-
if (log.level == log.Level.verbose) {
412-
log.verbose('');
413-
}
414-
log.info(
415-
'${_green}Translations generated successfully. ${stopwatch.elapsedSeconds}$_reset');
416-
}
417-
}
418-
419317
// returns current time in HH:mm:ss
420318
String get currentTime {
421319
final now = DateTime.now();
@@ -444,9 +342,3 @@ const _green = '\x1B[32m';
444342
const _yellow = '\x1B[33m';
445343
const _red = '\x1B[31m';
446344
const _reset = '\x1B[0m';
447-
448-
extension on Stopwatch {
449-
String get elapsedSeconds {
450-
return '(${elapsed.inMilliseconds / 1000} seconds)';
451-
}
452-
}

slang/lib/src/builder/utils/map_utils.dart

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,43 @@ class MapUtils {
338338
return resultMap;
339339
}
340340

341+
/// Merges two maps and returns a new map.
342+
/// Values from [other] take precedence over [base].
343+
/// Nested maps are merged recursively.
344+
static Map<String, dynamic> merge({
345+
required Map<String, dynamic> base,
346+
required Map<String, dynamic> other,
347+
}) {
348+
final resultMap = <String, dynamic>{};
349+
for (final entry in base.entries) {
350+
resultMap[entry.key] = entry.value;
351+
}
352+
353+
// Merge entries from other
354+
for (final entry in other.entries) {
355+
final keyWithoutModifier = entry.key.withoutModifiers;
356+
final existingKey = resultMap.keys
357+
.firstWhereOrNull((k) => k.withoutModifiers == keyWithoutModifier);
358+
359+
if (existingKey == null) {
360+
// Key doesn't exist in target, add it
361+
resultMap[entry.key] = entry.value;
362+
} else if (entry.value is Map<String, dynamic> &&
363+
resultMap[existingKey] is Map<String, dynamic>) {
364+
// Both are maps, merge recursively
365+
resultMap[existingKey] = merge(
366+
base: resultMap[existingKey],
367+
other: entry.value,
368+
);
369+
} else {
370+
// Overwrite with value from other
371+
resultMap[existingKey] = entry.value;
372+
}
373+
}
374+
375+
return resultMap;
376+
}
377+
341378
/// Removes all entries that are empty maps recursively.
342379
/// They are removed from the map in place.
343380
static void clearEmptyMaps(Map map) {

slang/lib/src/runner/analyze.dart

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,28 @@
11
import 'dart:io';
22

33
import 'package:collection/collection.dart';
4+
import 'package:slang/src/builder/builder/translation_map_builder.dart';
45
import 'package:slang/src/builder/builder/translation_model_list_builder.dart';
56
import 'package:slang/src/builder/model/enums.dart';
67
import 'package:slang/src/builder/model/i18n_data.dart';
78
import 'package:slang/src/builder/model/i18n_locale.dart';
89
import 'package:slang/src/builder/model/node.dart';
910
import 'package:slang/src/builder/model/raw_config.dart';
10-
import 'package:slang/src/builder/model/translation_map.dart';
11+
import 'package:slang/src/builder/model/slang_file_collection.dart';
1112
import 'package:slang/src/builder/utils/file_utils.dart';
1213
import 'package:slang/src/builder/utils/map_utils.dart';
1314
import 'package:slang/src/builder/utils/node_utils.dart';
1415
import 'package:slang/src/builder/utils/path_utils.dart';
1516
import 'package:slang/src/utils/log.dart' as log;
17+
import 'package:slang/src/utils/stopwatch.dart';
1618

1719
final _setEquality = SetEquality();
1820

19-
void runAnalyzeTranslations({
20-
required RawConfig rawConfig,
21-
required TranslationMap translationMap,
21+
Future<void> runAnalyzeTranslations({
22+
required SlangFileCollection fileCollection,
2223
required List<String> arguments,
23-
}) {
24+
Stopwatch? stopwatch,
25+
}) async {
2426
String? outDir;
2527
List<String>? sourceDirs;
2628

@@ -36,7 +38,7 @@ void runAnalyzeTranslations({
3638
sourceDirs ??= ['lib'];
3739

3840
if (outDir == null) {
39-
outDir = rawConfig.inputDirectory;
41+
outDir = fileCollection.config.inputDirectory;
4042
if (outDir == null) {
4143
throw 'input_directory or --outdir=<path> must be specified.';
4244
}
@@ -47,14 +49,22 @@ void runAnalyzeTranslations({
4749
final full = arguments.contains('--full');
4850
final exitIfChanged = arguments.contains('--exit-if-changed');
4951

52+
final rawConfig = fileCollection.config;
53+
final translationMap = await TranslationMapBuilder.build(
54+
fileCollection: fileCollection,
55+
);
56+
5057
// build translation model
5158
final translationModelList = TranslationModelListBuilder.build(
5259
rawConfig,
5360
translationMap,
5461
);
5562

63+
final baseTranslations =
64+
findBaseTranslations(rawConfig, translationModelList);
65+
5666
final missingTranslationsResult = getMissingTranslations(
57-
rawConfig: rawConfig,
67+
baseTranslations: baseTranslations,
5868
translations: translationModelList,
5969
);
6070

@@ -80,6 +90,7 @@ void runAnalyzeTranslations({
8090
);
8191

8292
final unusedTranslationsResult = getUnusedTranslations(
93+
baseTranslations: baseTranslations,
8394
rawConfig: rawConfig,
8495
translations: translationModelList,
8596
full: full,
@@ -111,14 +122,16 @@ void runAnalyzeTranslations({
111122
},
112123
result: unusedTranslationsResult,
113124
);
125+
126+
if (stopwatch != null) {
127+
log.info('Analysis done. ${stopwatch.elapsedSeconds}');
128+
}
114129
}
115130

116131
Map<I18nLocale, Map<String, dynamic>> getMissingTranslations({
117-
required RawConfig rawConfig,
132+
required I18nData baseTranslations,
118133
required List<I18nData> translations,
119134
}) {
120-
final baseTranslations = _findBaseTranslations(rawConfig, translations);
121-
122135
// use translation model and find missing translations
123136
Map<I18nLocale, Map<String, dynamic>> result = {};
124137
for (final currTranslations in translations) {
@@ -142,13 +155,12 @@ Map<I18nLocale, Map<String, dynamic>> getMissingTranslations({
142155
}
143156

144157
Map<I18nLocale, Map<String, dynamic>> getUnusedTranslations({
158+
required I18nData baseTranslations,
145159
required RawConfig rawConfig,
146160
required List<I18nData> translations,
147161
required bool full,
148162
List<String>? sourceDirs,
149163
}) {
150-
final baseTranslations = _findBaseTranslations(rawConfig, translations);
151-
152164
// use translation model and find missing translations
153165
Map<I18nLocale, Map<String, dynamic>> result = {};
154166
for (final localeData in translations) {
@@ -443,7 +455,7 @@ extension DartAnalysisExt on String {
443455
}
444456
}
445457

446-
I18nData _findBaseTranslations(RawConfig rawConfig, List<I18nData> i18nData) {
458+
I18nData findBaseTranslations(RawConfig rawConfig, List<I18nData> i18nData) {
447459
final baseTranslations = i18nData.firstWhereOrNull((element) => element.base);
448460
if (baseTranslations == null) {
449461
throw 'There are no base translations. Could not found ${rawConfig.baseLocale.languageTag} in ${i18nData.map((e) => e.locale.languageTag)}';

slang/lib/src/runner/apply.dart

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,15 @@ Future<void> runApplyTranslations({
5656
// To know what has been changed, we need to regenerate the analysis
5757
log.info('');
5858
log.info('Regenerating analysis...');
59+
60+
final translations = TranslationModelListBuilder.build(
61+
rawConfig,
62+
translationMap,
63+
);
64+
5965
final analysis = getMissingTranslations(
60-
rawConfig: rawConfig,
61-
translations: TranslationModelListBuilder.build(
62-
rawConfig,
63-
translationMap,
64-
),
66+
baseTranslations: findBaseTranslations(rawConfig, translations),
67+
translations: translations,
6568
);
6669

6770
final ignoreBecauseMissing = <I18nLocale>[];
@@ -114,7 +117,7 @@ Future<void> runApplyTranslations({
114117
final missingTranslations = entry.value;
115118

116119
log.info(' -> Apply <${locale.languageTag}>');
117-
await _applyTranslationsForOneLocale(
120+
await applyTranslationsForOneLocale(
118121
fileCollection: fileCollection,
119122
applyLocale: locale,
120123
baseTranslations: baseTranslationMap,
@@ -128,7 +131,7 @@ Future<void> runApplyTranslations({
128131
/// Throws an error if the file could not be found.
129132
///
130133
/// [newTranslations] is a map of "Namespace -> Translations"
131-
Future<void> _applyTranslationsForOneLocale({
134+
Future<void> applyTranslationsForOneLocale({
132135
required SlangFileCollection fileCollection,
133136
required I18nLocale applyLocale,
134137
required Map<String, Map<String, dynamic>> baseTranslations,

0 commit comments

Comments
 (0)