Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
id: D-ANS-134
title: Static label references warn and never hide a resource
date: 2026-09-01
status: open
coveredBy:
- LabelSearchTest::aStaticReferenceIsNamed
- LabelSearchTest::anUnreferencedResourceStaysVisible
---

# D-ANS-134 — Static label references warn and never hide a resource

**A label resource with no static reference stays in `typo3_label_lookup` and is
marked with a warning.**

## Evidence

- TYPO3 main's `LabelFileResolver::getAllLabelFilesOfPackage()` recursively
enumerates XLF files below `Resources/Private/Language/` and
`Configuration/Sets/`; it does not first establish a reference.
- `YamlSetDefinitionProvider::get()` selects `labels.xlf` beside a set's
`config.yaml` when the manifest declares no `labels` value. That reference is
implicit and no text search can find it.
- A translation domain is a string an application can assemble at runtime. A
static scan can establish a reference it found and cannot establish that no
runtime reference exists.

Both core classes were read on TYPO3 main on 2026-09-01. The official Site
folder reference lists the supported files below `config/sites/<identifier>/`
and names no XLF resource there, so a project-site XLF is reported as requiring
an explicit reference.

## Decided

- Search PHP, YAML, TypoScript, TSconfig, Fluid, JavaScript, TypeScript, JSON
and XML files in the project and its installed TYPO3 packages for the exact
file reference or its translation domain.
- Count the conventional site-set `labels.xlf` beside `config.yaml` as an
implicit reference.
- Report the paths that carry a reference and warn where the list is empty.
- Keep an unreferenced resource in the results. Excluding it would turn an
incomplete static reading into a runtime claim.

## Assumed

- A reference in executable or configuration source is useful evidence even
where the branch that carries it is not active in this installation.
- Documentation prose is not a reference and is outside the scanned file types.

## Wrong if

- TYPO3 exposes a complete runtime usage graph for translation resources. Then
that graph should replace the static warning.
- A reference can be established only by parsing each supported source language.
Then plain containment produces enough false positives to mislead callers and
the scan needs language-specific readers.
2 changes: 2 additions & 0 deletions decisions/answers/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ one is added.
- [`D-ANS-129`][D-ANS-129] — A list in an answer names the call it raises · 2026-09-01
- [`D-ANS-130`][D-ANS-130] — A hint answer says how much of the question it carries · 2026-09-01
- [`D-ANS-131`][D-ANS-131] — An icon answer says whose picture the identifier already is · 2026-09-01
- [`D-ANS-134`][D-ANS-134] — Static label references warn and never hide a resource · 2026-09-01
- [`D-ANS-126`][D-ANS-126] — The runnable form of a declared command is a field · 2026-08-28
- [`D-ANS-114`][D-ANS-114] — A page read whole names the hints it declares · 2026-08-27
- [`D-ANS-115`][D-ANS-115] — A phrasing a requirement rests on is carried by the hint's own vocabulary · 2026-08-27
Expand Down Expand Up @@ -143,6 +144,7 @@ one is added.
[D-ANS-129]: ans-129-a-list-in-an-answer-names-the-call-it-raises.md
[D-ANS-130]: ans-130-a-hint-answer-says-how-much-of-the-question-it-carries.md
[D-ANS-131]: ans-131-an-icon-answer-says-whose-picture-the-identifier-already-is.md
[D-ANS-134]: ans-134-static-label-references-warn-and-never-hide-a-resource.md
[D-ANS-126]: ans-126-the-runnable-form-of-a-declared-command-is-a-field.md
[D-ANS-114]: ans-114-a-page-read-whole-names-the-hints-it-declares.md
[D-ANS-115]: ans-115-a-phrasing-a-requirement-rests-on-is-carried-by-the-hints-own-vocabulary.md
Expand Down
2 changes: 2 additions & 0 deletions decisions/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ and not a defect. What was decided lately is `bin/cli decisions:list`.
- [`D-ANS-129`][D-ANS-129] — A list in an answer names the call it raises · 2026-09-01
- [`D-ANS-130`][D-ANS-130] — A hint answer says how much of the question it carries · 2026-09-01
- [`D-ANS-131`][D-ANS-131] — An icon answer says whose picture the identifier already is · 2026-09-01
- [`D-ANS-134`][D-ANS-134] — Static label references warn and never hide a resource · 2026-09-01
- [`D-ANS-126`][D-ANS-126] — The runnable form of a declared command is a field · 2026-08-28
- [`D-ANS-114`][D-ANS-114] — A page read whole names the hints it declares · 2026-08-27
- [`D-ANS-115`][D-ANS-115] — A phrasing a requirement rests on is carried by the hint's own vocabulary · 2026-08-27
Expand Down Expand Up @@ -226,6 +227,7 @@ and not a defect. What was decided lately is `bin/cli decisions:list`.
[D-ANS-129]: answers/ans-129-a-list-in-an-answer-names-the-call-it-raises.md
[D-ANS-130]: answers/ans-130-a-hint-answer-says-how-much-of-the-question-it-carries.md
[D-ANS-131]: answers/ans-131-an-icon-answer-says-whose-picture-the-identifier-already-is.md
[D-ANS-134]: answers/ans-134-static-label-references-warn-and-never-hide-a-resource.md
[D-ANS-126]: answers/ans-126-the-runnable-form-of-a-declared-command-is-a-field.md
[D-ANS-114]: answers/ans-114-a-page-read-whole-names-the-hints-it-declares.md
[D-ANS-115]: answers/ans-115-a-phrasing-a-requirement-rests-on-is-carried-by-the-hints-own-vocabulary.md
Expand Down
2 changes: 1 addition & 1 deletion documentation/server/tools/readme.rst
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ at all in the state the machine is in.
:action: Open reference

Search the labels registered in the TYPO3 installation you are working
in.
in and the XLF files below project config/sites.

.. card:: :doc:`typo3_permalink_lookup <typo3_permalink_lookup>`
:label: Lookup
Expand Down
40 changes: 28 additions & 12 deletions documentation/server/tools/typo3_label_lookup.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
``typo3_label_lookup``
======================

Search the labels registered in the TYPO3 installation you are working in. Reuse
is local to the translation resource already used at the consuming code: pass
resource whenever it is known, and do not reference a match from another module
or package merely because its text is identical. The console answers with the
resource overrides the installation applies; where it cannot be reached — an
installed TYPO3 whose database has no schema yet is the common case — the same
packages' XLF files are read instead. Every match comes back as a translation
domain reference; computing that reference for a file this installation does not
have, one a patch is about to add, is typo3_translation_domain_lookup. Answers
from: installation, packages.
Search the labels registered in the TYPO3 installation you are working in and
the XLF files below project config/sites. Reuse is local to the translation
resource already used at the consuming code: pass resource whenever it is known,
and do not reference a match from another module or package merely because its
text is identical. The console answers with the resource overrides the
installation applies; the files supply an answer when it cannot be reached and
report non-standard names or resources with no static reference. Every match
comes back as a translation domain reference; computing that reference for a
file this installation does not have, one a patch is about to add, is
typo3_translation_domain_lookup. Answers from: installation, packages.

``readOnlyHint: true`` · ``destructiveHint: false`` · ``idempotentHint: true`` · ``openWorldHint: false``

Expand Down Expand Up @@ -75,10 +75,26 @@ Answers with
# a path that was guessed can be replaced by one that exists. Empty means no
# resource holds such a label.
resources: [string] # optional
resourceDiagnostics: # optional
- # The XLF resource this diagnosis describes.
resource: string
# Where it was found: package, site-set, or project-site.
location: string
# Whether the file follows the naming convention for its location.
conventionalName: boolean
# Whether an implicit or static reference was found.
referenced: boolean
# Source files that name the resource. A conventional site-set labels.xlf
# names its adjacent config.yaml as an implicit reference.
references: [string]
# Naming, discovery, and static-reference warnings for this resource.
warnings: [string]
labels: # optional
- # Translation domain reference (package.resource:key) — the canonical
# form.
- # The reusable label reference: a translation domain for package labels or
# an LLL file reference for project-site labels.
ref: string
# The translation domain, empty for a project-site XLF that TYPO3 does not
# register as a package resource.
domain: string
# The trans-unit id.
key: string
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
id: R-ANS-038
title: A file-read label names what makes its resource usable
status: held
heldBy:
- LabelSearchTest::aNonStandardLabelFileIsWarned
- LabelSearchTest::aProjectSiteLabelFileIsReadBesideAnEmptyConsoleAnswer
- LabelSearchTest::aSiteSetLabelsFileCarriesItsImplicitReference
- LabelSearchTest::aStaticReferenceIsNamed
- LabelSearchTest::anUnreferencedResourceStaysVisible
---

# R-ANS-038 — A file-read label names what makes its resource usable

**A label read directly from a file says whether its name follows the convention
of its directory and where a static reference reaches it.**

`typo3_label_lookup` also reads XLF files below the project's `config/sites/`
directory. TYPO3 does not discover those as package language resources, so the
answer says that an explicit reference is required. A resource for which the
scan finds no reference stays in the answer and is warned: a domain assembled at
runtime cannot be proved absent from source files.

The automatic site-set case is a reference of its own. A `labels.xlf` beside a
set's `config.yaml` is selected by TYPO3 without the path being written into
that file, and the answer names that implicit reference rather than warning that
none exists.

## From

The maintainer's request of 2026-09-01, after the package fallback was found to
enumerate every matching trans-unit without saying whether its resource could be
reached.
2 changes: 2 additions & 0 deletions requirements/answers/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ when it is added.
- [`R-ANS-035`][R-ANS-035] — An answer that names a target branch names the lines that take a patch · held
- [`R-ANS-036`][R-ANS-036] — A suite list names the paths no suite covers · held
- [`R-ANS-037`][R-ANS-037] — An answer says how to read the current version rather than naming one · not guarded
- [`R-ANS-038`][R-ANS-038] — A file-read label names what makes its resource usable · held

[R-ANS-001]: ans-001-could-not-ask-never-looks-like-does-not-exist.md
[R-ANS-002]: ans-002-the-reason-is-in-the-data-not-only-in-the-text.md
Expand Down Expand Up @@ -84,3 +85,4 @@ when it is added.
[R-ANS-035]: ans-035-an-answer-that-names-a-target-branch-names-the-lines-that-take-a-patch.md
[R-ANS-036]: ans-036-a-suite-list-names-the-paths-no-suite-covers.md
[R-ANS-037]: ans-037-an-answer-says-how-to-read-the-current-version-rather-than-naming-one.md
[R-ANS-038]: ans-038-a-file-read-label-names-what-makes-its-resource-usable.md
2 changes: 2 additions & 0 deletions requirements/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ test. Both are legitimate, nothing fails on either, and
- [`R-ANS-035`][R-ANS-035] — An answer that names a target branch names the lines that take a patch · held
- [`R-ANS-036`][R-ANS-036] — A suite list names the paths no suite covers · held
- [`R-ANS-037`][R-ANS-037] — An answer says how to read the current version rather than naming one · not guarded
- [`R-ANS-038`][R-ANS-038] — A file-read label names what makes its resource usable · held

[R-ANS-001]: answers/ans-001-could-not-ask-never-looks-like-does-not-exist.md
[R-ANS-002]: answers/ans-002-the-reason-is-in-the-data-not-only-in-the-text.md
Expand Down Expand Up @@ -165,6 +166,7 @@ test. Both are legitimate, nothing fails on either, and
[R-ANS-035]: answers/ans-035-an-answer-that-names-a-target-branch-names-the-lines-that-take-a-patch.md
[R-ANS-036]: answers/ans-036-a-suite-list-names-the-paths-no-suite-covers.md
[R-ANS-037]: answers/ans-037-an-answer-says-how-to-read-the-current-version-rather-than-naming-one.md
[R-ANS-038]: answers/ans-038-a-file-read-label-names-what-makes-its-resource-usable.md

### documentation

Expand Down
127 changes: 127 additions & 0 deletions src/Installation/LabelReference.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php

declare(strict_types=1);

namespace TYPO3\DevCompanion\Installation;

use Symfony\Component\Finder\Finder;

/** Static source locations that name a label resource. */
final class LabelReference
{
/** @var array<int, string> */
private const SOURCE_FILES = [
'*.php', '*.yaml', '*.yml', '*.typoscript', '*.tsconfig', '*.html',
'*.xml', '*.js', '*.mjs', '*.cjs', '*.ts', '*.tsx', '*.json',
];

/**
* @param array<int, array{absolute: string, resource: string, domain: string, implicitReferences: array<int, string>}> $resources
* @param array<string, string> $packages
* @return array<string, array<int, string>> Resource to source paths.
*/
public static function find(array $resources, string $projectRoot, array $packages): array
{
$references = [];
foreach ($resources as $resource) {
$references[$resource['resource']] = $resource['implicitReferences'];
}

$seen = [];
$roots = array_map(
static fn(string $path): array => ['path' => $path, 'project' => false],
array_values($packages),
);
$roots[] = ['path' => $projectRoot, 'project' => true];

foreach ($roots as $root) {
if (!is_dir($root['path'])) {
continue;
}
$finder = Finder::create()
->files()
->in($root['path'])
->exclude(['.git', '.Build', 'node_modules', 'var'])
->name(self::SOURCE_FILES)
->sortByName();
if ($root['project']) {
$finder->exclude('vendor');
}

foreach ($finder as $source) {
$path = $source->getPathname();
if (isset($seen[$path]) || $source->getSize() > 2_000_000) {
continue;
}
$seen[$path] = true;
$content = file_get_contents($path);
if (!is_string($content)) {
continue;
}

foreach ($resources as $resource) {
if ($path === $resource['absolute'] || !self::isNamed($content, $resource)) {
continue;
}
$references[$resource['resource']][] = self::displayPath(
$path,
$projectRoot,
$packages,
);
}
}
}

foreach ($resources as $resource) {
$configuration = dirname($resource['absolute']) . '/config.yaml';
if (!is_file($configuration)) {
continue;
}
$content = file_get_contents($configuration);
if (is_string($content) && str_contains($content, basename($resource['absolute']))) {
$references[$resource['resource']][] = self::displayPath(
$configuration,
$projectRoot,
$packages,
);
}
}

foreach ($references as &$paths) {
$paths = array_values(array_unique($paths));
sort($paths);
}

return $references;
}

/** @param array{resource: string, domain: string} $resource */
private static function isNamed(string $content, array $resource): bool
{
if (str_contains($content, $resource['resource'])) {
return true;
}

$withoutExtension = preg_replace('/\.xlf$/i', '', $resource['resource']);
if (is_string($withoutExtension) && str_contains($content, $withoutExtension)) {
return true;
}

return $resource['domain'] !== '' && str_contains($content, $resource['domain']);
}

/** @param array<string, string> $packages */
private static function displayPath(string $path, string $projectRoot, array $packages): string
{
foreach ($packages as $key => $package) {
$prefix = rtrim($package, '/') . '/';
if (str_starts_with($path, $prefix)) {
return 'EXT:' . $key . '/' . substr($path, strlen($prefix));
}
}

$prefix = rtrim($projectRoot, '/') . '/';

return str_starts_with($path, $prefix) ? substr($path, strlen($prefix)) : $path;
}
}
Loading