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