From 6247a7c432852853efcd7dd1eecb284213bcf141 Mon Sep 17 00:00:00 2001 From: Donovan Preston Date: Thu, 13 Jul 2017 08:15:05 -0400 Subject: [PATCH] Fix #1714 Switch localization from l20n to fluent-react for better react integration. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squashed commit of the following: commit ef5340cee59c48f297caca601606f4dc5031d8af Author: Donovan Preston Date: Wed Jul 12 16:03:42 2017 -0400 Add fluent-react to flow-typed commit 7c789982e6d12ef8460f0da0381fee5f4ee007da Author: Donovan Preston Date: Wed Jul 12 14:42:59 2017 -0400 Use fluent-langneg commit 5ae67cf95518f3a603d78f3a7654627d1638dfbc Author: Donovan Preston Date: Wed Jul 12 14:41:40 2017 -0400 Fix lint commit 68cd0bfd60e8c6b233140cbf187925f7c8b9f51d Author: Donovan Preston Date: Wed Jul 12 11:59:31 2017 -0400 Implement LocalizedHtml component and use it to parse the strings on the ExperimentPage that contain html commit 04469f408d47a58ade58c124e7c786b31d36e1a8 Author: Donovan Preston Date: Wed Jul 12 10:41:31 2017 -0400 Use fluent-react/compat so we don't include the un-babelified version in vendor.js commit 22fa6beb8ac05f21dc1253285e27cfe2d370264b Merge: f6f99898 e4a0efa0 Author: Donovan Preston Date: Fri Jun 30 13:31:47 2017 -0400 Merge pull request #4 from stasm/fluent-react-tests WIP Fix test failures caused by fluent-react commit e4a0efa03c6430cd5badda20a5c390add3728243 Author: Staś Małolepszy Date: Thu Jun 29 19:52:37 2017 +0200 WIP Fix test failures caused by fluent-react fluent-react 0.4.1 relaxes the restriction about Localized components being only valid inside of LocalizationProvider. The tests now pass but React reports the following warning: Warning: Failed context type: The l10n context field must be an instance of ReactLocalization. in Localized (created by NewsletterForm) in label (created by NewsletterForm) in form (created by NewsletterForm) in NewsletterForm (created by Unknown) in Unknown The warning could be silenced by temporarily muting console.warn, or by implementing a wrapper around Enzyme's mount which provides a fake ReactLocalization in the context. Here's what the wrapper could look like: https://github.com/stasm/fluent.js/blob/fluent-enzyme/fluent-enzyme/src/index.js#L35-L49 I also factored the findLocalizedById function out of tests and into the util.js module. There are 7 skipped tests. I wasn't sure what their result HTML should be. commit f6f99898597b073705ff4f30edeacb607e110061 Author: Donovan Preston Date: Thu Jun 29 18:23:33 2017 -0400 Remove embedded html from the rest of app.ftl strings commit 9bcf9253b8e72213d0ed176b97b60dda61d80485 Author: Donovan Preston Date: Thu Jun 29 17:29:53 2017 -0400 Remove some of the html embedded in strings. commit ca044ff661b93283696f4b0f51a2d5b48a653975 Author: Donovan Preston Date: Wed Jun 28 17:39:57 2017 -0400 Remove weird double nested span. commit 796af3bf5d6513c477a058179c4b7d8c5067e3b2 Author: Donovan Preston Date: Wed Jun 28 15:49:19 2017 -0400 Use … commit 2e38e0d9660b65dbdbde285d1139b6da0b61066f Author: Donovan Preston Date: Wed Jun 28 15:48:29 2017 -0400 Remove empty className commit 6fa3622a7e567812aa771b20a8a3543c77ea191e Author: Donovan Preston Date: Wed Jun 28 15:40:42 2017 -0400 Rename commit 96a55315334ad321dd592f3fe433d666312105e9 Author: Donovan Preston Date: Wed Jun 28 11:47:29 2017 -0400 Annotate the correct return type commit 764008094389422e56dc52bbbc7624c4692755f9 Author: Donovan Preston Date: Wed Jun 28 11:45:27 2017 -0400 Quote apostrophe commit e1411821f42d410b0dfc78e240d2ee77277d35fe Author: Donovan Preston Date: Wed Jun 28 11:39:25 2017 -0400 Fix the type of titleL10nArgs commit 6d98fad937766873554d9890a9136f6fc8b68d5a Author: Donovan Preston Date: Wed Jun 28 11:38:58 2017 -0400 Remove copy-pasted junk commit 40354f7f8b8def38a3b5812dbec054af6019fe6f Author: Donovan Preston Date: Wed Jun 28 11:30:07 2017 -0400 Don't try to JSON.parse undefined commit f6f648fbb7d31abf7d1e386f330fef23e5bb7d7d Author: Donovan Preston Date: Wed Jun 28 10:23:27 2017 -0400 Remove trailing whitespace commit 6f88564179d76f2d93743ea364e82e1dc7a740da Author: Donovan Preston Date: Wed Jun 28 10:22:21 2017 -0400 Move the key attributes to the right element. commit 8bd23831b5aa8c53309f7fedffaf8b0eb5e158ca Author: Donovan Preston Date: Wed Jun 28 10:19:10 2017 -0400 Fix typos. commit 36947d014d4590b80869c15fb18a53bcaf7bb009 Author: Donovan Preston Date: Wed Jun 28 10:18:28 2017 -0400 Convert Warning to fluent-react commit c62f623d050befc9865e984d404fb77d3ba9b535 Author: Donovan Preston Date: Wed Jun 28 10:07:55 2017 -0400 Convert UpdateList to fluent-react. commit 9b9eed3f6738e0da776611d44b3f6e8877eea039 Author: Donovan Preston Date: Wed Jun 28 10:05:23 2017 -0400 Convert Settings to fluent-react. commit b321c1fdaee507712888770699b0a6edb5f93159 Author: Donovan Preston Date: Wed Jun 28 09:56:08 2017 -0400 Convert RetireConfirmationDialog to fluent-react. commit 6b74e108b3245110368d22c16fe118b432aa53e9 Author: Donovan Preston Date: Wed Jun 28 09:53:45 2017 -0400 Convert PastExperiments to fluent-react. commit 57553c5a06e576faf5a31f1400a0961a49b6bc8e Author: Donovan Preston Date: Wed Jun 28 09:47:58 2017 -0400 Convert NewsletterForm to fluent-react. commit 7d313f59e90a68df119e678e92c74a390bd8accc Author: Donovan Preston Date: Wed Jun 28 09:43:21 2017 -0400 Convert NewsletterFooter to fluent-react. commit 30e2fb90519754903f4295b32eb7046dcc505619 Author: Donovan Preston Date: Wed Jun 28 09:40:53 2017 -0400 Convert MainInstallButton to fluent-react commit 9871e9045cc0f63363ea11e65d7677244036654c Author: Donovan Preston Date: Wed Jun 28 09:25:59 2017 -0400 Convert Header to fluent-react commit 8521f28144ed989c21210f3ad856ac9712d00b22 Author: Donovan Preston Date: Wed Jun 28 09:25:06 2017 -0400 Convert Footer to fluent-react commit ecbff0d1f4220da68a0cbbd5c7955eecfc0532a4 Author: Donovan Preston Date: Wed Jun 28 09:15:50 2017 -0400 Convert ExperimentTourDialog to fluent-react commit 386306647752c6049cb88cb09044b428c41c893d Author: Donovan Preston Date: Wed Jun 28 09:11:55 2017 -0400 Convert ExperimentRowCard to fluent-react. commit eb2931b238440dce098023403b716b6f071e46bf Author: Donovan Preston Date: Wed Jun 28 09:04:46 2017 -0400 Convert ExperimentPreFeedbackDialog to fluent-react. commit f3ccd94158ba4a95a26b49d187720573a89ca276 Author: Donovan Preston Date: Wed Jun 28 09:02:19 2017 -0400 Convert ExperimentEolDialog to fluent-react commit 9fc04513fa3a7c7cfe76914b54b3d232cd55493f Author: Donovan Preston Date: Wed Jun 28 08:59:48 2017 -0400 Convert ExperimentDisableDialog to fluent-react commit e7ed44e3365c5ffd4a9d8c53163af8dce0d01c13 Author: Donovan Preston Date: Wed Jun 28 08:57:42 2017 -0400 Convert EmailDialog to fluent-react. commit b19989da47bd0163c62b0db52abf88d0280995b5 Author: Donovan Preston Date: Wed Jun 28 08:46:29 2017 -0400 Convert SharePage to fluent-react. commit aeb5e33f2006fabd0ff6e245d704e365afa3d48f Author: Donovan Preston Date: Wed Jun 28 08:43:52 2017 -0400 Convert RetirePage to fluent-react. commit 55f89755f07624c921f4e8a41dc48df5e99bbfdc Author: Donovan Preston Date: Wed Jun 28 08:40:42 2017 -0400 Convert RestartPage to fluent-react. commit 6af6a164dddfc52b49ec6bffddcc2e90e5836840 Author: Donovan Preston Date: Wed Jun 28 08:38:35 2017 -0400 Convert OnboardingPage to fluent-react. commit cbd9b263cca1a3d698660cf95528ddee7acea3c4 Author: Donovan Preston Date: Wed Jun 28 08:37:28 2017 -0400 Convert NotFoundPage to fluent-react commit 97cb105e33f31c2c6fb1d8c22ef613af1f0d6e0e Author: Donovan Preston Date: Wed Jun 28 08:35:09 2017 -0400 Convert HomePageWithAddon to fluent-react commit 0627c32b6f99094793cdcab1d9d6763d46eff371 Author: Donovan Preston Date: Wed Jun 28 08:27:32 2017 -0400 Convert HomePageNoAddon to fluent-react commit ff13e4e9797c874ec81385ea223d3a6aa8847e59 Author: Donovan Preston Date: Tue Jun 27 11:44:30 2017 -0400 Convert ExperimentPage to fluent-react commit 425aea65ce7edebc8891bb1bce8b4104aa060bb3 Author: Donovan Preston Date: Tue Jun 27 10:49:04 2017 -0400 Switch ErrorPage to fluent-react commit a6f742057ba2d8c9cee858815a54f06fdff3fadd Author: Donovan Preston Date: Tue Jun 27 10:39:11 2017 -0400 Switch View over to fluent-react commit 9491234ef579c99db43ab9fe8af47abf456bdde2 Author: Donovan Preston Date: Tue Jun 27 10:38:59 2017 -0400 Remove console.log commit ecc5573435d51178b266254839f9b19072e55554 Author: Donovan Preston Date: Mon Jun 26 21:05:05 2017 -0400 Step 1 get it working commit 4aeafe5bfcd86155f0f3adbbcd174fc513b2269a Merge: aabd2283 a4b561da Author: Donovan Preston Date: Wed Jun 21 09:59:10 2017 -0400 Merge remote-tracking branch 'mozilla/master' commit aabd2283cce08c077f6fa60e2c978e60782205a8 Merge: a697c28b 9c85f55d Author: Donovan Preston Date: Tue Jun 20 11:37:05 2017 -0400 Merge branch 'master' of https://github.com/mozilla/testpilot commit a697c28bc7cda2918b5a4dd54b019b54bbaa416f Merge: 32018a9c 89a6d963 Author: Donovan Preston Date: Thu Jun 8 12:30:16 2017 -0400 Merge remote-tracking branch 'mozilla/master' --- flow-typed/fluent-react/compat.js | 4 + frontend/.flowconfig | 3 + frontend/src/app/actions/localizations.js | 13 + frontend/src/app/components/EmailDialog.js | 39 +- .../app/components/ExperimentDisableDialog.js | 32 +- .../src/app/components/ExperimentEolDialog.js | 13 +- .../components/ExperimentPreFeedbackDialog.js | 22 +- .../src/app/components/ExperimentRowCard.js | 62 +++- .../app/components/ExperimentTourDialog.js | 26 +- frontend/src/app/components/Footer.js | 22 +- frontend/src/app/components/Header.js | 5 +- frontend/src/app/components/LocalizedHtml.js | 14 + .../src/app/components/MainInstallButton.js | 66 +++- .../src/app/components/NewsletterFooter.js | 40 ++- frontend/src/app/components/NewsletterForm.js | 47 ++- .../src/app/components/PastExperiments.js | 19 +- .../components/RetireConfirmationDialog.js | 18 +- frontend/src/app/components/Settings.js | 52 ++- frontend/src/app/components/UpdateList.js | 23 +- frontend/src/app/components/View.js | 56 ++- frontend/src/app/components/Warning.js | 24 +- frontend/src/app/containers/App.js | 55 ++- frontend/src/app/containers/ErrorPage.js | 12 +- frontend/src/app/containers/ExperimentPage.js | 332 ++++++++++++------ .../src/app/containers/HomePageNoAddon.js | 34 +- .../src/app/containers/HomePageWithAddon.js | 56 +-- frontend/src/app/containers/NotFoundPage.js | 9 +- frontend/src/app/containers/OnboardingPage.js | 5 +- frontend/src/app/containers/RestartPage.js | 17 +- frontend/src/app/containers/RetirePage.js | 27 +- frontend/src/app/containers/SharePage.js | 20 +- frontend/src/app/reducers/localizations.js | 37 ++ frontend/src/app/store.js | 2 + frontend/tasks/pages.js | 24 +- frontend/tasks/scripts.js | 8 +- .../test/app/components/EmailDialog-test.js | 10 +- .../ExperimentDisableDialog-test.js | 5 +- .../components/ExperimentEolDialog-test.js | 8 +- .../ExperimentPreFeedbackDialog-test.js | 9 +- .../app/components/ExperimentRowCard-test.js | 36 +- .../components/ExperimentTourDialog-test.js | 18 +- frontend/test/app/components/Header-test.js | 5 +- .../app/components/MainInstallButton-test.js | 15 +- .../RetireConfirmationDialog-test.js | 7 +- .../test/app/components/UpdateList-test.js | 2 +- frontend/test/app/components/Warning-test.js | 4 +- .../test/app/containers/ErrorPage-test.js | 6 +- .../app/containers/ExperimentPage-test.js | 56 +-- .../app/containers/HomePageNoAddon-test.js | 5 +- .../test/app/containers/NotFoundPage-test.js | 7 +- .../test/app/containers/RestartPage-test.js | 12 +- .../test/app/containers/RetirePage-test.js | 22 +- frontend/test/app/util.js | 14 + gulpfile.babel.js | 2 + locales/en-US/app.ftl | 55 +-- package.json | 3 + 56 files changed, 1053 insertions(+), 486 deletions(-) create mode 100644 flow-typed/fluent-react/compat.js create mode 100644 frontend/src/app/actions/localizations.js create mode 100644 frontend/src/app/components/LocalizedHtml.js create mode 100644 frontend/src/app/reducers/localizations.js create mode 100644 frontend/test/app/util.js diff --git a/flow-typed/fluent-react/compat.js b/flow-typed/fluent-react/compat.js new file mode 100644 index 0000000000..8571d7ce5a --- /dev/null +++ b/flow-typed/fluent-react/compat.js @@ -0,0 +1,4 @@ + +declare module 'fluent-react/compat' { + declare function Localized(): Object; +} diff --git a/frontend/.flowconfig b/frontend/.flowconfig index 429f35239f..2f9a67ba3a 100644 --- a/frontend/.flowconfig +++ b/frontend/.flowconfig @@ -6,6 +6,9 @@ ../node_modules/react/*. ../node_modules/html-react-parser/*. ../node_modules/moment/*. +../node_modules/fluent-react/*. [libs] ../flow-typed + + diff --git a/frontend/src/app/actions/localizations.js b/frontend/src/app/actions/localizations.js new file mode 100644 index 0000000000..08b8da0841 --- /dev/null +++ b/frontend/src/app/actions/localizations.js @@ -0,0 +1,13 @@ +// @flow + +import type { + Localizations, + SetLocalizationsAction +} from '../reducers/localizations.js'; + +export function setLocalizations(localizations: Localizations): SetLocalizationsAction { + return { + type: 'SET_LOCALIZATIONS', + payload: localizations + }; +} diff --git a/frontend/src/app/components/EmailDialog.js b/frontend/src/app/components/EmailDialog.js index 324e6b8311..1319ea5875 100644 --- a/frontend/src/app/components/EmailDialog.js +++ b/frontend/src/app/components/EmailDialog.js @@ -1,6 +1,7 @@ // @flow - +import { Localized } from 'fluent-react/compat'; import React from 'react'; + import NewsletterForm from './NewsletterForm'; import { subscribeToBasket } from '../lib/utils'; @@ -49,11 +50,15 @@ export default class EmailDialog extends React.Component { return (
-

Welcome to Test Pilot!

+ +

Welcome to Test Pilot!

+
this.skip(e)}/>
-

Find out about new experiments and see test results for experiments you've tried.

+ +

Find out about new experiments and see test results for experiments you've tried.

+
this.setState({ email: newEmail })} @@ -68,15 +73,21 @@ export default class EmailDialog extends React.Component { return (
-

Thanks!

+ +

Thanks!

+
this.continue(e)} />
-

+ +

Thank you!

+
- + + +
); @@ -86,17 +97,23 @@ export default class EmailDialog extends React.Component { return (
-

Oh no!

+ +

Oh no!

+
this.continue(e)} />
-

- There was an error submitting your email address. Try again? -

+ +

+ There was an error submitting your email address. Try again? +

+
- + + +
); diff --git a/frontend/src/app/components/ExperimentDisableDialog.js b/frontend/src/app/components/ExperimentDisableDialog.js index 3d66e8c002..8ce0c8546d 100644 --- a/frontend/src/app/components/ExperimentDisableDialog.js +++ b/frontend/src/app/components/ExperimentDisableDialog.js @@ -1,5 +1,5 @@ // @flow - +import { Localized } from 'fluent-react/compat'; import React from 'react'; import Copter from './Copter'; @@ -27,25 +27,29 @@ export default class ExperimentDisableDialog extends React.Component {
-

+ +

+
this.cancel(e)} />

-

- Your participation in Firefox Test Pilot means a lot! - Please check out our other experiments, and stay tuned for more to come! -

+ +

+ Your participation in Firefox Test Pilot means a lot! + Please check out our other experiments, and stay tuned for more to come! +

+
diff --git a/frontend/src/app/components/ExperimentEolDialog.js b/frontend/src/app/components/ExperimentEolDialog.js index 686df5048f..466b71fe00 100644 --- a/frontend/src/app/components/ExperimentEolDialog.js +++ b/frontend/src/app/components/ExperimentEolDialog.js @@ -1,5 +1,6 @@ // @flow +import { Localized } from 'fluent-react/compat'; import React from 'react'; type ExperimentEolDialogProps = { @@ -17,15 +18,21 @@ export default class ExperimentEolDialog extends React.Component {
-

Disable Experiment?

+ +

Disable Experiment?

+
this.cancel(e)}/>
-

+ +

+
- + + +
diff --git a/frontend/src/app/components/ExperimentPreFeedbackDialog.js b/frontend/src/app/components/ExperimentPreFeedbackDialog.js index 6cdb1892a4..d6c43a4c99 100644 --- a/frontend/src/app/components/ExperimentPreFeedbackDialog.js +++ b/frontend/src/app/components/ExperimentPreFeedbackDialog.js @@ -1,8 +1,9 @@ // @flow -import React from 'react'; import classnames from 'classnames'; +import { Localized } from 'fluent-react/compat'; import parser from 'html-react-parser'; +import React from 'react'; type ExperimentPreFeedbackDialogProps = { experiment: Object, @@ -17,18 +18,14 @@ export default class ExperimentPreFeedbackDialog extends React.Component { render() { const { experiment, surveyURL } = this.props; - const l10nArgs = JSON.stringify({ - title: experiment.title - }); - return (
-

-
this.cancel(e)}/> + +

+
+
this.cancel(e)}/>
@@ -39,9 +36,10 @@ export default class ExperimentPreFeedbackDialog extends React.Component { {parser(experiment.pre_feedback_copy)}
diff --git a/frontend/src/app/components/ExperimentRowCard.js b/frontend/src/app/components/ExperimentRowCard.js index cdeb6bb359..5d8a4e8de5 100644 --- a/frontend/src/app/components/ExperimentRowCard.js +++ b/frontend/src/app/components/ExperimentRowCard.js @@ -1,7 +1,8 @@ // @flow -import React from 'react'; import classnames from 'classnames'; +import { Localized } from 'fluent-react/compat'; +import React from 'react'; import { buildSurveyURL, experimentL10nId } from '../lib/utils'; @@ -49,9 +50,15 @@ export default class ExperimentRowCard extends React.Component { })} >
- {enabled &&
} - {this.justLaunched() &&
} - {this.justUpdated() &&
} + {enabled && +
+
} + {this.justLaunched() && +
+
} + {this.justUpdated() && +
+
}
@@ -60,12 +67,16 @@ export default class ExperimentRowCard extends React.Component {

{title}

- {subtitle &&

{subtitle}

} + {subtitle && +

{subtitle}

+
}

{this.statusMsg()}

{this.renderFeedbackButton()}
-

{description}

+ +

{description}

+
{ this.renderInstallationCount(installation_count, isCompleted) } { this.renderManageButton(enabled, hasAddon, isCompleted) }
@@ -79,10 +90,11 @@ export default class ExperimentRowCard extends React.Component { // telemetry data coming in from prod renderInstallationCount(installation_count: number, isCompleted: Boolean) { if (installation_count <= 100 || isCompleted) return ''; + const installation_count_node = {installation_count}; return ( - {installation_count} + + {installation_count_node} participants + ); } @@ -94,11 +106,13 @@ export default class ExperimentRowCard extends React.Component { const surveyURL = buildSurveyURL('givefeedback', title, installed, clientUUID, survey_url); return ( ); } @@ -115,15 +129,21 @@ export default class ExperimentRowCard extends React.Component { renderManageButton(enabled: Boolean, hasAddon: Boolean, isCompleted: Boolean) { if (enabled && hasAddon) { return ( -
Manage
+ +
Manage
+
); } else if (isCompleted) { return ( -
Learn More
+ +
Learn More
+
); } return ( -
Get Started
+ +
Get Started
+
); } @@ -171,9 +191,13 @@ export default class ExperimentRowCard extends React.Component { if (delta < 0) { return ''; } else if (delta < ONE_DAY) { - return Ending Tomorrow; + return + Ending Tomorrow + ; } else if (delta < ONE_WEEK) { - return Ending Soon; + return + Ending Soon + ; } } return ''; diff --git a/frontend/src/app/components/ExperimentTourDialog.js b/frontend/src/app/components/ExperimentTourDialog.js index 10a1da2cbd..e462da8586 100644 --- a/frontend/src/app/components/ExperimentTourDialog.js +++ b/frontend/src/app/components/ExperimentTourDialog.js @@ -1,7 +1,8 @@ // @flow -import React from 'react'; import classnames from 'classnames'; +import { Localized } from 'fluent-react/compat'; +import React from 'react'; import { experimentL10nId } from '../lib/utils'; @@ -40,14 +41,10 @@ export default class ExperimentTourDialog extends React.Component { const atStart = (currentStep === 0); const atEnd = (currentStep === tourSteps.length - 1); - const l10nArgs = JSON.stringify({ - title: experiment.title - }); - - const headerTitle = enabled ? (

) : - (

{experiment.title}

); + const headerTitle = enabled ? ( + +

+
) : (

{experiment.title}

); return (
@@ -68,7 +65,9 @@ export default class ExperimentTourDialog extends React.Component {
{step.copy &&
-

{step.copy}

+ +

{step.copy}

+
}
))} @@ -77,9 +76,10 @@ export default class ExperimentTourDialog extends React.Component { className={classnames('tour-back', { hidden: atStart })}>
this.tourNext()} className={classnames('tour-next', { 'no-display': atEnd })}>
-
this.complete(e)} - className={classnames('tour-done', { 'no-display': !atEnd })} - data-l10n-id="tourDoneButton">Done
+ +
this.complete(e)} + className={classnames('tour-done', { 'no-display': !atEnd })}>Done
+
diff --git a/frontend/src/app/components/Footer.js b/frontend/src/app/components/Footer.js index 1e38ab8436..57eb75051e 100644 --- a/frontend/src/app/components/Footer.js +++ b/frontend/src/app/components/Footer.js @@ -1,5 +1,5 @@ // @flow - +import { Localized } from 'fluent-react/compat'; import React from 'react'; import LayoutWrapper from './LayoutWrapper'; @@ -17,11 +17,21 @@ export default class Footer extends React.Component {
this.eventToGA(e)} href="https://github.com/mozilla/testpilot" diff --git a/frontend/src/app/components/Header.js b/frontend/src/app/components/Header.js index 660a7118b9..f471579083 100644 --- a/frontend/src/app/components/Header.js +++ b/frontend/src/app/components/Header.js @@ -1,5 +1,6 @@ // @flow +import { Localized } from 'fluent-react/compat'; import React from 'react'; import LayoutWrapper from './LayoutWrapper'; @@ -97,7 +98,9 @@ export default class Header extends React.Component {

- Firefox Test Pilot + + Firefox Test Pilot +

{this.renderSettingsMenu()} diff --git a/frontend/src/app/components/LocalizedHtml.js b/frontend/src/app/components/LocalizedHtml.js new file mode 100644 index 0000000000..58d967a8e1 --- /dev/null +++ b/frontend/src/app/components/LocalizedHtml.js @@ -0,0 +1,14 @@ + +import { Localized } from 'fluent-react/compat'; +import parser from 'html-react-parser'; +import React from 'react'; + +export default class LocalizedHtml extends Localized { + render() { + const result = super.render(); + if (typeof result.props.children === 'string') { + return React.cloneElement(result, { children: parser(result.props.children) }); + } + return result; + } +} diff --git a/frontend/src/app/components/MainInstallButton.js b/frontend/src/app/components/MainInstallButton.js index d99ebcd87d..0cd14ba16e 100644 --- a/frontend/src/app/components/MainInstallButton.js +++ b/frontend/src/app/components/MainInstallButton.js @@ -1,8 +1,9 @@ -import React from 'react'; import classnames from 'classnames'; -import { VariantTests, VariantTestCase, VariantTestDefault } from './VariantTests'; +import { Localized } from 'fluent-react/compat'; +import React from 'react'; import LayoutWrapper from './LayoutWrapper'; +import { VariantTests, VariantTestCase, VariantTestDefault } from './VariantTests'; import config from '../config'; @@ -34,12 +35,21 @@ export default class MainInstallButton extends React.Component { render() { const { isFirefox, isMinFirefox, isMobile, hasAddon } = this.props; const isInstalling = this.state.isInstalling && !hasAddon; + const terms = + Terms of Use + ; + const privacy = + Privacy Notice + ; return ( {(isMinFirefox && !isMobile) ? this.renderInstallButton(isInstalling, hasAddon) : this.renderAltButton(isFirefox, isMobile) } - {isMinFirefox && !isMobile &&

By proceeding, - you agree to the Terms of Use and Privacy Notice of Test Pilot.

} + {isMinFirefox && !isMobile && +

+ By proceeding, you agree to the {terms} and {privacy} of Test Pilot. +

+
}
); } @@ -47,8 +57,12 @@ export default class MainInstallButton extends React.Component { renderOneClickInstallButton(title) { return (
- Install Test Pilot & - + + Install Test Pilot & + + + Enable {title} +
); } @@ -59,17 +73,23 @@ export default class MainInstallButton extends React.Component { if (experimentTitle) { installButton = this.renderOneClickInstallButton(experimentTitle); } else { - installButton = - Install the Test Pilot Add-on - ; + installButton = + + Install the Test Pilot Add-on + + ; } const makeInstallButton = (extraClass = '') => { return ; }; @@ -88,16 +108,22 @@ export default class MainInstallButton extends React.Component { if (isFirefox && isMobile) { return (
- Test Pilot requires Firefox for Desktop on Windows, Mac or Linux + + Test Pilot requires Firefox for Desktop on Windows, Mac or Linux +
); } return (
{!isFirefox ? ( - (Test Pilot is available for Firefox on Windows, OS X and Linux) + + (Test Pilot is available for Firefox on Windows, OS X and Linux) + ) : ( - Test Pilot requires Firefox { config.minFirefoxVersion } or higher. + + Test Pilot requires Firefox { config.minFirefoxVersion } or higher. + ) } {!isMobile && @@ -106,12 +132,18 @@ export default class MainInstallButton extends React.Component {
{(!isFirefox) ? ( -
Firefox
+ +
Firefox
+
) : ( -
Upgrade Firefox
+ +
Upgrade Firefox
+
) } -
Free Download
+ +
Free Download
+
}
diff --git a/frontend/src/app/components/NewsletterFooter.js b/frontend/src/app/components/NewsletterFooter.js index d9a073f888..b7487ae497 100644 --- a/frontend/src/app/components/NewsletterFooter.js +++ b/frontend/src/app/components/NewsletterFooter.js @@ -1,6 +1,6 @@ // @flow - import classnames from 'classnames'; +import { Localized } from 'fluent-react/compat'; import React from 'react'; import LayoutWrapper from './LayoutWrapper'; @@ -20,9 +20,11 @@ export default class NewsletterFooter extends React.Component { renderError() { if (this.props.newsletterForm.failed) { return ( -
- There was an error submitting your email address. Try again? -
+ +
+ There was an error submitting your email address. Try again? +
+
); } return null; @@ -31,12 +33,16 @@ export default class NewsletterFooter extends React.Component { renderSuccess() { return (
-

Thanks!

-

- If you haven't previously confirmed a subscription to a Mozilla-related - newsletter you may have to do so. Please check your inbox or your spam - filter for an email from us. -

+ +

Thanks!

+
+ +

+ If you haven't previously confirmed a subscription to a Mozilla-related + newsletter you may have to do so. Please check your inbox or your spam + filter for an email from us. +

+
); } @@ -48,11 +54,15 @@ export default class NewsletterFooter extends React.Component { return (
-

Stay Informed

-

- Find out about new experiments and see test results for experiments - you've tried. -

+ +

Stay Informed

+
+ +

+ Find out about new experiments and see test results for experiments + you've tried. +

+
); } diff --git a/frontend/src/app/components/NewsletterForm.js b/frontend/src/app/components/NewsletterForm.js index 16acd50239..ce7506b731 100644 --- a/frontend/src/app/components/NewsletterForm.js +++ b/frontend/src/app/components/NewsletterForm.js @@ -1,6 +1,7 @@ // @flow import classnames from 'classnames'; +import { Localized } from 'fluent-react/compat'; import React from 'react'; import { defaultState } from '../reducers/newsletter-form'; @@ -53,7 +54,7 @@ export default class NewsletterForm extends React.Component { return ( + + this privacy notice + + ; return ( ); } @@ -90,21 +97,25 @@ export default class NewsletterForm extends React.Component { renderSubmitButton() { if (this.props.submitting) { return ( - + + + ); } - return ; + return + + ; } renderDisclaimer() { return ( -

- We will only send you Test Pilot-related information. -

+ +

+ We will only send you Test Pilot-related information. +

+
); } diff --git a/frontend/src/app/components/PastExperiments.js b/frontend/src/app/components/PastExperiments.js index 568d6e9118..980f300936 100644 --- a/frontend/src/app/components/PastExperiments.js +++ b/frontend/src/app/components/PastExperiments.js @@ -1,6 +1,7 @@ // @flow import classnames from 'classnames'; +import { Localized } from 'fluent-react/compat'; import React from 'react'; import ExperimentCardList from '../components/ExperimentCardList'; @@ -33,14 +34,20 @@ export default class PastExperiments extends React.Component { return ( {pastExperiments.length > 0 && !showPastExperiments && -
this.setState({ showPastExperiments: true })} - data-l10n-id="viewPastExperiments">View Past Experiments
} + +
this.setState({ showPastExperiments: true })}> + View Past Experiments +
+
} {showPastExperiments &&
-
this.setState({ showPastExperiments: false })} - data-l10n-id="hidePastExperiments">Hide Past Experiments
+ +
this.setState({ showPastExperiments: false })}> + Hide Past Experiments +
+
} diff --git a/frontend/src/app/components/RetireConfirmationDialog.js b/frontend/src/app/components/RetireConfirmationDialog.js index b27898e4f9..778bcc74b6 100644 --- a/frontend/src/app/components/RetireConfirmationDialog.js +++ b/frontend/src/app/components/RetireConfirmationDialog.js @@ -1,5 +1,5 @@ // @flow - +import { Localized } from 'fluent-react/compat'; import React from 'react'; type RetireConfirmationDialogProps = { @@ -17,17 +17,25 @@ export default class RetireConfirmationDialog extends React.Component {
-

Uninstall Test Pilot?

+ +

Uninstall Test Pilot?

+
this.cancel(e)}/>
-

As you wish. This will disable any active tests, uninstall the Test Pilot add-on, and remove your account information from our servers.

-

To opt out of email updates, simply click the unsubscribe link on any Test Pilot email.

+ +

As you wish. This will disable any active tests, uninstall the Test Pilot add-on, and remove your account information from our servers.

+
+ +

To opt out of email updates, simply click the unsubscribe link on any Test Pilot email.

+
- + + +
diff --git a/frontend/src/app/components/Settings.js b/frontend/src/app/components/Settings.js index 194cf95b20..8829fccece 100644 --- a/frontend/src/app/components/Settings.js +++ b/frontend/src/app/components/Settings.js @@ -1,7 +1,8 @@ // @flow -import React from 'react'; import classnames from 'classnames'; +import { Localized } from 'fluent-react/compat'; +import React from 'react'; type SettingsProps = { hasAddon: any, @@ -77,31 +78,46 @@ export default class Settings extends React.Component { return (
-
this.toggleSettings(e)} - data-l10n-id="menuTitle"> - Settings -
+ +
this.toggleSettings(e)}> + Settings +
+
{ this.showSettingsMenu() &&
this.settingsClick()}>
} diff --git a/frontend/src/app/components/UpdateList.js b/frontend/src/app/components/UpdateList.js index ab4d2ad734..02260104ff 100644 --- a/frontend/src/app/components/UpdateList.js +++ b/frontend/src/app/components/UpdateList.js @@ -1,8 +1,10 @@ // @flow -import React from 'react'; import classnames from 'classnames'; +import { Localized } from 'fluent-react/compat'; import moment from 'moment'; +import React from 'react'; + import LayoutWrapper from './LayoutWrapper'; import { newsUpdateL10nId } from '../lib/utils'; @@ -32,12 +34,19 @@ export class Update extends React.Component {
- {experiment ?

{categoryTitle}

: -

{title}

} + {experiment ? +

{categoryTitle}

+
: +

{title}

+
}

{prettyDate(published || created)}

- {experiment ?

{title}

: null} -

{content}

+ {experiment ? +

{title}

+
: null} + +

{content}

+
  @@ -67,7 +76,9 @@ export default class UpdateList extends React.Component { return (
-

Latest Updates

+ +

Latest Updates

+
{newsUpdates.map((update, index) => Something is wrong!; - let copy =

Something has gone wrong with Test Pilot. Please file a bug and mention this error message.

; + let title = + Something is wrong! + ; + let link = + file a bug + ; + let copy = +

Something has gone wrong with Test Pilot. Please {link} and mention this error message.

+
; if (!this.props.isMinFirefox) { - title = Upgrade Firefox to continue!; - copy =

Test Pilot requires the latest version of Firefox. Upgrade Firefox to get started.

; + title = + Upgrade Firefox to continue! + ; + link = + Upgrade Firefox + ; + copy = +

Test Pilot reqires the latest version of Firefox. {link} to get started.

+
; } else if (window.location.protocol !== 'https:') { - title = HTTPS required!; - copy =

Test Pilot must be accessed over HTTPS. Please see our documentation for details.

; + title = + HTTPS required! + ; + link = + our documentation + ; + copy = +

Test Pilot must be accessed over HTTPS. Please see {link} for details.

+
; } else if (['example.com:8000', 'testpilot.dev.mozaws.net', 'testpilot.stage.mozaws.net'].includes(window.location.host)) { - title = Developing Test Pilot?; - copy =

When running Test Pilot locally or in development environments, special configuration is required. Please see our documentation for details.

; + title = + Developing Test Pilot? + ; + link = + our documentation + ; + copy = +

When running Test Pilot locally or in development environments, special configuration is required. Please see {link} for details.

+
; } else if (window.location.host !== 'testpilot.firefox.com') { - title = Unapproved hostname!; - copy =

The Test Pilot site may only be accessed from testpilot.firefox.com, testpilot.stage.mozaws.net, testpilot.dev.mozaws.net, or example.com:8000. Please see our documentation for details.

; + title = + Unapproved hostname! + ; + link = + our documentation + ; + copy = +

The Test Pilot site may only be accessed from testpilot.firefox.com, testpilot.stage.mozaws.net, testpilot.dev.mozaws.net, or example.com:8000. Please see {link} for details.

+
; } return
diff --git a/frontend/src/app/components/Warning.js b/frontend/src/app/components/Warning.js index 818b4cb88b..b8e7141341 100644 --- a/frontend/src/app/components/Warning.js +++ b/frontend/src/app/components/Warning.js @@ -1,14 +1,14 @@ // @flow - +import { Localized } from 'fluent-react/compat'; import React from 'react'; type WarningProps = { title: string, titleL10nId: string, - titleL10nArgs: Array, + titleL10nArgs: string, subtitle: string, subtitleL10nId: string, - subtitleL10nArgs: Array, + subtitleL10nArgs: string, children: Array } @@ -17,8 +17,15 @@ export default class Warning extends React.Component { renderSubtitle() { if (this.props.subtitle) { + const parsed = this.props.subtitleL10nArgs ? JSON.parse(this.props.subtitleL10nArgs) : {}; + const args = {}; + Object.keys(parsed).map(key => { + return args[`$${key}`] = parsed[key]; + }); return ( -

{this.props.subtitle}

+ +

{this.props.subtitle}

+
); } return null; @@ -26,9 +33,16 @@ export default class Warning extends React.Component { renderHeader() { if (this.props.title) { + const parsed = this.props.titleL10nArgs ? JSON.parse(this.props.titleL10nArgs) : {}; + const args = {}; + Object.keys(parsed).map(key => { + return args[`$${key}`] = parsed[key]; + }); return (
-

{this.props.title}

+ +

{this.props.title}

+
{this.renderSubtitle()}
); diff --git a/frontend/src/app/containers/App.js b/frontend/src/app/containers/App.js index 958af64e7f..4c641d525b 100644 --- a/frontend/src/app/containers/App.js +++ b/frontend/src/app/containers/App.js @@ -1,4 +1,7 @@ /* global ga */ +import { MessageContext } from 'fluent/compat'; +import negotiateLanguages from 'fluent-langneg/compat'; +import { LocalizationProvider } from 'fluent-react/compat'; import React, { Component } from 'react'; import { connect } from 'react-redux'; @@ -13,10 +16,12 @@ import { getChosenTest } from '../reducers/varianttests'; import experimentSelector from '../selectors/experiment'; import { uninstallAddon, installAddon, enableExperiment, disableExperiment, pollAddon } from '../lib/InstallManager'; import { fetchUserCounts } from '../actions/experiments'; +import { setLocalizations } from '../actions/localizations'; import { chooseTests } from '../actions/varianttests'; import addonActions from '../actions/addon'; import newsletterFormActions from '../actions/newsletter-form'; import RestartPage from '../containers/RestartPage'; +import Loading from '../components/Loading'; import { isFirefox, isMinFirefoxVersion, isMobile } from '../lib/utils'; import newsUpdatesSelector from '../selectors/news'; import config from '../config'; @@ -100,6 +105,32 @@ class App extends Component { } }); this.measurePageview(); + + const langs = {}; + + function addLang(lang, response) { + if (response.ok) { + response.text().then(data => { + langs[lang] = `${langs[lang] || ''}${data} +`; + }); + } + } + + const negotiated = negotiateLanguages( + navigator.languages, + config.AVAILABLE_LOCALES, + { defaultLocale: 'en-US' } + ); + + Promise.all(negotiated.map(language => + Promise.all( + [ + fetch(`/static/locales/${language}/app.ftl`).then(response => addLang(language, response)), + fetch(`/static/locales/${language}/experiments.ftl`).then(response => addLang(language, response)) + ] + ) + )).then(() => this.props.setLocalizations(langs)); } render() { @@ -107,7 +138,24 @@ class App extends Component { if (restart.isRequired) { return ; } - return React.cloneElement(this.props.children, this.props); + + function* generateMessages(languages, localizations) { + for (const lang of languages) { + const cx = new MessageContext(lang); + cx.addMessages(localizations[lang]); + yield cx; + } + } + + if (Object.keys(this.props.localizations).length === 0) { + return ; + } + return + { React.cloneElement(this.props.children, this.props) } + ; } } @@ -133,6 +181,7 @@ function sendToGA(type, dataIn) { const mapStateToProps = state => ({ addon: state.addon, experiments: experimentSelector(state), + localizations: state.localizations, newsUpdates: newsUpdatesSelector(state), slug: state.experiments.slug, getExperimentBySlug: slug => @@ -178,7 +227,9 @@ const mapDispatchToProps = dispatch => ({ dispatch(newsletterFormActions.newsletterFormSetPrivacy(privacy)), subscribe: (email) => dispatch(newsletterFormActions.newsletterFormSubscribe(dispatch, email, '' + window.location)) - } + }, + setLocalizations: localizations => + dispatch(setLocalizations(localizations)) }); diff --git a/frontend/src/app/containers/ErrorPage.js b/frontend/src/app/containers/ErrorPage.js index 43088874c5..09380bb6b5 100644 --- a/frontend/src/app/containers/ErrorPage.js +++ b/frontend/src/app/containers/ErrorPage.js @@ -1,5 +1,6 @@ // @flow +import { Localized } from 'fluent-react/compat'; import React from 'react'; import Copter from '../components/Copter'; @@ -22,10 +23,17 @@ export default class ErrorPage extends React.Component {
-

Whoops!

+ +

Whoops!

+
-

Looks like we broke something.
Maybe try again later.

+ +

Looks like we broke something.

+
+ +

Maybe try again later.

+
diff --git a/frontend/src/app/containers/ExperimentPage.js b/frontend/src/app/containers/ExperimentPage.js index 66a39c2018..05633cf43b 100644 --- a/frontend/src/app/containers/ExperimentPage.js +++ b/frontend/src/app/containers/ExperimentPage.js @@ -1,3 +1,4 @@ +import { Localized } from 'fluent-react/compat'; import React from 'react'; import moment from 'moment'; @@ -12,6 +13,7 @@ import EmailDialog from '../components/EmailDialog'; import ExperimentDisableDialog from '../components/ExperimentDisableDialog'; import ExperimentEolDialog from '../components/ExperimentEolDialog'; import ExperimentTourDialog from '../components/ExperimentTourDialog'; +import LocalizedHtml from '../components/LocalizedHtml'; import MainInstallButton from '../components/MainInstallButton'; import ExperimentCardList from '../components/ExperimentCardList'; import ExperimentPreFeedbackDialog from '../components/ExperimentPreFeedbackDialog'; @@ -142,16 +144,23 @@ export class ExperimentDetail extends React.Component { if (installed.length === 0) return null; const helpUrl = 'https://support.mozilla.org/kb/disable-or-remove-add-ons'; + const disableLink = + disabling these add-ons + ; return (
-

- This experiment may not be compatible with add-ons you have installed. -

-

- We recommend disabling these add-ons before activating this experiment: -

+ +

+ This experiment may not be compatible with add-ons you have installed. +

+
+ +

+ We recommend {disableLink} before activating this experiment: +

+
    @@ -189,14 +198,17 @@ export class ExperimentDetail extends React.Component { -
    - {parser(experiment.eol_warning)} -
    + +
    + {parser(experiment.eol_warning)} +
    +
    - - Learn more - + + + Learn more + + ); } @@ -286,9 +298,13 @@ export class ExperimentDetail extends React.Component {

    - Ready for Takeoff? + + Ready for Takeoff? +

    -

    + +

    +
    @@ -307,13 +323,19 @@ export class ExperimentDetail extends React.Component { }) }>
    - {(statusType === 'enabled') && } - {(statusType === 'error') && } + {(statusType === 'enabled') && + {title} is enabled. + } + {(statusType === 'error') && + + }

    {title}

    - {subtitle &&

    {subtitle}

    } + {subtitle && +

    {subtitle}

    +
    }
    { this.renderExperimentControls() } { this.renderMinimumVersionNotice(title, hasAddon, min_release) } @@ -333,61 +355,90 @@ export class ExperimentDetail extends React.Component { { this.renderInstallationCount() }
{!hasAddon &&
- {!!introduction &&
- {!!warning &&
{warning}
} - {!graduated &&
- {parser(introduction)} -
} + {!!introduction &&
+ {!!warning &&
+ + {warning} + +
} + {!graduated && +
+ {parser(introduction)} +
+
}
}
} {!graduated &&
{hasAddon && - - + + + + } {changelog_url && - + + + } - + + + - + + + - + + +
Tour this.showTour(e)} href="#">Launch TourTour + + this.showTour(e)} href="#">Launch Tour + +
ChangelogChangelog {changelog_url && {changelog_url}}
ContributeContribute {contribute_url}
Bug ReportsBug Reports {bug_report_url}
{discourse_url}
}
-

Brought to you by

+ +

Brought to you by

+
    {contributors.map((contributor, idx) => (
  • {contributor.display_name}

    - {contributor.title &&

    {contributor.title}

    } + {contributor.title && +

    {contributor.title}

    +
    }
  • ))}
{contributors_extra &&

- {contributors_extra} + + {contributors_extra} + {contributors_extra_url &&   - Learn more. - } + + Learn more + + . + }

}
@@ -395,22 +446,35 @@ export class ExperimentDetail extends React.Component {
{measurements &&
-

Your privacy

+ +

Your privacy

+
- {privacy_preamble &&

{privacy_preamble}

} -

- -

+ {privacy_preamble && +

{privacy_preamble}

+
} + + + data + + } + $experimentTitle={experiment.title}> +

+ In addition to the data collected by all Test Pilot experiments, here are the key things you should know about what is happening when you use {experiment.title}: +

+
    - {measurements.map((note, idx) => ( -
  • { + {measurements.map((note, idx) => +
  • { EXPERIMENT_MEASUREMENT_URLS[idx] === null ? null : }
  • - ))} + )}
- {privacy_notice_url && } + {privacy_notice_url && + + }
}
} @@ -423,10 +487,16 @@ export class ExperimentDetail extends React.Component { {this.renderLocaleWarning()} {hasAddon &&
{!!introduction &&
- {!!warning &&
{warning}
} -
- {parser(introduction)} -
+ {!!warning &&
+ + {warning} + +
} + +
+ {parser(introduction)} +
+
}
}
@@ -435,10 +505,14 @@ export class ExperimentDetail extends React.Component {

- {detail.headline && {detail.headline}} - {detail.copy && - {parser(detail.copy)} - } + {detail.headline && + {detail.headline} + } + {detail.copy && + + {parser(detail.copy)} + + }

@@ -447,23 +521,37 @@ export class ExperimentDetail extends React.Component { {hasAddon &&
{measurements &&
-

Your privacy

+ +

Your privacy

+
- {privacy_preamble &&

{privacy_preamble}

} -

- In addition to the data collected by all Test Pilot experiments, here are the - key things you should know about what is happening when you use {experiment.title}: -

+ {privacy_preamble && +

{privacy_preamble}

+
} + + + data + + } + $experimentTitle={experiment.title}> +

+ In addition to the asdf collected by all Test Pilot experiments, here are the key things you should know about what is happening when you use {experiment.title}: +

+
    {measurements.map((note, idx) => ( -
  • { - EXPERIMENT_MEASUREMENT_URLS[idx] === null ? null : - }
  • + +
  • { + EXPERIMENT_MEASUREMENT_URLS[idx] === null ? null : + }
  • +
    ))}
- {privacy_notice_url && } + {privacy_notice_url && + + }
}
}
} @@ -478,21 +566,29 @@ export class ExperimentDetail extends React.Component {
-
We are working on a full report. Check back soon for the details.
+ +
We are working on a full report. Check back soon for the details.
+
-
- {parser(introduction)} -
+ +
+ {parser(introduction)} +
+
{details.map((detail, idx) => (

- {detail.headline && {detail.headline}} - {detail.copy && - {parser(detail.copy)} - } + {detail.headline && + {detail.headline} + } + {detail.copy && + + {parser(detail.copy)} + + }

@@ -506,7 +602,9 @@ export class ExperimentDetail extends React.Component {
-

Try out these experiments as well

+ +

Try out these experiments as well

+
{installation_count}; if (isAfterCompletedDate(experiment)) { - const completedDate = formatDate(completed); - return ( - - ); + const completedDate = + {formatDate(completed)} + ; + return + Experiment End Date: {completedDate} + ; } if (!installation_count || installation_count <= 100) { - return ( - - ); + return + Just launched! + ; } - return ( - // Note: this doesn't include the text content because of a conflict - // in how l20n and react modify the dom. - // https://github.com/mozilla/testpilot/pull/1712 - - ); + return + There are {installation_count_node} people trying {title} right now! + ; } maxVersionCheck(max) { @@ -630,8 +728,12 @@ export class ExperimentDetail extends React.Component { if (hasAddon && !this.minVersionCheck(min_release)) { return ( ); } @@ -642,8 +744,12 @@ export class ExperimentDetail extends React.Component { if (hasAddon && !this.maxVersionCheck(max_release)) { return ( ); } @@ -664,7 +770,15 @@ export class ExperimentDetail extends React.Component { if (enabled) { return (
- +
); } @@ -672,21 +786,41 @@ export class ExperimentDetail extends React.Component { } if (installed && installed[experiment.addon_id] && installed[experiment.addon_id].manuallyDisabled) { return
- +
; } if (enabled) { - return ( -
- this.handleFeedback(e)} data-l10n-id="giveFeedback" id="feedback-button" className="button default" href={surveyURL} target="_blank" rel="noopener noreferrer">Give Feedback - -
- ); + return
+ + this.handleFeedback(e)} id="feedback-button" className="button default" href={surveyURL} target="_blank" rel="noopener noreferrer">Give Feedback + + +
; } return (
- this.highlightPrivacy(e)} className="highlight-privacy" data-l10n-id="highlightPrivacy">Your privacy - + + this.highlightPrivacy(e)} className="highlight-privacy">Your privacy + +
); } diff --git a/frontend/src/app/containers/HomePageNoAddon.js b/frontend/src/app/containers/HomePageNoAddon.js index f84b0a1b92..e282d446d5 100644 --- a/frontend/src/app/containers/HomePageNoAddon.js +++ b/frontend/src/app/containers/HomePageNoAddon.js @@ -1,5 +1,5 @@ // @flow - +import { Localized } from 'fluent-react/compat'; import React from 'react'; import Banner from '../components/Banner'; @@ -38,9 +38,15 @@ export default class HomePageNoAddon extends React.Component {
-

Test new features.

-

Give your feedback.

-

Help build Firefox.

+ +

Test new features.

+
+ +

Give your feedback.

+
+ +

Help build Firefox.

+
@@ -59,26 +65,36 @@ export default class HomePageNoAddon extends React.Component { -

Try out the latest experimental features

+ +

Try out the latest experimental features

+
-

Get started in 3 easy steps

+ +

Get started in 3 easy steps

+
-
Get the Test Pilot add-on
+ +
Get the Test Pilot add-on
+
-
Enable experimental features
+ +
Enable experimental features
+
-
Tell us what you think
+ +
Tell us what you think
+
diff --git a/frontend/src/app/containers/HomePageWithAddon.js b/frontend/src/app/containers/HomePageWithAddon.js index 14379d804a..2a5f04cd47 100644 --- a/frontend/src/app/containers/HomePageWithAddon.js +++ b/frontend/src/app/containers/HomePageWithAddon.js @@ -2,8 +2,9 @@ // @flow -import React from 'react'; import classnames from 'classnames'; +import { Localized } from 'fluent-react/compat'; +import React from 'react'; import Banner from '../components/Banner'; import Copter from '../components/Copter'; @@ -64,25 +65,34 @@ export default class HomePageWithAddon extends React.Component { renderSplash() { if (typeof window !== 'undefined' && window.location.search.includes('utm_content=no-experiments-installed')) { + const link = + this.onNotInterestedSurveyClick()} + href="https://qsurvey.mozilla.com/s3/TxP-User" target="_blank" + className="banner__link"> + Let us know why + + ; return ( -
-

- Let's get this baby off the ground! -

-

- Ready to try a new Test Pilot experiment? Select one to enable, take - it for a spin, and let us know what you think. -

-

- Not interested? - this.onNotInterestedSurveyClick()} - href="https://qsurvey.mozilla.com/s3/TxP-User" target="_blank" - className="banner__link"> - Let us know why - . -

+
+ +

+ Let's get this baby off the ground! +

+
+ +

+ Ready to try a new Test Pilot experiment? Select one to enable, take + it for a spin, and let us know what you think. +

+
+ +

+ Not interested? + {link}. +

+
@@ -92,9 +102,11 @@ export default class HomePageWithAddon extends React.Component { return ( -

- Welcome to Test Pilot! -

+ +

+ Welcome to Test Pilot! +

+
@@ -120,7 +132,9 @@ export default class HomePageWithAddon extends React.Component { -

Pick your experiments!

+ +

Pick your experiments!

+
diff --git a/frontend/src/app/containers/NotFoundPage.js b/frontend/src/app/containers/NotFoundPage.js index 2a5f31b88b..47a4b38768 100644 --- a/frontend/src/app/containers/NotFoundPage.js +++ b/frontend/src/app/containers/NotFoundPage.js @@ -1,3 +1,4 @@ +import { Localized } from 'fluent-react/compat'; import React from 'react'; import Copter from '../components/Copter'; @@ -12,10 +13,14 @@ export default class NotFoundPage extends React.Component { {...this.props}>
-

Four Oh Four!

+ +

Four Oh Four!

+

- Home + + Home +
diff --git a/frontend/src/app/containers/OnboardingPage.js b/frontend/src/app/containers/OnboardingPage.js index 72e45d7b23..cfb118cf97 100644 --- a/frontend/src/app/containers/OnboardingPage.js +++ b/frontend/src/app/containers/OnboardingPage.js @@ -1,3 +1,4 @@ +import { Localized } from 'fluent-react/compat'; import React from 'react'; import Copter from '../components/Copter'; @@ -13,7 +14,9 @@ export default class OnboardingPage extends React.Component {
-

We put an icon in your toolbar so you can always find Test Pilot.

+ +

We put an icon in your toolbar so you can always find Test Pilot.

+
diff --git a/frontend/src/app/containers/RestartPage.js b/frontend/src/app/containers/RestartPage.js index fcf526da55..9919ed2782 100644 --- a/frontend/src/app/containers/RestartPage.js +++ b/frontend/src/app/containers/RestartPage.js @@ -1,5 +1,6 @@ // @flow +import { Localized } from 'fluent-react/compat'; import React from 'react'; import Banner from '../components/Banner'; @@ -35,11 +36,19 @@ export default class Restart extends React.Component {
- Preflight checklist + + Preflight checklist +
    -
  1. Restart your browser
  2. -
  3. Locate the Test Pilot add-on
  4. -
  5. Select your experiments
  6. + +
  7. Restart your browser
  8. +
    + +
  9. Locate the Test Pilot add-on
  10. +
    + +
  11. Select your experiments
  12. +
diff --git a/frontend/src/app/containers/RetirePage.js b/frontend/src/app/containers/RetirePage.js index 417df3d400..da3ab86639 100644 --- a/frontend/src/app/containers/RetirePage.js +++ b/frontend/src/app/containers/RetirePage.js @@ -1,7 +1,7 @@ // @flow - -import React from 'react'; import classnames from 'classnames'; +import { Localized } from 'fluent-react/compat'; +import React from 'react'; import Copter from '../components/Copter'; import LayoutWrapper from '../components/LayoutWrapper'; @@ -60,20 +60,33 @@ export default class RetirePage extends React.Component { {!uninstalled &&
-

Shutting down...

+ +

Shutting down...

+
 
} {uninstalled && diff --git a/frontend/src/app/containers/SharePage.js b/frontend/src/app/containers/SharePage.js index b21fd7dd0e..0859bfbbfc 100644 --- a/frontend/src/app/containers/SharePage.js +++ b/frontend/src/app/containers/SharePage.js @@ -1,5 +1,5 @@ // @flow - +import { Localized } from 'fluent-react/compat'; import React from 'react'; import Copter from '../components/Copter'; @@ -23,17 +23,27 @@ export default class SharePage extends React.Component {
-

Love Test Pilot? Help us find some new recruits.

+ +

Love Test Pilot? Help us find some new recruits.

+
-

or just copy and paste this link...

+ +

or just copy and paste this link...

+
- + + +
diff --git a/frontend/src/app/reducers/localizations.js b/frontend/src/app/reducers/localizations.js new file mode 100644 index 0000000000..b0e3e0c802 --- /dev/null +++ b/frontend/src/app/reducers/localizations.js @@ -0,0 +1,37 @@ +// @flow + +export type Localizations = { + [language: string]: string +}; + +function defaultState(): Localizations { + return { + }; +} + +export type SetLocalizationsAction = { + type: 'SET_LOCALIZATIONS', + payload: Localizations +}; + +function setLocalizations( + state: Localizations, + action: SetLocalizationsAction +): Localizations { + return action.payload; +} + +export default function localizationsReducer( + state: ?Localizations, + action: SetLocalizationsAction +): Localizations { + if (!state) { + return defaultState(); + } + switch (action.type) { + case 'SET_LOCALIZATIONS': + return setLocalizations(state, action); + default: + return state; + } +} diff --git a/frontend/src/app/store.js b/frontend/src/app/store.js index 0e0622ec0a..da77c480e9 100644 --- a/frontend/src/app/store.js +++ b/frontend/src/app/store.js @@ -4,6 +4,7 @@ import promise from 'redux-promise'; import addonReducer from './reducers/addon'; import browserReducer from './reducers/browser'; import experimentsReducer from './reducers/experiments'; +import localizationsReducer from './reducers/localizations'; import newsletterFormReducer from './reducers/newsletter-form'; import varianttestsReducer from './reducers/varianttests'; import newsReducer from './reducers/news'; @@ -15,6 +16,7 @@ export const reducers = combineReducers({ addon: addonReducer, browser: browserReducer, experiments: experimentsReducer, + localizations: localizationsReducer, newsletterForm: newsletterFormReducer, varianttests: varianttestsReducer, news: newsReducer diff --git a/frontend/tasks/pages.js b/frontend/tasks/pages.js index 7715dd1918..26346eb90e 100644 --- a/frontend/tasks/pages.js +++ b/frontend/tasks/pages.js @@ -201,16 +201,13 @@ function generateStaticPage(prepareForClient, pageName, pageParam, component, { - - - - { meta_title } + { meta_title } @@ -228,12 +225,12 @@ function generateStaticPage(prepareForClient, pageName, pageParam, component, {
-

Uh oh...

+

Uh oh...

-

Test Pilot requires JavaScript.
Sorry about that.

+

Test Pilot requires JavaScript.
Sorry about that.

@@ -244,7 +241,6 @@ function generateStaticPage(prepareForClient, pageName, pageParam, component, { { prepareForClient ? : null } { prepareForClient ? : null } - { enable_pontoon ? : null }
; return makeStaticString(prepareForClient, pageName, pageParam, headComponent, bodyComponent, component); @@ -254,7 +250,7 @@ function generateStaticPageFromMarkdown(pageName, pageParam, markdown, params) { const body =

- Firefox Test Pilot + Firefox Test Pilot

@@ -265,11 +261,11 @@ function generateStaticPageFromMarkdown(pageName, pageParam, markdown, params) {