Browse Source

feat(i18n): detect best-fit user currency

Use the locale information in order to select a best fit default
currency for the user.
renovate/lint-staged-8.x
Tom Kirkpatrick 6 years ago
parent
commit
27e46ccde8
No known key found for this signature in database GPG Key ID: 72203A8EC5967EA8
  1. 4
      app/components/Settings/Locale/Locale.js
  2. 7
      app/index.js
  3. 109
      app/lib/utils/i18n.js
  4. 7
      app/lib/zap/menuBuilder.js
  5. 28
      app/reducers/ticker.js
  6. 2
      package.json
  7. 9
      test/unit/__mocks__/electron.js
  8. 15
      yarn.lock

4
app/components/Settings/Locale/Locale.js

@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import FaAngleLeft from 'react-icons/lib/fa/angle-left' import FaAngleLeft from 'react-icons/lib/fa/angle-left'
import ISO6391 from 'iso-639-1' import { getLanguageName } from 'lib/utils/i18n'
import Isvg from 'react-inlinesvg' import Isvg from 'react-inlinesvg'
import checkIcon from 'icons/check.svg' import checkIcon from 'icons/check.svg'
@ -29,7 +29,7 @@ const Translate = ({ locales, disableSubMenu, currentLocale, setLocale }) => {
className={currentLocale === lang ? styles.active : ''} className={currentLocale === lang ? styles.active : ''}
onClick={() => changeLocale(lang)} onClick={() => changeLocale(lang)}
> >
<span>{ISO6391.getName(lang.split('-')[0])}</span> <span>{getLanguageName(lang)}</span>
{currentLocale === lang && <Isvg src={checkIcon} />} {currentLocale === lang && <Isvg src={checkIcon} />}
</li> </li>
) )

7
app/index.js

@ -7,12 +7,13 @@ import Root from './containers/Root'
import { configureStore, history } from './store/configureStore' import { configureStore, history } from './store/configureStore'
import './styles/app.global.scss' import './styles/app.global.scss'
import { translationMessages, DEFAULT_LOCALE } from './lib/utils/i18n' import { translationMessages, getLocale } from './lib/utils/i18n'
const locale = getLocale()
const initialState = { const initialState = {
intl: { intl: {
locale: DEFAULT_LOCALE, locale,
messages: translationMessages[DEFAULT_LOCALE], messages: translationMessages[locale],
timeZone: jstz.determine().name() timeZone: jstz.determine().name()
} }
} }

109
app/lib/utils/i18n.js

@ -1,6 +1,9 @@
import { app, remote } from 'electron' import { app, remote } from 'electron'
import Store from 'electron-store'
import { addLocaleData } from 'react-intl' import { addLocaleData } from 'react-intl'
import Store from 'electron-store'
import get from 'lodash.get'
import { lookup } from 'country-data-lookup'
import createDebug from 'debug'
// Load locale data. // Load locale data.
import bg from 'react-intl/locale-data/bg' import bg from 'react-intl/locale-data/bg'
@ -41,6 +44,8 @@ import ukTranslationMessages from '../../translations/uk-UA.json'
import zhCNTranslationMessages from '../../translations/zh-CN.json' import zhCNTranslationMessages from '../../translations/zh-CN.json'
import zhTWTranslationMessages from '../../translations/zh-TW.json' import zhTWTranslationMessages from '../../translations/zh-TW.json'
const debug = createDebug('zap:i18n')
// Add locale data. // Add locale data.
addLocaleData([ addLocaleData([
...bg, ...bg,
@ -83,24 +88,31 @@ export const locales = [
'zh' 'zh'
] ]
function getDefaltLocale() { // Defaine list of currencies that we will support.
const store = new Store({ name: 'settings' }) export const currencies = [
'USD',
// Detect user language. 'EUR',
let language = store.get('locale') || (app || remote.app).getLocale() 'JPY',
'GBP',
// If the detected language is not available, strip out any regional component and check again. 'CAD',
if (!locales.includes(language)) { 'KRW',
language = language.toLowerCase().split(/[_-]+/)[0] 'AUD',
} 'BRL',
// If we still can't find the users language, default to english. 'CHF',
if (!locales.includes(language)) { 'CLP',
language = 'en' 'CNY',
} 'DKK',
return language 'HKD',
} 'INR',
'ISK',
export const DEFAULT_LOCALE = getDefaltLocale() 'NZD',
'PLN',
'RUB',
'SEK',
'SGD',
'THB',
'TWB'
]
// Collate all translations. // Collate all translations.
export const translationMessages = { export const translationMessages = {
@ -123,3 +135,62 @@ export const translationMessages = {
tr: trTranslationMessages, tr: trTranslationMessages,
uk: ukTranslationMessages uk: ukTranslationMessages
} }
/**
* Get the most appropriate language code.
* @return {string} Language code.
*/
export const getLocale = () => {
const store = new Store({ name: 'settings' })
const userLocale = store.get('locale')
if (userLocale) {
debug('Determined locale as %s from settings', userLocale)
return userLocale
}
const defaultLocale = (app || remote.app).getLocale() || 'en-US'
const language = defaultLocale.toLowerCase().split(/[_-]+/)[0]
let locale = 'en'
if (locales.includes(language)) {
locale = language
}
if (locales.includes(defaultLocale)) {
locale = userLocale
}
debug('Determined locale as %s', locale)
return locale
}
/**
* Get the most appropriate language code.
* @return {string} Language code.
*/
export const getLanguageName = lang => {
const language = lang.toLowerCase().split(/[_-]+/)[0]
const data = lookup.languages({ alpha2: language })
const name = get(data, '[0]name', language)
debug('Determined language as %s', name)
return name
}
/**
* Get the most appropriate currency code.
* @return {string} Currency code.
*/
export const getCurrency = () => {
const store = new Store({ name: 'settings' })
const userCurrency = store.get('fiatTicker')
if (userCurrency) {
debug('Determined currency as %s from settings', userCurrency)
return userCurrency
}
const defaultLocale = (app || remote.app).getLocale() || 'en-US'
const country = defaultLocale.split(/[_-]+/)[1]
const data = lookup.countries({ alpha2: country })
const detectedCurrency = get(data, '[0]currencies[0]', 'USD')
let currency = 'USD'
if (currencies.includes(detectedCurrency)) {
currency = detectedCurrency
}
debug('Determined currency as %s', currency)
return currency
}

7
app/lib/zap/menuBuilder.js

@ -1,7 +1,6 @@
// @flow // @flow
import { app, Menu, shell, BrowserWindow, ipcMain } from 'electron' import { app, Menu, shell, BrowserWindow, ipcMain } from 'electron'
import ISO6391 from 'iso-639-1' import { locales, getLocale, getLanguageName } from '../utils/i18n'
import { locales, DEFAULT_LOCALE } from '../utils/i18n'
export default class ZapMenuBuilder { export default class ZapMenuBuilder {
mainWindow: BrowserWindow mainWindow: BrowserWindow
@ -9,7 +8,7 @@ export default class ZapMenuBuilder {
constructor(mainWindow: BrowserWindow) { constructor(mainWindow: BrowserWindow) {
this.mainWindow = mainWindow this.mainWindow = mainWindow
this.locale = DEFAULT_LOCALE this.locale = getLocale()
ipcMain.on('setLocale', (event, locale) => this.buildMenu(locale)) ipcMain.on('setLocale', (event, locale) => this.buildMenu(locale))
} }
@ -281,7 +280,7 @@ export default class ZapMenuBuilder {
label: 'Language', label: 'Language',
submenu: locales.map(locale => { submenu: locales.map(locale => {
return { return {
label: ISO6391.getName(locale.split('-')[0]), label: getLanguageName(locale),
type: 'radio', type: 'radio',
checked: this.locale === locale, checked: this.locale === locale,
click: () => this.mainWindow.webContents.send('receiveLocale', locale) click: () => this.mainWindow.webContents.send('receiveLocale', locale)

28
app/reducers/ticker.js

@ -1,6 +1,7 @@
import { createSelector } from 'reselect' import { createSelector } from 'reselect'
import Store from 'electron-store' import Store from 'electron-store'
import { requestTicker } from 'lib/utils/api' import { requestTicker } from 'lib/utils/api'
import { currencies, getCurrency } from 'lib/utils/i18n'
import { infoSelectors } from './info' import { infoSelectors } from './info'
// Settings store // Settings store
@ -141,31 +142,8 @@ const initialState = {
crypto: '', crypto: '',
btcTicker: null, btcTicker: null,
ltcTicker: null, ltcTicker: null,
fiatTicker: store.get('fiatTicker', 'USD'), fiatTicker: getCurrency(),
fiatTickers: [ fiatTickers: currencies,
'USD',
'EUR',
'JPY',
'GBP',
'CAD',
'KRW',
'AUD',
'BRL',
'CHF',
'CLP',
'CNY',
'DKK',
'HKD',
'INR',
'ISK',
'NZD',
'PLN',
'RUB',
'SEK',
'SGD',
'THB',
'TWB'
],
currencyFilters: [ currencyFilters: [
{ {
key: 'btc', key: 'btc',

2
package.json

@ -286,6 +286,7 @@
"axios": "^0.18.0", "axios": "^0.18.0",
"bitcoinjs-lib": "^4.0.1", "bitcoinjs-lib": "^4.0.1",
"copy-to-clipboard": "^3.0.8", "copy-to-clipboard": "^3.0.8",
"country-data-lookup": "^0.0.3",
"debug": "^4.0.1", "debug": "^4.0.1",
"debug-logger": "^0.4.1", "debug-logger": "^0.4.1",
"devtron": "^1.4.0", "devtron": "^1.4.0",
@ -295,7 +296,6 @@
"electron-store": "^2.0.0", "electron-store": "^2.0.0",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"history": "^4.7.2", "history": "^4.7.2",
"iso-639-1": "^2.0.3",
"javascript-state-machine": "^3.1.0", "javascript-state-machine": "^3.1.0",
"jstimezonedetect": "^1.0.6", "jstimezonedetect": "^1.0.6",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",

9
test/unit/__mocks__/electron.js

@ -5,9 +5,14 @@ module.exports = {
match: jest.fn(), match: jest.fn(),
app: { app: {
getPath: name => normalize(`/tmp/zap-test/${name}`), getPath: name => normalize(`/tmp/zap-test/${name}`),
getAppPath: () => normalize('/tmp/zap-test') getAppPath: () => normalize('/tmp/zap-test'),
getLocale: jest.fn()
},
remote: {
app: {
getLocale: jest.fn()
}
}, },
remote: jest.fn(),
dialog: jest.fn(), dialog: jest.fn(),
BrowserWindow: jest.fn(), BrowserWindow: jest.fn(),
ipcMain: { ipcMain: {

15
yarn.lock

@ -3438,6 +3438,13 @@ cosmiconfig@^5.0.6:
js-yaml "^3.9.0" js-yaml "^3.9.0"
parse-json "^4.0.0" parse-json "^4.0.0"
country-data-lookup@^0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/country-data-lookup/-/country-data-lookup-0.0.3.tgz#59babadbd70c74a870f78e518b517b210bd8e3dd"
dependencies:
currency-symbol-map "~4.0.3"
lodash "~4.17.4"
coveralls@^3.0.2: coveralls@^3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/coveralls/-/coveralls-3.0.2.tgz#f5a0bcd90ca4e64e088b710fa8dda640aea4884f" resolved "https://registry.yarnpkg.com/coveralls/-/coveralls-3.0.2.tgz#f5a0bcd90ca4e64e088b710fa8dda640aea4884f"
@ -3663,6 +3670,10 @@ cssstyle@^1.0.0:
dependencies: dependencies:
cssom "0.3.x" cssom "0.3.x"
currency-symbol-map@~4.0.3:
version "4.0.4"
resolved "https://registry.yarnpkg.com/currency-symbol-map/-/currency-symbol-map-4.0.4.tgz#3cfba625974dd3f86822d327ecbd10248695e95e"
currently-unhandled@^0.4.1: currently-unhandled@^0.4.1:
version "0.4.1" version "0.4.1"
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
@ -6835,10 +6846,6 @@ isexe@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
iso-639-1@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/iso-639-1/-/iso-639-1-2.0.3.tgz#72dd3448ac5629c271628c5ac566369428d6ccd0"
isobject@^2.0.0: isobject@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"

Loading…
Cancel
Save