run method

  1. @override
Future<int> run()
override

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);
}