mirror of
https://github.com/joinmarket-webui/jam.git
synced 2026-08-13 12:33:26 +02:00
feat: i18n (#153)
* Add basic i18n with i18next * Add transifex config * Add translation strings for wallets screen * Delete transifex config for now * Remove suspension * Add exemplary german translation * Add language switcher * Add link to telegram group for translations * Update tests * Fix hook deps * Add tx config * Move i18n code to subfolder * Update i18n usage in tests * Remove unused call * Pull de translation from transifex * Add npm scripts for pushing and pulling * Put back deps * Put deps into dev * Add tx gh config * Add readme * test: add empty new lang * Remove test langs * Remove cli integration * Update src/i18n/README.md Co-authored-by: Thebora Kompanioni <theborakompanioni@users.noreply.github.com> * Update src/i18n/README.md Co-authored-by: Thebora Kompanioni <theborakompanioni@users.noreply.github.com> * Update src/i18n/README.md Co-authored-by: Thebora Kompanioni <theborakompanioni@users.noreply.github.com> * Update public/sprite.svg Co-authored-by: Thebora Kompanioni <theborakompanioni@users.noreply.github.com> * Change link * Run npm install * Update readme * Update readme again Co-authored-by: Thebora Kompanioni <theborakompanioni@users.noreply.github.com>
This commit is contained in:
parent
26ffe8cdeb
commit
cc168eb49f
16 changed files with 365 additions and 80 deletions
9
.tx/transifex.yml
Normal file
9
.tx/transifex.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
git:
|
||||
filters:
|
||||
- filter_type: file
|
||||
file_format: KEYVALUEJSON
|
||||
source_file: src/i18n/locales/en/translation.json
|
||||
source_language: en
|
||||
translation_files_expression: src/i18n/locales/<lang>/translation.json
|
||||
settings:
|
||||
pr_branch_name: tx_translations_<br_unique_id>
|
||||
120
package-lock.json
generated
120
package-lock.json
generated
|
|
@ -22,6 +22,8 @@
|
|||
"conventional-changelog": "^3.1.25",
|
||||
"http-proxy-middleware": "^2.0.1",
|
||||
"husky": "^7.0.4",
|
||||
"i18next": "^21.6.12",
|
||||
"i18next-browser-languagedetector": "^6.1.3",
|
||||
"jest-watch-typeahead": "^0.6.5",
|
||||
"jest-websocket-mock": "^2.3.0",
|
||||
"lint-staged": "^12.3.1",
|
||||
|
|
@ -32,6 +34,7 @@
|
|||
"react": "^17.0.2",
|
||||
"react-bootstrap": "^2.1.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-i18next": "^11.15.5",
|
||||
"react-router-bootstrap": "^0.26.0",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"react-scripts": "^5.0.0",
|
||||
|
|
@ -9158,6 +9161,15 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-webpack-plugin": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz",
|
||||
|
|
@ -9326,6 +9338,38 @@
|
|||
"url": "https://github.com/sponsors/typicode"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "21.6.12",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-21.6.12.tgz",
|
||||
"integrity": "sha512-xlGTPdu2g5PZEUIE6TA1mQ9EIAAv9nMFONzgwAIrKL/KTmYYWufQNGgOmp5Og1PvgUji+6i1whz0rMdsz1qaKw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-browser-languagedetector": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.1.3.tgz",
|
||||
"integrity": "sha512-T+oGXHXtrur14CGnZZ7qQ07X38XJQEI00b/4ILrtO6xPbwTlQ1wtMZC2H+tBULixHuVUXv8LKbxfjyITJkezUg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.14.6"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
|
|
@ -15445,6 +15489,29 @@
|
|||
"integrity": "sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "11.15.5",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.15.5.tgz",
|
||||
"integrity": "sha512-vBWuVEQgrhZrGKpyv8FmJ7Zs5jRQWl794Tte7yzJ0okZqqi3jd6j2pLYNg441WcREsbIOvWdiDXbY7W6E93p1A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.14.5",
|
||||
"html-escaper": "^2.0.2",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 19.0.0",
|
||||
"react": ">= 16.8.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
|
|
@ -18282,6 +18349,15 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-hr-time": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
|
||||
|
|
@ -26188,6 +26264,15 @@
|
|||
"terser": "^5.10.0"
|
||||
}
|
||||
},
|
||||
"html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"html-webpack-plugin": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz",
|
||||
|
|
@ -26310,6 +26395,24 @@
|
|||
"integrity": "sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==",
|
||||
"dev": true
|
||||
},
|
||||
"i18next": {
|
||||
"version": "21.6.12",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-21.6.12.tgz",
|
||||
"integrity": "sha512-xlGTPdu2g5PZEUIE6TA1mQ9EIAAv9nMFONzgwAIrKL/KTmYYWufQNGgOmp5Og1PvgUji+6i1whz0rMdsz1qaKw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.0"
|
||||
}
|
||||
},
|
||||
"i18next-browser-languagedetector": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.1.3.tgz",
|
||||
"integrity": "sha512-T+oGXHXtrur14CGnZZ7qQ07X38XJQEI00b/4ILrtO6xPbwTlQ1wtMZC2H+tBULixHuVUXv8LKbxfjyITJkezUg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.14.6"
|
||||
}
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
|
|
@ -30815,6 +30918,17 @@
|
|||
"integrity": "sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA==",
|
||||
"dev": true
|
||||
},
|
||||
"react-i18next": {
|
||||
"version": "11.15.5",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.15.5.tgz",
|
||||
"integrity": "sha512-vBWuVEQgrhZrGKpyv8FmJ7Zs5jRQWl794Tte7yzJ0okZqqi3jd6j2pLYNg441WcREsbIOvWdiDXbY7W6E93p1A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.14.5",
|
||||
"html-escaper": "^2.0.2",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "16.13.1",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
|
|
@ -32937,6 +33051,12 @@
|
|||
"version": "1.1.2",
|
||||
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
|
||||
},
|
||||
"void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=",
|
||||
"dev": true
|
||||
},
|
||||
"w3c-hr-time": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -33,7 +33,10 @@
|
|||
"react-router-bootstrap": "^0.26.0",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"react-scripts": "^5.0.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
"web-vitals": "^2.1.4",
|
||||
"i18next": "^21.6.12",
|
||||
"i18next-browser-languagedetector": "^6.1.3",
|
||||
"react-i18next": "^11.15.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"serve": "13.0.2"
|
||||
|
|
@ -75,7 +78,7 @@
|
|||
"lint-staged": {
|
||||
"src/**/*.{js,jsx,json,css,md}": "prettier --no-error-on-unmatched-pattern --write"
|
||||
},
|
||||
"jest": {
|
||||
"jest": {
|
||||
"extraGlobals": [
|
||||
"Math",
|
||||
"localStorage"
|
||||
|
|
|
|||
|
|
@ -21,4 +21,5 @@
|
|||
<symbol id="checkmark" viewBox="0 0 24 24" fill="currentColor"><path fill-rule="evenodd" d="M18.381 5.354a.75.75 0 01.265 1.027l-7.087 12a.75.75 0 01-1.164.16L5.48 13.838a.75.75 0 011.038-1.084l4.23 4.051 6.605-11.185a.75.75 0 011.027-.265z" clip-rule="evenodd"></path></symbol>
|
||||
<symbol id="caret-up" viewBox="0 0 24 24" fill="currentColor"><path fill-rule="evenodd" d="M12.248 8.237a.25.25 0 00-.354 0L5.53 14.601a.75.75 0 11-1.06-1.06l6.363-6.364a1.75 1.75 0 012.475 0l6.364 6.364a.75.75 0 01-1.06 1.06l-6.364-6.364z" clip-rule="evenodd"></path></symbol>
|
||||
<symbol id="caret-down" viewBox="0 0 24 24" fill="currentColor"><path fill-rule="evenodd" d="M4.47 9.4a.75.75 0 011.06 0l6.364 6.364a.25.25 0 00.354 0L18.612 9.4a.75.75 0 011.06 1.06l-6.364 6.364a1.75 1.75 0 01-2.475 0L4.47 10.46a.75.75 0 010-1.06z" clip-rule="evenodd"></path></symbol>
|
||||
<symbol id="globe" viewBox="0 0 24 24" fill="currentColor"><path fill-rule="evenodd" d="M10.51 6.617c-.518 1.341-.855 3.245-.855 5.385 0 2.14.337 4.044.856 5.385.26.673.552 1.166.838 1.478.285.312.506.385.651.385.145 0 .366-.073.652-.385.285-.312.577-.805.837-1.478.52-1.341.856-3.245.856-5.385 0-2.14-.337-4.044-.856-5.385-.26-.673-.552-1.166-.838-1.478-.285-.312-.506-.385-.651-.385-.145 0-.366.073-.651.385-.286.312-.578.805-.838 1.478zm-.268-2.49c.455-.499 1.048-.873 1.758-.873s1.303.374 1.758.872c.455.497.83 1.175 1.13 1.95.601 1.553.957 3.649.957 5.926s-.356 4.373-.957 5.926c-.3.774-.675 1.453-1.13 1.95-.455.497-1.048.872-1.758.872s-1.303-.375-1.758-.872c-.455-.497-.83-1.175-1.13-1.95-.601-1.553-.957-3.649-.957-5.926s.356-4.373.957-5.926c.3-.775.675-1.453 1.13-1.95z" clip-rule="evenodd"></path><path fill-rule="evenodd" d="M12 4.746a7.25 7.25 0 100 14.5 7.25 7.25 0 000-14.5zm-8.75 7.25a8.75 8.75 0 1117.5 0 8.75 8.75 0 01-17.5 0z" clip-rule="evenodd"></path><path fill-rule="evenodd" d="M3.25 11.99a.75.75 0 01.75-.75l16 .012a.75.75 0 110 1.5l-16-.011a.75.75 0 01-.75-.75z" clip-rule="evenodd"></path></symbol>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB |
|
|
@ -1,17 +0,0 @@
|
|||
import { BrowserRouter } from 'react-router-dom'
|
||||
|
||||
import { SettingsProvider } from '../context/SettingsContext'
|
||||
import { WalletProvider } from '../context/WalletContext'
|
||||
import { WebsocketProvider } from '../context/WebsocketContext'
|
||||
|
||||
export const AllTheProviders = ({ children }) => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<SettingsProvider>
|
||||
<WalletProvider>
|
||||
<WebsocketProvider>{children}</WebsocketProvider>
|
||||
</WalletProvider>
|
||||
</SettingsProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,46 +1,38 @@
|
|||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { render, screen } from '../testUtils'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import user from '@testing-library/user-event'
|
||||
|
||||
import { AllTheProviders } from '../__util__/AllTheProviders'
|
||||
|
||||
import App from './App'
|
||||
|
||||
it('should display Onboarding screen initially', () => {
|
||||
act(() => {
|
||||
render(<App />, {
|
||||
wrapper: AllTheProviders,
|
||||
})
|
||||
})
|
||||
render(<App />)
|
||||
|
||||
// Onboarding screen
|
||||
expect(screen.getByText('Get started')).toBeInTheDocument()
|
||||
expect(screen.getByText('Skip intro')).toBeInTheDocument()
|
||||
|
||||
// Wallets screen shown after Intro is skipped
|
||||
expect(screen.queryByText('Your wallets')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('wallets.title')).not.toBeInTheDocument()
|
||||
|
||||
act(() => {
|
||||
const skipIntro = screen.getByText('Skip intro')
|
||||
user.click(skipIntro)
|
||||
})
|
||||
|
||||
expect(screen.getByText('Your wallets')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display Wallets screen directly when Onboarding screen has been shown', () => {
|
||||
global.__DEV__.addToAppSettings({ showOnboarding: false })
|
||||
|
||||
act(() => {
|
||||
render(<App />, {
|
||||
wrapper: AllTheProviders,
|
||||
})
|
||||
render(<App />)
|
||||
})
|
||||
|
||||
// Wallets screen
|
||||
expect(screen.getByText('Your wallets')).toBeInTheDocument()
|
||||
expect(screen.getByText('Create new wallet')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.title')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.button_new_wallet')).toBeInTheDocument()
|
||||
|
||||
// footer
|
||||
expect(screen.getByText('Docs')).toBeInTheDocument()
|
||||
|
|
@ -53,9 +45,7 @@ it('should display a modal with alpha warning information', () => {
|
|||
global.__DEV__.addToAppSettings({ showOnboarding: false })
|
||||
|
||||
act(() => {
|
||||
render(<App />, {
|
||||
wrapper: AllTheProviders,
|
||||
})
|
||||
render(<App />)
|
||||
})
|
||||
|
||||
expect(screen.getByText('Read this before using.')).toBeInTheDocument()
|
||||
|
|
@ -74,9 +64,7 @@ it('should display a websocket connection indicator', async () => {
|
|||
global.__DEV__.addToAppSettings({ showOnboarding: false })
|
||||
|
||||
act(() => {
|
||||
render(<App />, {
|
||||
wrapper: AllTheProviders,
|
||||
})
|
||||
render(<App />)
|
||||
})
|
||||
|
||||
expect(screen.getByText('•').classList.contains('text-danger')).toBe(true)
|
||||
|
|
|
|||
|
|
@ -4,10 +4,13 @@ import Sprite from './Sprite'
|
|||
import PageTitle from './PageTitle'
|
||||
import { useSettings, useSettingsDispatch } from '../context/SettingsContext'
|
||||
import { SATS, BTC } from '../utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import languages from '../i18n/languages'
|
||||
|
||||
export default function Settings({ currentWallet }) {
|
||||
const settings = useSettings()
|
||||
const settingsDispatch = useSettingsDispatch()
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
const setTheme = (theme) => {
|
||||
if (window.JM.THEMES.includes(theme)) {
|
||||
|
|
@ -86,6 +89,29 @@ export default function Settings({ currentWallet }) {
|
|||
/>
|
||||
Use {settings.useAdvancedWalletMode ? 'magic' : 'advanced'} wallet mode
|
||||
</rb.Button>
|
||||
|
||||
<rb.Dropdown>
|
||||
<rb.Dropdown.Toggle variant="outline-dark" className="border-0 mb-2 d-inline-flex align-items-center">
|
||||
<Sprite symbol="globe" width="24" height="24" className="me-2" />
|
||||
{languages.find((lng) => lng.key === i18n.language).description}
|
||||
</rb.Dropdown.Toggle>
|
||||
|
||||
<rb.Dropdown.Menu variant={settings.theme === 'light' ? 'light' : 'dark'}>
|
||||
{languages.map((lng, index) => {
|
||||
return (
|
||||
<rb.Dropdown.Item key={index} onClick={() => i18n.changeLanguage(lng.key)}>
|
||||
{lng.description}
|
||||
</rb.Dropdown.Item>
|
||||
)
|
||||
})}
|
||||
<rb.Dropdown.Item
|
||||
href="https://github.com/joinmarket-webui/joinmarket-webui/tree/master/src/i18n/README.md"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Missing your language? Help us out!
|
||||
</rb.Dropdown.Item>
|
||||
</rb.Dropdown.Menu>
|
||||
</rb.Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,29 +5,31 @@ import Alert from './Alert'
|
|||
import Wallet from './Wallet'
|
||||
import PageTitle from './PageTitle'
|
||||
import { useCurrentWallet } from '../context/WalletContext'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { walletDisplayName } from '../utils'
|
||||
import * as Api from '../libs/JmWalletApi'
|
||||
|
||||
export default function Wallets({ startWallet, stopWallet }) {
|
||||
const { t } = useTranslation()
|
||||
const currentWallet = useCurrentWallet()
|
||||
const [walletList, setWalletList] = useState(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [alert, setAlert] = useState(
|
||||
currentWallet?.name && {
|
||||
variant: 'info',
|
||||
message: `There can be only one active wallet. If you want to open another wallet, please lock ${walletDisplayName(
|
||||
currentWallet.name
|
||||
)} first.`,
|
||||
message: t('wallets.alert_wallet_open', { currentWalletName: walletDisplayName(currentWallet.name) }),
|
||||
dismissible: true,
|
||||
}
|
||||
)
|
||||
|
||||
const walletsFailedError = t('wallets.error_loading_failed')
|
||||
|
||||
useEffect(() => {
|
||||
const abortCtrl = new AbortController()
|
||||
|
||||
setIsLoading(true)
|
||||
Api.getWalletAll({ signal: abortCtrl.signal })
|
||||
.then((res) => (res.ok ? res.json() : Promise.reject(new Error(res.message || 'Loading wallets failed.'))))
|
||||
.then((res) => (res.ok ? res.json() : Promise.reject(new Error(res.message || walletsFailedError))))
|
||||
.then((data) => {
|
||||
const { wallets = [] } = data
|
||||
if (currentWallet) {
|
||||
|
|
@ -47,20 +49,20 @@ export default function Wallets({ startWallet, stopWallet }) {
|
|||
})
|
||||
|
||||
return () => abortCtrl.abort()
|
||||
}, [currentWallet])
|
||||
}, [currentWallet, walletsFailedError])
|
||||
|
||||
return (
|
||||
<div className="wallets">
|
||||
<PageTitle
|
||||
title="Your wallets"
|
||||
subtitle={walletList?.length === 0 ? 'It looks like you do not have a wallet, yet.' : null}
|
||||
title={t('wallets.title')}
|
||||
subtitle={walletList?.length === 0 ? t('wallets.subtitle_no_wallets') : null}
|
||||
center={true}
|
||||
/>
|
||||
{alert && <Alert {...alert} />}
|
||||
{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" />
|
||||
<span>Loading wallets</span>
|
||||
<span>{t('wallets.text_loading')}</span>
|
||||
</div>
|
||||
)}
|
||||
{walletList?.map((wallet, index) => (
|
||||
|
|
@ -81,7 +83,7 @@ export default function Wallets({ startWallet, stopWallet }) {
|
|||
to="/create-wallet"
|
||||
className={`btn mt-4 ${walletList?.length === 0 ? 'btn-lg btn-dark' : 'btn-outline-dark'}`}
|
||||
>
|
||||
Create new wallet
|
||||
{t('wallets.button_new_wallet')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import React from 'react'
|
||||
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react'
|
||||
import { render, screen, waitForElementToBeRemoved } from '../testUtils'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
|
||||
import * as apiMock from '../libs/JmWalletApi'
|
||||
import { AllTheProviders } from '../__util__/AllTheProviders'
|
||||
|
||||
import Wallets from './Wallets'
|
||||
|
||||
|
|
@ -13,14 +12,12 @@ it('should render without errors', () => {
|
|||
apiMock.getWalletAll.mockResolvedValueOnce(new Promise((r) => setTimeout(r, 1_000)))
|
||||
|
||||
act(() => {
|
||||
render(<Wallets />, {
|
||||
wrapper: AllTheProviders,
|
||||
})
|
||||
render(<Wallets />)
|
||||
})
|
||||
|
||||
expect(screen.getByText('Your wallets')).toBeInTheDocument()
|
||||
expect(screen.getByText('Loading wallets')).toBeInTheDocument()
|
||||
expect(screen.getByText('Create new wallet')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.title')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.text_loading')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.button_new_wallet')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display error message when loading wallets fails', async () => {
|
||||
|
|
@ -29,18 +26,16 @@ it('should display error message when loading wallets fails', async () => {
|
|||
})
|
||||
|
||||
act(() => {
|
||||
render(<Wallets />, {
|
||||
wrapper: AllTheProviders,
|
||||
})
|
||||
render(<Wallets />)
|
||||
})
|
||||
|
||||
expect(screen.getByText('Loading wallets')).toBeInTheDocument()
|
||||
expect(screen.getByText('Create new wallet')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.title')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.button_new_wallet')).toBeInTheDocument()
|
||||
|
||||
await waitForElementToBeRemoved(screen.getByText('Loading wallets'))
|
||||
await waitForElementToBeRemoved(screen.getByText('wallets.text_loading'))
|
||||
|
||||
expect(screen.getByText('Loading wallets failed.')).toBeInTheDocument()
|
||||
expect(screen.getByText('Create new wallet')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.error_loading_failed')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.button_new_wallet')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display big call-to-action button if no wallet has been created yet', async () => {
|
||||
|
|
@ -50,22 +45,20 @@ it('should display big call-to-action button if no wallet has been created yet',
|
|||
})
|
||||
|
||||
act(() => {
|
||||
render(<Wallets />, {
|
||||
wrapper: AllTheProviders,
|
||||
})
|
||||
render(<Wallets />)
|
||||
})
|
||||
|
||||
expect(screen.getByText('Loading wallets')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.text_loading')).toBeInTheDocument()
|
||||
|
||||
const callToActionButtonBefore = screen.getByText('Create new wallet')
|
||||
const callToActionButtonBefore = screen.getByText('wallets.button_new_wallet')
|
||||
expect(callToActionButtonBefore.classList.contains('btn')).toBe(true)
|
||||
expect(callToActionButtonBefore.classList.contains('btn-lg')).toBe(false)
|
||||
|
||||
await waitForElementToBeRemoved(screen.getByText('Loading wallets'))
|
||||
await waitForElementToBeRemoved(screen.getByText('wallets.text_loading'))
|
||||
|
||||
expect(screen.getByText('It looks like you do not have a wallet, yet.')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.subtitle_no_wallets')).toBeInTheDocument()
|
||||
|
||||
const callToActionButtonAfter = screen.getByText('Create new wallet')
|
||||
const callToActionButtonAfter = screen.getByText('wallets.button_new_wallet')
|
||||
expect(callToActionButtonAfter.classList.contains('btn-lg')).toBe(true)
|
||||
})
|
||||
|
||||
|
|
@ -76,20 +69,18 @@ it('should display login for available wallets', async () => {
|
|||
})
|
||||
|
||||
act(() => {
|
||||
render(<Wallets />, {
|
||||
wrapper: AllTheProviders,
|
||||
})
|
||||
render(<Wallets />)
|
||||
})
|
||||
|
||||
expect(screen.getByText('Loading wallets')).toBeInTheDocument()
|
||||
await waitForElementToBeRemoved(screen.getByText('Loading wallets'))
|
||||
expect(screen.getByText('wallets.text_loading')).toBeInTheDocument()
|
||||
await waitForElementToBeRemoved(screen.getByText('wallets.text_loading'))
|
||||
|
||||
expect(screen.queryByText('It looks like you do not have a wallet, yet.')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('wallets.alert_wallet_open')).not.toBeInTheDocument()
|
||||
|
||||
expect(screen.getByText('wallet0')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallet1')).toBeInTheDocument()
|
||||
|
||||
const callToActionButton = screen.getByText('Create new wallet')
|
||||
const callToActionButton = screen.getByText('wallets.button_new_wallet')
|
||||
expect(callToActionButton.classList.contains('btn')).toBe(true)
|
||||
expect(callToActionButton.classList.contains('btn-lg')).toBe(false)
|
||||
})
|
||||
|
|
|
|||
82
src/i18n/README.md
Normal file
82
src/i18n/README.md
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# Translating Jam
|
||||
|
||||
We use [Transifex](https://www.transifex.com/joinmarket/jam/) for managing Jam's translations.
|
||||
|
||||
## For Developers
|
||||
|
||||
Our source language, i.e. the language we develop in, is English.
|
||||
Any missing translations will fall back to using the English translation.
|
||||
If you add text to the app, make sure not to use plain language in source files.
|
||||
Instead add any strings to the English translation file:
|
||||
|
||||
```
|
||||
src/i18n/locales/en/translation.json
|
||||
```
|
||||
|
||||
Then, use the `i18next-react` [translation hook](https://react.i18next.com/latest/usetranslation-hook) in source files.
|
||||
Don't worry about other languages.
|
||||
|
||||
### Example
|
||||
|
||||
Source File:
|
||||
|
||||
```jsx
|
||||
// src/components/Hello.jsx
|
||||
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function HelloComponent() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return <h1>{t('hello.heading')}</h1>
|
||||
}
|
||||
```
|
||||
|
||||
Translation file:
|
||||
|
||||
```json
|
||||
// src/i18n/locales/en/translation.json
|
||||
|
||||
{
|
||||
"hello": {
|
||||
"heading": "Hello, world!"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Background
|
||||
|
||||
Transifex [is connected with our GitHub repo](https://docs.transifex.com/transifex-github-integrations/github-tx-ui).
|
||||
Whenever new translation strings land on `master` (e.g. when being added as part of a new feature in a PR), [Transifex](https://www.transifex.com/joinmarket/jam/) will automatically add those new strings to the Transifex web app.
|
||||
Whenever translations on Transifex are done and reviewed, Transifex will open a PR on GitHub to integrate newly translated texts into the app.
|
||||
|
||||
### Adding a new Language
|
||||
|
||||
To add a new language:
|
||||
|
||||
1. [Add it](https://docs.transifex.com/projects/adding-and-removing-project-languages) on [Transifex](https://www.transifex.com/joinmarket/jam/languages/).
|
||||
1. Trigger a [manual sync](https://docs.transifex.com/transifex-github-integrations/github-tx-ui#manual-sync) from Transifex to GitHub.
|
||||
1. Transifex will open a pull request. On this branch, add the new language in `src/i18n/languages.js`. For example, when adding `de`:
|
||||
|
||||
```js
|
||||
import en from './locales/en/translation.json'
|
||||
import de from './locales/de/translation.json' // This file should have been created by Transifex in the PR.
|
||||
|
||||
const languages = [
|
||||
{
|
||||
key: 'en',
|
||||
description: 'English',
|
||||
translation: en,
|
||||
},
|
||||
{
|
||||
key: 'de',
|
||||
description: 'Deutsch',
|
||||
translation: de,
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
## Translators
|
||||
|
||||
Let us know [on Telegram](https://t.me/JoinMarketWebUI) or [GitHub](https://github.com/joinmarket-webui/joinmarket-webui/issues/new) if you would like to help out translating!
|
||||
18
src/i18n/config.js
Normal file
18
src/i18n/config.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import i18n from 'i18next'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||
import languages from './languages'
|
||||
|
||||
const resources = languages.reduce((acc, lng) => {
|
||||
return {
|
||||
...acc,
|
||||
[lng['key']]: { translation: lng['translation'] },
|
||||
}
|
||||
}, {})
|
||||
|
||||
i18n.use(LanguageDetector).use(initReactI18next).init({
|
||||
resources,
|
||||
fallbackLng: 'en',
|
||||
})
|
||||
|
||||
export default i18n
|
||||
11
src/i18n/languages.js
Normal file
11
src/i18n/languages.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import en from './locales/en/translation.json'
|
||||
|
||||
const languages = [
|
||||
{
|
||||
key: 'en',
|
||||
description: 'English',
|
||||
translation: en,
|
||||
},
|
||||
]
|
||||
|
||||
export default languages
|
||||
10
src/i18n/locales/en/translation.json
Normal file
10
src/i18n/locales/en/translation.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"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."
|
||||
}
|
||||
}
|
||||
10
src/i18n/testConfig.js
Normal file
10
src/i18n/testConfig.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import i18n from 'i18next'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
lng: 'en',
|
||||
fallbackLng: 'en',
|
||||
resources: { en: { translations: {} } },
|
||||
})
|
||||
|
||||
export default i18n
|
||||
|
|
@ -8,6 +8,7 @@ import { WebsocketProvider } from './context/WebsocketContext'
|
|||
import reportWebVitals from './reportWebVitals'
|
||||
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||
import './index.css'
|
||||
import './i18n/config'
|
||||
|
||||
ReactDOM.render(
|
||||
<BrowserRouter basename={window.JM.PUBLIC_PATH}>
|
||||
|
|
|
|||
30
src/testUtils.jsx
Normal file
30
src/testUtils.jsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import React from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { I18nextProvider } from 'react-i18next'
|
||||
import { SettingsProvider } from './context/SettingsContext'
|
||||
import { WalletProvider } from './context/WalletContext'
|
||||
import { WebsocketProvider } from './context/WebsocketContext'
|
||||
import i18n from './i18n/testConfig'
|
||||
|
||||
const AllTheProviders = ({ children }) => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<SettingsProvider>
|
||||
<WalletProvider>
|
||||
<WebsocketProvider>{children}</WebsocketProvider>
|
||||
</WalletProvider>
|
||||
</SettingsProvider>
|
||||
</I18nextProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
const customRender = (ui, options) => render(ui, { wrapper: AllTheProviders, ...options })
|
||||
|
||||
// re-export everything
|
||||
export * from '@testing-library/react'
|
||||
|
||||
// override render method
|
||||
export { customRender as render }
|
||||
Loading…
Add table
Add a link
Reference in a new issue