parse static method
Parse ARB content from a UTF-8 string. sourcePath is recorded on
the returned ArbFile so the check-report formatter can produce
file:line hints.
Implementation
static ArbFile parse(String content, {String? sourcePath}) {
final root = jsonDecode(content);
if (root is! Map) {
throw const FormatException('ARB root must be a JSON object.');
}
String? locale;
final fileMetadata = <String, Object?>{};
final partials = <String, _PartialEntry>{};
final insertionOrder = <String>[];
root.forEach((rawKey, value) {
if (rawKey is! String) {
throw FormatException(
'ARB key must be a string, got ${rawKey.runtimeType}.',
);
}
if (rawKey == '@@locale') {
if (value is! String) {
throw const FormatException('@@locale must be a string.');
}
locale = _nfc(value);
return;
}
if (rawKey.startsWith('@@')) {
// Other ARB-level metadata (e.g. Flutter gen_l10n's @@last_modified,
// custom @@x-context). Preserve verbatim so the writer round-trips
// them — silent stripping would corrupt user data on `dialect sync`.
fileMetadata[rawKey] = value;
return;
}
if (rawKey.startsWith('@')) {
final dataKey = rawKey.substring(1);
final partial = partials.putIfAbsent(
dataKey,
() => _PartialEntry(dataKey),
);
if (value is! Map) {
throw FormatException(
'Metadata for "$dataKey" must be a JSON object, got '
'${value.runtimeType}.',
);
}
partial.metadata = _parseMetadata(value);
if (!insertionOrder.contains(dataKey)) insertionOrder.add(dataKey);
return;
}
if (value is! String) {
throw FormatException(
'Value for "$rawKey" must be a string, got ${value.runtimeType}.',
);
}
final partial = partials.putIfAbsent(rawKey, () => _PartialEntry(rawKey));
partial.value = _nfc(value);
if (!insertionOrder.contains(rawKey)) insertionOrder.add(rawKey);
});
if (locale == null) {
throw const FormatException('ARB file missing @@locale.');
}
final entries = <ArbEntry>[];
final orphans = <String, ArbMetadata>{};
for (final key in insertionOrder) {
final p = partials[key]!;
if (p.value == null) {
// Metadata-only entry — `@key` block without a corresponding
// key/value pair. Preserve so `dialect check` (M4) can surface it
// as a structural error. `dialect check --fix` strips orphans by
// construction — the writer never emits orphanMetadata.
if (p.metadata != null) orphans[key] = p.metadata!;
continue;
}
entries.add(ArbEntry(key: key, value: p.value!, metadata: p.metadata));
}
return ArbFile(
locale: locale!,
entries: entries,
fileMetadata: fileMetadata,
orphanMetadata: orphans,
entryLines: _lineMap(content),
sourcePath: sourcePath,
);
}