run method
Runs this command.
The return value is wrapped in a Future if necessary and returned by
CommandRunner.runCommand.
Implementation
@override
Future<int> run() async {
final results = argResults!;
final force = results['force'] as bool;
final dryRun = results['dry-run'] as bool;
final adopt = results['adopt'] as bool;
final prune = results['prune'] as bool;
final verify = results['verify'] as bool;
final onlyPlatform = results.option('platform');
final rest = results.rest;
if (rest.length > 1) {
stderr.writeln('sync takes at most one positional argument.');
return 64;
}
final root = rest.isEmpty ? Directory.current.path : rest.first;
DialectProject project;
try {
project = DialectProject.load(root);
} on FileSystemException catch (e) {
stderr.writeln(e.message);
stderr.writeln(
'Run `dialect init` first, or pass the project root as an argument.',
);
return 66;
} on FormatException catch (e) {
stderr.writeln('dialect.yaml or an ARB file is malformed:');
stderr.writeln(' ${e.message}');
return 65;
}
if (project.config.platforms.isEmpty) {
stdout.writeln(
'! dialect sync: no `platforms:` configured in dialect.yaml.',
);
stdout.writeln(
' Add a platform block (e.g. flutter:) to start emitting files.',
);
return 0;
}
final platforms = project.config.platforms.values.toList();
if (onlyPlatform != null) {
final match = platforms.where((p) => p.name == onlyPlatform).toList();
if (match.isEmpty) {
stderr.writeln(
'No platform named `$onlyPlatform` in dialect.yaml. '
'Configured: ${platforms.map((p) => p.name).join(", ")}.',
);
return 64;
}
platforms
..clear()
..addAll(match);
}
// Non-destructive guard: never silently delete keys that live in the
// generated output but not in the source (the out-of-band-edit trap
// that quietly lost 7 live keys in Dialect's first field use). Scan
// first; refuse, adopt, or prune — but never drop them by surprise.
var scan = OutputScan.run(project, platforms: platforms);
if (adopt && !dryRun && scan.adoptable.isNotEmpty) {
// Read the metadata split BEFORE adopting: once the keys are in the
// source, they are no longer orphans and the scan forgets them.
final incomplete = scan.keysNeedingMetadata.toList()..sort();
final adopted = _adoptOrphans(project, scan);
_reportAdoption(adopted, incomplete);
// Re-load so generation sees the newly-adopted source keys, then
// re-scan (adopted keys are no longer orphans).
project = DialectProject.load(root);
scan = OutputScan.run(project, platforms: platforms);
}
if (scan.isNotEmpty && !prune) {
_printOrphanRefusal(project, scan, dryRun: dryRun, adoptTried: adopt);
return dryRun ? 1 : 65;
}
// Pruning happens BEFORE generation, not after it. An orphan that is
// still in dialect/translations/<locale>.arb is regenerated straight back
// into the output, so a prune that ran afterwards reported a deletion it
// had not performed and the next `check` reported the same drift forever.
var pruneRemovals = const <String, Map<String, String>>{};
if (prune && scan.isNotEmpty) {
pruneRemovals = _translationBackedOrphans(project, scan);
_reportPrune(project, scan, pruneRemovals, dryRun: dryRun);
if (!dryRun && pruneRemovals.isNotEmpty) {
_pruneTranslations(project, pruneRemovals);
// Re-load so generation reads the translations as they are now.
project = DialectProject.load(root);
}
}
var totalWritten = 0;
var totalSkipped = 0;
final unnamespacedPerPlatform = <String, Set<String>>{};
for (final platform in platforms) {
final _PlatformOutcome outcome;
try {
if (platform.format == 'arb') {
outcome = _syncArbPlatform(
project,
platform,
force: force,
dryRun: dryRun,
);
} else if (JsonAdapter.handles(platform.format)) {
outcome = _syncJsonPlatform(
project,
platform,
force: force,
dryRun: dryRun,
);
} else {
stdout.writeln(
'! ${platform.name} (format: ${platform.format}) — unknown '
'format; expected one of arb, icu-json, flat-json. Skipping.',
);
totalSkipped++;
continue;
}
} on FormatException catch (e) {
stderr.writeln('✗ ${platform.name}: ${e.message}');
return 65;
}
totalWritten += outcome.filesWritten;
if (outcome.unnamespacedKeys.isNotEmpty) {
unnamespacedPerPlatform[platform.name] = outcome.unnamespacedKeys;
}
if (outcome.pluralStrippedKeys.isNotEmpty) {
_warnPluralStripped(platform, outcome.pluralStrippedKeys);
}
}
_maybeWarnUnnamespaced(unnamespacedPerPlatform);
_maybeWarnUnroutedNamespaces(project);
if (dryRun) {
// A pending translation deletion is work even when every output file
// happens to match on disk — under --dry-run the orphan is still in
// the translations, so generation reproduces the bytes already there.
// Reporting "up to date" then would be the same lie in a new place.
final pendingRemovals = pruneRemovals.values.fold<int>(
0,
(a, m) => a + m.length,
);
if (totalWritten == 0 && pendingRemovals == 0) {
stdout.writeln('✓ dialect sync --dry-run: every output is up to date.');
return 0;
}
final parts = <String>[
if (totalWritten > 0) '$totalWritten file(s) would change',
if (pendingRemovals > 0)
'$pendingRemovals translation entr'
'${pendingRemovals == 1 ? 'y' : 'ies'} would be deleted',
];
stdout.writeln(
'✗ dialect sync --dry-run: ${parts.join(', ')}. '
'Run `dialect sync${prune ? ' --prune' : ''}` to apply.',
);
return 1;
}
if (totalWritten == 0 && totalSkipped == 0) {
stdout.writeln(
'✓ dialect sync: nothing to do (every output is already up to date).',
);
} else if (totalWritten == 0) {
stdout.writeln(
'✓ dialect sync: $totalSkipped platform(s) skipped, no ARB output.',
);
} else {
stdout.writeln('✓ dialect sync: wrote $totalWritten file(s).');
}
return _reportPostState(root, verify: verify);
}