From e715454acb3be4f6225cd4ae7ee9e6816db55faa Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Tue, 8 Jul 2025 10:53:15 +0200 Subject: [PATCH] Localize layout, credential components, email page (#992) --- .../src/entrypoints/popup/App.tsx | 12 ++-- .../CredentialDetails/AliasBlock.tsx | 14 +++-- .../LoginCredentialsBlock.tsx | 10 ++-- .../CredentialDetails/NotesBlock.tsx | 4 +- .../CredentialDetails/TotpBlock.tsx | 10 ++-- .../popup/components/EmailPreview.tsx | 26 ++++---- .../entrypoints/popup/components/Modal.tsx | 4 +- .../entrypoints/popup/hooks/useVaultMutate.ts | 16 ++--- .../entrypoints/popup/hooks/useVaultSync.ts | 8 ++- .../popup/pages/CredentialAddEdit.tsx | 60 ++++++++++--------- .../popup/pages/CredentialDetails.tsx | 10 ++-- .../entrypoints/popup/pages/EmailDetails.tsx | 30 +++++----- .../src/locales/en/common.json | 8 ++- .../src/locales/en/credentials.json | 23 +++++++ .../src/locales/en/emails.json | 12 +++- .../src/locales/nl/common.json | 8 ++- .../src/locales/nl/credentials.json | 23 +++++++ .../src/locales/nl/emails.json | 12 +++- 18 files changed, 192 insertions(+), 98 deletions(-) diff --git a/apps/browser-extension/src/entrypoints/popup/App.tsx b/apps/browser-extension/src/entrypoints/popup/App.tsx index 3e7980193..46598488f 100644 --- a/apps/browser-extension/src/entrypoints/popup/App.tsx +++ b/apps/browser-extension/src/entrypoints/popup/App.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { HashRouter as Router, Routes, Route } from 'react-router-dom'; import BottomNav from '@/entrypoints/popup/components/Layout/BottomNav'; @@ -41,6 +42,7 @@ type RouteConfig = { * App component. */ const App: React.FC = () => { + const { t } = useTranslation(['common', 'credentials', 'emails', 'settings']); const authContext = useAuth(); const { isInitialLoading } = useLoading(); const [isLoading, setIsLoading] = useMinDurationLoading(true, 150); @@ -55,13 +57,13 @@ const App: React.FC = () => { { path: '/unlock', element: , showBackButton: false }, { path: '/unlock-success', element: , showBackButton: false }, { path: '/upgrade', element: , showBackButton: false }, - { path: '/auth-settings', element: , showBackButton: true, title: 'Settings' }, + { path: '/auth-settings', element: , showBackButton: true, title: t('settings:title') }, { path: '/credentials', element: , showBackButton: false }, - { path: '/credentials/add', element: , showBackButton: true, title: 'Add credential' }, - { path: '/credentials/:id', element: , showBackButton: true, title: 'Credential details' }, - { path: '/credentials/:id/edit', element: , showBackButton: true, title: 'Edit credential' }, + { path: '/credentials/add', element: , showBackButton: true, title: t('credentials:addCredential') }, + { path: '/credentials/:id', element: , showBackButton: true, title: t('credentials:credentialDetails') }, + { path: '/credentials/:id/edit', element: , showBackButton: true, title: t('credentials:editCredential') }, { path: '/emails', element: , showBackButton: false }, - { path: '/emails/:id', element: , showBackButton: true, title: 'Email details' }, + { path: '/emails/:id', element: , showBackButton: true, title: t('emails:title') }, { path: '/settings', element: , showBackButton: false }, { path: '/logout', element: , showBackButton: false }, ]; diff --git a/apps/browser-extension/src/entrypoints/popup/components/CredentialDetails/AliasBlock.tsx b/apps/browser-extension/src/entrypoints/popup/components/CredentialDetails/AliasBlock.tsx index c435aebcc..d9dfda358 100644 --- a/apps/browser-extension/src/entrypoints/popup/components/CredentialDetails/AliasBlock.tsx +++ b/apps/browser-extension/src/entrypoints/popup/components/CredentialDetails/AliasBlock.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; import { FormInputCopyToClipboard } from '@/entrypoints/popup/components/FormInputCopyToClipboard'; @@ -13,6 +14,7 @@ type AliasBlockProps = { * Render the alias block. */ const AliasBlock: React.FC = ({ credential }) => { + const { t } = useTranslation('common'); const hasFirstName = Boolean(credential.Alias?.FirstName?.trim()); const hasLastName = Boolean(credential.Alias?.LastName?.trim()); const hasNickName = Boolean(credential.Alias?.NickName?.trim()); @@ -24,39 +26,39 @@ const AliasBlock: React.FC = ({ credential }) => { return (
-

Alias

+

{t('alias')}

{(hasFirstName || hasLastName) && ( )} {hasFirstName && ( )} {hasLastName && ( )} {hasBirthDate && ( )} {hasNickName && ( )} diff --git a/apps/browser-extension/src/entrypoints/popup/components/CredentialDetails/LoginCredentialsBlock.tsx b/apps/browser-extension/src/entrypoints/popup/components/CredentialDetails/LoginCredentialsBlock.tsx index 35e1ac282..16d84c57a 100644 --- a/apps/browser-extension/src/entrypoints/popup/components/CredentialDetails/LoginCredentialsBlock.tsx +++ b/apps/browser-extension/src/entrypoints/popup/components/CredentialDetails/LoginCredentialsBlock.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; import { FormInputCopyToClipboard } from '@/entrypoints/popup/components/FormInputCopyToClipboard'; @@ -12,6 +13,7 @@ type LoginCredentialsBlockProps = { * Render the login credentials block. */ const LoginCredentialsBlock: React.FC = ({ credential }) => { + const { t } = useTranslation('common'); const email = credential.Alias?.Email?.trim(); const username = credential.Username?.trim(); const password = credential.Password?.trim(); @@ -22,25 +24,25 @@ const LoginCredentialsBlock: React.FC = ({ credentia return (
-

Login credentials

+

{t('loginCredentials')}

{email && ( )} {username && ( )} {password && ( diff --git a/apps/browser-extension/src/entrypoints/popup/components/CredentialDetails/NotesBlock.tsx b/apps/browser-extension/src/entrypoints/popup/components/CredentialDetails/NotesBlock.tsx index 9b51d712c..7dc8dc910 100644 --- a/apps/browser-extension/src/entrypoints/popup/components/CredentialDetails/NotesBlock.tsx +++ b/apps/browser-extension/src/entrypoints/popup/components/CredentialDetails/NotesBlock.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; type NotesBlockProps = { notes: string | undefined; @@ -20,6 +21,7 @@ const convertUrlsToLinks = (text: string): string => { * Render the notes block. */ const NotesBlock: React.FC = ({ notes }) => { + const { t } = useTranslation('common'); if (!notes) { return null; } @@ -28,7 +30,7 @@ const NotesBlock: React.FC = ({ notes }) => { return (
-

Notes

+

{t('notes')}

= ({ credentialId }) => { + const { t } = useTranslation('common'); const [totpCodes, setTotpCodes] = useState([]); const [loading, setLoading] = useState(true); const [currentCodes, setCurrentCodes] = useState>({}); @@ -138,8 +140,8 @@ const TotpBlock: React.FC = ({ credentialId }) => { if (loading) { return (

-

Two-factor authentication

- Loading TOTP codes... +

{t('twoFactorAuthentication')}

+ {t('loadingTotpCodes')}
); } @@ -151,7 +153,7 @@ const TotpBlock: React.FC = ({ credentialId }) => { return (
-

Two-factor authentication

+

{t('twoFactorAuthentication')}

{totpCodes.map(totpCode => (
)}
-

Service

+

{t('credentials:service')}

setValue('ServiceName', value)} @@ -560,7 +562,7 @@ const CredentialAddEdit: React.FC = () => { /> setValue('ServiceUrl', value)} error={errors.ServiceUrl?.message} @@ -571,11 +573,11 @@ const CredentialAddEdit: React.FC = () => { {(mode === 'manual' || isEditMode) && ( <>
-

Login Credentials

+

{t('credentials:loginCredentials')}

setValue('Username', value)} error={errors.Username?.message} @@ -583,13 +585,13 @@ const CredentialAddEdit: React.FC = () => { { icon: 'refresh', onClick: generateRandomUsername, - title: 'Generate random username' + title: t('credentials:generateRandomUsername') } ]} /> setValue('Password', value)} @@ -600,7 +602,7 @@ const CredentialAddEdit: React.FC = () => { { icon: 'refresh', onClick: generateRandomPassword, - title: 'Generate random password' + title: t('credentials:generateRandomPassword') } ]} /> @@ -609,11 +611,11 @@ const CredentialAddEdit: React.FC = () => { onClick={handleGenerateRandomAlias} className="w-full bg-primary-500 text-white py-2 px-4 rounded hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2" > - Generate Random Alias + {t('credentials:generateRandomAlias')} setValue('Alias.Email', value)} error={errors.Alias?.Email?.message} @@ -622,40 +624,40 @@ const CredentialAddEdit: React.FC = () => {
-

Alias

+

{t('credentials:alias')}

setValue('Alias.FirstName', value)} error={errors.Alias?.FirstName?.message} /> setValue('Alias.LastName', value)} error={errors.Alias?.LastName?.message} /> setValue('Alias.NickName', value)} error={errors.Alias?.NickName?.message} /> setValue('Alias.Gender', value)} error={errors.Alias?.Gender?.message} /> setValue('Alias.BirthDate', value)} error={errors.Alias?.BirthDate?.message} @@ -664,11 +666,11 @@ const CredentialAddEdit: React.FC = () => {
-

Metadata

+

{t('credentials:metadata')}

setValue('Notes', value)} multiline diff --git a/apps/browser-extension/src/entrypoints/popup/pages/CredentialDetails.tsx b/apps/browser-extension/src/entrypoints/popup/pages/CredentialDetails.tsx index 95b557d00..f9643f603 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/CredentialDetails.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/CredentialDetails.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { useNavigate, useParams } from 'react-router-dom'; import { @@ -22,6 +23,7 @@ import type { Credential } from '@/utils/dist/shared/models/vault'; * Credential details page. */ const CredentialDetails: React.FC = (): React.ReactElement => { + const { t } = useTranslation(['common', 'credentials']); const { id } = useParams(); const navigate = useNavigate(); const dbContext = useDb(); @@ -74,20 +76,20 @@ const CredentialDetails: React.FC = (): React.ReactElement => { {!PopoutUtility.isPopup() && ( )}
); setHeaderButtons(headerButtonsJSX); return () => {}; - }, [setHeaderButtons, handleEdit, openInNewPopup]); + }, [setHeaderButtons, handleEdit, openInNewPopup, t]); // Clear header buttons on unmount useEffect((): (() => void) => { @@ -95,7 +97,7 @@ const CredentialDetails: React.FC = (): React.ReactElement => { }, [setHeaderButtons]); if (!credential) { - return
Loading...
; + return
{t('common:loading')}
; } return ( diff --git a/apps/browser-extension/src/entrypoints/popup/pages/EmailDetails.tsx b/apps/browser-extension/src/entrypoints/popup/pages/EmailDetails.tsx index 69563cb07..94a6ead0b 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/EmailDetails.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/EmailDetails.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { useParams, useNavigate } from 'react-router-dom'; import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner'; @@ -22,6 +23,7 @@ import { HeaderIconType } from '../components/Icons/HeaderIcons'; * Email details page. */ const EmailDetails: React.FC = (): React.ReactElement => { + const { t } = useTranslation(['common', 'emails']); const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const dbContext = useDb(); @@ -149,13 +151,13 @@ const EmailDetails: React.FC = (): React.ReactElement => { {!PopoutUtility.isPopup() && ( )} setShowDeleteModal(true)} - title="Delete email" + title={t('emails:deleteEmail')} iconType={HeaderIconType.DELETE} variant="danger" /> @@ -166,7 +168,7 @@ const EmailDetails: React.FC = (): React.ReactElement => { setHeaderButtonsConfigured(true); } return () => {}; - }, [setHeaderButtons, headerButtonsConfigured, openInNewPopup]); + }, [setHeaderButtons, headerButtonsConfigured, openInNewPopup, t]); // Clear header buttons on unmount useEffect((): (() => void) => { @@ -182,11 +184,11 @@ const EmailDetails: React.FC = (): React.ReactElement => { } if (error) { - return
Error: {error}
; + return
{t('common:error')} {error}
; } if (!email) { - return
Email not found
; + return
{t('emails:emailNotFound')}
; } return ( @@ -198,10 +200,10 @@ const EmailDetails: React.FC = (): React.ReactElement => { setShowDeleteModal(false); void handleDelete(); }} - title="Delete Email" - message="Are you sure you want to delete this email? This action cannot be undone." - confirmText="Delete" - cancelText="Cancel" + title={t('emails:deleteEmailTitle')} + message={t('emails:deleteEmailConfirm')} + confirmText={t('common:delete')} + cancelText={t('common:cancel')} variant="danger" /> @@ -212,9 +214,9 @@ const EmailDetails: React.FC = (): React.ReactElement => {

{email.subject}

-

From: {email.fromDisplay} ({email.fromLocal}@{email.fromDomain})

-

To: {email.toLocal}@{email.toDomain}

-

Date: {new Date(email.dateSystem).toLocaleString()}

+

{t('emails:from')} {email.fromDisplay} ({email.fromLocal}@{email.fromDomain})

+

{t('emails:to')} {email.toLocal}@{email.toDomain}

+

{t('emails:date')} {new Date(email.dateSystem).toLocaleString()}

@@ -224,7 +226,7 @@ const EmailDetails: React.FC = (): React.ReactElement => {