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, + ))); +});