diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index f73b5b23e..9923dc0f0 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -637,6 +637,14 @@ "gasPriceNoDenom": { "message": "Gas Price" }, + "gdprMessage": { + "message": "This data is aggregated and is therefore anonymous for the purposes of General Data Protection Regulation (EU) 2016/679. For more information in relation to our privacy practices, please see our $1.", + "description": "$1 refers to the gdprMessagePrivacyPolicy message, the translation of which is meant to be used exclusively in the context of gdprMessage" + }, + "gdprMessagePrivacyPolicy": { + "message": "Privacy Policy here", + "description": "this translation is intended to be exclusively used as the replacement for the $1 in the gdprMessage translation" + }, "general": { "message": "General" }, diff --git a/ui/app/helpers/utils/i18n-helper.js b/ui/app/helpers/utils/i18n-helper.js index 29e068648..66de8b558 100644 --- a/ui/app/helpers/utils/i18n-helper.js +++ b/ui/app/helpers/utils/i18n-helper.js @@ -1,4 +1,5 @@ // cross-browser connection to extension i18n API +import React from 'react' import log from 'loglevel' import * as Sentry from '@sentry/browser' @@ -39,13 +40,30 @@ export const getMessage = (localeCode, localeMessages, key, substitutions) => { } const entry = localeMessages[key] let phrase = entry.message + + const hasSubstitutions = Boolean(substitutions && substitutions.length) + const hasReactSubstitutions = hasSubstitutions && + substitutions.some((element) => typeof element === 'function' || typeof element === 'object') + // perform substitutions - if (substitutions && substitutions.length) { - substitutions.forEach((substitution, index) => { - const regex = new RegExp(`\\$${index + 1}`, 'g') - phrase = phrase.replace(regex, substitution) + if (hasSubstitutions) { + const parts = phrase.split(/(\$\d)/g) + const partsToReplace = phrase.match(/(\$\d)/g) + + if (partsToReplace.length > substitutions.length) { + throw new Error(`Insufficient number of substitutions for message: '${phrase}'`) + } + + const substitutedParts = parts.map((part) => { + const subMatch = part.match(/\$(\d)/) + return subMatch ? substitutions[Number(subMatch[1]) - 1] : part }) + + phrase = hasReactSubstitutions + ? { substitutedParts } + : substitutedParts.join('') } + return phrase } diff --git a/ui/app/helpers/utils/i18n-helper.test.js b/ui/app/helpers/utils/i18n-helper.test.js new file mode 100644 index 000000000..2832022e6 --- /dev/null +++ b/ui/app/helpers/utils/i18n-helper.test.js @@ -0,0 +1,160 @@ +import { getMessage } from './i18n-helper' +import React from 'react' +import { shallow } from 'enzyme' +import assert from 'assert' + +describe('i18n helper', function () { + const TEST_LOCALE_CODE = 'TEST_LOCALE_CODE' + + const TEST_KEY_1 = 'TEST_KEY_1' + const TEST_KEY_2 = 'TEST_KEY_2' + const TEST_KEY_3 = 'TEST_KEY_3' + const TEST_KEY_4 = 'TEST_KEY_4' + const TEST_KEY_5 = 'TEST_KEY_5' + const TEST_KEY_6 = 'TEST_KEY_6' + const TEST_KEY_6_HELPER = 'TEST_KEY_6_HELPER' + const TEST_KEY_7 = 'TEST_KEY_7' + const TEST_KEY_7_HELPER_1 = 'TEST_KEY_7_HELPER_1' + const TEST_KEY_7_HELPER_2 = 'TEST_KEY_7_HELPER_2' + const TEST_KEY_8 = 'TEST_KEY_8' + const TEST_KEY_8_HELPER_1 = 'TEST_KEY_8_HELPER_1' + const TEST_KEY_8_HELPER_2 = 'TEST_KEY_8_HELPER_2' + + const TEST_SUBSTITUTION_1 = 'TEST_SUBSTITUTION_1' + const TEST_SUBSTITUTION_2 = 'TEST_SUBSTITUTION_2' + const TEST_SUBSTITUTION_3 = 'TEST_SUBSTITUTION_3' + const TEST_SUBSTITUTION_4 = 'TEST_SUBSTITUTION_4' + const TEST_SUBSTITUTION_5 = 'TEST_SUBSTITUTION_5' + + const testLocaleMessages = { + [TEST_KEY_1]: { + message: 'This is a simple message.', + expectedResult: 'This is a simple message.', + }, + [TEST_KEY_2]: { + message: 'This is a message with a single non-react substitution $1.', + }, + [TEST_KEY_3]: { + message: 'This is a message with two non-react substitutions $1 and $2.', + }, + [TEST_KEY_4]: { + message: '$1 - $2 - $3 - $4 - $5', + }, + [TEST_KEY_5]: { + message: '$1 - $2 - $3', + }, + [TEST_KEY_6]: { + 'message': 'Testing a react substitution $1.', + }, + [TEST_KEY_6_HELPER]: { + 'message': TEST_SUBSTITUTION_1, + }, + [TEST_KEY_7]: { + 'message': 'Testing a react substitution $1 and another $2.', + }, + [TEST_KEY_7_HELPER_1]: { + 'message': TEST_SUBSTITUTION_1, + }, + [TEST_KEY_7_HELPER_2]: { + 'message': TEST_SUBSTITUTION_2, + }, + [TEST_KEY_8]: { + 'message': 'Testing a mix $1 of react substitutions $2 and string substitutions $3 + $4.', + }, + [TEST_KEY_8_HELPER_1]: { + 'message': TEST_SUBSTITUTION_3, + }, + [TEST_KEY_8_HELPER_2]: { + 'message': TEST_SUBSTITUTION_4, + }, + } + const t = getMessage.bind(null, TEST_LOCALE_CODE, testLocaleMessages) + + const TEST_SUBSTITUTION_6 = ( +
+ { t(TEST_KEY_6_HELPER) } +
+ ) + const TEST_SUBSTITUTION_7_1 = ( +
+ { t(TEST_KEY_7_HELPER_1) } +
+ ) + const TEST_SUBSTITUTION_7_2 = ( +
+ { t(TEST_KEY_7_HELPER_2) } +
+ ) + const TEST_SUBSTITUTION_8_1 = ( +
+ { t(TEST_KEY_8_HELPER_1) } +
+ ) + const TEST_SUBSTITUTION_8_2 = ( +
+ { t(TEST_KEY_8_HELPER_2) } +
+ ) + + describe('getMessage', function () { + it('should return the exact message paired with key if there are no substitutions', function () { + const result = t(TEST_KEY_1) + assert.equal(result, 'This is a simple message.') + }) + + it('should return the correct message when a single non-react substitution is made', function () { + const result = t(TEST_KEY_2, [ TEST_SUBSTITUTION_1 ]) + assert.equal(result, `This is a message with a single non-react substitution ${TEST_SUBSTITUTION_1}.`) + }) + + it('should return the correct message when two non-react substitutions are made', function () { + const result = t(TEST_KEY_3, [ TEST_SUBSTITUTION_1, TEST_SUBSTITUTION_2 ]) + assert.equal(result, `This is a message with two non-react substitutions ${TEST_SUBSTITUTION_1} and ${TEST_SUBSTITUTION_2}.`) + }) + + it('should return the correct message when multiple non-react substitutions are made', function () { + const result = t(TEST_KEY_4, [ TEST_SUBSTITUTION_1, TEST_SUBSTITUTION_2, TEST_SUBSTITUTION_3, TEST_SUBSTITUTION_4, TEST_SUBSTITUTION_5 ]) + assert.equal(result, `${TEST_SUBSTITUTION_1} - ${TEST_SUBSTITUTION_2} - ${TEST_SUBSTITUTION_3} - ${TEST_SUBSTITUTION_4} - ${TEST_SUBSTITUTION_5}`) + }) + + it('should throw an error when not passed as many substitutions as a message requires', function () { + assert.throws( + () => { + t(TEST_KEY_5, [ TEST_SUBSTITUTION_1, TEST_SUBSTITUTION_2 ]) + }, + Error, + `Insufficient number of substitutions for message: '$1 - $2 - $3'` + ) + }) + + it('should return the correct message when a single react substitution is made', function () { + const result = t(TEST_KEY_6, [ TEST_SUBSTITUTION_6 ]) + assert.equal(shallow(result).html(), ' Testing a react substitution
TEST_SUBSTITUTION_1
.
') + }) + + it('should return the correct message when two react substitutions are made', function () { + const result = t(TEST_KEY_7, [ TEST_SUBSTITUTION_7_1, TEST_SUBSTITUTION_7_2 ]) + assert.equal(shallow(result).html(), ' Testing a react substitution
TEST_SUBSTITUTION_1
and another
TEST_SUBSTITUTION_2
.
') + }) + + it('should return the correct message when substituting a mix of react elements and strings', function () { + const result = t(TEST_KEY_8, [ TEST_SUBSTITUTION_1, TEST_SUBSTITUTION_8_1, TEST_SUBSTITUTION_2, TEST_SUBSTITUTION_8_2 ]) + assert.equal(shallow(result).html(), ' Testing a mix TEST_SUBSTITUTION_1 of react substitutions
TEST_SUBSTITUTION_3
and string substitutions TEST_SUBSTITUTION_2 +
TEST_SUBSTITUTION_4
.
') + }) + }) +}) diff --git a/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js index 14c85b923..e5d622ef2 100644 --- a/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js +++ b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js @@ -14,10 +14,11 @@ export default class MetaMetricsOptIn extends Component { static contextTypes = { metricsEvent: PropTypes.func, + t: PropTypes.func, } render () { - const { metricsEvent } = this.context + const { metricsEvent, t } = this.context const { nextRoute, history, @@ -142,14 +143,15 @@ export default class MetaMetricsOptIn extends Component { disabled={false} />
- This data is aggregated and is therefore anonymous for the purposes of General Data Protection Regulation (EU) 2016/679. For more information in relation to our privacy practices, please see our  - - Privacy Policy here - . + { t('gdprMessage', [ + { t('gdprMessagePrivacyPolicy') } + ]) + }