Repository: joinmarket-webui/joinmarket-webui
Pull request #174 (2022-03-14 14:19:02)

feat: translate screens
Translate all screen. I hope I didn't miss to much. Naming is certainly not ideal everywhere and there are quite a few repetitions still. Open for suggestions!

theborakompanioni: lgtm :heavy_check_mark: 

Think you had to make some compromises. It's a very unrewarding task, thank you so much for your effort. :muscle:

One last thing, as this would be a great opportunity for simplification: What do you think about getting rid of the changing "loading texts", e.g. "Lock" -> "Locking", "Unlock" -> "Unlocking", etc and just show the loading indicator next to the static text? Would generally decrease "complexity" of the translations and the code as well. Also, I really do not know how it is expressed in other languages, say Hindi, Finnish, German, whatever.


Patch:
diff --git a/src/components/App.jsx b/src/components/App.jsx
index cd772c5..48b0302 100644
--- a/src/components/App.jsx
+++ b/src/components/App.jsx
@@ -1,6 +1,7 @@
 import React, { useState, useEffect, useCallback } from 'react'
 import { Route, Routes, Navigate } from 'react-router-dom'
 import * as rb from 'react-bootstrap'
+import { Trans, useTranslation } from 'react-i18next'
 import Wallets from './Wallets'
 import CreateWallet from './CreateWallet'
 import Send from './Send'
@@ -27,6 +28,7 @@ import Onboarding from './Onboarding'
 const SESSION_REQUEST_INTERVAL = 10_000
 
 export default function App() {
+  const { t } = useTranslation()
   const currentWallet = useCurrentWallet()
   const setCurrentWallet = useSetCurrentWallet()
   const setCurrentWalletInfo = useSetCurrentWalletInfo()
@@ -137,13 +139,11 @@ export default function App() {
         <div className="warning-card-wrapper">
           <rb.Card className="warning-card translate-middle shadow-lg">
             <rb.Card.Body>
-              <rb.Card.Title className="text-center mb-3">Warning</rb.Card.Title>
-              <p className="text-secondary">
-                While JoinMarket is tried and tested, Jam is not. It is in an alpha stage, so use with caution.
-              </p>
+              <rb.Card.Title className="text-center mb-3">{t('footer.warning_alert_title')}</rb.Card.Title>
+              <p className="text-secondary">{t('footer.warning_alert_text')}</p>
               <div className="text-center mt-3">
                 <rb.Button variant="secondary" onClick={() => setShowAlphaWarning(false)}>
-                  Fine with me.
+                  {t('footer.warning_alert_button_ok')}
                 </rb.Button>
               </div>
             </rb.Card.Body>
@@ -153,7 +153,7 @@ export default function App() {
       <Navbar coinjoinInProcess={coinjoinInProcess} makerRunning={makerRunning} connectionError={connectionError} />
       <rb.Container as="main" className="py-5">
         {connectionError ? (
-          <rb.Alert variant="danger">No connection to backend: {connectionError}.</rb.Alert>
+          <rb.Alert variant="danger">{t('app.alert_no_connection', { connectionError })}.</rb.Alert>
         ) : (
           <Routes>
             <Route element={<Layout />}>
@@ -200,14 +200,18 @@ export default function App() {
       <rb.Nav as="footer" className="border-top py-2">
         <rb.Container fluid="xl" className="d-flex flex-column flex-md-row justify-content-center py-2 px-4">
           <div className="d-flex flex-1 order-2 order-md-0 flex-column justify-content-center align-items-center align-items-md-start">
-            <div className="warning-hint text-start text-secondary d-none d-md-block">This is pre-alpha software.</div>
-            <rb.Button
-              variant="link"
-              className="warning-hint text-start border-0 p-0 text-secondary"
-              onClick={() => setShowAlphaWarning(true)}
-            >
-              Read this before using.
-            </rb.Button>
+            <div className="warning-hint text-start text-secondary d-none d-md-block">
+              <Trans i18nKey="footer.warning">
+                This is pre-alpha software.
+                <rb.Button
+                  variant="link"
+                  className="warning-hint text-start border-0 p-0 text-secondary"
+                  onClick={() => setShowAlphaWarning(true)}
+                >
+                  Read this before using.
+                </rb.Button>
+              </Trans>
+            </div>
           </div>
           <div className="d-flex order-1 flex-1 flex-grow-0 justify-content-center align-items-center px-4">
             <rb.Nav.Item>
@@ -217,7 +221,7 @@ export default function App() {
                 rel="noopener noreferrer"
                 className="nav-link text-secondary px-2"
               >
-                Docs
+                {t('footer.docs')}
               </a>
             </rb.Nav.Item>
             <rb.Nav.Item>
@@ -227,7 +231,7 @@ export default function App() {
                 rel="noopener noreferrer"
                 className="nav-link text-secondary px-2"
               >
-                Features
+                {t('footer.features')}
               </a>
             </rb.Nav.Item>
             <rb.Nav.Item>
@@ -237,7 +241,7 @@ export default function App() {
                 rel="noopener noreferrer"
                 className="nav-link text-secondary px-2"
               >
-                GitHub
+                {t('footer.github')}
               </a>
             </rb.Nav.Item>
             <rb.Nav.Item>
@@ -247,13 +251,15 @@ export default function App() {
                 rel="noopener noreferrer"
                 className="nav-link text-secondary px-2"
               >
-                Twitter
+                {t('footer.twitter')}
               </a>
             </rb.Nav.Item>
           </div>
           <div className="d-flex order-0 order-md-2 flex-1 justify-content-center justify-content-md-end align-items-center">
             <span className={`mx-1 ${websocketConnected ? 'text-success' : 'text-danger'}`}>•</span>
-            <span className="text-secondary">{websocketConnected ? 'Connected' : 'Disconnected'}</span>
+            <span className="text-secondary">
+              {websocketConnected ? t('footer.connected') : t('footer.disconnected')}
+            </span>
           </div>
         </rb.Container>
       </rb.Nav>
diff --git a/src/components/CreateWallet.jsx b/src/components/CreateWallet.jsx
index 0a4226b..dfaa216 100644
--- a/src/components/CreateWallet.jsx
+++ b/src/components/CreateWallet.jsx
@@ -1,6 +1,7 @@
 import React, { useState } from 'react'
 import * as rb from 'react-bootstrap'
 import { Link, useNavigate } from 'react-router-dom'
+import { Trans, useTranslation } from 'react-i18next'
 import PageTitle from './PageTitle'
 import Seedphrase from './Seedphrase'
 import ToggleSwitch from './ToggleSwitch'
@@ -9,6 +10,7 @@ import { useCurrentWallet } from '../context/WalletContext'
 import * as Api from '../libs/JmWalletApi'
 
 const WalletCreationForm = ({ createWallet, isCreating }) => {
+  const { t } = useTranslation()
   const [validated, setValidated] = useState(false)
 
   const onSubmit = (e) => {
@@ -28,32 +30,41 @@ const WalletCreationForm = ({ createWallet, isCreating }) => {
     <>
       <rb.Form onSubmit={onSubmit} validated={validated} noValidate>
         <rb.Form.Group className="mb-4" controlId="walletName">
-          <rb.Form.Label>Wallet Name</rb.Form.Label>
-          <rb.Form.Control name="wallet" placeholder="Your wallet..." disabled={isCreating} required />
-          <rb.Form.Control.Feedback>Looks good!</rb.Form.Control.Feedback>
-          <rb.Form.Control.Feedback type="invalid">Please set a wallet name.</rb.Form.Control.Feedback>
+          <rb.Form.Label>{t('create_wallet.label_wallet_name')}</rb.Form.Label>
+          <rb.Form.Control
+            name="wallet"
+            placeholder={t('create_wallet.placeholder_wallet_name')}
+            disabled={isCreating}
+            required
+          />
+          <rb.Form.Control.Feedback>{t('create_wallet.feedback_valid')}</rb.Form.Control.Feedback>
+          <rb.Form.Control.Feedback type="invalid">
+            {t('create_wallet.feedback_invalid_wallet_name')}
+          </rb.Form.Control.Feedback>
         </rb.Form.Group>
         <rb.Form.Group className="mb-4" controlId="password">
-          <rb.Form.Label>Password</rb.Form.Label>
+          <rb.Form.Label>{t('create_wallet.label_password')}</rb.Form.Label>
           <rb.Form.Control
             name="password"
             type="password"
-            placeholder="Choose a secure password..."
+            placeholder={t('create_wallet.placeholder_password')}
             disabled={isCreating}
             autoComplete="new-password"
             required
           />
-          <rb.Form.Control.Feedback>Looks good!</rb.Form.Control.Feedback>
-          <rb.Form.Control.Feedback type="invalid">Please set a password.</rb.Form.Control.Feedback>
+          <rb.Form.Control.Feedback>{t('create_wallet.feedback_valid')}</rb.Form.Control.Feedback>
+          <rb.Form.Control.Feedback type="invalid">
+            {t('create_wallet.feedback_invalid_password')}
+          </rb.Form.Control.Feedback>
         </rb.Form.Group>
         <rb.Button variant="dark" type="submit" disabled={isCreating}>
           {isCreating ? (
             <div>
               <rb.Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" className="me-2" />
-              Creating
+              {t('create_wallet.button_creating')}
             </div>
           ) : (
-            'Create'
+            t('create_wallet.button_create')
           )}
         </rb.Button>
       </rb.Form>
@@ -62,6 +73,7 @@ const WalletCreationForm = ({ createWallet, isCreating }) => {
 }
 
 const WalletCreationConfirmation = ({ createdWallet, walletConfirmed }) => {
+  const { t } = useTranslation()
   const [userConfirmed, setUserConfirmed] = useState(false)
   const [revealSensitiveInfo, setRevealSensitiveInfo] = useState(false)
   const [sensitiveInfoWasRevealed, setSensitiveInfoWasRevealed] = useState(false)
@@ -69,21 +81,21 @@ const WalletCreationConfirmation = ({ createdWallet, walletConfirmed }) => {
   return (
     <div>
       <div className="mb-4">
-        <div>Wallet Name</div>
+        <div>{t('create_wallet.confirmation_label_wallet_name')}</div>
         <div className="fs-4">{walletDisplayName(createdWallet.name)}</div>
       </div>
       <div className="mb-4">
         <Seedphrase seedphrase={createdWallet.seedphrase} isBlurred={!revealSensitiveInfo} />
       </div>
       <div className="mb-4">
-        <div>Password</div>
+        <div>{t('create_wallet.confirmation_label_password')}</div>
         <div className={`fs-4${revealSensitiveInfo ? '' : ' blurred-text'}`}>
           {!revealSensitiveInfo ? 'randomrandom' : createdWallet.password}
         </div>
       </div>
       <div className="mb-2">
         <ToggleSwitch
-          label="Reveal sensitive information"
+          label={t('create_wallet.confirmation_toggle_reveal_info')}
           onToggle={(isToggled) => {
             setRevealSensitiveInfo(isToggled)
             setSensitiveInfoWasRevealed(true)
@@ -92,7 +104,7 @@ const WalletCreationConfirmation = ({ createdWallet, walletConfirmed }) => {
       </div>
       <div className="mb-4">
         <ToggleSwitch
-          label="I've written down the information above."
+          label={t('create_wallet.confirmation_toggle_info_written_down')}
           onToggle={(isToggled) => setUserConfirmed(isToggled)}
         />
       </div>
@@ -102,13 +114,14 @@ const WalletCreationConfirmation = ({ createdWallet, walletConfirmed }) => {
         disabled={!sensitiveInfoWasRevealed || !userConfirmed}
         onClick={() => userConfirmed && walletConfirmed()}
       >
-        Fund wallet
+        {t('create_wallet.confirmation_button_fund_wallet')}
       </rb.Button>
     </div>
   )
 }
 
 export default function CreateWallet({ startWallet }) {
+  const { t } = useTranslation()
   const currentWallet = useCurrentWallet()
   const navigate = useNavigate()
 
@@ -142,7 +155,7 @@ export default function CreateWallet({ startWallet }) {
       startWallet(createdWallet.name, createdWallet.token)
       navigate('/wallet')
     } else {
-      setAlert({ variant: 'danger', message: 'Wallet confirmation failed.' })
+      setAlert({ variant: 'danger', message: t('alert_confirmation_failed') })
     }
   }
 
@@ -153,23 +166,26 @@ export default function CreateWallet({ startWallet }) {
     <div className="create-wallet">
       {isCreated ? (
         <PageTitle
-          title="Wallet created successfully!"
-          subtitle="Please write down your seed phrase and password! Without this information you will not be able to access and recover your wallet!"
+          title={t('create_wallet.title_wallet_created')}
+          subtitle={t('create_wallet.subtitle_wallet_created')}
           success
         />
       ) : (
-        <PageTitle title="Create Wallet" />
+        <PageTitle title={t('create_wallet.title')} />
       )}
       {alert && <rb.Alert variant={alert.variant}>{alert.message}</rb.Alert>}
       {canCreate && <WalletCreationForm createWallet={createWallet} isCreating={isCreating} />}
       {isCreated && <WalletCreationConfirmation createdWallet={createdWallet} walletConfirmed={walletConfirmed} />}
       {!canCreate && !isCreated && (
         <rb.Alert variant="warning">
-          Currently <strong>{walletDisplayName(currentWallet.name)}</strong> is active. You need to lock it first.{' '}
-          <Link to="/" className="alert-link">
-            Go back
-          </Link>
-          .
+          <Trans i18nKey="create_wallet.alert_other_wallet_unlocked">
+            Currently <strong>{{ walletName: walletDisplayName(currentWallet.name) }}</strong> is active. You need to
+            lock it first.
+            <Link to="/" className="alert-link">
+              Go back
+            </Link>
+            .
+          </Trans>
         </rb.Alert>
       )}
     </div>
diff --git a/src/components/CurrentWalletAdvanced.jsx b/src/components/CurrentWalletAdvanced.jsx
index 0146207..70975f4 100644
--- a/src/components/CurrentWalletAdvanced.jsx
+++ b/src/components/CurrentWalletAdvanced.jsx
@@ -1,5 +1,6 @@
 import React, { useState, useEffect } from 'react'
 import * as rb from 'react-bootstrap'
+import { useTranslation } from 'react-i18next'
 import DisplayAccounts from './DisplayAccounts'
 import DisplayAccountUTXOs from './DisplayAccountUTXOs'
 import DisplayUTXOs from './DisplayUTXOs'
@@ -7,6 +8,7 @@ import { useCurrentWallet, useCurrentWalletInfo, useSetCurrentWalletInfo } from
 import * as Api from '../libs/JmWalletApi'
 
 export default function CurrentWalletAdvanced() {
+  const { t } = useTranslation()
   const currentWallet = useCurrentWallet()
   const walletInfo = useCurrentWalletInfo()
   const setWalletInfo = useSetCurrentWalletInfo()
@@ -29,14 +31,20 @@ export default function CurrentWalletAdvanced() {
     setIsLoading(true)
 
     const loadingWallet = Api.getWalletDisplay({ walletName: name, token, signal: abortCtrl.signal })
-      .then((res) => (res.ok ? res.json() : Promise.reject(new Error(res.message || 'Loading wallet failed.'))))
+      .then((res) =>
+        res.ok ? res.json() : Promise.reject(new Error(res.message || t('current_wallet.error_loading_failed')))
+      )
       .then((data) => setWalletInfo(data.walletinfo))
       .catch((err) => {
         !abortCtrl.signal.aborted && setAlert({ variant: 'danger', message: err.message })
       })
 
     const loadingUtxos = Api.getWalletUtxos({ walletName: name, token, signal: abortCtrl.signal })
-      .then((res) => (res.ok ? res.json() : Promise.reject(new Error(res.message || 'Loading UTXOs failed.'))))
+      .then((res) =>
+        res.ok
+          ? res.json()
+          : Promise.reject(new Error(res.message || t('current_wallet_advanced.error_loading_utxos_failed')))
+      )
       .then((data) => setUtxoData(data.utxos))
       .catch((err) => {
         !abortCtrl.signal.aborted && setAlert({ variant: 'danger', message: err.message })
@@ -45,7 +53,7 @@ export default function CurrentWalletAdvanced() {
     Promise.all([loadingWallet, loadingUtxos]).finally(() => !abortCtrl.signal.aborted && setIsLoading(false))
 
     return () => abortCtrl.abort()
-  }, [currentWallet, setWalletInfo])
+  }, [currentWallet, setWalletInfo, t])
 
   return (
     <div>
@@ -55,7 +63,7 @@ export default function CurrentWalletAdvanced() {
           <rb.Col className="flex-grow-0">
             <div className="d-flex mb-3">
               <rb.Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" className="me-2" />
-              Loading
+              {t('current_wallet.text_loading')}
             </div>
           </rb.Col>
         </rb.Row>
@@ -63,7 +71,7 @@ export default function CurrentWalletAdvanced() {
       {!isLoading && walletInfo && <DisplayAccounts accounts={walletInfo.accounts} className="mb-4" />}
       {!!fidelityBonds?.length && (
         <div className="mt-5 mb-3 pe-3">
-          <h5>Fidelity Bonds</h5>
+          <h5>{t('current_wallet_advanced.title_fidelity_bonds')}</h5>
           <DisplayUTXOs utxos={fidelityBonds} className="pe-2" />
         </div>
       )}
@@ -76,12 +84,12 @@ export default function CurrentWalletAdvanced() {
             }}
             className="mb-3"
           >
-            {showUTXO ? 'Hide UTXOs' : 'Show UTXOs'}
+            {showUTXO ? t('current_wallet_advanced.button_hide_utxos') : t('current_wallet_advanced.button_show_utxos')}
           </rb.Button>
           <rb.Fade in={showUTXO} mountOnEnter={true} unmountOnExit={true}>
             <div>
               {utxos.length === 0 ? (
-                <rb.Alert variant="info">No UTXOs available.</rb.Alert>
+                <rb.Alert variant="info">{t('current_wallet_advanced.alert_no_utxos')}</rb.Alert>
               ) : (
                 <DisplayAccountUTXOs utxos={utxos} className="mt-3" />
               )}
diff --git a/src/components/CurrentWalletMagic.jsx b/src/components/CurrentWalletMagic.jsx
index c79f775..7ab732c 100644
--- a/src/components/CurrentWalletMagic.jsx
+++ b/src/components/CurrentWalletMagic.jsx
@@ -1,6 +1,7 @@
 import React, { useEffect, useState } from 'react'
 import * as rb from 'react-bootstrap'
 import { Link } from 'react-router-dom'
+import { useTranslation } from 'react-i18next'
 import { useSettings, useSettingsDispatch } from '../context/SettingsContext'
 import { useCurrentWallet, useCurrentWalletInfo, useSetCurrentWalletInfo } from '../context/WalletContext'
 import Balance from './Balance'
@@ -61,6 +62,7 @@ const PrivacyLevel = ({ numAccounts, level, balance }) => {
 }
 
 export default function CurrentWalletMagic() {
+  const { t } = useTranslation()
   const settings = useSettings()
   const settingsDispatch = useSettingsDispatch()
   const currentWallet = useCurrentWallet()
@@ -78,7 +80,9 @@ export default function CurrentWalletMagic() {
     setIsLoading(true)
 
     Api.getWalletDisplay({ walletName, token, signal: abortCtrl.signal })
-      .then((res) => (res.ok ? res.json() : Promise.reject(new Error(res.message || 'Loading wallet failed.'))))
+      .then((res) =>
+        res.ok ? res.json() : Promise.reject(new Error(res.message || t('current_wallet.error_loading_failed')))
+      )
       .then((data) => setWalletInfo(data.walletinfo))
       .catch((err) => {
         !abortCtrl.signal.aborted && setAlert({ variant: 'danger', message: err.message })
@@ -86,7 +90,7 @@ export default function CurrentWalletMagic() {
       .finally(() => !abortCtrl.signal.aborted && setIsLoading(false))
 
     return () => abortCtrl.abort()
-  }, [currentWallet, setWalletInfo])
+  }, [currentWallet, setWalletInfo, t])
 
   return (
     <div className="privacy-levels">
@@ -102,7 +106,7 @@ export default function CurrentWalletMagic() {
           <rb.Col className="flex-grow-0">
             <div className="d-flex justify-content-center align-items-center">
               <rb.Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" className="me-2" />
-              Loading
+              {t('current_wallet.text_loading')}
             </div>
           </rb.Col>
         </rb.Row>
@@ -124,14 +128,14 @@ export default function CurrentWalletMagic() {
             <rb.Col>
               {/* Always receive on first mixdepth. */}
               <Link to="/receive" state={{ account: 0 }} className="btn btn-outline-dark w-100">
-                Deposit
+                {t('current_wallet.button_deposit')}
               </Link>
             </rb.Col>
             <rb.Col>
               {/* Todo: Withdrawing needs to factor in the privacy levels as well.
                 Depending on the mixdepth/account there will be different amounts available. */}
               <Link to="/send" className="btn btn-outline-dark w-100">
-                Withdraw
+                {t('current_wallet.button_withdraw')}
               </Link>
             </rb.Col>
           </rb.Row>
@@ -148,7 +152,7 @@ export default function CurrentWalletMagic() {
             <Link to="/" className="btn btn-outline-dark">
               <div className="d-flex justify-content-center align-items-center">
                 <Sprite symbol="wallet" width="24" height="24" />
-                <div className="ps-1">Switch Wallet</div>
+                <div className="ps-1">{t('current_wallet.button_switch_wallet')}</div>
               </div>
             </Link>
           </rb.Row>
diff --git a/src/components/DisplayAccountUTXOs.jsx b/src/components/DisplayAccountUTXOs.jsx
index 9e90ec7..8de1f57 100644
--- a/src/components/DisplayAccountUTXOs.jsx
+++ b/src/components/DisplayAccountUTXOs.jsx
@@ -1,7 +1,7 @@
 import React from 'react'
 import * as rb from 'react-bootstrap'
+import { useTranslation } from 'react-i18next'
 import DisplayUTXOs from './DisplayUTXOs'
-import { useSettings } from '../context/SettingsContext'
 
 const byAccount = (utxos) => {
   const ret = utxos.reduce((res, utxo) => {
@@ -14,7 +14,7 @@ const byAccount = (utxos) => {
 }
 
 export default function DisplayAccountUTXOs({ utxos, ...props }) {
-  const settings = useSettings()
+  const { t } = useTranslation()
 
   return (
     <rb.Accordion {...props}>
@@ -22,7 +22,7 @@ export default function DisplayAccountUTXOs({ utxos, ...props }) {
         <rb.Accordion.Item key={account} eventKey={account}>
           <rb.Accordion.Header className="head">
             <h5 className="mb-0">
-              {settings.useAdvancedWalletMode ? 'Account' : 'Privacy Level'} {account}
+              {t('current_wallet_advanced.acount')} {account}
             </h5>
           </rb.Accordion.Header>
           <rb.Accordion.Body>
diff --git a/src/components/DisplayAccounts.jsx b/src/components/DisplayAccounts.jsx
index 5154e6c..d9529cf 100644
--- a/src/components/DisplayAccounts.jsx
+++ b/src/components/DisplayAccounts.jsx
@@ -1,7 +1,7 @@
 import React from 'react'
 import { Link } from 'react-router-dom'
 import * as rb from 'react-bootstrap'
-import { titleize } from '../utils'
+import { useTranslation } from 'react-i18next'
 import Balance from './Balance'
 import { useSettings } from '../context/SettingsContext'
 
@@ -31,6 +31,7 @@ const BranchEntry = ({ entry, ...props }) => {
 }
 
 export default function DisplayAccounts({ accounts, ...props }) {
+  const { t } = useTranslation()
   const settings = useSettings()
 
   return (
@@ -41,7 +42,7 @@ export default function DisplayAccounts({ accounts, ...props }) {
             <rb.Row className="w-100  me-1">
               <rb.Col xs={'auto'}>
                 <h5 className="mb-0">
-                  {settings.useAdvancedWalletMode ? 'Account' : 'Privacy Level'} {account}
+                  {t('current_wallet_advanced.account')} {account}
                 </h5>
               </rb.Col>
               <rb.Col className="d-flex align-items-center justify-content-end">
@@ -51,10 +52,10 @@ export default function DisplayAccounts({ accounts, ...props }) {
           </rb.Accordion.Header>
           <rb.Accordion.Body>
             <Link to="/send" state={{ account }} className="btn btn-outline-dark">
-              Send
+              {t('current_wallet_advanced.account_button_send')}
             </Link>{' '}
             <Link to="/receive" state={{ account }} className="btn btn-outline-dark">
-              Receive
+              {t('current_wallet_advanced.account_button_receive')}
             </Link>
             {branches.map(({ balance, branch, entries }) => {
               const [type, derivation, xpub] = branch.split('\t')
@@ -62,7 +63,13 @@ export default function DisplayAccounts({ accounts, ...props }) {
                 <article key={derivation}>
                   <rb.Row className="mt-4 pe-3">
                     <rb.Col>
-                      <h6>{titleize(type)}</h6>
+                      {type === 'external addresses' && (
+                        <h6>{t('current_wallet_advanced.account_heading_external_addresses')}</h6>
+                      )}
+                      {type === 'internal addresses' && (
+                        <h6>{t('current_wallet_advanced.account_heading_internal_addresses')}</h6>
+                      )}
+                      {!['internal addresses', 'external addresses'].includes(type) && <h6>{type}</h6>}
                     </rb.Col>
                     <rb.Col className="d-flex align-items-center justify-content-end">
                       <Balance valueString={balance} convertToUnit={settings.unit} showBalance={settings.showBalance} />
diff --git a/src/components/DisplayUTXOs.jsx b/src/components/DisplayUTXOs.jsx
index 9ec6bd7..ae5042a 100644
--- a/src/components/DisplayUTXOs.jsx
+++ b/src/components/DisplayUTXOs.jsx
@@ -1,6 +1,7 @@
 import React from 'react'
 import { useState } from 'react'
 import * as rb from 'react-bootstrap'
+import { useTranslation } from 'react-i18next'
 import { displayDate } from '../utils'
 import Balance from './Balance'
 import Alert from './Alert'
@@ -9,6 +10,7 @@ import { useCurrentWallet } from '../context/WalletContext'
 import * as Api from '../libs/JmWalletApi'
 
 const Utxo = ({ utxo, ...props }) => {
+  const { t } = useTranslation()
   const settings = useSettings()
   const currentWallet = useCurrentWallet()
 
@@ -29,7 +31,7 @@ const Utxo = ({ utxo, ...props }) => {
           return true
         } else {
           const { message } = await res.json()
-          throw new Error(message || 'Request failed.')
+          throw new Error(message || t('current_wallet_advanced.error_freeze_failed'))
         }
       })
       .then((_) => (utxo.frozen = !utxo.frozen))
@@ -51,10 +53,16 @@ const Utxo = ({ utxo, ...props }) => {
                 <code className="text-break">{utxo.address}</code>
               </div>
               <div>
-                {utxo.locktime && <span className="me-2">Locked until {displayDate(utxo.locktime)}</span>}
+                {utxo.locktime && (
+                  <span className="me-2">
+                    {t('current_wallet_advanced.label_locked_until')} {displayDate(utxo.locktime)}
+                  </span>
+                )}
                 {utxo.label && <span className="me-2 badge bg-light">{utxo.label}</span>}
-                {utxo.frozen && <span className="me-2 badge bg-info">frozen</span>}
-                {utxo.confirmations === 0 && <span className="badge bg-secondary">unconfirmed</span>}
+                {utxo.frozen && <span className="me-2 badge bg-info">{t('current_wallet_advanced.label_frozen')}</span>}
+                {utxo.confirmations === 0 && (
+                  <span className="badge bg-secondary">{t('current_wallet_advanced.label_unconfirmed')}</span>
+                )}
               </div>
             </rb.Stack>
           </rb.Col>
@@ -83,7 +91,9 @@ const Utxo = ({ utxo, ...props }) => {
                       className="ms-1 me-2"
                     />
                   )}
-                  {utxo.frozen ? 'unfreeze' : 'freeze'}
+                  {utxo.frozen
+                    ? t('current_wallet_advanced.button_unfreeze')
+                    : t('current_wallet_advanced.button_freeze')}
                 </rb.Button>
               </div>
             </rb.Stack>
diff --git a/src/components/Earn.jsx b/src/components/Earn.jsx
index ffe0fa5..f1709f7 100644
--- a/src/components/Earn.jsx
+++ b/src/components/Earn.jsx
@@ -1,6 +1,7 @@
 import React from 'react'
 import { useEffect, useState } from 'react'
 import * as rb from 'react-bootstrap'
+import { useTranslation } from 'react-i18next'
 import { useSettings } from '../context/SettingsContext'
 import Sprite from './Sprite'
 import PageTitle from './PageTitle'
@@ -11,6 +12,7 @@ const OFFERTYPE_REL = 'sw0reloffer'
 const OFFERTYPE_ABS = 'sw0absoffer'
 
 const YieldgenReport = ({ lines, maxAmountOfRows = 25 }) => {
+  const { t } = useTranslation()
   const settings = useSettings()
 
   const empty = !lines || lines.length < 2
@@ -27,7 +29,7 @@ const YieldgenReport = ({ lines, maxAmountOfRows = 25 }) => {
 
   return (
     <div className="mt-2 mb-3">
-      {empty && <rb.Alert variant="info">The report is empty.</rb.Alert>}
+      {empty && <rb.Alert variant="info">{t('earn.alert_empty_report')}</rb.Alert>}
       {!empty && (
         <>
           <rb.Table striped bordered hover variant={settings.theme} responsive>
@@ -57,7 +59,10 @@ const YieldgenReport = ({ lines, maxAmountOfRows = 25 }) => {
           </rb.Table>
           <div className="mt-1 d-flex justify-content-end">
             <small>
-              Showing {visibleLines.length} of {linesWithoutHeader.length} entries
+              {t('earn.text_report_length', {
+                visibleLines: visibleLines.length,
+                linesWithoutHeader: linesWithoutHeader.length,
+              })}
             </small>
           </div>
         </>
@@ -67,6 +72,7 @@ const YieldgenReport = ({ lines, maxAmountOfRows = 25 }) => {
 }
 
 export default function Earn({ currentWallet, coinjoinInProcess, makerRunning }) {
+  const { t } = useTranslation()
   const settings = useSettings()
   const [validated, setValidated] = useState(false)
   const [alert, setAlert] = useState(null)
@@ -159,16 +165,16 @@ export default function Earn({ currentWallet, coinjoinInProcess, makerRunning })
 
     const waitingForMakerToStart = isWaitingMakerStart && !makerRunning
     setIsWaitingMakerStart(waitingForMakerToStart)
-    waitingForMakerToStart && setAlert({ variant: 'success', message: 'Service is starting.' })
+    waitingForMakerToStart && setAlert({ variant: 'success', message: t('earn.alert_starting') })
 
     const waitingForMakerToStop = isWaitingMakerStop && makerRunning
     setIsWaitingMakerStop(waitingForMakerToStop)
-    waitingForMakerToStop && setAlert({ variant: 'success', message: 'Service is stopping.' })
+    waitingForMakerToStop && setAlert({ variant: 'success', message: t('earn.alert_stopping') })
 
     const waiting = waitingForMakerToStart || waitingForMakerToStop
     setIsWaiting(waiting)
-    !waiting && makerRunning && setAlert({ variant: 'success', message: 'Service is running.' })
-  }, [makerRunning, isWaitingMakerStart, isWaitingMakerStop])
+    !waiting && makerRunning && setAlert({ variant: 'success', message: t('earn.alert_running') })
+  }, [makerRunning, isWaitingMakerStart, isWaitingMakerStop, t])
 
   useEffect(() => {
     if (!isShowReport) return
@@ -181,7 +187,7 @@ export default function Earn({ currentWallet, coinjoinInProcess, makerRunning })
         if (res.ok) return res.json()
         // 404 is returned till the maker is started at least once
         if (res.status === 404) return {}
-        return Promise.reject(new Error(res.message || 'Failed to load yield generator report.'))
+        return Promise.reject(new Error(res.message || t('earn.error_loading_report_failed')))
       })
       .then((data) => setYieldgenReportLines(data.yigen_data))
       .catch((err) => {
@@ -194,7 +200,7 @@ export default function Earn({ currentWallet, coinjoinInProcess, makerRunning })
       })
 
     return () => abortCtrl.abort()
-  }, [makerRunning, isShowReport])
+  }, [makerRunning, isShowReport, t])
 
   const stopMakerService = async () => {
     const { name: walletName, token } = currentWallet
@@ -242,14 +248,11 @@ export default function Earn({ currentWallet, coinjoinInProcess, makerRunning })
     <div className="earn">
       <rb.Row>
         <rb.Col>
-          <PageTitle
-            title="Earn bitcoin"
-            subtitle="By making your bitcoin available for others, you help them improve their privacy and can also earn a yield."
-          />
+          <PageTitle title={t('earn.title')} subtitle={t('earn.subtitle')} />
 
           <rb.Fade in={coinjoinInProcess} mountOnEnter={true} unmountOnExit={true}>
             <div className="mb-4 p-3 border border-1 rounded">
-              <small className="text-secondary">A collaborative transaction is currently in progress.</small>
+              <small className="text-secondary">{t('earn.alert_coinjoin_in_progress')}</small>
             </div>
           </rb.Fade>
 
@@ -262,7 +265,7 @@ export default function Earn({ currentWallet, coinjoinInProcess, makerRunning })
                   {settings.useAdvancedWalletMode && (
                     <rb.Form.Group className="mb-3" controlId="offertype">
                       <ToggleSwitch
-                        label="Relative offer"
+                        label={t('earn.toggle_rel_offer')}
                         initialValue={isRelOffer}
                         onToggle={(isToggled) => setAndPersistOffertype(isToggled ? OFFERTYPE_REL : OFFERTYPE_ABS)}
                       />
@@ -271,12 +274,12 @@ export default function Earn({ currentWallet, coinjoinInProcess, makerRunning })
                   {isRelOffer ? (
                     <rb.Form.Group className="mb-3" controlId="feeRel">
                       <rb.Form.Label className="mb-0">
-                        Relative fee {feeRel !== '' && `(${factorToPercentage(feeRel)}%)`}
+                        {t('earn.label_rel_fee', {
+                          fee: feeRel !== '' ? `(${factorToPercentage(feeRel)}%)` : '',
+                        })}
                       </rb.Form.Label>
                       <div className="mb-2">
-                        <rb.Form.Text className="text-secondary">
-                          As a percentage of the amounts you help others with improved privacy.
-                        </rb.Form.Text>
+                        <rb.Form.Text className="text-secondary">{t('earn.description_rel_fee')}</rb.Form.Text>
                       </div>
                       <rb.Form.Control
                         type="number"
@@ -290,16 +293,17 @@ export default function Earn({ currentWallet, coinjoinInProcess, makerRunning })
                         onChange={(e) => setAndPersistFeeRel(percentageToFactor(e.target.value))}
                       />
                       <rb.Form.Control.Feedback type="invalid">
-                        Please provide a relative fee between {feeRelPercentageMin}% and {feeRelPercentageMax}%.
+                        {t('feedback_invalid_rel_fee', {
+                          feeRelPercentageMin: `${feeRelPercentageMin}%`,
+                          feeRelPercentageMax: `${feeRelPercentageMax}%`,
+                        })}
                       </rb.Form.Control.Feedback>
                     </rb.Form.Group>
                   ) : (
                     <rb.Form.Group className="mb-3" controlId="feeAbs">
-                      <rb.Form.Label className="mb-0">Absolute fee in sats</rb.Form.Label>
+                      <rb.Form.Label className="mb-0">{t('earn.label_abs_fee')}</rb.Form.Label>
                       <div className="mb-2">
-                        <rb.Form.Text className="text-secondary">
-                          An absolute amount you get for helping others to improve their privacy.
-                        </rb.Form.Text>
+                        <rb.Form.Text className="text-secondary">{t('earn.description_abs_fee')}</rb.Form.Text>
                       </div>
                       <rb.Form.Control
                         type="number"
@@ -312,13 +316,13 @@ export default function Earn({ currentWallet, coinjoinInProcess, makerRunning })
                         onChange={(e) => setAndPersistFeeAbs(e.target.value)}
                       />
                       <rb.Form.Control.Feedback type="invalid">
-                        Please provide an absolute fee.
+                        {t('earn.feedback_invalid_abs_fee')}
                       </rb.Form.Control.Feedback>
                     </rb.Form.Group>
                   )}
                   {settings.useAdvancedWalletMode && (
                     <rb.Form.Group className="mb-3" controlId="minsize">
-                      <rb.Form.Label>Minimum amount in sats</rb.Form.Label>
+                      <rb.Form.Label>{t('earn.label_min_amount')}</rb.Form.Label>
                       <rb.Form.Control
                         type="number"
                         name="minsize"
@@ -330,7 +334,7 @@ export default function Earn({ currentWallet, coinjoinInProcess, makerRunning })
                         onChange={(e) => setAndPersistMinsize(e.target.value)}
                       />
                       <rb.Form.Control.Feedback type="invalid">
-                        Please provide a minimum amount.
+                        {t('earn.feedback_invalid_min_amount')}
                       </rb.Form.Control.Feedback>
                     </rb.Form.Group>
                   )}
@@ -348,12 +352,12 @@ export default function Earn({ currentWallet, coinjoinInProcess, makerRunning })
                       aria-hidden="true"
                       className="me-2"
                     />
-                    {makerRunning === true ? 'Stopping' : 'Starting'}
+                    {makerRunning === true ? t('earn.text_stopping') : t('earn.text_starting')}
                   </>
                 ) : makerRunning === true ? (
-                  'Stop'
+                  t('earn.button_stop')
                 ) : (
-                  'Start'
+                  t('earn.button_start')
                 )}
               </rb.Button>
             </rb.Form>
@@ -373,7 +377,7 @@ export default function Earn({ currentWallet, coinjoinInProcess, makerRunning })
               }}
             >
               <Sprite symbol={isShowReport ? 'hide' : 'show'} width="24" height="24" className="me-2" />
-              {isShowReport ? 'Hide' : 'Show'} report
+              {isShowReport ? t('earn.button_hide_report') : t('earn.button_show_report')}
               {isReportLoading && (
                 <rb.Spinner
                   as="span"
diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx
index 57c3ef5..605c354 100644
--- a/src/components/Navbar.jsx
+++ b/src/components/Navbar.jsx
@@ -1,6 +1,7 @@
 import React, { useState } from 'react'
 import { Link, NavLink } from 'react-router-dom'
 import * as rb from 'react-bootstrap'
+import { useTranslation } from 'react-i18next'
 import Sprite from './Sprite'
 import Balance from './Balance'
 import { useSettings } from '../context/SettingsContext'
@@ -32,6 +33,7 @@ const ActivityIndicator = ({ isOn }) => {
 }
 
 const CenterNav = ({ makerRunning, onClick }) => {
+  const { t } = useTranslation()
   return (
     <rb.Nav className="justify-content-center align-items-stretch">
       <rb.Nav.Item className="d-flex align-items-stretch">
@@ -42,7 +44,7 @@ const CenterNav = ({ makerRunning, onClick }) => {
             'center-nav-link nav-link d-flex align-items-center justify-content-center' + (isActive ? ' active' : '')
           }
         >
-          Send
+          {t('navbar.tab_send')}
         </NavLink>
       </rb.Nav.Item>
       <rb.Nav.Item className="d-flex align-items-stretch">
@@ -53,7 +55,7 @@ const CenterNav = ({ makerRunning, onClick }) => {
             'center-nav-link nav-link d-flex align-items-center justify-content-center' + (isActive ? ' active' : '')
           }
         >
-          Receive
+          {t('navbar.tab_receive')}
         </NavLink>
       </rb.Nav.Item>
       <rb.Nav.Item className="d-flex align-items-stretch">
@@ -65,7 +67,7 @@ const CenterNav = ({ makerRunning, onClick }) => {
           }
         >
           <div className="d-flex align-items-start">
-            Earn
+            {t('navbar.tab_earn')}
             <ActivityIndicator isOn={makerRunning} />
           </div>
         </NavLink>
@@ -75,12 +77,14 @@ const CenterNav = ({ makerRunning, onClick }) => {
 }
 
 const TrailingNav = ({ coinjoinInProcess, onClick }) => {
+  const { t } = useTranslation()
+
   return (
     <rb.Nav className="justify-content-center align-items-stretch">
       {coinjoinInProcess && (
         <rb.Nav.Item className="d-flex align-items-center justify-content-center pe-2">
           <div className="d-flex align-items-center px-0">
-            <rb.Navbar.Text>Joining</rb.Navbar.Text>
+            <rb.Navbar.Text>{t('navbar.joining_in_progress')}</rb.Navbar.Text>
             <Sprite symbol="joining" width="30" height="30" className="p-1 navbar-text" />
           </div>
         </rb.Nav.Item>
@@ -94,7 +98,7 @@ const TrailingNav = ({ coinjoinInProcess, onClick }) => {
           }
         >
           <Sprite symbol="gear" width="30" height="30" className="d-none d-md-inline-block" />
-          <span className="d-inline-block d-md-none">Settings</span>
+          <span className="d-inline-block d-md-none">{t('navbar.menu_mobile_settings')}</span>
         </NavLink>
       </rb.Nav.Item>
       <rb.Nav.Item className="d-flex align-items-stretch">
@@ -106,7 +110,7 @@ const TrailingNav = ({ coinjoinInProcess, onClick }) => {
           }
         >
           <Sprite symbol="grid" width="30" height="30" className="d-none d-md-inline-block" />
-          <span className="d-inline-block d-md-none">Wallets</span>
+          <span className="d-inline-block d-md-none">{t('navbar.menu_mobile_wallets')}</span>
         </NavLink>
       </rb.Nav.Item>
     </rb.Nav>
@@ -114,6 +118,7 @@ const TrailingNav = ({ coinjoinInProcess, onClick }) => {
 }
 
 export default function Navbar({ connectionError, makerRunning, coinjoinInProcess }) {
+  const { t } = useTranslation()
   const settings = useSettings()
   const currentWallet = useCurrentWallet()
   const currentWalletInfo = useCurrentWalletInfo()
@@ -148,16 +153,16 @@ export default function Navbar({ connectionError, makerRunning, coinjoinInProces
                   style={{ height: height }}
                 >
                   <Sprite symbol="logo" width="30" height="30" className="d-inline-block align-top" />
-                  <span className="ms-2">Jam</span>
+                  <span className="ms-2">{t('navbar.title')}</span>
                 </Link>
                 <div className="d-flex d-md-none align-items-center">
                   <rb.Navbar.Toggle id="mainNavToggle">
-                    <span>Menu</span>
+                    <span>{t('navbar.menu')}</span>
                   </rb.Navbar.Toggle>
                 </div>
                 <rb.Navbar.Offcanvas className={`navbar-${settings.theme}`} placement="end">
                   <rb.Offcanvas.Header>
-                    <rb.Offcanvas.Title>Jam</rb.Offcanvas.Title>
+                    <rb.Offcanvas.Title>{t('navbar.title')}</rb.Offcanvas.Title>
                   </rb.Offcanvas.Header>
                   <rb.Offcanvas.Body>
                     <rb.Nav className="ms-auto">
@@ -167,7 +172,7 @@ export default function Navbar({ connectionError, makerRunning, coinjoinInProces
                           onClick={() => isExpanded && setIsExpanded(false)}
                           className="nav-link"
                         >
-                          Create Wallet
+                          {t('navbar.button_create_wallet')}
                         </Link>
                       </rb.Nav.Item>
                     </rb.Nav>
@@ -198,12 +203,12 @@ export default function Navbar({ connectionError, makerRunning, coinjoinInProces
                 </rb.Nav>
                 <div className="d-flex d-md-none align-items-center">
                   <rb.Navbar.Toggle id="mainNavToggle">
-                    <span>Menu</span>
+                    <span>{t('navbar.menu_mobile')}</span>
                   </rb.Navbar.Toggle>
                 </div>
                 <rb.Navbar.Offcanvas className={`navbar-${settings.theme}`} placement="end">
                   <rb.Offcanvas.Header>
-                    <rb.Offcanvas.Title>Jam</rb.Offcanvas.Title>
+                    <rb.Offcanvas.Title>{t('navbar.title')}</rb.Offcanvas.Title>
                   </rb.Offcanvas.Header>
                   <rb.Offcanvas.Body>
                     <CenterNav makerRunning={makerRunning} onClick={() => setIsExpanded(!isExpanded)} />
diff --git a/src/components/Onboarding.jsx b/src/components/Onboarding.jsx
index 8e713e3..2c10e4d 100644
--- a/src/components/Onboarding.jsx
+++ b/src/components/Onboarding.jsx
@@ -1,9 +1,11 @@
 import * as rb from 'react-bootstrap'
 import React, { useState } from 'react'
+import { Trans, useTranslation } from 'react-i18next'
 import Sprite from './Sprite'
 import { useSettingsDispatch } from '../context/SettingsContext'
 
 export default function Onboarding() {
+  const { t } = useTranslation()
   const [step, setStep] = useState(0)
   const settingsDispatch = useSettingsDispatch()
 
@@ -17,33 +19,28 @@ export default function Onboarding() {
 
   const steps = [
     {
-      title: 'Welcome to Jam for JoinMarket!',
-      description:
-        'JoinMarket is a privacy-focused software solution that aims to improve the confidentiality and privacy of your bitcoin transactions. It facilitates the creation of collaborative transactions through a peer-to-peer marketplace.',
+      title: t('onboarding.screen_1_title'),
+      description: t('onboarding.screen_1_description'),
       icon: 'welcome',
     },
     {
-      title: 'Collaborative Transactions',
-      description:
-        'To have strong privacy guarantees in the open and transparent world of bitcoin, special kinds of transactions have to be created. JoinMarket helps you to create these transactions in an easy and automated way.',
+      title: t('onboarding.screen_2_title'),
+      description: t('onboarding.screen_2_description'),
       icon: 'collab',
     },
     {
-      title: 'You Are In Control',
-      description:
-        'JoinMarket is fully non-custodial, meaning that you always have full control over your funds. The system uses Bitcoin’s smart contracts to make sure that all transactions are atomic and your funds are secure at all times.',
+      title: t('onboarding.screen_3_title'),
+      description: t('onboarding.screen_3_description'),
       icon: 'key',
     },
     {
-      title: 'No Trusted Third Parties',
-      description:
-        'Since JoinMarket is a peer-to-peer system, trusted third parties are eliminated from the get-go. This unique market-driven approach reduces counterparty risk to a minimum.',
+      title: t('onboarding.screen_4_title'),
+      description: t('onboarding.screen_4_description'),
       icon: 'handshake',
     },
     {
-      title: 'Privacy for All',
-      description:
-        'JoinMarket is free and open-source software without a single point of failure. Everyone is free to use it and build upon it.',
+      title: t('onboarding.screen_5_title'),
+      description: t('onboarding.screen_5_description'),
       icon: 'shield-outline',
     },
   ]
@@ -54,35 +51,37 @@ export default function Onboarding() {
         <div className="d-flex justify-content-center">
           <Sprite symbol="logo" width="8rem" height="8rem" className="mb-4" />
         </div>
-        <h1 className="text-center">Jam</h1>
-        <h2 className="text-center fw-normal text-secondary mb-5">A friendly UI for JoinMarket</h2>
+        <h1 className="text-center">{t('onboarding.splashscreen_title')}</h1>
+        <h2 className="text-center fw-normal text-secondary mb-5">{t('onboarding.splashscreen_subtitle')}</h2>
         <div className="d-flex justify-content-center mb-2">
           <rb.Button size="lg" variant="dark" onClick={next}>
-            Get started
+            {t('onboarding.splashscreen_button_get_started')}
           </rb.Button>
         </div>
         <div className="d-flex justify-content-center">
           <rb.Button size="lg" variant="outline-dark" onClick={() => settingsDispatch({ showOnboarding: false })}>
-            Skip intro
+            {t('onboarding.splashscreen_button_skip_intro')}
           </rb.Button>
         </div>
         <p className="mt-4 text-center text-secondary">
-          Your wallet, your coins. <br />
-          100% open-source & open-design.
+          {t('onboarding.splashscreen_description_line1')}
+          <br />
+          {t('onboarding.splashscreen_description_line2')}
         </p>
         <div className="mt-4 text-secondary text-center">
-          <div className="fw-bolder">Warning</div>
+          <div className="fw-bolder">{t('onboarding.splashscreen_warning_title')}</div>
           <div>
-            While JoinMarket is tried and tested, Jam is not. It is in an alpha stage, so use with caution.
-            <a
-              href="https://github.com/joinmarket-webui/joinmarket-webui/issues"
-              target="_blank"
-              rel="noopener noreferrer"
-              className="link-secondary"
-            >
-              Help us improve the project on GitHub.
-            </a>
-            on GitHub.
+            <Trans i18nKey="onboarding.splashscreen_warning_text">
+              While JoinMarket is tried and tested, Jam is not. It is in an alpha stage, so use with caution.
+              <a
+                href="https://github.com/joinmarket-webui/joinmarket-webui/issues"
+                target="_blank"
+                rel="noopener noreferrer"
+                className="link-secondary"
+              >
+                Help us improve the project on GitHub.
+              </a>
+            </Trans>
           </div>
         </div>
       </div>
@@ -101,7 +100,7 @@ export default function Onboarding() {
         </div>
         <div className="button d-flex justify-content-center">
           <rb.Button size="lg" variant="dark" onClick={next}>
-            {step === steps.length ? "Let's go!" : 'Next'}
+            {step === steps.length ? t('onboarding.button_complete') : t('onboarding.button_next')}
           </rb.Button>
         </div>
       </>
diff --git a/src/components/Receive.jsx b/src/components/Receive.jsx
index 6e4018d..ddb3424 100644
--- a/src/components/Receive.jsx
+++ b/src/components/Receive.jsx
@@ -1,8 +1,9 @@
 import React from 'react'
 import { useState, useEffect, useRef } from 'react'
-import { BitcoinQR } from './BitcoinQR'
 import { useLocation } from 'react-router-dom'
 import * as rb from 'react-bootstrap'
+import { useTranslation } from 'react-i18next'
+import { BitcoinQR } from './BitcoinQR'
 import { ACCOUNTS } from '../utils'
 import { useSettings } from '../context/SettingsContext'
 import * as Api from '../libs/JmWalletApi'
@@ -10,6 +11,7 @@ import PageTitle from './PageTitle'
 import Sprite from './Sprite'
 
 export default function Receive({ currentWallet }) {
+  const { t } = useTranslation()
   const location = useLocation()
   const settings = useSettings()
   const addressCopyFallbackInputRef = useRef()
@@ -32,7 +34,9 @@ export default function Receive({ currentWallet }) {
       setAlert(null)
       setIsLoading(true)
       Api.getAddressNew({ walletName, accountNr, token, signal: abortCtrl.signal })
-        .then((res) => (res.ok ? res.json() : Promise.reject(new Error(res.message || 'Loading new address failed.'))))
+        .then((res) =>
+          res.ok ? res.json() : Promise.reject(new Error(res.message || t('receive.error_loading_address_failed')))
+        )
         .then((data) => setAddress(data.address))
         .catch((err) => {
           !abortCtrl.signal.aborted && setAlert({ variant: 'danger', message: err.message })
@@ -45,7 +49,7 @@ export default function Receive({ currentWallet }) {
     }
 
     return () => abortCtrl.abort()
-  }, [account, currentWallet, addressCount])
+  }, [account, currentWallet, addressCount, t])
 
   useEffect(() => {
     if (addressCopiedFlag < 1) return
@@ -64,7 +68,7 @@ export default function Receive({ currentWallet }) {
         inputField.select()
         const success = document.execCommand && document.execCommand('copy')
         inputField.blur()
-        success ? resolve(success) : reject(new Error('Could not copy address.'))
+        success ? resolve(success) : reject(new Error(t('receive.error_copy_address_failed')))
       })
 
     // `navigator.clipboard` might not be available, e.g. on sites served over plain `http`.
@@ -90,7 +94,7 @@ export default function Receive({ currentWallet }) {
 
   return (
     <div className="receive">
-      <PageTitle title="Receive bitcoin" subtitle="Send bitcoin to the address below." />
+      <PageTitle title={t('receive.title')} subtitle={t('receive.subtitle')} />
       {alert && <rb.Alert variant={alert.variant}>{alert.message}</rb.Alert>}
       {address && (
         <div className="qr-container">
@@ -118,11 +122,11 @@ export default function Receive({ currentWallet }) {
                 >
                   {showAddressCopiedConfirmation ? (
                     <>
-                      Copied
+                      {t('receive.text_copy_address_confirmed')}
                       <Sprite color="green" symbol="checkmark" className="ms-1" width="20" height="20" />
                     </>
                   ) : (
-                    'Copy'
+                    t('receive.button_copy_address')
                   )}
                 </rb.Button>
 
@@ -148,7 +152,7 @@ export default function Receive({ currentWallet }) {
           className="ps-0 border-0 d-flex align-items-center"
           onClick={() => setShowSettings(!showSettings)}
         >
-          Settings
+          {t('receive.button_settings')}
           <Sprite symbol={`caret-${showSettings ? 'up' : 'down'}`} className="ms-1" width="20" height="20" />
         </rb.Button>
       </div>
@@ -157,7 +161,7 @@ export default function Receive({ currentWallet }) {
           <>
             {settings.useAdvancedWalletMode && (
               <rb.Form.Group className="mt-4" controlId="account">
-                <rb.Form.Label>Choose account</rb.Form.Label>
+                <rb.Form.Label>{t('receive.label_choose_account')}</rb.Form.Label>
                 <rb.Form.Select
                   defaultValue={account}
                   onChange={(e) => setAccount(parseInt(e.target.value, 10))}
@@ -165,14 +169,14 @@ export default function Receive({ currentWallet }) {
                 >
                   {ACCOUNTS.map((val) => (
                     <option key={val} value={val}>
-                      Account {val}
+                      {t('receive.account_selector_option_dev_mode', { number: val })}
                     </option>
                   ))}
                 </rb.Form.Select>
               </rb.Form.Group>
             )}
             <rb.Form.Group className="my-4" controlId="amountSats">
-              <rb.Form.Label>Amount to request in sats</rb.Form.Label>
+              <rb.Form.Label>{t('receive.label_amount')}</rb.Form.Label>
               <rb.Form.Control
                 className="slashed-zeroes"
                 name="amount"
@@ -182,7 +186,7 @@ export default function Receive({ currentWallet }) {
                 min={0}
                 onChange={(e) => setAmount(e.target.value)}
               />
-              <rb.Form.Control.Feedback type="invalid">Please provide a valid amount.</rb.Form.Control.Feedback>
+              <rb.Form.Control.Feedback type="invalid">{t('receive.feedback_invalid_amount')}</rb.Form.Control.Feedback>
             </rb.Form.Group>
           </>
         )}
@@ -191,10 +195,10 @@ export default function Receive({ currentWallet }) {
           {isLoading ? (
             <div>
               <rb.Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" className="me-2" />
-              Getting new address
+              {t('receive.text_getting_address')}
             </div>
           ) : (
-            'Get new address'
+            t('receive.button_new_address')
           )}
         </rb.Button>
       </rb.Form>
diff --git a/src/components/Send.jsx b/src/components/Send.jsx
index 13ce609..0c7d28b 100644
--- a/src/components/Send.jsx
+++ b/src/components/Send.jsx
@@ -1,6 +1,7 @@
 import React, { useEffect } from 'react'
 import { useState } from 'react'
 import { useLocation } from 'react-router-dom'
+import { useTranslation } from 'react-i18next'
 import * as rb from 'react-bootstrap'
 import PageTitle from './PageTitle'
 import ToggleSwitch from './ToggleSwitch'
@@ -36,6 +37,7 @@ const isValidNumCollaborators = (candidate, minNumCollaborators) => {
 }
 
 const CollaboratorsSelector = ({ numCollaborators, setNumCollaborators, minNumCollaborators }) => {
+  const { t } = useTranslation()
   const settings = useSettings()
 
   const [usesCustomNumCollaborators, setUsesCustomNumCollaborators] = useState(false)
@@ -56,11 +58,9 @@ const CollaboratorsSelector = ({ numCollaborators, setNumCollaborators, minNumCo
   return (
     <rb.Form noValidate className="collaborators-selector">
       <rb.Form.Group>
-        <rb.Form.Label className="mb-0">Number of collaborators: {numCollaborators}</rb.Form.Label>
+        <rb.Form.Label className="mb-0">{t('send.label_num_collaborators', { numCollaborators })}</rb.Form.Label>
         <div className="mb-2">
-          <rb.Form.Text className="text-secondary">
-            A higher number is better for privacy, but also increases the fee.
-          </rb.Form.Text>
+          <rb.Form.Text className="text-secondary">{t('send.description_num_collaborators')}</rb.Form.Text>
         </div>
         <div className="d-flex flex-row flex-wrap">
           {defaultCollaboratorsSelection.map((number) => {
@@ -89,7 +89,7 @@ const CollaboratorsSelector = ({ numCollaborators, setNumCollaborators, minNumCo
             min={minNumCollaborators}
             max={99}
             isInvalid={!isValidNumCollaborators(numCollaborators, minNumCollaborators)}
-            placeholder="Other"
+            placeholder={t('send.input_num_collaborators_placeholder')}
             defaultValue=""
             className={`p-2 border border-1 rounded text-center${
               usesCustomNumCollaborators ? (settings.theme === 'light' ? ' border-dark' : ' selected-dark') : ''
@@ -107,7 +107,7 @@ const CollaboratorsSelector = ({ numCollaborators, setNumCollaborators, minNumCo
           />
           {usesCustomNumCollaborators && (
             <rb.Form.Control.Feedback type="invalid">
-              Please use between {minNumCollaborators} and 99 collaborators.
+              {t('send.error_invalid_num_collaborators', { minNumCollaborators, maxNumCollaborators: 99 })}
             </rb.Form.Control.Feedback>
           )}
         </div>
@@ -129,11 +129,7 @@ const enhanceTakerErrorMessageIfNecessary = async (requestContext, httpStatus, e
       .catch(() => false)
 
     if (!maxFeeSettingsPresent) {
-      const maxFeeSettingsMissingMessage = `
-        Config variables 'max_cj_fee_rel' and 'max_cj_fee_abs' must be set in your joinmarket.cfg in order to send collaborative transactions. 
-        Consider adding them to your config manually.
-      `
-      return `${errorMessage} ${maxFeeSettingsMissingMessage}`
+      return `${errorMessage}`
     }
   }
 
@@ -141,6 +137,7 @@ const enhanceTakerErrorMessageIfNecessary = async (requestContext, httpStatus, e
 }
 
 export default function Send({ makerRunning, coinjoinInProcess }) {
+  const { t } = useTranslation()
   const wallet = useCurrentWallet()
   const walletInfo = useCurrentWalletInfo()
   const setWalletInfo = useSetCurrentWalletInfo()
@@ -207,7 +204,9 @@ export default function Send({ makerRunning, coinjoinInProcess }) {
     const loadingWalletInfo = walletInfo
       ? Promise.resolve()
       : Api.getWalletDisplay(requestContext)
-          .then((res) => (res.ok ? res.json() : Promise.reject(new Error(res.message || 'Loading wallet failed.'))))
+          .then((res) =>
+            res.ok ? res.json() : Promise.reject(new Error(res.message || t('send.error_loading_wallet_failed')))
+          )
           .then((data) => setWalletInfo(data.walletinfo))
           .catch((err) => {
             !abortCtrl.signal.aborted && setAlert({ variant: 'danger', message: err.message })
@@ -215,7 +214,7 @@ export default function Send({ makerRunning, coinjoinInProcess }) {
 
     const loadingMinimumMakerConfig = Api.postConfigGet(requestContext, { section: 'POLICY', field: 'minimum_makers' })
       .then((res) =>
-        res.ok ? res.json() : Promise.reject(new Error(res.message || 'Loading config value "minimum_makers" failed.'))
+        res.ok ? res.json() : Promise.reject(new Error(res.message || t('send.error_loading_min_makers_failed')))
       )
       .then((data) => {
         const minimumMakers = parseInt(data.configvalue, 10)
@@ -231,7 +230,7 @@ export default function Send({ makerRunning, coinjoinInProcess }) {
     )
 
     return () => abortCtrl.abort()
-  }, [wallet, walletInfo, setWalletInfo])
+  }, [wallet, walletInfo, setWalletInfo, t])
 
   const sendPayment = async (account, destination, amount_sats) => {
     const { name: walletName, token } = wallet
@@ -248,7 +247,7 @@ export default function Send({ makerRunning, coinjoinInProcess }) {
         const output = outputs.find((o) => o.address === destination)
         setAlert({
           variant: 'success',
-          message: `Payment successful: Sent ${output.value_sats} sats to ${output.address}.`,
+          message: t('send.alert_payment_successful', { amount: output.value_sats, address: output.address }),
         })
         success = true
       } else {
@@ -275,11 +274,15 @@ export default function Send({ makerRunning, coinjoinInProcess }) {
       if (res.ok) {
         const data = await res.json()
         console.log(data)
-        setAlert({ variant: 'success', message: 'Collaborative transaction started' })
+        setAlert({ variant: 'success', message: t('send.alert_coinjoin_started') })
         success = true
       } else {
         const { message } = await res.json()
-        const displayMessage = await enhanceTakerErrorMessageIfNecessary(requestContext, res.status, message)
+        const displayMessage = await enhanceTakerErrorMessageIfNecessary(
+          requestContext,
+          res.status,
+          `${message} ${t('send.taker_error_message_max_fees_config_missing')}`
+        )
 
         setAlert({ variant: 'danger', message: displayMessage })
       }
@@ -321,20 +324,17 @@ export default function Send({ makerRunning, coinjoinInProcess }) {
       {isLoading ? (
         <div className="d-flex justify-content-center align-items-center">
           <rb.Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" className="me-2" />
-          Loading
+          {t('send.loading')}
         </div>
       ) : (
         <div className="send">
-          <PageTitle
-            title="Send bitcoin"
-            subtitle="Collaborative transactions increase the privacy of yourself and others."
-          />
+          <PageTitle title={t('send.title')} subtitle={t('send.subtitle')} />
 
           <rb.Fade in={!isCoinjoinOptionEnabled} mountOnEnter={true} unmountOnExit={true}>
             <div className="mb-4 p-3 border border-1 rounded">
               <small className="text-secondary">
-                {makerRunning && <>Earn is active. Stop the service in order to send collaborative transactions.</>}
-                {coinjoinInProcess && <>A collaborative transaction is currently in progress.</>}
+                {makerRunning && t('send.text_maker_running')}
+                {coinjoinInProcess && t('send.text_coinjoin_already_running')}
               </small>
             </div>
           </rb.Fade>
@@ -347,20 +347,22 @@ export default function Send({ makerRunning, coinjoinInProcess }) {
 
           <rb.Form onSubmit={onSubmit} noValidate id="send-form">
             <rb.Form.Group className="mb-4" controlId="destination">
-              <rb.Form.Label>Recipient</rb.Form.Label>
+              <rb.Form.Label>{t('send.label_recipient')}</rb.Form.Label>
               <rb.Form.Control
                 name="destination"
-                placeholder="Enter address..."
+                placeholder={t('send.placeholder_recipient')}
                 className="slashed-zeroes"
                 value={destination || ''}
                 required
                 onChange={(e) => setDestination(e.target.value)}
                 isInvalid={destination !== null && !isValidAddress(destination)}
               />
-              <rb.Form.Control.Feedback type="invalid">Please provide a recipient address.</rb.Form.Control.Feedback>
+              <rb.Form.Control.Feedback type="invalid">{t('send.feedback_invalid_recipient')}</rb.Form.Control.Feedback>
             </rb.Form.Group>
             <rb.Form.Group className="mb-4 flex-grow-1" controlId="account">
-              <rb.Form.Label>{settings.useAdvancedWalletMode ? 'Account' : 'Privacy level'} to send from</rb.Form.Label>
+              <rb.Form.Label>
+                {settings.useAdvancedWalletMode ? t('send.label_account_dev_mode') : t('send.label_account')}
+              </rb.Form.Label>
               <rb.Form.Select
                 defaultValue={account}
                 onChange={(e) => setAccount(parseInt(e.target.value, 10))}
@@ -373,35 +375,34 @@ export default function Send({ makerRunning, coinjoinInProcess }) {
                     .sort((lhs, rhs) => lhs.account - rhs.account)
                     .map(({ account, account_balance: balance }) => (
                       <option key={account} value={account}>
-                        {settings.useAdvancedWalletMode ? 'Account' : 'Privacy Level'} {account}{' '}
+                        {settings.useAdvancedWalletMode
+                          ? t('send.account_selector_option_dev_mode', { number: account })
+                          : t('send.account_selector_option', { number: account })}{' '}
                         {settings.showBalance && `(\u20BF${balance})`}
                       </option>
                     ))}
               </rb.Form.Select>
             </rb.Form.Group>
             <rb.Form.Group className="mb-4" controlId="amount">
-              <rb.Form.Label form="send-form">Amount in sats</rb.Form.Label>
+              <rb.Form.Label form="send-form">{t('send.label_amount')}</rb.Form.Label>
               <rb.Form.Control
                 name="amount"
                 type="number"
                 value={amount || ''}
                 className="slashed-zeroes"
                 min={1}
-                placeholder="Enter amount..."
+                placeholder={t('send.placeholder_amount')}
                 required
                 onChange={(e) => setAmount(parseInt(e.target.value, 10))}
                 isInvalid={amount !== null && !isValidAmount(amount)}
               />
               <rb.Form.Control.Feedback form="send-form" type="invalid">
-                Please provide a valid amount.
+                {t('send.feedback_invalid_amount')}
               </rb.Form.Control.Feedback>
             </rb.Form.Group>
             {isCoinjoinOptionEnabled && (
               <rb.Form.Group controlId="isCoinjoin" className={`${isCoinjoin ? 'mb-3' : ''}`}>
-                <ToggleSwitch
-                  label="Send as collaborative transaction for improved privacy"
-                  onToggle={(isToggled) => setIsCoinjoin(isToggled)}
-                />
+                <ToggleSwitch label={t('send.toggle_coinjoin')} onToggle={(isToggled) => setIsCoinjoin(isToggled)} />
               </rb.Form.Group>
             )}
           </rb.Form>
@@ -422,10 +423,10 @@ export default function Send({ makerRunning, coinjoinInProcess }) {
             {isSending ? (
               <div>
                 <rb.Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" className="me-2" />
-                Sending
+                {t('send.text_sending')}
               </div>
             ) : (
-              'Send'
+              t('send.button_send')
             )}
           </rb.Button>
         </div>
diff --git a/src/components/Settings.jsx b/src/components/Settings.jsx
index e8fcda6..3c2a7ee 100644
--- a/src/components/Settings.jsx
+++ b/src/components/Settings.jsx
@@ -1,5 +1,6 @@
 import React, { useState } from 'react'
 import * as rb from 'react-bootstrap'
+import { useTranslation } from 'react-i18next'
 import Sprite from './Sprite'
 import PageTitle from './PageTitle'
 import Seedphrase from './Seedphrase'
@@ -7,10 +8,10 @@ import ToggleSwitch from './ToggleSwitch'
 import { useSettings, useSettingsDispatch } from '../context/SettingsContext'
 import { SATS, BTC } from '../utils'
 import * as Api from '../libs/JmWalletApi'
-import { useTranslation } from 'react-i18next'
 import languages from '../i18n/languages'
 
 export default function Settings({ currentWallet }) {
+  const { t } = useTranslation()
   const [seed, setSeed] = useState('')
   const [showingSeed, setShowingSeed] = useState(false)
   const [revealSeed, setRevealSeed] = useState(false)
@@ -31,7 +32,7 @@ export default function Settings({ currentWallet }) {
 
   return (
     <div>
-      <PageTitle title="Settings" />
+      <PageTitle title={t('settings.title')} />
       <div style={{ marginLeft: '-.75rem' }}>
         <rb.Button
           variant="outline-dark"
@@ -42,7 +43,7 @@ export default function Settings({ currentWallet }) {
           }}
         >
           <Sprite symbol={settings.showBalance ? 'hide' : 'show'} width="24" height="24" className="me-2" />
-          {settings.showBalance ? 'Hide' : 'Show'} balance
+          {settings.showBalance ? t('settings.hide_balance') : t('settings.show_balance')}
         </rb.Button>
 
         <br />
@@ -56,7 +57,7 @@ export default function Settings({ currentWallet }) {
           }}
         >
           <Sprite symbol={isSats ? BTC : SATS} width="24" height="24" className="me-2" />
-          Display amounts in {isSats ? BTC : SATS}
+          {isSats ? t('settings.use_btc') : t('settings.use_sats')}
         </rb.Button>
 
         <br />
@@ -75,7 +76,7 @@ export default function Settings({ currentWallet }) {
             height="24"
             className="me-2"
           />
-          Switch to {isLightTheme ? 'dark' : 'light'} theme
+          {isLightTheme ? t('settings.use_dark_theme') : t('settings.use_light_theme')}
         </rb.Button>
 
         <br />
@@ -94,7 +95,7 @@ export default function Settings({ currentWallet }) {
             height="24"
             className="me-2"
           />
-          Use {settings.useAdvancedWalletMode ? 'magic' : 'advanced'} wallet mode
+          {settings.useAdvancedWalletMode ? t('settings.use_normal_mode') : t('settings.use_dev_mode')}
         </rb.Button>
 
         <br />
@@ -118,7 +119,7 @@ export default function Settings({ currentWallet }) {
               href="https://github.com/joinmarket-webui/joinmarket-webui/tree/master/src/i18n/README.md"
               rel="noopener noreferrer"
             >
-              Missing your language? Help us out!
+              {t('settings.text_help_translate')}
             </rb.Dropdown.Item>
           </rb.Dropdown.Menu>
         </rb.Dropdown>
@@ -146,11 +147,11 @@ export default function Settings({ currentWallet }) {
           }}
         >
           <Sprite symbol="mnemonic" width="24" height="24" className="me-2" />
-          {showingSeed ? 'Hide' : 'Show'} seed phrase
+          {showingSeed ? t('settings.hide_seed') : t('settings.show_seed')}
         </rb.Button>
         {seedError && (
           <div className="text-danger" style={{ marginLeft: '1rem' }}>
-            Could not retreive seedphrase.
+            {t('settings.error_loading_seed_failed')}
           </div>
         )}
         {showingSeed && (
@@ -160,7 +161,7 @@ export default function Settings({ currentWallet }) {
             </div>
             <div className="mb-2">
               <ToggleSwitch
-                label="Reveal sensitive information"
+                label={t('settings.reveal_seed')}
                 onToggle={(isToggled) => {
                   setRevealSeed(isToggled)
                 }}
diff --git a/src/components/Wallet.jsx b/src/components/Wallet.jsx
index 108b5c2..e3e42de 100644
--- a/src/components/Wallet.jsx
+++ b/src/components/Wallet.jsx
@@ -1,10 +1,12 @@
 import React, { useState } from 'react'
 import { Link, useNavigate } from 'react-router-dom'
 import * as rb from 'react-bootstrap'
+import { useTranslation } from 'react-i18next'
 import { serialize, walletDisplayName } from '../utils'
 import * as Api from '../libs/JmWalletApi'
 
 export default function Wallet({ name, currentWallet, startWallet, stopWallet, setAlert, ...props }) {
+  const { t } = useTranslation()
   const [validated, setValidated] = useState(false)
   const [isLocking, setIsLocking] = useState(false)
   const [isUnlocking, setIsUnlocking] = useState(false)
@@ -17,9 +19,9 @@ export default function Wallet({ name, currentWallet, startWallet, stopWallet, s
         message:
           currentWallet.name === walletName
             ? // unlocking same wallet
-              `${walletDisplayName(walletName)} is already unlocked.`
+              t('wallets.wallet_preview.alert_wallet_already_unlocked', { walletName: walletDisplayName(walletName) })
             : // unlocking another wallet while one is already unlocked
-              `${walletDisplayName(currentWallet.name)} is currently in use, please lock it first.`,
+              t('wallets.wallet_preview.alert_other_wallet_unlocked', { walletName: walletDisplayName(walletName) }),
       })
     } else {
       setAlert(null)
@@ -68,9 +70,13 @@ export default function Wallet({ name, currentWallet, startWallet, stopWallet, s
 
         setAlert({
           variant: already_locked ? 'warning' : 'success',
-          message: `${walletDisplayName(lockedWalletName)} ${
-            already_locked ? 'already locked' : 'locked successfully'
-          }.`,
+          message: already_locked
+            ? t('wallets.wallet_preview.alert_wallet_already_locked', {
+                walletName: walletDisplayName(lockedWalletName),
+              })
+            : t('wallets.wallet_preview.alert_wallet_locked_successfully', {
+                walletName: walletDisplayName(lockedWalletName),
+              }),
           dismissible: true,
         })
       } else {
@@ -126,14 +132,18 @@ export default function Wallet({ name, currentWallet, startWallet, stopWallet, s
                   <>{walletDisplayName(name)}</>
                 )}
               </rb.Card.Title>
-              {isActive ? <span className="text-success">Active</span> : <span className="text-muted">Inactive</span>}
+              {isActive ? (
+                <span className="text-success">{t('wallets.wallet_preview.wallet_active')}</span>
+              ) : (
+                <span className="text-muted">{t('wallets.wallet_preview.wallet_inactive')}</span>
+              )}
             </div>
             <div>
               {isActive ? (
                 hasToken ? (
                   <>
                     <Link className="btn btn-outline-dark me-2" to="/wallet">
-                      Open
+                      {t('wallets.wallet_preview.button_open')}
                     </Link>
                     <rb.FormControl type="hidden" name="action" value="lock" />
                     <rb.Button variant="outline-dark" type="submit" disabled={isLocking}>
@@ -147,17 +157,16 @@ export default function Wallet({ name, currentWallet, startWallet, stopWallet, s
                             aria-hidden="true"
                             className="me-2"
                           />
-                          Locking
+                          {t('wallets.wallet_preview.button_locking')}
                         </>
                       ) : (
-                        <>Lock</>
+                        <>{t('wallets.wallet_preview.button_lock')}</>
                       )}
                     </rb.Button>
                   </>
                 ) : (
                   <rb.Alert variant="warning" className="mb-0">
-                    This wallet is active, but there is no token to interact with it. Please remove the lock file on the
-                    server.
+                    {t('wallets.wallet_preview.alert_missing_token')}
                   </rb.Alert>
                 )
               ) : (
@@ -165,7 +174,7 @@ export default function Wallet({ name, currentWallet, startWallet, stopWallet, s
                   <rb.InputGroup hasValidation={true}>
                     <rb.FormControl
                       type="password"
-                      placeholder="Password"
+                      placeholder={t('wallets.wallet_preview.placeholder_password')}
                       name="password"
                       disabled={isUnlocking}
                       required
@@ -182,14 +191,14 @@ export default function Wallet({ name, currentWallet, startWallet, stopWallet, s
                             aria-hidden="true"
                             className="me-2"
                           />
-                          Unlocking
+                          {t('wallets.wallet_preview.button_unlocking')}
                         </>
                       ) : (
-                        <>Unlock</>
+                        <>{t('wallets.wallet_preview.button_unlock')}</>
                       )}
                     </rb.Button>
                     <rb.Form.Control.Feedback type="invalid">
-                      Please set the wallet's password.
+                      {t('wallets.wallet_preview.feedback_missing_password')}
                     </rb.Form.Control.Feedback>
                   </rb.InputGroup>
                 )
diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json
index 4401f8d..b9dfe37 100644
--- a/src/i18n/locales/en/translation.json
+++ b/src/i18n/locales/en/translation.json
@@ -1,10 +1,206 @@
 {
+  "navbar": {
+    "title": "Jam",
+    "button_create_wallet": "Create Wallet",
+    "tab_send": "Send",
+    "tab_receive": "Receive",
+    "tab_earn": "Earn",
+    "joining_in_progress": "Joining",
+    "menu_mobile": "Menu",
+    "menu_mobile_settings": "Settings",
+    "menu_mobile_wallets": "Wallets"
+  },
+  "app": {
+    "alert_no_connection": "No connecction to backend: {{ connectionError }}"
+  },
+  "footer": {
+    "warning": "This is alpha software.<br /><1>Read this before using.</1>",
+    "warning_alert_title": "Warning",
+    "warning_alert_text": "While JoinMarket is tried and tested, Jam is not. It is in an alpha stage, so use with caution.",
+    "warning_alert_button_ok": "Fine with me.",
+    "docs": "Docs",
+    "features": "Features",
+    "github": "GitHub",
+    "twitter": "Twitter",
+    "connected": "Connected",
+    "disconnected": "Disconnected"
+  },
+  "onboarding": {
+    "splashscreen_title": "Jam",
+    "splashscreen_subtitle": "A friendly UI for JoinMarket",
+    "splashscreen_button_get_started": "Get started",
+    "splashscreen_button_skip_intro": "Skip intro",
+    "splashscreen_description_line1": "Your wallet, your coins.",
+    "splashscreen_description_line2": "100% open-source & open-design.",
+    "splashscreen_warning_title": "Warning",
+    "splashscreen_warning_text": "While JoinMarket is tried and tested, Jam is not. It is in an alpha stage, so use with caution. <1>Help us improve the project on GitHub.</1>",
+    "screen_1_title": "Welcome to Jam for JoinMarket!",
+    "screen_1_description": "JoinMarket is a privacy-focused software solution that aims to improve the confidentiality and privacy of your bitcoin transactions. It facilitates the creation of collaborative transactions through a peer-to-peer marketplace.",
+    "screen_2_title": "Collaborative Transactions",
+    "screen_2_description": "To have strong privacy guarantees in the open and transparent world of bitcoin, special kinds of transactions have to be created. JoinMarket helps you to create these transactions in an easy and automated way.",
+    "screen_3_title": "You Are In Control",
+    "screen_3_description": "JoinMarket is fully non-custodial, meaning that you always have full control over your funds. The system uses Bitcoin’s smart contracts to make sure that all transactions are atomic and your funds are secure at all times.",
+    "screen_4_title": "No Trusted Third Parties",
+    "screen_4_description": "Since JoinMarket is a peer-to-peer system, trusted third parties are eliminated from the get-go. This unique market-driven approach reduces counterparty risk to a minimum.",
+    "screen_5_title": "Privacy for All",
+    "screen_5_description": "JoinMarket is free and open-source software without a single point of failure. Everyone is free to use it and build upon it.",
+    "button_next": "Next",
+    "button_complete": "Let's go!"
+  },
   "wallets": {
     "title": "Your Wallets",
     "subtitle_no_wallets": "It looks like you do not have a wallet, yet.",
     "text_loading": "Loading wallets",
     "button_new_wallet": "Create new wallet",
     "alert_wallet_open": "There can be only one active wallet. If you want to open another wallet, please lock '{{ currentWalletName }}' first.",
-    "error_loading_failed": "Loading wallets failed."
+    "error_loading_failed": "Loading wallets failed.",
+    "wallet_preview": {
+      "alert_wallet_already_unlocked": "{{ walletName }} is already unlocked.",
+      "alert_other_wallet_unlocked": "{{ walletName }} is currently in use, please lock it first.",
+      "alert_wallet_locked_successfully": "{{ walletName }} locked successfully.",
+      "alert_wallet_already_locked": "{{ walletName }} already locked.",
+      "wallet_active": "Active",
+      "wallet_inactive": "Inactive",
+      "placeholder_password": "Password",
+      "button_open": "Open",
+      "button_lock": "Lock",
+      "button_unlock": "Unlock",
+      "button_locking": "Locking",
+      "button_unlocking": "Unlocking",
+      "feedback_missing_password": "Please set the wallet's password.",
+      "alert_missing_token": "This wallet is active, but there is no token to interact with it. Please remove the lock file on the server."
+    }
+  },
+  "create_wallet": {
+    "title": "Create Wallet",
+    "title_wallet_created": "Wallet created successfully!",
+    "subtitle_wallet_created": "Please write down your seed phrase and password! Without this information you will not be able to access and recover your wallet!",
+    "alert_other_wallet_unlocked": "Currently <1>{{ walletName }}</1> is active. You need to lock it first. <3>Go back</3>.",
+    "feedback_valid": "Looks good!",
+    "label_wallet_name": "Wallet Name",
+    "placeholder_wallet_name": "Your Wallet...",
+    "feedback_invalid_wallet_name": "Please set a wallet name.",
+    "label_password": "Password",
+    "placeholder_password": "Choose a secure password...",
+    "feedback_invalid_password": "Please set a password.",
+    "button_create": "Create",
+    "button_creating": "Creating",
+    "alert_confirmation_failed": "Wallet confirmation failed.",
+    "confirmation_label_wallet_name": "Wallet Name",
+    "confirmation_label_password": "Password",
+    "confirmation_toggle_reveal_info": "Reveal sensitive information",
+    "confirmation_toggle_info_written_down": "I've written down the information above.",
+    "confirmation_button_fund_wallet": "Fund Wallet"
+  },
+  "current_wallet": {
+    "text_loading": "Loading",
+    "button_deposit": "Deposit",
+    "button_withdraw": "Withdraw",
+    "button_switch_wallet": "Switch wallet",
+    "error_loading_failed": "Loading wallet failed."
+  },
+  "current_wallet_advanced": {
+    "error_loading_utxos_failed": "Loading UTXOs failed.",
+    "title_fidelity_bonds": "Fidelity Bonds",
+    "button_show_utxos": "Show UTXOs",
+    "button_hide_utxos": "Hide UTXOs",
+    "alert_no_utxos": "No UTXOs available.",
+    "account": "Account",
+    "error_freeze_failed": "Request failed.",
+    "label_locked_until": "Locked until",
+    "label_frozen": "frozen",
+    "label_unconfirmed": "unconfirmed",
+    "button_freeze": "freeze",
+    "button_unfreeze": "unfreeze",
+    "account_button_send": "Send",
+    "account_button_receive": "Receive",
+    "account_heading_external_addresses": "External Addresses",
+    "account_heading_internal_addresses": "Internal Addresses"
+  },
+  "settings": {
+    "title": "Settings",
+    "show_balance": "Show Balance",
+    "hide_balance": "Hide Balance",
+    "use_sats": "Display amounts in sats",
+    "use_btc": "Display amounts in bitcoin",
+    "use_dark_theme": "Switch to dark theme",
+    "use_light_theme": "Switch to light theme",
+    "use_normal_mode": "Use normal wallet mode",
+    "use_dev_mode": "Use developer wallet mode",
+    "text_help_translate": "Missing your language? Help us out!",
+    "show_seed": "Show seed phrase",
+    "hide_seed": "Hide seed phrase",
+    "reveal_seed": "Reveal seed phrase",
+    "error_loading_seed_failed": "Could not retrieve seed phrase."
+  },
+  "send": {
+    "loading": "Loading",
+    "title": "Send bitcoin",
+    "subtitle": "Collaborative transactions increase the privacy of yourself and others.",
+    "label_num_collaborators": "Number of collaborators: {{ numCollaborators }}",
+    "input_num_collaborators_placeholder": "Other",
+    "description_num_collaborators": "A higher number is better for privacy, but also increases the fee.",
+    "error_invalid_num_collaborators": "Please use between {{ minNumCollaborators }} and {{ maxNumCollaborators }} collaborators.",
+    "taker_error_message_max_fees_config_missing": "Config variables 'max_cj_fee_rel' and 'max_cj_fee_abs' must be set in your joinmarket.cfg in order to send collaborative transactions. Consider adding them to your config manually.",
+    "error_loading_wallet_failed": "Loading wallet failed.",
+    "error_loading_min_makers_failed": "Loading config value 'minimum_makers' failed.",
+    "alert_payment_successful": "Payment successful: Sent {{ amount }} sats to {{ address }}.",
+    "alert_coinjoin_started": "Collaborative transaction started",
+    "text_maker_running": "Earn is active. Stop the service in order to send collaborative transactions.",
+    "text_coinjoin_already_running": "A collaborative transaction is currently in progress.",
+    "label_recipient": "Recipient",
+    "placeholder_recipient": "Enter address...",
+    "feedback_invalid_recipient": "Please provide a recipient address.",
+    "label_account": "Privacy level to send from",
+    "label_account_dev_mode": "Account to send from",
+    "account_selector_option": "Privacy Level {{ number }}",
+    "account_selector_option_dev_mode": "Account {{ number }}",
+    "label_amount": "Amount in sats",
+    "placeholder_amount": "Enter amount...",
+    "feedback_invalid_amount": "Please provide a valid amount.",
+    "toggle_coinjoin": "Send as collaborative transaction for improved privacy",
+    "button_send": "Send",
+    "text_sending": "Sending"
+  },
+  "receive": {
+    "title": "Receive",
+    "subtitle": "Send bitcoin to the address below.",
+    "error_loading_address_failed": "Loading new address failed.",
+    "error_copy_address_failed": "Could not copy address.",
+    "button_copy_address": "Copy",
+    "text_copy_address_confirmed": "Copied",
+    "button_settings": "Settings",
+    "label_choose_account": "Choose account",
+    "account_selector_option_dev_mode": "Account {{ number }}",
+    "label_amount": "Amount to request in sats",
+    "feedback_invalid_amount": "Please provide a valid amount.",
+    "button_new_address": "Get new address",
+    "text_getting_address": "Getting new address"
+  },
+  "earn": {
+    "title": "Earn bitcoin",
+    "subtitle": "By making your bitcoin available for others, you help them improve their privacy and can also earn a yield.",
+    "alert_empty_report": "The report is empty.",
+    "text_report_length": "Showing {{ visibleLines }} of {{ linesWithoutHeader }} entries",
+    "error_loading_report_failed": "Failed to load yield generator report.",
+    "alert_starting": "Service is starting.",
+    "alert_stopping": "Service is stopping.",
+    "alert_running": "Service is running.",
+    "alert_coinjoin_in_progress": "A collaborative transaction is currently in progress.",
+    "toggle_rel_offer": "Relative offer",
+    "label_rel_fee": "Relative fee {{ fee }}",
+    "description_rel_fee": "As a percentage of the amounts you help others with improved privacy.",
+    "feedback_invalid_rel_fee": "Please provide a relative fee between {{feeRelPercentageMin}} and {{feeRelPercentageMax}}.",
+    "label_abs_fee": "Absolute fee in sats",
+    "description_abs_fee": "An absolute amount you get for helping others to improve their privacy.",
+    "feedback_invalid_abs_fee": "Please provide an absolute fee.",
+    "label_min_amount": "Minimum amount in sats",
+    "feedback_invalid_min_amount": "Please provide a minimum amount.",
+    "button_start": "Start",
+    "button_stop": "Stop",
+    "text_starting": "Starting",
+    "text_stopping": "Stopping",
+    "button_show_report": "Show report",
+    "button_hide_report": "Hide report"
   }
 }
diff --git a/src/utils.js b/src/utils.js
index 9f8bde2..b11bdb8 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -4,18 +4,6 @@ export const SATS = 'sats'
 
 export const serialize = (form) => Object.fromEntries(new FormData(form).entries())
 
-export const dasherize = (string) => String(string).replace(/\W+/gi, '-')
-
-export const lowercaseFirstChar = (string) => string.charAt(0).toLowerCase() + string.slice(1)
-
-export const upcaseFirstChar = (string) => string.charAt(0).toUpperCase() + string.slice(1)
-
-export const titleize = (string) =>
-  string
-    .split(/\W+/gi)
-    .map((w) => upcaseFirstChar(w))
-    .join(' ')
-
 export const walletDisplayName = (name) => name.replace('.jmdat', '')
 
 export const displayDate = (string) => new Date(string).toLocaleString()


Test patch:
diff --git a/src/components/App.test.jsx b/src/components/App.test.jsx
index 0796f1b..75230f0 100644
--- a/src/components/App.test.jsx
+++ b/src/components/App.test.jsx
@@ -9,14 +9,14 @@ it('should display Onboarding screen initially', () => {
   render(<App />)
 
   // Onboarding screen
-  expect(screen.getByText('Get started')).toBeInTheDocument()
-  expect(screen.getByText('Skip intro')).toBeInTheDocument()
+  expect(screen.getByText('onboarding.splashscreen_button_get_started')).toBeInTheDocument()
+  expect(screen.getByText('onboarding.splashscreen_button_skip_intro')).toBeInTheDocument()
 
   // Wallets screen shown after Intro is skipped
   expect(screen.queryByText('wallets.title')).not.toBeInTheDocument()
 
   act(() => {
-    const skipIntro = screen.getByText('Skip intro')
+    const skipIntro = screen.getByText('onboarding.splashscreen_button_skip_intro')
     user.click(skipIntro)
   })
 
@@ -35,10 +35,10 @@ it('should display Wallets screen directly when Onboarding screen has been shown
   expect(screen.getByText('wallets.button_new_wallet')).toBeInTheDocument()
 
   // footer
-  expect(screen.getByText('Docs')).toBeInTheDocument()
-  expect(screen.getByText('Features')).toBeInTheDocument()
-  expect(screen.getByText('GitHub')).toBeInTheDocument()
-  expect(screen.getByText('Twitter')).toBeInTheDocument()
+  expect(screen.getByText('footer.docs')).toBeInTheDocument()
+  expect(screen.getByText('footer.features')).toBeInTheDocument()
+  expect(screen.getByText('footer.github')).toBeInTheDocument()
+  expect(screen.getByText('footer.twitter')).toBeInTheDocument()
 })
 
 it('should display a modal with alpha warning information', () => {
@@ -56,8 +56,8 @@ it('should display a modal with alpha warning information', () => {
     user.click(readThis)
   })
 
-  expect(screen.getByText(/While JoinMarket is tried and tested, Jam is not./)).toBeInTheDocument()
-  expect(screen.getByText('Fine with me.')).toBeInTheDocument()
+  expect(screen.getByText('footer.warning_alert_text')).toBeInTheDocument()
+  expect(screen.getByText('footer.warning_alert_button_ok')).toBeInTheDocument()
 })
 
 it('should display a websocket connection indicator', async () => {
@@ -69,13 +69,13 @@ it('should display a websocket connection indicator', async () => {
 
   expect(screen.getByText('•').classList.contains('text-danger')).toBe(true)
   expect(screen.getByText('•').classList.contains('text-success')).toBe(false)
-  expect(screen.getByText('Disconnected')).toBeInTheDocument()
-  expect(screen.queryByText('Connected')).not.toBeInTheDocument()
+  expect(screen.getByText('footer.disconnected')).toBeInTheDocument()
+  expect(screen.queryByText('footer.connected')).not.toBeInTheDocument()
 
   await global.__DEV__.JM_WEBSOCKET_SERVER_MOCK.connected
 
-  expect(screen.queryByText('Disconnected')).not.toBeInTheDocument()
-  expect(screen.getByText('Connected')).toBeInTheDocument()
+  expect(screen.queryByText('footer.disconnected')).not.toBeInTheDocument()
+  expect(screen.getByText('footer.connected')).toBeInTheDocument()
   expect(screen.getByText('•').classList.contains('text-success')).toBe(true)
   expect(screen.getByText('•').classList.contains('text-danger')).toBe(false)
 })
diff --git a/src/components/Settings.test.jsx b/src/components/Settings.test.jsx
index 869070d..869ba73 100644
--- a/src/components/Settings.test.jsx
+++ b/src/components/Settings.test.jsx
@@ -12,11 +12,11 @@ describe('<Settings />', () => {
   it('should render settings without errors', () => {
     act(setup)
 
-    expect(screen.getByText('Settings')).toBeInTheDocument()
-    expect(screen.queryByText(/(Show|Hide) balance/)).toBeInTheDocument()
-    expect(screen.queryByText(/Display amounts in (sats|BTC)/)).toBeInTheDocument()
-    expect(screen.queryByText(/Switch to (dark|light) theme/)).toBeInTheDocument()
-    expect(screen.queryByText(/Use (advanced|magic) wallet mode/)).toBeInTheDocument()
+    expect(screen.getByText('settings.title')).toBeInTheDocument()
+    expect(screen.queryByText(/settings.(show|hide)_balance/)).toBeInTheDocument()
+    expect(screen.queryByText(/settings.use_(sats|bitcoin)/)).toBeInTheDocument()
+    expect(screen.queryByText(/settings.use_(dark|light)_theme/)).toBeInTheDocument()
+    expect(screen.queryByText(/settings.use_(normal|dev)_mode/)).toBeInTheDocument()
     expect(screen.queryByText(/English/)).toBeInTheDocument()
   })
 })
diff --git a/src/components/Wallet.test.jsx b/src/components/Wallet.test.jsx
index 022a6fd..0d08baf 100644
--- a/src/components/Wallet.test.jsx
+++ b/src/components/Wallet.test.jsx
@@ -47,11 +47,11 @@ describe('<Wallet />', () => {
     act(() => setup({ name: dummyWalletName }))
 
     expect(screen.getByText('dummy')).toBeInTheDocument()
-    expect(screen.getByText('Inactive')).toBeInTheDocument()
-    expect(screen.getByPlaceholderText('Password')).toBeInTheDocument()
-    expect(screen.getByText('Unlock')).toBeInTheDocument()
-    expect(screen.queryByText('Open')).not.toBeInTheDocument()
-    expect(screen.queryByText('Lock')).not.toBeInTheDocument()
+    expect(screen.getByText('wallets.wallet_preview.wallet_inactive')).toBeInTheDocument()
+    expect(screen.getByPlaceholderText('wallets.wallet_preview.placeholder_password')).toBeInTheDocument()
+    expect(screen.getByText('wallets.wallet_preview.button_unlock')).toBeInTheDocument()
+    expect(screen.queryByText('wallets.wallet_preview.button_open')).not.toBeInTheDocument()
+    expect(screen.queryByText('wallets.wallet_preview.button_lock')).not.toBeInTheDocument()
   })
 
   it('should unlock inactive wallet successfully', async () => {
@@ -60,19 +60,22 @@ describe('<Wallet />', () => {
       json: () => Promise.resolve({ walletname: dummyWalletName, token: dummyToken }),
     })
 
-    act(() => setup({ name: dummyWalletName }))
+    await act(async () => setup({ name: dummyWalletName }))
 
-    expect(screen.getByText('Inactive')).toBeInTheDocument()
-    expect(screen.getByText('Unlock')).toBeInTheDocument()
-    expect(screen.getByPlaceholderText('Password')).toBeInTheDocument()
+    expect(screen.getByText('wallets.wallet_preview.wallet_inactive')).toBeInTheDocument()
+    expect(screen.getByText('wallets.wallet_preview.button_unlock')).toBeInTheDocument()
+    expect(screen.getByPlaceholderText('wallets.wallet_preview.placeholder_password')).toBeInTheDocument()
 
     await act(async () => {
-      user.type(screen.getByPlaceholderText('Password'), 'correct horse battery staple')
-      const unlockWalletButton = screen.getByText('Unlock')
+      user.type(
+        screen.getByPlaceholderText('wallets.wallet_preview.placeholder_password'),
+        'correct horse battery staple'
+      )
+      const unlockWalletButton = screen.getByText('wallets.wallet_preview.button_unlock')
       user.click(unlockWalletButton)
 
-      await waitFor(() => screen.findByText(/Unlocking/))
-      await waitFor(() => screen.findByText('Unlock'))
+      await waitFor(() => screen.findByText(/wallets.wallet_preview.button_unlocking/))
+      await waitFor(() => screen.findByText('wallets.wallet_preview.button_unlock'))
     })
 
     expect(mockStartWallet).toHaveBeenCalledWith(dummyWalletName, dummyToken)
@@ -88,17 +91,20 @@ describe('<Wallet />', () => {
 
     act(() => setup({ name: dummyWalletName }))
 
-    expect(screen.getByText('Inactive')).toBeInTheDocument()
-    expect(screen.getByText('Unlock')).toBeInTheDocument()
-    expect(screen.getByPlaceholderText('Password')).toBeInTheDocument()
+    expect(screen.getByText('wallets.wallet_preview.wallet_inactive')).toBeInTheDocument()
+    expect(screen.getByText('wallets.wallet_preview.button_unlock')).toBeInTheDocument()
+    expect(screen.getByPlaceholderText('wallets.wallet_preview.placeholder_password')).toBeInTheDocument()
 
     await act(async () => {
-      user.type(screen.getByPlaceholderText('Password'), 'correct horse battery staple')
-      const unlockWalletButton = screen.getByText('Unlock')
+      user.type(
+        screen.getByPlaceholderText('wallets.wallet_preview.placeholder_password'),
+        'correct horse battery staple'
+      )
+      const unlockWalletButton = screen.getByText('wallets.wallet_preview.button_unlock')
       user.click(unlockWalletButton)
 
-      await waitFor(() => screen.findByText(/Unlocking/))
-      await waitFor(() => screen.findByText('Unlock'))
+      await waitFor(() => screen.findByText(/wallets.wallet_preview.button_unlocking/))
+      await waitFor(() => screen.findByText('wallets.wallet_preview.button_unlock'))
     })
 
     expect(mockStartWallet).not.toHaveBeenCalled()
@@ -113,11 +119,11 @@ describe('<Wallet />', () => {
     act(() => setup({ name: dummyWalletName, currentWallet: { name: dummyWalletName, token: dummyToken } }))
 
     expect(screen.getByText('dummy')).toBeInTheDocument()
-    expect(screen.getByText('Active')).toBeInTheDocument()
-    expect(screen.getByText('Open')).toBeInTheDocument()
-    expect(screen.getByText('Lock')).toBeInTheDocument()
+    expect(screen.getByText('wallets.wallet_preview.wallet_active')).toBeInTheDocument()
+    expect(screen.getByText('wallets.wallet_preview.button_open')).toBeInTheDocument()
+    expect(screen.getByText('wallets.wallet_preview.button_lock')).toBeInTheDocument()
     expect(screen.queryByPlaceholderText('Password')).not.toBeInTheDocument()
-    expect(screen.queryByText('Unlock')).not.toBeInTheDocument()
+    expect(screen.queryByText('wallets.wallet_preview.button_unlock')).not.toBeInTheDocument()
   })
 
   it('should lock active wallet successfully', async () => {
@@ -128,21 +134,21 @@ describe('<Wallet />', () => {
 
     act(() => setup({ name: dummyWalletName, currentWallet: { name: dummyWalletName, token: dummyToken } }))
 
-    expect(screen.getByText('Active')).toBeInTheDocument()
-    expect(screen.getByText('Lock')).toBeInTheDocument()
+    expect(screen.getByText('wallets.wallet_preview.wallet_active')).toBeInTheDocument()
+    expect(screen.getByText('wallets.wallet_preview.button_lock')).toBeInTheDocument()
 
     await act(async () => {
-      const lockWalletButton = screen.getByText('Lock')
+      const lockWalletButton = screen.getByText('wallets.wallet_preview.button_lock')
       user.click(lockWalletButton)
 
-      await waitFor(() => screen.findByText(/Locking/))
-      await waitFor(() => screen.findByText('Lock'))
+      await waitFor(() => screen.findByText(/wallet_preview.button_locking/))
+      await waitFor(() => screen.findByText('wallets.wallet_preview.button_lock'))
     })
 
     expect(mockStopWallet).toHaveBeenCalled()
     expect(mockSetAlert).toHaveBeenCalledWith({
       variant: 'success',
-      message: `${walletDisplayName(dummyWalletName)} locked successfully.`,
+      message: `wallets.wallet_preview.alert_wallet_locked_successfully`,
       dismissible: true,
     })
   })
@@ -157,15 +163,15 @@ describe('<Wallet />', () => {
 
     act(() => setup({ name: dummyWalletName, currentWallet: { name: dummyWalletName, token: dummyToken } }))
 
-    expect(screen.getByText('Active')).toBeInTheDocument()
-    expect(screen.getByText('Lock')).toBeInTheDocument()
+    expect(screen.getByText('wallets.wallet_preview.wallet_active')).toBeInTheDocument()
+    expect(screen.getByText('wallets.wallet_preview.button_lock')).toBeInTheDocument()
 
     await act(async () => {
-      const lockWalletButton = screen.getByText('Lock')
+      const lockWalletButton = screen.getByText('wallets.wallet_preview.button_lock')
       user.click(lockWalletButton)
 
-      await waitFor(() => screen.findByText(/Locking/))
-      await waitFor(() => screen.findByText('Lock'))
+      await waitFor(() => screen.findByText(/wallets.wallet_preview.button_locking/))
+      await waitFor(() => screen.findByText('wallets.wallet_preview.button_lock'))
     })
 
     // on errors other than 401, stopWallet should not have been called
@@ -186,15 +192,15 @@ describe('<Wallet />', () => {
 
     act(() => setup({ name: dummyWalletName, currentWallet: { name: dummyWalletName, token: dummyToken } }))
 
-    expect(screen.getByText('Active')).toBeInTheDocument()
-    expect(screen.getByText('Lock')).toBeInTheDocument()
+    expect(screen.getByText('wallets.wallet_preview.wallet_active')).toBeInTheDocument()
+    expect(screen.getByText('wallets.wallet_preview.button_lock')).toBeInTheDocument()
 
     await act(async () => {
-      const lockWalletButton = screen.getByText('Lock')
+      const lockWalletButton = screen.getByText('wallets.wallet_preview.button_lock')
       user.click(lockWalletButton)
 
-      await waitFor(() => screen.findByText(/Locking/))
-      await waitFor(() => screen.findByText('Lock'))
+      await waitFor(() => screen.findByText(/wallets.wallet_preview.button_locking/))
+      await waitFor(() => screen.findByText('wallets.wallet_preview.button_lock'))
     })
 
     // on 401 errors, stopWallet *should* have been called
@@ -208,17 +214,13 @@ describe('<Wallet />', () => {
   it('should render active wallet when token is missing', () => {
     act(() => setup({ name: dummyWalletName, currentWallet: { name: dummyWalletName, token: null } }))
 
-    expect(
-      screen.getByText(
-        'This wallet is active, but there is no token to interact with it. Please remove the lock file on the server.'
-      )
-    ).toBeInTheDocument()
+    expect(screen.getByText('wallets.wallet_preview.alert_missing_token')).toBeInTheDocument()
 
     expect(screen.getByText('dummy')).toBeInTheDocument()
-    expect(screen.getByText('Active')).toBeInTheDocument()
+    expect(screen.getByText('wallets.wallet_preview.wallet_active')).toBeInTheDocument()
     expect(screen.queryByPlaceholderText('Password')).not.toBeInTheDocument()
-    expect(screen.queryByText('Open')).not.toBeInTheDocument()
-    expect(screen.queryByText('Lock')).not.toBeInTheDocument()
-    expect(screen.queryByText('Unlock')).not.toBeInTheDocument()
+    expect(screen.queryByText('wallets.wallet_preview.button_open')).not.toBeInTheDocument()
+    expect(screen.queryByText('wallets.wallet_preview.button_lock')).not.toBeInTheDocument()
+    expect(screen.queryByText('wallets.wallet_preview.button_unlock')).not.toBeInTheDocument()
   })
 })