From ed82d748ea458958889f69ee5d1bdbe63b62ba8b Mon Sep 17 00:00:00 2001 From: denkfabrik-li <274324701+denkfabrik-li@users.noreply.github.com> Date: Sat, 29 Aug 2026 02:03:26 +0200 Subject: [PATCH] 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 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 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. --- resources/js/pages/auth/confirm-password.tsx | 15 ++-- resources/js/pages/auth/forgot-password.tsx | 14 ++-- resources/js/pages/auth/reset-password.tsx | 19 +++-- resources/js/pages/auth/verify-email.tsx | 12 +-- resources/js/pages/settings/password.tsx | 38 +++++---- resources/js/pages/settings/profile.tsx | 32 +++---- tests/Unit/TranslationUsageTest.php | 87 ++++++++++++++++++++ 7 files changed, 160 insertions(+), 57 deletions(-) create mode 100644 tests/Unit/TranslationUsageTest.php diff --git a/resources/js/pages/auth/confirm-password.tsx b/resources/js/pages/auth/confirm-password.tsx index 5b3e7dbe..355ea9f6 100644 --- a/resources/js/pages/auth/confirm-password.tsx +++ b/resources/js/pages/auth/confirm-password.tsx @@ -7,9 +7,12 @@ import InputError from '@/components/input-error'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import { useTranslation } from '@/hooks/use-translation'; import AuthLayout from '@/layouts/auth-layout'; export default function ConfirmPassword() { + const { t } = useTranslation(); + const { data, setData, post, processing, errors, reset } = useForm({ password: '', }); @@ -24,20 +27,20 @@ export default function ConfirmPassword() { return ( - +
- +
diff --git a/resources/js/pages/auth/forgot-password.tsx b/resources/js/pages/auth/forgot-password.tsx index 3a0c4a89..dac42f14 100644 --- a/resources/js/pages/auth/forgot-password.tsx +++ b/resources/js/pages/auth/forgot-password.tsx @@ -9,6 +9,7 @@ import TextLink from '@/components/text-link'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import { useTranslation } from '@/hooks/use-translation'; import AuthLayout from '@/layouts/auth-layout'; interface ForgotPasswordForm { @@ -19,6 +20,7 @@ interface ForgotPasswordForm { } export default function ForgotPassword({ status }: { status?: string }) { + const { t } = useTranslation(); const captcha = useRef(null); const captchaToken = useRef(null); @@ -39,15 +41,15 @@ export default function ForgotPassword({ status }: { status?: string }) { }; return ( - - + + {status &&
{status}
}
- +
- Or, return to - log in + {t('Or, return to')} + {t('log in')}
diff --git a/resources/js/pages/auth/reset-password.tsx b/resources/js/pages/auth/reset-password.tsx index 213910c2..41ba828c 100644 --- a/resources/js/pages/auth/reset-password.tsx +++ b/resources/js/pages/auth/reset-password.tsx @@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { PasswordRequirements } from '@/components/password-requirements'; +import { useTranslation } from '@/hooks/use-translation'; import AuthLayout from '@/layouts/auth-layout'; interface ResetPasswordProps { @@ -23,6 +24,8 @@ interface ResetPasswordForm { } export default function ResetPassword({ token, email }: ResetPasswordProps) { + const { t } = useTranslation(); + const { data, setData, post, processing, errors, reset } = useForm({ token: token, email: email, @@ -38,13 +41,13 @@ export default function ResetPassword({ token, email }: ResetPasswordProps) { }; return ( - - + +
- +
- + setData('password', e.target.value)} - placeholder="Password" + placeholder={t('Password')} />
- + setData('password_confirmation', e.target.value)} - placeholder="Confirm password" + placeholder={t('Confirm password')} />
diff --git a/resources/js/pages/auth/verify-email.tsx b/resources/js/pages/auth/verify-email.tsx index b4f7846b..a696f30c 100644 --- a/resources/js/pages/auth/verify-email.tsx +++ b/resources/js/pages/auth/verify-email.tsx @@ -5,9 +5,11 @@ import { FormEventHandler } from 'react'; import TextLink from '@/components/text-link'; import { Button } from '@/components/ui/button'; +import { useTranslation } from '@/hooks/use-translation'; import AuthLayout from '@/layouts/auth-layout'; export default function VerifyEmail({ status }: { status?: string }) { + const { t } = useTranslation(); const { post, processing } = useForm({}); const submit: FormEventHandler = (e) => { @@ -17,23 +19,23 @@ export default function VerifyEmail({ status }: { status?: string }) { }; return ( - - + + {status === 'verification-link-sent' && (
- A new verification link has been sent to the email address you provided during registration. + {t('A new verification link has been sent to the email address you provided during registration.')}
)}
- Log out + {t('Log out')}
diff --git a/resources/js/pages/settings/password.tsx b/resources/js/pages/settings/password.tsx index f8f024e5..457bfd85 100644 --- a/resources/js/pages/settings/password.tsx +++ b/resources/js/pages/settings/password.tsx @@ -10,15 +10,18 @@ import { SaveButton } from '@/components/save-button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { PasswordRequirements } from '@/components/password-requirements'; - -const breadcrumbs: BreadcrumbItem[] = [ - { - title: 'Password settings', - href: '/settings/password', - }, -]; +import { useTranslation } from '@/hooks/use-translation'; export default function Password() { + const { t } = useTranslation(); + + const breadcrumbs: BreadcrumbItem[] = [ + { + title: t('Password settings'), + href: '/settings/password', + }, + ]; + const passwordInput = useRef(null); const currentPasswordInput = useRef(null); @@ -50,15 +53,18 @@ export default function Password() { return ( - +
- +
- +
- + @@ -93,7 +99,7 @@ export default function Password() {
- +
- Save password + {t('Save password')}
diff --git a/resources/js/pages/settings/profile.tsx b/resources/js/pages/settings/profile.tsx index d59d0ae0..fcc85298 100644 --- a/resources/js/pages/settings/profile.tsx +++ b/resources/js/pages/settings/profile.tsx @@ -13,13 +13,6 @@ import { useTranslation } from '@/hooks/use-translation'; import AppLayout from '@/layouts/app-layout'; import SettingsLayout from '@/layouts/settings/layout'; -const breadcrumbs: BreadcrumbItem[] = [ - { - title: 'Profile settings', - href: '/settings/profile', - }, -]; - export default function Profile({ mustVerifyEmail, status, @@ -38,6 +31,13 @@ export default function Profile({ const { t } = useTranslation(); const { auth } = usePage().props; + const breadcrumbs: BreadcrumbItem[] = [ + { + title: t('Profile settings'), + href: '/settings/profile', + }, + ]; + const { data, setData, patch, errors, processing, recentlySuccessful } = useForm({ name: auth.user.name, email: auth.user.email, @@ -53,15 +53,15 @@ export default function Profile({ return ( - +
- +
- + setData('name', e.target.value)} required autoComplete="name" - placeholder="Full name" + placeholder={t('Full name')} />
- + setData('email', e.target.value)} required autoComplete="username" - placeholder="Email address" + placeholder={t('Email address')} /> @@ -115,20 +115,20 @@ export default function Profile({ {mustVerifyEmail && auth.user.email_verified_at === null && (

- Your email address is unverified. + {t('Your email address is unverified.')} - Click here to re-send the verification email. + {t('Click here to re-send the verification email.')}

{status === 'verification-link-sent' && (
- A new verification link has been sent to your email address. + {t('A new verification link has been sent to your email address.')}
)}
diff --git a/tests/Unit/TranslationUsageTest.php b/tests/Unit/TranslationUsageTest.php new file mode 100644 index 00000000..0f760149 --- /dev/null +++ b/tests/Unit/TranslationUsageTest.php @@ -0,0 +1,87 @@ + 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 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, 'toBe([], implode("\n", array_merge( + ['These titles are string literals, so the tab reads English in every language.'], + ['Pass the title through t() — .'], + $literalTitles, + ))); +});