Coverage for app/venv/lib/python3.14/site-packages/weblate/checks/fluent/references.py: 14%

275 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 07:15 +0000

1# Copyright © Henry Wilkes <henry@torproject.org> 

2# 

3# SPDX-License-Identifier: GPL-3.0-or-later 

4 

5from __future__ import annotations 

6 

7from typing import TYPE_CHECKING 

8 

9from django.utils.translation import gettext, gettext_lazy 

10 

11from weblate.checks.base import TargetCheck 

12from weblate.checks.fluent.utils import ( 

13 FluentPatterns, 

14 FluentUnitConverter, 

15 format_html_code, 

16 format_html_error_list, 

17 translation_from_check, 

18 variant_name, 

19) 

20from weblate.utils.html import format_html_join_comma, list_to_tuples 

21 

22if TYPE_CHECKING: 22 ↛ 23line 22 didn't jump to line 23 because the condition on line 22 was never true

23 from collections.abc import Iterable, Iterator 

24 

25 from django.utils.safestring import SafeString 

26 from django_stubs_ext import StrOrPromise 

27 from translate.storage.fluent import ( 

28 FluentPart, 

29 FluentReference, 

30 FluentSelectorBranch, 

31 FluentSelectorNode, 

32 ) 

33 

34 from weblate.checks.fluent.utils import CheckModel, HighlightsType, TransUnitModel 

35 

36 

37class _Reference: 

38 """A class that wraps a FluentReference.""" 

39 

40 def __init__(self, fluent_ref: FluentReference) -> None: 

41 self.fluent_ref = fluent_ref 

42 # A reference can be flagged as shared if it was borrowed from another 

43 # branch. 

44 self.shared = False 

45 

46 def share(self) -> _Reference: 

47 copy = self.__class__(self.fluent_ref) 

48 copy.shared = True 

49 return copy 

50 

51 def present(self) -> str: 

52 """Show the reference in a presentable form.""" 

53 ref_name = self.fluent_ref.name 

54 if self.fluent_ref.type_name == "term": 

55 ref_name = f"-{ref_name}" 

56 elif self.fluent_ref.type_name == "variable": 

57 ref_name = f"${ref_name}" 

58 return f"{{\xa0{ref_name}\xa0}}" 

59 

60 def matches(self, other: _Reference) -> bool: 

61 """Whether the two references match in name and type.""" 

62 return ( 

63 self.fluent_ref.type_name == other.fluent_ref.type_name 

64 and self.fluent_ref.name == other.fluent_ref.name 

65 ) 

66 

67 

68class _CountedReferences: 

69 """ 

70 Tracks the number of matching _References in a list. 

71 

72 This will collect matching references together so they can be counted and 

73 compared, without caring about the order. 

74 """ 

75 

76 def __init__(self, ref_list: Iterable[_Reference]) -> None: 

77 self.matching_refs: list[list[_Reference]] = [] 

78 for ref in ref_list: 

79 add = True 

80 for other_refs in self.matching_refs: 

81 if other_refs[0].matches(ref): 

82 add = False 

83 other_refs.append(ref) 

84 break 

85 if add: 

86 self.matching_refs.append([ref]) 

87 

88 def matches(self, other: _CountedReferences) -> bool: 

89 """ 

90 Whether the two instances match. 

91 

92 This will match if the two instances have matching _References with the 

93 same count in both. 

94 """ 

95 if len(self.matching_refs) != len(other.matching_refs): 

96 return False 

97 for refs in self.matching_refs: 

98 has_match = False 

99 for _index, other_refs in enumerate(other.matching_refs): 

100 if refs[0].matches(other_refs[0]): 

101 if len(refs) != len(other_refs): 

102 return False 

103 has_match = True 

104 break 

105 if not has_match: 

106 return False 

107 # Both are the same length and each ref in self was matched, so there 

108 # shouldn't be any refs in other that were not matched once. 

109 return True 

110 

111 def count(self, ref: _Reference, include_shared: bool = True) -> int: 

112 """ 

113 How many references match the given reference. 

114 

115 If `include_shared` is False, this will only count references that are 

116 not flagged as "shared". 

117 """ 

118 for other_refs in self.matching_refs: 

119 if ref.matches(other_refs[0]): 

120 if include_shared: 

121 return len(other_refs) 

122 count = 0 

123 for equiv_ref in other_refs: 

124 if not equiv_ref.shared: 

125 count += 1 

126 return count 

127 return 0 

128 

129 

130class _VariantReferences: 

131 """Represents a variant string and the references it contains.""" 

132 

133 def __init__( 

134 self, 

135 path: list[FluentSelectorBranch], 

136 references: list[_Reference], 

137 ) -> None: 

138 self.path = path 

139 self.references = references 

140 self.counted_references = _CountedReferences(references) 

141 

142 def name(self) -> str: 

143 """Generate the name for this variant.""" 

144 return variant_name(self.path) 

145 

146 

147class _DifferentBranchCountError(Exception): 

148 # Generic exception for when two branches under the same node have different 

149 # counts for some reference. 

150 pass 

151 

152 

153class _SelectorReferences: 

154 """Class for extracting the references found underneath some selector branch.""" 

155 

156 def __init__(self, top_branch: FluentSelectorBranch) -> None: 

157 # Map from the selector branches to a list of their direct references. 

158 self._branch_references: dict[FluentSelectorBranch, list[_Reference]] = {} 

159 # Map from the selector node to a list of references found in their 

160 # SelectExpression selector expression. 

161 self._selector_references: dict[FluentSelectorNode, list[_Reference]] = {} 

162 

163 # Populate the maps with references. 

164 self._set_refs(top_branch) 

165 

166 # Try and share references between branches. 

167 self._share_refs_between_branches(top_branch) 

168 

169 # Generate all possible variants and assign them the references that 

170 # would appear in their flat form, plus any shared references. 

171 self.variant_references = [ 

172 _VariantReferences(path, list(self._refs_for_path(top_branch, path))) 

173 for path in top_branch.branch_paths() 

174 ] 

175 

176 def _refs_for_path( 

177 self, 

178 top_branch: FluentSelectorBranch, 

179 branches: list[FluentSelectorBranch], 

180 ) -> Iterator[_Reference]: 

181 """ 

182 Fetch all the references for a given branch path. 

183 

184 Each branch path represents a possible variant of the original fluent 

185 entry. 

186 """ 

187 yield from self._branch_references[top_branch] 

188 for branch in branches: 

189 yield from self._branch_references[branch] 

190 

191 def _set_refs(self, branch: FluentSelectorBranch) -> None: 

192 self._branch_references[branch] = [ 

193 _Reference(fluent_ref) for fluent_ref in branch.top_references 

194 ] 

195 for node in branch.child_nodes: 

196 # Only want unique refs for the selector references, otherwise this 

197 # can lead to double-sharing of these references. 

198 selector_refs: list[_Reference] = [] 

199 for fluent_ref in node.selector_references: 

200 ref = _Reference(fluent_ref) 

201 add = True 

202 for other in selector_refs: 

203 if other.matches(ref): 

204 add = False 

205 break 

206 if add: 

207 selector_refs.append(ref) 

208 self._selector_references[node] = selector_refs 

209 

210 for child in node.child_branches: 

211 self._set_refs(child) 

212 

213 def _branch_ref_count( 

214 self, 

215 branch: FluentSelectorBranch, 

216 ref: _Reference, 

217 ) -> int: 

218 """ 

219 Get the number of times a reference appears in a branch. 

220 

221 If, for each node in the branch, each child branch of the node has the 

222 same reference count, then this will be added to the returned count. 

223 Otherwise, if they differ in count, this will raise the 

224 _DifferentBranchCountError exception to indicate that there is no 

225 consistent number to return. 

226 """ 

227 count = 0 

228 for node in branch.child_nodes: 

229 same_count = None 

230 for child in node.child_branches: 

231 child_count = self._branch_ref_count(child, ref) 

232 if same_count is None: 

233 # same_count is uninitialized so set it using this first 

234 # branch. 

235 same_count = child_count 

236 elif same_count != child_count: 

237 # Two branches differ, so no consistent counting. 

238 raise _DifferentBranchCountError 

239 if same_count is None: 

240 # Unexpected since each selector node should have at least one 

241 # child. 

242 raise _DifferentBranchCountError 

243 count += same_count 

244 # Add the count for the top references. 

245 # NOTE: this includes shared references that have been added earlier. 

246 for other in self._branch_references[branch]: 

247 if other.matches(ref): 

248 count += 1 

249 return count 

250 

251 def _share_refs_between_branches( 

252 self, 

253 branch: FluentSelectorBranch, 

254 ) -> None: 

255 """ 

256 Try and share references between branches. 

257 

258 Each node below the given branch will represent a SelectExpression. If 

259 the SelectExpression's selector contains a reference that matches one of 

260 the references within one of its Variants, we want to share that 

261 reference between all the Variants *as if* each variant contains the 

262 reference. 

263 

264 This should help adjust for the fact that a Variant's key may make the 

265 reference unnecessary. E.g. we might select over the number $num, but if 

266 we match with the [zero] or [one] category, then we might not need to or 

267 want to reference the { $num } value. Moreover, this could vary across 

268 different locales. 

269 """ 

270 # Share depth-first to try and equalise the reference count between 

271 # sub-branches before moving up. 

272 for node in branch.child_nodes: 

273 for child in node.child_branches: 

274 self._share_refs_between_branches(child) 

275 

276 for node in branch.child_nodes: 

277 try: 

278 # For each reference found in the selector, we want to count how 

279 # many times it appears in each child branch. 

280 ref_counts = { 

281 ref: { 

282 child: self._branch_ref_count(child, ref) 

283 for child in node.child_branches 

284 } 

285 for ref in self._selector_references[node] 

286 } 

287 except _DifferentBranchCountError: 

288 # For at least one of the references, one of the children does 

289 # not have a consistent count. 

290 # E.g. 

291 # | { $num -> 

292 # | [one] one 

293 # | *[other] { $var -> 

294 # | [a] { $num } 

295 # | *[b] none 

296 # | } 

297 # | } 

298 # Here the [other] branch does not have a consistent count for 

299 # $num between its child branches [a] and [b], so we do not 

300 # share it. 

301 continue 

302 for ref, child_counts in ref_counts.items(): 

303 # We want each child to have the same reference count for the 

304 # purpose of comparison, so we find the maximum count and pad 

305 # the other branches with shared references. 

306 # NOTE: Each ref in selector_references should be unique to 

307 # avoid double adding at this stage. 

308 max_count = max(child_counts.values()) 

309 for child, count in child_counts.items(): 

310 self._branch_references[child].extend( 

311 ref.share() for _num in range(max_count - count) 

312 ) 

313 

314 

315class _VariantReferencesDifference: 

316 """ 

317 The difference between the references found in the source and target. 

318 

319 Each variant in the source will be compared against each variant in the 

320 target to see if they have a matching set of references with the same number 

321 or appearances, but not necessarily in the same order. 

322 

323 If there is any source variant that does not have at least one match in the 

324 target, it will be flagged as a missing variant. Similarly, if there is any 

325 target variant with no matching source variant, it will be flagged as an 

326 extra variant. 

327 """ 

328 

329 def __init__( 

330 self, 

331 source_part: FluentPart, 

332 target_part: FluentPart, 

333 ) -> None: 

334 self._part_name = source_part.name 

335 self._source_variants = _SelectorReferences( 

336 source_part.top_branch 

337 ).variant_references 

338 self._target_variants = _SelectorReferences( 

339 target_part.top_branch 

340 ).variant_references 

341 

342 self._missing_variants = [ 

343 variant 

344 for variant in self._source_variants 

345 if not self._has_match(variant, self._target_variants) 

346 ] 

347 self._extra_variants = [ 

348 variant 

349 for variant in self._target_variants 

350 if not self._has_match(variant, self._source_variants) 

351 ] 

352 

353 @classmethod 

354 def _has_match( 

355 cls, variant: _VariantReferences, search_list: list[_VariantReferences] 

356 ) -> bool: 

357 return any( 

358 variant.counted_references.matches(other.counted_references) 

359 for other in search_list 

360 ) 

361 

362 def __bool__(self) -> bool: 

363 return bool(self._missing_variants or self._extra_variants) 

364 

365 def _missing_ref_message( 

366 self, 

367 ref: str, 

368 variants: str, 

369 ) -> SafeString: 

370 if self._part_name: 

371 if not variants: 

372 return format_html_code( 

373 gettext( 

374 "Fluent {attribute} attribute is missing a " 

375 "{reference} Fluent reference." 

376 ), 

377 attribute=self._part_name, 

378 reference=ref, 

379 ) 

380 return format_html_code( 

381 gettext( 

382 "Fluent {attribute} attribute is missing a {reference} " 

383 "Fluent reference for the following variants: {variant_list}." 

384 ), 

385 attribute=self._part_name, 

386 reference=ref, 

387 variant_list=variants, 

388 ) 

389 if not variants: 

390 return format_html_code( 

391 gettext("Fluent value is missing a {reference} Fluent reference."), 

392 reference=ref, 

393 ) 

394 return format_html_code( 

395 gettext( 

396 "Fluent value is missing a {reference} Fluent reference " 

397 "for the following variants: {variant_list}." 

398 ), 

399 reference=ref, 

400 variant_list=variants, 

401 ) 

402 

403 def _extra_ref_message( 

404 self, 

405 ref: str, 

406 variants: str, 

407 ) -> SafeString: 

408 if self._part_name: 

409 if not variants: 

410 return format_html_code( 

411 gettext( 

412 "Fluent {attribute} attribute has an unexpected extra " 

413 "{reference} Fluent reference." 

414 ), 

415 attribute=self._part_name, 

416 reference=ref, 

417 ) 

418 return format_html_code( 

419 gettext( 

420 "Fluent {attribute} attribute has an unexpected extra {reference} " 

421 "Fluent reference for the following variants: {variant_list}." 

422 ), 

423 attribute=self._part_name, 

424 reference=ref, 

425 variant_list=variants, 

426 ) 

427 if not variants: 

428 return format_html_code( 

429 gettext( 

430 "Fluent value has an unexpected extra {reference} Fluent reference." 

431 ), 

432 reference=ref, 

433 ) 

434 return format_html_code( 

435 gettext( 

436 "Fluent value has an unexpected extra {reference} Fluent " 

437 "reference for the following variants: {variant_list}." 

438 ), 

439 reference=ref, 

440 variant_list=variants, 

441 ) 

442 

443 @staticmethod 

444 def _present_variant_list( 

445 variant_list: list[_VariantReferences] | None, 

446 ) -> str: 

447 if not variant_list: 

448 return "" 

449 return format_html_join_comma( 

450 "{}", list_to_tuples(variant.name() for variant in variant_list) 

451 ) 

452 

453 def _unique_target_refs(self) -> Iterator[_Reference]: 

454 unique_refs: list[_Reference] = [] 

455 for variant in self._target_variants: 

456 for ref in variant.references: 

457 add = True 

458 for other in unique_refs: 

459 if other.matches(ref): 

460 add = False 

461 break 

462 if add: 

463 unique_refs.append(ref) 

464 yield ref 

465 

466 def _errors_relative_to( 

467 self, 

468 source_counted_refs: _CountedReferences, 

469 ) -> Iterator[SafeString]: 

470 # NOTE: The source_counted_refs may contain shared references, but we 

471 # ignore this property since at least one of the source variants 

472 # contains the actual reference, and we expect it to appear in the 

473 # targets as well. 

474 for refs in source_counted_refs.matching_refs: 

475 count = len(refs) 

476 variants_missing_ref = [] 

477 all_variants = True 

478 for variant in self._target_variants: 

479 if variant.counted_references.count(refs[0]) < count: 

480 variants_missing_ref.append(variant) 

481 else: 

482 all_variants = False 

483 if not variants_missing_ref: 

484 continue 

485 yield self._missing_ref_message( 

486 refs[0].present(), 

487 self._present_variant_list( 

488 None if all_variants else variants_missing_ref 

489 ), 

490 ) 

491 

492 for ref in self._unique_target_refs(): 

493 count = source_counted_refs.count(ref) 

494 variants_extra_ref = [] 

495 all_variants = True 

496 for variant in self._target_variants: 

497 # Here we are looking for extra references that shouldn't 

498 # appear, we only want to highlight the ones that are not shared 

499 # to avoid mentioning extra references for variants that do not 

500 # contain them explicitly. 

501 # NOTE: If there is a shared reference, then we expect at least 

502 # one of the target variants will have the excessive count, so 

503 # will be reported. 

504 if variant.counted_references.count(ref, False) > count: 

505 variants_extra_ref.append(variant) 

506 else: 

507 all_variants = False 

508 if not variants_extra_ref: 

509 continue 

510 yield self._extra_ref_message( 

511 ref.present(), 

512 self._present_variant_list( 

513 None if all_variants else variants_extra_ref 

514 ), 

515 ) 

516 

517 def _missing_variants_message( 

518 self, 

519 variants: list[_VariantReferences], 

520 ) -> SafeString: 

521 # NOTE: variants should all have names since the source contains at 

522 # least two variants in order to reach this step. 

523 variant_list = self._present_variant_list(variants) 

524 if self._part_name: 

525 return format_html_code( 

526 gettext( 

527 "The following variants in the original Fluent {attribute} " 

528 "attribute do not have at least one matching variant in the " 

529 "translation with the same set of Fluent references: " 

530 "{variant_list}." 

531 ), 

532 attribute=self._part_name, 

533 variant_list=variant_list, 

534 ) 

535 return format_html_code( 

536 gettext( 

537 "The following variants in the original Fluent value do not " 

538 "have at least one matching variant in the translation with " 

539 "the same set of Fluent references: {variant_list}." 

540 ), 

541 variant_list=variant_list, 

542 ) 

543 

544 def _extra_variants_message( 

545 self, 

546 variants: list[_VariantReferences] | None, 

547 ) -> SafeString: 

548 variant_list = self._present_variant_list(variants) 

549 if self._part_name: 

550 if not variant_list: 

551 return format_html_code( 

552 gettext( 

553 "The translated Fluent {attribute} attribute does not " 

554 "have a matching variant in the original with the same " 

555 "set of Fluent references." 

556 ), 

557 attribute=self._part_name, 

558 ) 

559 return format_html_code( 

560 gettext( 

561 "The following variants in the translated Fluent " 

562 "{attribute} attribute do not have a matching variant in " 

563 "the original with the same set of Fluent references: " 

564 "{variant_list}." 

565 ), 

566 attribute=self._part_name, 

567 variant_list=variant_list, 

568 ) 

569 if not variant_list: 

570 return format_html_code( 

571 gettext( 

572 "The translated Fluent value does not " 

573 "have a matching variant in the original with the same " 

574 "set of Fluent references." 

575 ), 

576 ) 

577 return format_html_code( 

578 gettext( 

579 "The following variants in the translated Fluent " 

580 "value do not have a matching variant in " 

581 "the original with the same set of Fluent references: " 

582 "{variant_list}." 

583 ), 

584 variant_list=variant_list, 

585 ) 

586 

587 def _errors_for_unmatched_variants( 

588 self, 

589 ) -> Iterator[SafeString]: 

590 if self._missing_variants: 

591 yield self._missing_variants_message(self._missing_variants) 

592 if self._extra_variants: 

593 # Don't want to print a list of variants if we only have one in the 

594 # original. 

595 have_target_variants = len(self._target_variants) > 1 

596 yield self._extra_variants_message( 

597 self._extra_variants if have_target_variants else None 

598 ) 

599 

600 def description(self) -> SafeString: 

601 # We want to be able to compare each target variant against some common 

602 # set of expected references. This allows us to determine which specific 

603 # references are missing or extra. 

604 # This is only possible if each variant in the source has the same set 

605 # of references (after sharing) to allow for this one-to-one comparison. 

606 # But we expect this will happen in most cases. 

607 common_refs: _CountedReferences | None = None 

608 for variant in self._source_variants: 

609 if common_refs is None: 

610 common_refs = variant.counted_references 

611 elif not common_refs.matches(variant.counted_references): 

612 common_refs = None 

613 break 

614 

615 if common_refs is not None: 

616 return format_html_error_list(self._errors_relative_to(common_refs)) 

617 # The source contains multiple variants with different references. 

618 return format_html_error_list(self._errors_for_unmatched_variants()) 

619 

620 

621class FluentReferencesCheck(TargetCheck): 

622 r""" 

623 Check that the target uses the same Fluent references as the source. 

624 

625 A Fluent Message or Term can reference another Message, Term, Attribute, or 

626 a variable. For example: 

627 

628 | Here is a { message }, a { message.attribute } a { -term } and a { $variable }. 

629 | Within a function { NUMBER($num, minimumFractionDigits: 2) } 

630 

631 Generally, translated Messages or Terms are expected to contain the same 

632 references as the source, although not necessarily in the same order of 

633 appearance. So this check ensures that translations use the same references 

634 in their value as the source value, the same number of times, and with no 

635 additions. For Messages, this will also check that each Attribute in the 

636 translation uses the same references as the matching Attribute in the 

637 source. 

638 

639 When the source or translation contains Fluent Select Expressions, then each 

640 possible variant in the source must be matched with at least one variant in 

641 the translation with the same references, and vice versa. 

642 

643 Moreover, if a variable reference appears both in the Select Expression's 

644 selector and within one of its variants, then all variants may also be 

645 considered as if they also contain that reference. The assumption being that 

646 the variant's key may have made the reference redundant for that variant. 

647 For example: 

648 

649 | { $num -> 

650 | [one] an apple 

651 | *[other] { $num } apples 

652 | } 

653 

654 Here, for the purposes of this check, the ``[one]`` variant will also be 

655 considered to contain the ``$num`` reference. 

656 

657 However, a reference within the Select Expression's selector, which can only 

658 be a variable of a Term Attribute in Fluent's syntax, will not by itself 

659 count as a required reference because they do not form the actual text 

660 content of the string that the end-user will see, and the presence of a 

661 Select Expression is considered locale-specific. For example: 

662 

663 | { -term.starts-with-vowel -> 

664 | [yes] an { -term } 

665 | *[no] a { -term } 

666 | } 

667 

668 Here a reference to ``-term.starts-with-vowel`` is not expected to appear in 

669 translations, but a reference to ``-term`` is. 

670 """ 

671 

672 check_id = "fluent-references" 

673 name = gettext_lazy("Fluent references") 

674 description = gettext_lazy("Fluent references should match.") 

675 default_disabled = True 

676 

677 @classmethod 

678 def _compare_references( 

679 cls, unit: TransUnitModel, source: str, target: str 

680 ) -> list[_VariantReferencesDifference]: 

681 # NOTE: If the source or target contains a syntax error, then 

682 # to_fluent_parts will return None. In this case, we just make it an 

683 # empty list. Then the returned difference will naturally be empty. 

684 source_unit = FluentUnitConverter(unit, source) 

685 is_message = source_unit.fluent_type() == "Message" 

686 source_parts = { 

687 part.name: part 

688 for part in (FluentUnitConverter(unit, source).to_fluent_parts() or []) 

689 # Always compare references in the values, but only compare 

690 # references in attributes for Messages (rather than Terms). 

691 if is_message or not part.name 

692 } 

693 differences: list[_VariantReferencesDifference] = [] 

694 for part in FluentUnitConverter(unit, target).to_fluent_parts() or []: 

695 if part.name not in source_parts: 

696 # Ignore this part since we don't have anything to compare it 

697 # against. The FluentParts checker will capture this. 

698 continue 

699 diff = _VariantReferencesDifference(source_parts[part.name], part) 

700 if not diff: 

701 continue 

702 differences.append(diff) 

703 return differences 

704 

705 def check_single( 

706 self, 

707 source: str, 

708 target: str, 

709 unit: TransUnitModel, 

710 ) -> bool: 

711 return bool(self._compare_references(unit, source, target)) 

712 

713 @classmethod 

714 def _get_all_references_in_branch( 

715 cls, 

716 branch: FluentSelectorBranch, 

717 ) -> Iterator[FluentReference]: 

718 """Get all references found across all variants as a single iterator.""" 

719 yield from branch.top_references 

720 for node in branch.child_nodes: 

721 for child in node.child_branches: 

722 yield from cls._get_all_references_in_branch(child) 

723 

724 def check_highlight( 

725 self, 

726 source: str, 

727 unit: TransUnitModel, 

728 ) -> HighlightsType: 

729 if self.should_skip(unit): 729 ↛ 732line 729 didn't jump to line 732 because the condition on line 729 was always true

730 return [] 

731 

732 fluent_unit = FluentUnitConverter(unit, source) 

733 is_message = fluent_unit.fluent_type() == "Message" 

734 

735 highlight_patterns: list[str] = [] 

736 # We simply match all references found in the source. 

737 for part in fluent_unit.to_fluent_parts() or []: 

738 if part.name and not is_message: 

739 continue 

740 highlight_patterns.extend( 

741 FluentPatterns.reference(fluent_ref) 

742 for fluent_ref in self._get_all_references_in_branch(part.top_branch) 

743 ) 

744 return FluentPatterns.highlight_source(source, highlight_patterns) 

745 

746 def get_description(self, check_model: CheckModel) -> StrOrPromise: 

747 (unit, source, target) = translation_from_check(check_model) 

748 differences = self._compare_references(unit, source, target) 

749 if not differences: 

750 return super().get_description(check_model) 

751 

752 return format_html_error_list(diff.description() for diff in differences)