File size: 2,902 Bytes
9d2d895
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
 * Whether — and how — a news panel discloses that it is showing only part of
 * its source list (#5873).
 *
 * A CUSTOM news category (a panel the user added from another variant) is never
 * in the per-variant server digest, so direct per-feed fetch is its only path
 * and that path is capped. The cap now rotates and the panel accumulates across
 * refresh cycles, but for the first cycles the panel genuinely holds only some
 * of its sources while the Sources manager lists all of them as enabled. Saying
 * so is the difference between a panel that is partial and one that lies about
 * being complete.
 *
 * The decision lives here rather than in NewsPanel so its edge cases — full
 * coverage, an empty source list, an unresolved dictionary — are unit-testable
 * without standing up a panel and a DOM.
 */

export interface SourceCoverage {
  /** Distinct sources actually represented in what the panel is rendering. */
  covered: number;
  /** Sources the user has enabled for the category. */
  total: number;
}

export type CoverageStringKey = 'sourceCoverage' | 'sourceCoverageHint';

export type CoverageTranslate = (key: string, vars: Record<string, string>) => string;

/**
 * The badge detail (`"3/10 sources"`) or its explanatory title, or `undefined`
 * when the panel has nothing to disclose and should read plain `LIVE`.
 *
 * Returns `undefined` for four distinct reasons, all of which must stay silent
 * rather than render something wrong:
 *
 *  • `null` coverage — the digest-backed / preset case, i.e. almost every panel.
 *  • Full coverage — rotation gets there within a few cycles, and a permanent
 *    "10/10 sources" would be noise on every custom panel forever.
 *  • A non-positive total, or a `covered` that somehow exceeds it. `covered` is
 *    derived from rendered items and `total` from the enabled feed list, so a
 *    source disabled between the two reads could produce "11/10 sources".
 *  • An unresolved key. These strings sit outside the first-paint i18n shell
 *    deliberately (the badge only appears after a completed news load), so the
 *    lazy dictionary may not have landed. An unresolved key composes into
 *    `LIVE · components.newsPanel.sourceCoverage`, which the raw-key healer
 *    cannot repair — it only rewrites EXACT key matches.
 */
export function coverageBadgeString(
  coverage: SourceCoverage | null,
  key: CoverageStringKey,
  translate: CoverageTranslate,
): string | undefined {
  if (!coverage) return undefined;
  const { covered, total } = coverage;
  if (!Number.isFinite(covered) || !Number.isFinite(total)) return undefined;
  if (total <= 0 || covered < 0 || covered >= total) return undefined;

  const path = `components.newsPanel.${key}`;
  const translated = translate(path, { covered: String(covered), total: String(total) });
  return translated === path ? undefined : translated;
}