Files
projectsend/tests/Unit/TranslationUsageTest.php
denkfabrik-li ed82d748ea Run the auth and settings screens through the translator
use-translation.ts states the rule: every user-facing string in a
component must go through t(). Five screens never called it at all --
forgot-password, reset-password, confirm-password, verify-email and
settings/password had zero occurrences of useTranslation -- so a client
who had chosen Spanish reset their password in English, from the browser
tab down to the submit button. settings/profile had the hook but used it
for two strings, leaving its heading, labels and the whole
email-verification notice hardcoded around them.

The password page also carried a second, smaller mistake the miss was
hiding: its <Head> title said "Profile settings", copied from the
profile page, so the tab named the wrong screen in every language.
It says "Password settings" now, the wording its own breadcrumb and
the sibling "Notification settings" title already use.

Every string on the six screens goes through t() now. The two
module-level breadcrumb arrays moved inside their components to reach
the hook -- the shape two-factor, notifications and the other settings
pages already have. Where a key already exists in the catalogs (Email
address, Password, Confirm password, New password, Log out and friends,
shared with the login screen) the existing translations light up
immediately; the keys new to the catalogs fall back to their English
text, exactly what those lines rendered before, until the locales pick
them up.

TranslationUsageTest is the guard, a source scan like
DateFormattingUsageTest and for the same reason: no JavaScript test
runner gates this class of miss. It fails on any page under pages/auth
or pages/settings that never uses the hook -- those screens always carry
copy of their own, so a page there without it is a page somebody forgot
-- and on any literal <Head title="..."> anywhere, which is both a
user-facing string and where the copy-paste title above lived. Both
scans go red on the tree without this change: five pages and six
literal titles.
2026-08-29 08:58:00 +02:00

88 lines
3.3 KiB
PHP

<?php
declare(strict_types=1);
/**
* A source scan, like DateFormattingUsageTest and for the same reason: there
* is no JavaScript test runner here, and none of the checks that gate CI can
* tell a translated screen from a hardcoded one. The types are fine, the
* lint is fine, and the PHP suite never renders a component.
*
* What it protects: use-translation.ts promises that "every user-facing
* string in a component must go through t()". The way that promise broke was
* never one stray string on a busy page — it was whole pages that skipped
* the hook entirely: five auth and settings screens shipped with zero calls,
* so a client who had chosen Spanish reset their password in English. One of
* them had copied its <Head> title from the profile page too, so the browser
* tab said "Profile settings" over the password form.
*
* Two scans, matching the two shapes of that miss:
*
* - Every page under pages/auth and pages/settings must use the hook. These
* screens always carry copy of their own (a title at minimum), so a page
* here with no useTranslation is a page somebody forgot, not a page with
* nothing to say. Pages elsewhere are not scanned — a public theme page
* can legitimately render nothing but data.
* - No literal <Head title="..."> anywhere. A browser-tab title is user-
* facing copy like any other, and the literal form is also where the
* copy-paste mistake above lived.
*/
// dirname() rather than base_path(): this runs at file scope, where the
// application container is not booted yet.
$root = dirname(__DIR__, 2);
$untranslatedPages = [];
foreach (['auth', 'settings'] as $section) {
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($root.'/resources/js/pages/'.$section, FilesystemIterator::SKIP_DOTS)
);
foreach ($files as $file) {
if ($file->getExtension() !== 'tsx') {
continue;
}
if (! str_contains((string) file_get_contents($file->getPathname()), 'useTranslation')) {
$untranslatedPages[] = str_replace($root.'/', '', $file->getPathname());
}
}
}
$literalTitles = [];
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($root.'/resources/js', FilesystemIterator::SKIP_DOTS)
);
foreach ($files as $file) {
if ($file->getExtension() !== 'tsx') {
continue;
}
$relative = str_replace($root.'/', '', $file->getPathname());
foreach (file($file->getPathname()) as $number => $line) {
if (str_contains($line, '<Head title="')) {
$literalTitles[] = $relative.':'.($number + 1);
}
}
}
test('every auth and settings page goes through the translator', function () use ($untranslatedPages) {
expect($untranslatedPages)->toBe([], implode("\n", array_merge(
['These pages never call useTranslation(), so everything they say is English in every language.'],
['Wrap each user-facing string: t(\'...\') — see resources/js/hooks/use-translation.ts.'],
$untranslatedPages,
)));
});
test('no page hardcodes its browser-tab title', function () use ($literalTitles) {
expect($literalTitles)->toBe([], implode("\n", array_merge(
['These <Head> titles are string literals, so the tab reads English in every language.'],
['Pass the title through t() — <Head title={t(\'...\')} />.'],
$literalTitles,
)));
});