run method
Inspect project and return findings. May return an empty list.
Should NOT throw — turn unexpected conditions into Issues so the
report surfaces them rather than crashing.
Implementation
@override
List<Issue> run(DialectProject project) {
final slots = _parseSlots(project);
// Resolve each source key's budget once. No budget → key is invisible
// to this rule (the opt-in guarantee).
final budgetByKey = <String, _Budget>{};
for (final src in project.source.entries) {
final b = _resolveBudget(src, slots);
if (b != null) budgetByKey[src.key] = b;
}
if (budgetByKey.isEmpty) return const [];
final issues = <Issue>[];
final sourceByKey = <String, ArbEntry>{
for (final e in project.source.entries) e.key: e,
};
// Source side: an absolute cap the English itself busts is a genuine
// "this slot is impossible" finding. (A ratio budget's floor is
// >= sourceLen, so source never trips there.)
for (final src in project.source.entries) {
final b = budgetByKey[src.key];
if (b == null) continue;
final srcLen = _len(src.value);
if (srcLen == 0) continue;
final maxChars = b.maxChars(srcLen);
if (srcLen > maxChars) {
issues.add(
Issue(
severity: defaultSeverity,
ruleName: name,
message:
'Source `${src.key}` is $srcLen chars — over its '
'${b.label} budget of $maxChars. The slot is too tight even '
'for the source string.',
key: src.key,
file: project.source.sourcePath,
line: project.source.entryLines[src.key],
hint:
'Either widen the slot / raise the budget, or shorten the '
'source copy. Every translation inherits this budget, so an '
'impossible source guarantees downstream overflow.',
),
);
}
}
// Translation side: the common case — a faithful translation that
// outgrew the slot.
for (final entry in project.translations.entries) {
final locale = entry.key;
if (locale == project.config.sourceLocale) continue;
final arb = entry.value;
for (final t in arb.entries) {
final b = budgetByKey[t.key];
if (b == null) continue;
final src = sourceByKey[t.key];
if (src == null) continue;
final srcLen = _len(src.value);
if (srcLen == 0) continue;
final maxChars = b.maxChars(srcLen);
final tLen = _len(t.value);
if (tLen <= maxChars) continue;
issues.add(
Issue(
severity: defaultSeverity,
ruleName: name,
message:
'Translation for `${t.key}` is $tLen chars — over the '
'${b.label} budget of $maxChars (source is $srcLen).',
locale: locale,
key: t.key,
file: arb.sourcePath,
line: arb.entryLines[t.key],
hint:
'This renders in a tight UI slot. Use the shortest faithful '
'form (context often makes words droppable — a profile-header '
'"Edit profile" can be just "Edit"). Keep glossary terms '
'intact. If the slot genuinely has room, raise or remove the '
'budget on the source `@${t.key}` block, or ack this warning.',
),
);
}
}
return issues;
}