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 remove = argResults!.flag('remove');
  final root = argResults!.option('root') ?? Directory.current.path;
  final verb = remove ? 'unlock' : 'lock';

  final DialectProject project;
  try {
    project = DialectProject.load(root);
  } on FileSystemException catch (e) {
    stderr.writeln(e.message);
    stderr.writeln('Run `dialect init` first, or run from the project root.');
    return 66;
  } on FormatException catch (e) {
    stderr.writeln('dialect.yaml or an ARB file is malformed:');
    stderr.writeln('  ${e.message}');
    return 65;
  }

  final KeySelection selection;
  try {
    selection = resolveSelection(
      project: project,
      command: 'lock',
      positionals: argResults!.rest,
      namespace: argResults!.option('namespace'),
      locale: argResults!.option('locale'),
    );
  } on SelectionFailure catch (f) {
    f.lines.forEach(stderr.writeln);
    return f.code;
  }

  // key -> locales, per outcome. Grouping this way keeps the report the
  // same shape for one key and for a whole namespace.
  // locale -> (key -> rewritten entry). Collected first and flushed once
  // per file: writing inside the loop re-serialized the stale in-memory ARB
  // each time, so with more than one key only the last one survived.
  final pending = <String, Map<String, ArbEntry>>{};

  final changed = <String, Set<String>>{};
  final unchanged = <String, Set<String>>{};
  final skipped = <String, Set<String>>{};

  for (final key in selection.keys) {
    final hash = computeSourceHash(project.source.entryFor(key)!.value);
    for (final locale in selection.locales) {
      final arb = project.translations[locale]!;
      final entry = arb.entryFor(key);
      if (entry == null || entry.value.isEmpty) {
        // Locking asserts a human approved a specific value; there is no
        // value here to approve. That's missing_keys / empty_values.
        skipped.putIfAbsent(key, () => <String>{}).add(locale);
        continue;
      }
      final wasLocked = entry.metadata?.locked ?? false;
      final hashCurrent = entry.metadata?.sourceHash == hash;
      if (remove) {
        if (!wasLocked) {
          unchanged.putIfAbsent(key, () => <String>{}).add(locale);
          continue;
        }
      } else {
        // Already locked against the *current* source — nothing to restate.
        // A locked-but-stale entry falls through and is re-locked.
        if (wasLocked && hashCurrent) {
          unchanged.putIfAbsent(key, () => <String>{}).add(locale);
          continue;
        }
      }
      pending.putIfAbsent(locale, () => <String, ArbEntry>{})[key] = ArbEntry(
        key: key,
        value: entry.value,
        metadata: ArbMetadata(locked: !remove, sourceHash: hash),
      );
      changed.putIfAbsent(key, () => <String>{}).add(locale);
    }
  }

  for (final e in pending.entries) {
    _flush(project.translations[e.key]!, e.value);
  }

  if (changed.isEmpty && unchanged.isEmpty && skipped.isNotEmpty) {
    stderr.writeln(
      'Nothing to $verb: ${describeKeyCount(skipped.length)} '
      '(${previewKeys(skipped.keys)}) have no translation yet in '
      '${selection.locales.join(', ')}. Translate them first.',
    );
    return 65;
  }

  if (changed.isNotEmpty) {
    stdout.writeln(
      remove
          ? '✓ unlocked ${describeKeyCount(changed.length)} '
                '(source_hash kept, so staleness is still tracked):'
          : '✓ locked ${describeKeyCount(changed.length)} against the '
                'current source:',
    );
    stdout.writeln('    ${previewKeys(changed.keys)}');
    stdout.writeln('    in ${_localesOf(changed).join(', ')}');
  }
  if (unchanged.isNotEmpty) {
    stdout.writeln(
      remove
          ? '  already unlocked: ${describeKeyCount(unchanged.length)}'
          : '  already locked and current: '
                '${describeKeyCount(unchanged.length)}',
    );
  }
  if (skipped.isNotEmpty) {
    stdout.writeln(
      '  skipped (no translation yet): '
      '${describeKeyCount(skipped.length)} — ${previewKeys(skipped.keys)}',
    );
  }
  return 0;
}