diff --git a/.babelrc b/.babelrc index ffd1d119ff3..b86024af019 100644 --- a/.babelrc +++ b/.babelrc @@ -5,6 +5,7 @@ "react" ], "plugins": [ + "babel-plugin-dedent", "transform-class-properties", "transform-es2015-modules-commonjs" ] diff --git a/.eslintrc b/.eslintrc index 59cb6f27b7f..e50622f9bfd 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,6 +4,7 @@ }, "extends": "airbnb", "globals": { + "dedent", "CLIENT_CONFIG": true, "assert": true, "sinon": true, diff --git a/README.md b/README.md index f6d98c7c86f..a25fe88cb16 100644 --- a/README.md +++ b/README.md @@ -22,19 +22,20 @@ nvm. See https://github.com/creationix/nvm for more info. ## NPM scripts -| Script | Description | -|------------------------|-------------------------------------------------------| -| npm run start:disco | Starts the express server (prod mode discovery pane) | -| npm run start:search | Starts the express server (prod mode search) | -| npm run build | Builds the libs (all apps) | -| npm run build:disco | Builds the libs (discovery pane) | -| npm run build:search | Builds the libs (search) | -| npm run dev:search | Starts the dev server (search app) | -| npm run dev:disco | Starts the dev server (discovery pane) | -| npm run lint | Lints the files with `eslint` (Run in `npm test`) | -| npm run eslint | An alias for `npm run lint` | -| npm run version-check | Checks you have the minimum node + npm versions | -| npm test | Runs the tests | +| Script | Description | +|------------------------|-----------------------------------------------------| +| npm run start:disco | Starts the express server (prod mode disco pane) | +| npm run start:search | Starts the express server (prod mode search) | +| npm run build | Builds the libs (all apps) | +| npm run build-l10n | Builds the libs + extracts translations | +| npm run build:disco | Builds the libs (discovery pane) | +| npm run build:search | Builds the libs (search) | +| npm run dev:search | Starts the dev server (search app) | +| npm run dev:disco | Starts the dev server (discovery pane) | +| npm run lint | Lints the files with `eslint` (Run in `npm test`) | +| npm run eslint | An alias for `npm run lint` | +| npm run version-check | Checks you have the minimum node + npm versions | +| npm test | Runs the tests | ### Running a production build of a specific app: @@ -42,7 +43,7 @@ nvm. See https://github.com/creationix/nvm for more info. Running a specific prod build is as follows: ``` -npm run build:search && npm run start:search +NODE_APP_INSTANCE=search NODE_ENV=production npm run build && npm run start ``` ## Overview and rationale diff --git a/config/default.js b/config/default.js index 88b2c163319..9688b5d312d 100644 --- a/config/default.js +++ b/config/default.js @@ -107,4 +107,11 @@ module.exports = { // Set to true if you want to disable CSP on Android where it can be buggy. disableAndroid: false, }, + + supportedLocales: [ + 'af', 'ar', 'bg', 'bn-BD', 'ca', 'cs', 'da', 'de', 'el', 'en-GB', 'en-US', + 'es', 'eu', 'fa', 'fi', 'fr', 'ga-IE', 'he', 'hu', 'id', 'it', 'ja', 'ko', + 'mk', 'mn', 'nl', 'pl', 'pt-BR', 'pt-PT', 'ro', 'ru', 'sk', 'sl', 'sq', + 'sv-SE', 'uk', 'vi', 'zh-CN', 'zh-TW', + ], }; diff --git a/locale/templates/LC_MESSAGES/disco.pot b/locale/templates/LC_MESSAGES/disco.pot new file mode 100644 index 00000000000..c8f1c84010f --- /dev/null +++ b/locale/templates/LC_MESSAGES/disco.pot @@ -0,0 +1,48 @@ +msgid "" +msgstr "" +"Project-Id-Version: disco\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2016-05-20 11:26+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n!=1);\n" +"Content-Type: text/plain; charset=utf-8\n" + +#: src/disco/components/Addon.js:55 +msgid "An unexpected error occurred" +msgstr "" + +#: src/disco/components/Addon.js:80 +msgid "Preview %(name)s" +msgstr "" + +#: src/disco/containers/App.js:18 +msgid "Discover Add-ons" +msgstr "" + +#: src/disco/containers/DiscoPane.js:20 +msgid "Personalize Your Firefox" +msgstr "" + +#: src/disco/containers/DiscoPane.js:21 +msgid "" +"There are thousands of add-ons that let you make Firefox all your\n" +"own—everything from fun visual themes to powerful tools and features.\n" +"Here are a few great ones to check out." +msgstr "" + +#: src/disco/containers/DiscoPane.js:27 +msgid "Click to play" +msgstr "" + +#: src/disco/containers/DiscoPane.js:28 +msgid "to find out more about add-ons" +msgstr "" + +#: src/disco/containers/InstallButton.js:75 +msgid "Install" +msgstr "" \ No newline at end of file diff --git a/locale/templates/LC_MESSAGES/search.pot b/locale/templates/LC_MESSAGES/search.pot new file mode 100644 index 00000000000..acb630edea4 --- /dev/null +++ b/locale/templates/LC_MESSAGES/search.pot @@ -0,0 +1,126 @@ +msgid "" +msgstr "" +"Project-Id-Version: search\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2016-05-20 14:41+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n!=1);\n" +"Content-Type: text/plain; charset=utf-8\n" + +#: src/core/components/LoginPage/index.js:13 +msgid "Login Required" +msgstr "" + +#: src/core/components/LoginPage/index.js:20 +msgid "You must be logged in to access this page." +msgstr "" + +#: src/core/components/LoginPage/index.js:24 +msgid "Login" +msgstr "" + +#: src/core/components/NotFound.js:8 +msgid "We're sorry, but we can't find what you're looking for." +msgstr "" + +#: src/core/components/NotFound.js:9 +msgid "" +"The page or file you requested wasn't found on our site. It's possible that " +"you\n" +"clicked a link that's out of date, or typed in the address incorrectly." +msgstr "" + +#: src/core/containers/HandleLogin/index.js:41 +msgid "Logging you in..." +msgstr "" + +#: src/core/containers/HandleLogin/index.js:45 +msgid "There was an error logging you in, please try again." +msgstr "" + +#: src/search/components/SearchForm.js:30 +msgid "Search" +msgstr "" + +#: src/search/components/SearchPage.js:29 +msgid "Add-on Search" +msgstr "" + +#: src/search/components/SearchResult/index.js:18 +msgid "%(count)s file" +msgid_plural "%(count)s files" +msgstr[0] "" +msgstr[1] "" + +#: src/search/components/SearchResults.js:32 +msgid "Your search for \"%(query)s\" returned %(count)s results." +msgstr "" + +#: src/search/components/SearchResults.js:39 +msgid "Searching..." +msgstr "" + +#: src/search/components/SearchResults.js:41 +msgid "No results were found for \"%(query)s\"." +msgstr "" + +#: src/search/components/SearchResults.js:43 +msgid "Please supply a valid search" +msgstr "" + +#: src/search/containers/AddonPage/index.js:108 +msgid "Attributes" +msgstr "" + +#: src/search/containers/AddonPage/index.js:110 +msgid "Tags" +msgstr "" + +#: src/search/containers/AddonPage/index.js:31 +msgid "View on editors" +msgstr "" + +#: src/search/containers/AddonPage/index.js:35 +msgid "View homepage" +msgstr "" + +#: src/search/containers/AddonPage/index.js:41 +msgid "Email support" +msgstr "" + +#: src/search/containers/AddonPage/index.js:47 +msgid "View support site" +msgstr "" + +#: src/search/containers/AddonPage/index.js:70 +msgid "Current version" +msgstr "" + +#: src/search/containers/AddonPage/index.js:73 +msgid "View on site" +msgstr "" + +#: src/search/containers/AddonPage/index.js:74 +msgid "Edit on site" +msgstr "" + +#: src/search/containers/AddonPage/index.js:76 +msgid "Files" +msgstr "" + +#: src/search/containers/AddonPage/index.js:85 +msgid "Download" +msgstr "" + +#: src/search/containers/AddonPage/index.js:95 +msgid "No current version" +msgstr "" + +#: src/search/containers/App.js:18 +msgid "Add-ons Search" +msgstr "" \ No newline at end of file diff --git a/package.json b/package.json index c5cee212c9f..5ff8f4501fe 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "private": true, "scripts": { "build": "better-npm-run build", + "build-l10n": "better-npm-run build-l10n", "build:disco": "better-npm-run build:disco", "build:search": "better-npm-run build:search", "clean": "rimraf './dist/*+(css|js|map|json)' './webpack-assets.json'", @@ -54,6 +55,13 @@ "NODE_APP_INSTANCE": "search" } }, + "build-l10n": { + "command": "webpack --verbose --display-error-details --progress --colors --config webpack.l10n.config.babel.js", + "env": { + "NODE_PATH": "./:./src", + "NODE_ENV": "production" + } + }, "start-dev": { "command": "npm run clean && concurrently --kill-others 'npm run webpack-dev-server' 'node bin/server.js'", "env": { @@ -109,11 +117,11 @@ }, "homepage": "https://github.com/mozillla/addons-frontend#readme", "dependencies": { + "babel-plugin-dedent": "2.0.0", "better-npm-run": "0.0.8", "bunyan": "1.8.1", "camelcase": "3.0.0", "classnames": "2.2.5", - "common-tags": "0.1.1", "config": "1.20.3", "express": "4.13.4", "extract-text-webpack-plugin": "1.0.1", @@ -131,6 +139,7 @@ "redux-async-connect": "1.0.0-rc4", "redux-logger": "2.6.1", "serialize-javascript": "1.2.0", + "sprintf-js": "1.0.3", "url": "0.11.0", "url-loader": "0.5.7" }, @@ -138,6 +147,7 @@ "autoprefixer-loader": "3.2.0", "babel-core": "6.9.0", "babel-eslint": "6.0.4", + "babel-gettext-extractor": "git+https://github.com/muffinresearch/babel-gettext-extractor.git#f0f00b2afb71cba5edfb43d377bde9e1b08cdb46", "babel-istanbul": "0.8.0", "babel-istanbul-loader": "0.1.0", "babel-loader": "6.2.4", @@ -161,6 +171,7 @@ "eslint-plugin-import": "1.8.0", "eslint-plugin-jsx-a11y": "1.2.0", "eslint-plugin-react": "5.1.1", + "file-loader": "0.8.5", "json-loader": "0.5.4", "karma": "0.13.22", "karma-chai": "0.1.0", diff --git a/src/core/client/config.js b/src/core/client/config.js index 71199e9db91..f450a9bdf54 100644 --- a/src/core/client/config.js +++ b/src/core/client/config.js @@ -4,7 +4,6 @@ * When webpack builds the client-side code it exposes * the clientConfig config via the definePlugin as CLIENT_CONFIG. */ -import { oneLine } from 'common-tags'; export class ClientConfig { constructor(objData) { @@ -16,7 +15,7 @@ export class ClientConfig { if (this.has(key)) { return objData[key]; } - throw new Error(oneLine`Key was not found in clientConfig. Check the + throw new Error(dedent`Key was not found in clientConfig. Check the key has been added to clientConfigKeys`); }, }); diff --git a/src/core/components/NotFound.js b/src/core/components/NotFound.js index 6d80c417513..81599425569 100644 --- a/src/core/components/NotFound.js +++ b/src/core/components/NotFound.js @@ -6,8 +6,8 @@ export default class NotFound extends React.Component { return (

{_("We're sorry, but we can't find what you're looking for.")}

-

{_("The page or file you requested wasn't found on our site. It's possible that you " + - "clicked a link that's out of date, or typed in the address incorrectly.")}

+

{_(dedent`The page or file you requested wasn't found on our site. It's possible that you + clicked a link that's out of date, or typed in the address incorrectly.`)}

); } diff --git a/src/disco/components/Addon.js b/src/disco/components/Addon.js index 1c45e053676..9a112ba8faf 100644 --- a/src/disco/components/Addon.js +++ b/src/disco/components/Addon.js @@ -1,5 +1,7 @@ -import React, { PropTypes } from 'react'; import classNames from 'classnames'; +import React, { PropTypes } from 'react'; +import { sprintf } from 'sprintf-js'; + import themeAction from 'disco/themePreview'; import { gettext as _ } from 'core/utils'; @@ -75,7 +77,7 @@ export default class Addon extends React.Component { onFocus={this.previewTheme} onMouseOut={this.resetPreviewTheme} onMouseOver={this.previewTheme}> - {_(`Preview); + {sprintf(_('Preview); } return null; } diff --git a/src/disco/containers/DiscoPane.js b/src/disco/containers/DiscoPane.js index 5be0b09371c..9a40471983e 100644 --- a/src/disco/containers/DiscoPane.js +++ b/src/disco/containers/DiscoPane.js @@ -18,9 +18,9 @@ class DiscoPane extends React.Component {

{_('Personalize Your Firefox')}

-

{_(`There are thousands of add-ons that let you make Firefox all your - own—everything from fun visual themes to powerful tools and features. - Here are a few great ones to check out.`)}

+

{_(dedent`There are thousands of add-ons that let you make Firefox all your + own—everything from fun visual themes to powerful tools and features. + Here are a few great ones to check out.`)}

diff --git a/src/search/components/SearchResult/index.js b/src/search/components/SearchResult/index.js index cc606815b54..f4586bdae2b 100644 --- a/src/search/components/SearchResult/index.js +++ b/src/search/components/SearchResult/index.js @@ -1,5 +1,6 @@ import React, { PropTypes } from 'react'; import { Link } from 'react-router'; +import { sprintf } from 'sprintf-js'; import { ngettext } from 'core/utils'; @@ -14,7 +15,7 @@ function fileCount(version) { function fileCountText(version) { const count = fileCount(version); - return ngettext('{count} file', '{count} files', count).replace('{count}', count); + return sprintf(ngettext('%(count)s file', '%(count)s files', count), {count}); } export default class SearchResult extends React.Component { diff --git a/src/search/components/SearchResults.js b/src/search/components/SearchResults.js index c8355f0eb86..ce70be733a1 100644 --- a/src/search/components/SearchResults.js +++ b/src/search/components/SearchResults.js @@ -1,4 +1,5 @@ import React, { PropTypes } from 'react'; +import { sprintf } from 'sprintf-js'; import { gettext as _ } from 'core/utils'; import SearchResult from 'search/components/SearchResult'; @@ -27,7 +28,8 @@ export default class SearchResults extends React.Component { let messageText; if (query && count > 0) { - messageText = _(`Your search for "${query}" returned ${count} results.`); + messageText = sprintf( + _('Your search for "%(query)s" returned %(count)s results.'), {query, count}); searchResults = (

    {results.map((result) =>
  • )} @@ -36,7 +38,7 @@ export default class SearchResults extends React.Component { } else if (query && loading) { messageText = _('Searching...'); } else if (query && results.length === 0) { - messageText = _(`No results were found for "${query}".`); + messageText = sprintf(_('No results were found for "%(query)s".'), {query}); } else if (query !== null) { messageText = _('Please supply a valid search'); } diff --git a/webpack.l10n.config.babel.js b/webpack.l10n.config.babel.js new file mode 100644 index 00000000000..d0a1c39fb22 --- /dev/null +++ b/webpack.l10n.config.babel.js @@ -0,0 +1,73 @@ +/* eslint-disable no-console */ +import fs from 'fs'; +import config from 'config'; +import chalk from 'chalk'; + +import webpackConfig from './webpack.prod.config.babel'; + +const appName = config.get('appName'); + +if (!appName) { + console.log( + chalk.red('Please specify the appName with NODE_APP_INSTANCE')); + process.exit(1); +} + +if (process.env.NODE_ENV !== 'production') { + console.log( + chalk.red('This should be run with NODE_ENV="production"')); + process.exit(1); +} + +const babelrc = fs.readFileSync('./.babelrc'); +const babelrcObject = JSON.parse(babelrc); +const babelPlugins = babelrcObject.plugins || []; + +// Create UTC creation date in the correct format. +const potCreationDate = new Date().toISOString() + .replace('T', ' ') + .replace(/:\d{2}.\d{3}Z/, '+0000'); + +const babelL10nPlugins = [ + ['babel-gettext-extractor', { + headers: { + 'Project-Id-Version': appName, + 'Report-Msgid-Bugs-To': 'EMAIL@ADDRESS', + 'POT-Creation-Date': potCreationDate, + 'PO-Revision-Date': 'YEAR-MO-DA HO:MI+ZONE', + 'Last-Translator': 'FULL NAME ', + 'Language-Team': 'LANGUAGE ', + 'MIME-Version': '1.0', + 'Content-Type': 'text/plain; charset=utf-8', + 'Content-Transfer-Encoding': '8bit', + 'plural-forms': 'nplurals=2; plural=(n!=1);', + }, + functionNames: { + _: ['msgid'], + dgettext: ['domain', 'msgid'], + ngettext: ['msgid', 'msgid_plural', 'count'], + dngettext: ['domain', 'msgid', 'msgid_plural', 'count'], + pgettext: ['msgctxt', 'msgid'], + dpgettext: ['domain', 'msgctxt', 'msgid'], + npgettext: ['msgctxt', 'msgid', 'msgid_plural', 'count'], + dnpgettext: ['domain', 'msgctxt', 'msgid', 'msgid_plural', 'count'], + }, + fileName: `locale/templates/LC_MESSAGES/${appName}.pot`, + baseDirectory: process.cwd(), + }], +]; + +const BABEL_QUERY = Object.assign({}, babelrcObject, { + plugins: babelPlugins.concat(babelL10nPlugins), +}); + +const newLoaders = webpackConfig.module.loaders.slice(0); +// Assumes the js loader is the first one. +newLoaders[0].query = BABEL_QUERY; + +export default Object.assign({}, webpackConfig, { + entry: { [appName]: `src/${appName}/client` }, + module: { + loaders: newLoaders, + }, +}); diff --git a/webpack.prod.config.babel.js b/webpack.prod.config.babel.js index bfd52cd664b..c0b9d5603e7 100644 --- a/webpack.prod.config.babel.js +++ b/webpack.prod.config.babel.js @@ -39,7 +39,7 @@ export default { { test: /\.jsx?$/, exclude: /node_modules/, - loaders: ['babel'], + loader: 'babel', }, { test: /\.scss$/, loader: ExtractTextPlugin.extract('style', 'css?importLoaders=2&sourceMap!autoprefixer?browsers=last 2 version!sass?outputStyle=expanded&sourceMap=true&sourceMapContents=true'),