run method

  1. @override
List<Issue> run(
  1. DialectProject project
)
override

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