From 8f3548755887a11a4baa8ca1ca1bd1d0547f981c Mon Sep 17 00:00:00 2001 From: Robert Raposa Date: Tue, 19 Mar 2019 17:03:15 -0400 Subject: [PATCH] feat: initial localization of footer BREAKING CHANGE: Now requires containing app to use `react-intl`. See update to README for details. ARCH-460 --- .babelrc | 49 +-- .gitignore | 1 + .tx/config | 8 + Makefile | 36 +++ README.rst | 10 +- package-lock.json | 83 +++++ package.json | 12 +- src/i18n/i18n-concat.js | 57 ++++ src/i18n/transifex_input.json | 39 +++ src/index.jsx | 40 +-- .../SiteFooter/SiteFooter.messages.jsx | 91 ++++++ .../components/SiteFooter/SiteFooter.test.jsx | 133 ++++---- .../__snapshots__/SiteFooter.test.jsx.snap | 262 +++++++++++----- src/lib/components/SiteFooter/index.jsx | 288 +++++++++++++++--- 14 files changed, 870 insertions(+), 239 deletions(-) create mode 100644 .tx/config create mode 100755 Makefile create mode 100755 src/i18n/i18n-concat.js create mode 100644 src/i18n/transifex_input.json create mode 100644 src/lib/components/SiteFooter/SiteFooter.messages.jsx diff --git a/.babelrc b/.babelrc index e36900569..ecbbebfa8 100644 --- a/.babelrc +++ b/.babelrc @@ -1,43 +1,26 @@ { + "presets": [ + ["env", { + "targets": { + "browsers": ["last 2 versions", "ie 11"] + } + }], + "babel-preset-react" + ], + "plugins": [ + "transform-object-rest-spread" + ], "env": { - "development": { - "presets": [ - ["env", { - "targets": { - "browsers": ["last 2 versions", "ie 11"] - } - }], - "babel-preset-react" - ], - "plugins": [ - "transform-object-rest-spread" - ] - }, "test": { - "presets": [ - ["env", { - "targets": { - "browsers": ["last 2 versions", "ie 11"] - } - }], - "babel-preset-react" - ], "plugins": [ - "rewire", - "transform-object-rest-spread" + "rewire" ] }, - "production": { - "presets": [ - ["env", { - "targets": { - "browsers": ["last 2 versions", "ie 11"] - } - }], - "babel-preset-react" - ], + "i18n": { "plugins": [ - "transform-object-rest-spread" + ["react-intl", { + "messagesDir": "./temp" + }] ] } } diff --git a/.gitignore b/.gitignore index 510c02e80..360e54665 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ coverage dist node_modules +temp .idea/ diff --git a/.tx/config b/.tx/config new file mode 100644 index 000000000..eb01b4d90 --- /dev/null +++ b/.tx/config @@ -0,0 +1,8 @@ +[main] +host = https://www.transifex.com + +[edx-platform.frontend-component-footer] +file_filter = "src/i18n/messages/.json" +source_file = "src/i18n/transifex_input.json" +source_lang = en +type = KEYVALUEJSON diff --git a/Makefile b/Makefile new file mode 100755 index 000000000..a8769caaf --- /dev/null +++ b/Makefile @@ -0,0 +1,36 @@ +extract_translations: ## no prerequisites so we can control order of operations + echo "We have to define this target due to tooling assumptions" + echo "Also we have to npm install using this hook b/c there's no other place for it in the current setup" + npm install + npm run-script i18n_extract + +i18n.extract: + # Pulling display strings from .jsx files into .json files... + npm run-script i18n_extract + +i18n.concat: + # Gathering JSON messages into one file... + ./src/i18n/i18n-concat.js ./temp/src ./src/i18n/transifex_input.json + +i18n.pre_validate: | i18n.extract i18n.concat + git diff --exit-code ./src/i18n/transifex_input.json + +tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/frontend-component-footer/translation/en/strings/ +tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/frontend-component-footer/source/ + +# push translations to Transifex, doing magic so we can include the translator comments +push_translations: | i18n.extract + # Adding translator comments... + # Fetching strings from Transifex... + ./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1) + # Writing out comments to file... + ./src/i18n/i18n-concat.js ./temp/src --comments + # Adding comments to Transifex... + ./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2) + +# pull translations from Transifex +pull_translations: ## must be exactly this name for edx tooling support, see ecommerce-scripts/transifex/pull.py + tx pull -f --mode reviewed --language="ar,fr,es_419,zh_CN" + +validate-no-uncommitted-package-lock-changes: + git diff --exit-code package-lock.json diff --git a/README.rst b/README.rst index 51532fb59..d09471d95 100644 --- a/README.rst +++ b/README.rst @@ -7,8 +7,7 @@ frontend-component-footer frontend-component-footer is a library containing a site footer component for use when building edX frontend applications. -At this time, this componenet is hard-coded to match the legacy LMS site footer, including all of its links. -Note: As implemented, it should really be called the ``frontend-component-lms-footer``. +At this time, this component is hard-coded to match the legacy LMS site footer, including all of its links. As implemented, this component should probably be called the ``frontend-component-lms-footer``. Usage ----- @@ -18,9 +17,14 @@ To install frontend-component-footer into your project:: npm i --save @edx/frontend-component-footer The component expects properties specifying the various URLs that are -linked in the footer. See the sample app in src/index.jsx for an example +linked in the footer. See the sample app in `src/index.jsx `__ for an example of how the SiteFooter component can be specified. +Requirements +------------ + +This component uses ``react-intl``. Any containing app must provide ``react-intl`` as a peer dependency, and be wrapped inside an ``IntlProvider`` element, whether or not your consuming application is actually localized. For a basic default locale (English) version, follow the ``IntlProvider`` example in the sample application in `src/index.jsx `__. + Development ----------- diff --git a/package-lock.json b/package-lock.json index b055cd8fb..f078872b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -914,6 +914,23 @@ } } }, + "@babel/runtime": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.4.tgz", + "integrity": "sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.12.0" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", + "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==", + "dev": true + } + } + }, "@babel/template": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.2.2.tgz", @@ -3033,6 +3050,17 @@ "integrity": "sha1-5h+uBaHKiAGq3uV6bWa4zvr0QWc=", "dev": true }, + "babel-plugin-react-intl": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-react-intl/-/babel-plugin-react-intl-3.0.1.tgz", + "integrity": "sha512-FqnEO+Tq7kJVUPKsSG3s5jaHi3pAC4RUR11IrscvjsfkOApLP2DtzNo6dtQ+tX+OzEzJx7cUms8aCw5BFyW5xg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.0.0", + "intl-messageformat-parser": "^1.2.0", + "mkdirp": "^0.5.1" + } + }, "babel-plugin-rewire": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/babel-plugin-rewire/-/babel-plugin-rewire-1.2.0.tgz", @@ -9906,6 +9934,12 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "hoist-non-react-statics": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", + "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==", + "dev": true + }, "home-or-tmp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", @@ -10987,6 +11021,36 @@ "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", "dev": true }, + "intl-format-cache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/intl-format-cache/-/intl-format-cache-2.1.0.tgz", + "integrity": "sha1-BKNp/sv61tpgBbrh8UMzMy3PkxY=", + "dev": true + }, + "intl-messageformat": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-2.2.0.tgz", + "integrity": "sha1-NFvNRt5jC3aDMwwuUhd/9eq0hPw=", + "dev": true, + "requires": { + "intl-messageformat-parser": "1.4.0" + } + }, + "intl-messageformat-parser": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-1.4.0.tgz", + "integrity": "sha1-tD1FqXRoytvkQzHXS7Ho3qRPwHU=", + "dev": true + }, + "intl-relativeformat": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/intl-relativeformat/-/intl-relativeformat-2.1.0.tgz", + "integrity": "sha1-AQ8RBYAiUfQKxH0OPhogE0iiVd8=", + "dev": true, + "requires": { + "intl-messageformat": "^2.0.0" + } + }, "into-stream": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", @@ -20413,6 +20477,19 @@ "integrity": "sha512-xXUbDAZkU08aAkjtUvldqbvI04ogv+a1XdHxvYuHPYKIVk/42BIOD0zSKTHAWV4+gDy3yGm283z2072rA2gdtw==", "dev": true }, + "react-intl": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-2.8.0.tgz", + "integrity": "sha512-1cSasNkHxZOXYYhms9Q1tSEWF8AWZQNq3nPLB/j8mYV0ZTSt2DhGQXHfKrKQMu4cgj9J1Crqg7xFPICTBgzqtQ==", + "dev": true, + "requires": { + "hoist-non-react-statics": "^2.5.5", + "intl-format-cache": "^2.0.5", + "intl-messageformat": "^2.1.0", + "intl-relativeformat": "^2.1.0", + "invariant": "^2.1.1" + } + }, "react-is": { "version": "16.7.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.7.0.tgz", @@ -20448,6 +20525,12 @@ "scheduler": "^0.12.0" } }, + "reactifex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/reactifex/-/reactifex-1.1.1.tgz", + "integrity": "sha512-HH2N/b5tRxh7ypIgCRsiBl/CTxRkTEPf9DhIstaM6hne4WiwM5/bBbWuvVlRZc/i3FdqZED3pZ//6n4mtxma4w==", + "dev": true + }, "read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", diff --git a/package.json b/package.json index 164571675..a299d6c19 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "gc": "commit", "commitmsg": "commitlint -e $GIT_PARAMS", "coveralls": "cat ./coverage/lcov.info | coveralls", + "i18n_extract": "BABEL_ENV=i18n babel src --quiet > /dev/null", "lint": "eslint --ext .js --ext .jsx .", "precommit": "npm run lint", "prepublishOnly": "npm run build", @@ -37,9 +38,11 @@ "@commitlint/config-angular": "^6.0.2", "@commitlint/prompt": "^6.0.2", "@commitlint/prompt-cli": "^6.0.2", + "@edx/paragon": "^3.8.0", "babel-cli": "^6.26.0", "babel-core": "^6.26.3", "babel-loader": "^7.1.5", + "babel-plugin-react-intl": "^3.0.1", "babel-plugin-rewire": "^1.2.0", "babel-plugin-transform-object-rest-spread": "^6.26.0", "babel-preset-env": "^1.7.0", @@ -47,23 +50,25 @@ "clean-webpack-plugin": "^0.1.19", "coveralls": "^3.0.0", "css-loader": "^0.28.9", - "@edx/paragon": "^3.8.0", "enzyme": "^3.3.0", "enzyme-adapter-react-16": "^1.1.1", "eslint": "^5.2.0", "eslint-config-edx": "^4.0.4", "eslint-plugin-jsx-a11y": "^6.1.2", "file-loader": "^1.1.9", + "glob": "^7.1.3", "html-webpack-plugin": "^3.2.0", - "image-webpack-loader": "^4.2.0", "husky": "^0.14.3", + "image-webpack-loader": "^4.2.0", "jest": "23.6.0", "node-sass": "^4.7.2", "prop-types": "^15.5.10", "react": "^16.4.2", - "react-dom": "^16.2.0", "react-dev-utils": "^5.0.0", + "react-dom": "^16.2.0", + "react-intl": "^2.8.0", "react-test-renderer": "^16.6.0", + "reactifex": "^1.1.1", "sass-loader": "^6.0.6", "semantic-release": "^15.1.7", "source-map-loader": "^0.2.1", @@ -82,6 +87,7 @@ "prop-types": "^15.5.10", "react": "^16.4.2", "react-dom": "^16.2.0", + "react-intl": "2.x", "webpack": "^4.19.1", "webpack-merge": "^4.2.1" }, diff --git a/src/i18n/i18n-concat.js b/src/i18n/i18n-concat.js new file mode 100755 index 000000000..0ac0aa985 --- /dev/null +++ b/src/i18n/i18n-concat.js @@ -0,0 +1,57 @@ +#!/usr/bin/env node +/** + * This code originally came from https://github.com/efischer19/reactifex/blob/master/main.js, + * which should be edx/reactifex. It is temporarily being copied here until we find it a new home. + */ + +// NOTE: This script is called from Jenkins using devDependencies, so eslint is being +// disabled so it doesn't force you to make these real dependencies. +const fs = require('fs'); // eslint-disable-line import/no-extraneous-dependencies +const glob = require('glob'); // eslint-disable-line import/no-extraneous-dependencies +const path = require('path'); // eslint-disable-line import/no-extraneous-dependencies + +// Expected input: a directory, possibly containing subdirectories, with .json files. Each .json +// file is an array of translation triplets (id, description, defaultMessage). +function gatherJson(dir) { + const ret = []; + const files = glob.sync(`${dir}/**/*.json`); + + files.forEach((filename) => { + const messages = JSON.parse(fs.readFileSync(filename)); + ret.push(...messages); + }); + return ret; +} + +const jsonDir = process.argv[2]; +const messageObjects = gatherJson(jsonDir); + +if (process.argv[3] === '--comments') { // prepare to handle the translator notes + const thisFile = path.basename(`${__filename}`); + const bashScriptsPath = './node_modules/reactifex/bash_scripts'; + + process.stdout.write(`${thisFile}: generating bash scripts...\n`); + process.stdout.write(`${thisFile}: info file at ${bashScriptsPath}/hashmap.json\n`); + + const messageInfo = JSON.parse(fs.readFileSync(`${bashScriptsPath}/hashmap.json`)); + const dataPath = `${bashScriptsPath}/hashed_data.txt`; + + process.stdout.write(`${thisFile}: data path is ${dataPath}\n`); + fs.writeFileSync(dataPath, ''); + + messageObjects.forEach((message) => { + const info = messageInfo.find(mi => mi.key === message.id); + if (info) { + fs.appendFileSync(dataPath, `${info.string_hash}|${message.description}\n`); + } else { + process.stdout.write(`${thisFile}: string ${message.id} does not yet exist on transifex!\n`); + } + }); +} else { + const output = {}; + + messageObjects.forEach((message) => { + output[message.id] = message.defaultMessage; + }); + fs.writeFileSync(process.argv[3], JSON.stringify(output, null, 2)); +} diff --git a/src/i18n/transifex_input.json b/src/i18n/transifex_input.json new file mode 100644 index 000000000..388d0b352 --- /dev/null +++ b/src/i18n/transifex_input.json @@ -0,0 +1,39 @@ +{ + "footer.site-footer.link.about": "About", + "footer.site-footer.link.business": "{siteName} for Business", + "footer.site-footer.link.affiliates": "Affiliates", + "footer.site-footer.link.open-source": "Open {siteName}", + "footer.site-footer.link.careers": "Careers", + "footer.site-footer.link.news": "News", + "footer.site-footer.link.header.legal": "Legal", + "footer.site-footer.link.terms-of-service": "Terms of Service & Honor Code", + "footer.site-footer.link.privacy": "Privacy Policy", + "footer.site-footer.link.accessibility": "Accessibility Policy", + "footer.site-footer.link.trademark": "Trademark Policy", + "footer.site-footer.link.sitemap": "Sitemap", + "footer.site-footer.link.header.connect": "Connect", + "footer.site-footer.link.blog": "Blog", + "footer.site-footer.link.contact-us": "Contact Us", + "footer.site-footer.link.help-center": "Help Center", + "footer.site-footer.link.media-kit": "Media Kit", + "footer.site-footer.link.donate": "Donate", + "footer.site-footer.copyright-text": "{copyrightSymbol} {startDate}–{endDate} {siteName} Inc.", + "footer.site-footer.trademark-text": "EdX, Open edX, and MicroMasters are registered trademarks of edX Inc. | {icpLicense}", + "footer.site-footer.site-logo.alt-text": "{siteName} logo", + "footer.site-footer.site-logo.aria-label": "{siteName} Home", + "footer.site-footer.facebook.title": "Facebook", + "footer.site-footer.facebook.screen-reader-text": "Like {siteName} on Facebook", + "footer.site-footer.twitter.title": "Twitter", + "footer.site-footer.twitter.screen-reader-text": "Follow {siteName} on Twitter", + "footer.site-footer.youtube.title": "Youtube", + "footer.site-footer.youtube.screen-reader-text": "Subscribe to the {siteName} YouTube channel", + "footer.site-footer.linkedin.title": "LinkedIn", + "footer.site-footer.linkedin.screen-reader-text": "Follow {siteName} on LinkedIn", + "footer.site-footer.google-plus.title": "Google+", + "footer.site-footer.google-plus.screen-reader-text": "Follow {siteName} on Google+", + "footer.site-footer.reddit.title": "Reddit", + "footer.site-footer.reddit.screen-reader-text": "Subscribe to the {siteName} subreddit", + "footer.site-footer.apple-app-store.alt-text": "Download the {siteName} mobile app from the Apple App Store", + "footer.site-footer.google-play.alt-text": "Download the {siteName} mobile app from Google Play", + "footer.site-footer.footer.aria-label": "Page Footer" +} \ No newline at end of file diff --git a/src/index.jsx b/src/index.jsx index f6e051524..cc06f080e 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -1,29 +1,33 @@ import React from 'react'; import { render } from 'react-dom'; +import { IntlProvider } from 'react-intl'; import SiteFooter from './lib'; import './index.scss'; import FooterLogo from './edx-footer.png'; const App = () => ( - + + {}} + /> + ); render(, document.getElementById('root')); diff --git a/src/lib/components/SiteFooter/SiteFooter.messages.jsx b/src/lib/components/SiteFooter/SiteFooter.messages.jsx new file mode 100644 index 000000000..d3bf4d90c --- /dev/null +++ b/src/lib/components/SiteFooter/SiteFooter.messages.jsx @@ -0,0 +1,91 @@ +import { defineMessages } from 'react-intl'; + +const messages = defineMessages({ + 'footer.site-footer.site-logo.alt-text': { + id: 'footer.site-footer.site-logo.alt-text', + defaultMessage: '{siteName} logo', + description: 'The alt description of the site logo', + }, + 'footer.site-footer.site-logo.aria-label': { + id: 'footer.site-footer.site-logo.aria-label', + defaultMessage: '{siteName} Home', + description: 'Aria label for the site logo which goes to the marketing site', + }, + 'footer.site-footer.facebook.title': { + id: 'footer.site-footer.facebook.title', + defaultMessage: 'Facebook', + description: 'Facebook button title', + }, + 'footer.site-footer.facebook.screen-reader-text': { + id: 'footer.site-footer.facebook.screen-reader-text', + defaultMessage: 'Like {siteName} on Facebook', + description: 'Facebook button screen reader text', + }, + 'footer.site-footer.twitter.title': { + id: 'footer.site-footer.twitter.title', + defaultMessage: 'Twitter', + description: 'Twitter button title', + }, + 'footer.site-footer.twitter.screen-reader-text': { + id: 'footer.site-footer.twitter.screen-reader-text', + defaultMessage: 'Follow {siteName} on Twitter', + description: 'Twitter button screen reader text', + }, + 'footer.site-footer.youtube.title': { + id: 'footer.site-footer.youtube.title', + defaultMessage: 'Youtube', + description: 'Youtube button title', + }, + 'footer.site-footer.youtube.screen-reader-text': { + id: 'footer.site-footer.youtube.screen-reader-text', + defaultMessage: 'Subscribe to the {siteName} YouTube channel', + description: 'Youtube button screen reader text', + }, + 'footer.site-footer.linkedin.title': { + id: 'footer.site-footer.linkedin.title', + defaultMessage: 'LinkedIn', + description: 'LinkedIn button title', + }, + 'footer.site-footer.linkedin.screen-reader-text': { + id: 'footer.site-footer.linkedin.screen-reader-text', + defaultMessage: 'Follow {siteName} on LinkedIn', + description: 'LinkedIn button screen reader text', + }, + 'footer.site-footer.google-plus.title': { + id: 'footer.site-footer.google-plus.title', + defaultMessage: 'Google+', + description: 'Google+ button title', + }, + 'footer.site-footer.google-plus.screen-reader-text': { + id: 'footer.site-footer.google-plus.screen-reader-text', + defaultMessage: 'Follow {siteName} on Google+', + description: 'Google+ button screen reader text', + }, + 'footer.site-footer.reddit.title': { + id: 'footer.site-footer.reddit.title', + defaultMessage: 'Reddit', + description: 'Reddit button title', + }, + 'footer.site-footer.reddit.screen-reader-text': { + id: 'footer.site-footer.reddit.screen-reader-text', + defaultMessage: 'Subscribe to the {siteName} subreddit', + description: 'Reddit button screen reader text', + }, + 'footer.site-footer.apple-app-store.alt-text': { + id: 'footer.site-footer.apple-app-store.alt-text', + defaultMessage: 'Download the {siteName} mobile app from the Apple App Store', + description: 'Apple App Store button alt description', + }, + 'footer.site-footer.google-play.alt-text': { + id: 'footer.site-footer.google-play.alt-text', + defaultMessage: 'Download the {siteName} mobile app from Google Play', + description: 'Google Play button alt description', + }, + 'footer.site-footer.footer.aria-label': { + id: 'footer.site-footer.footer.aria-label', + defaultMessage: 'Page Footer', + description: 'Aria label for the footer', + }, +}); + +export default messages; diff --git a/src/lib/components/SiteFooter/SiteFooter.test.jsx b/src/lib/components/SiteFooter/SiteFooter.test.jsx index 63d680cf0..e7e3b54c8 100644 --- a/src/lib/components/SiteFooter/SiteFooter.test.jsx +++ b/src/lib/components/SiteFooter/SiteFooter.test.jsx @@ -1,30 +1,35 @@ import React from 'react'; import renderer from 'react-test-renderer'; import { mount } from 'enzyme'; +import { IntlProvider } from 'react-intl'; import FooterLogo from '../../../edx-footer.png'; import SiteFooter, { EVENT_NAMES } from './index'; const completeSiteFooterComponent = mockHandleAllTrackEvents => - (); + ( + + + + ); describe('', () => { describe('renders correctly', () => { @@ -38,53 +43,59 @@ describe('', () => { it('does not render social links', () => { const tree = renderer - .create() - .toJSON(); + .create(( + + + + )).toJSON(); expect(tree).toMatchSnapshot(); }); it('does not render mobile links', () => { const tree = renderer - .create() - .toJSON(); + .create(( + + + + )).toJSON(); expect(tree).toMatchSnapshot(); }); }); diff --git a/src/lib/components/SiteFooter/__snapshots__/SiteFooter.test.jsx.snap b/src/lib/components/SiteFooter/__snapshots__/SiteFooter.test.jsx.snap index 13cce3284..cb9cae513 100644 --- a/src/lib/components/SiteFooter/__snapshots__/SiteFooter.test.jsx.snap +++ b/src/lib/components/SiteFooter/__snapshots__/SiteFooter.test.jsx.snap @@ -37,44 +37,54 @@ exports[` renders correctly does not render mobile links 1`] = ` - About + + About +
  • - example - for Business + + example for Business +
  • - Affiliates + + Affiliates +
  • - Open - example + + Open example +
  • - Careers + + Careers +
  • - News + + News +
  • @@ -83,7 +93,9 @@ exports[` renders correctly does not render mobile links 1`] = ` className="area-3" >

    - Legal + + Legal +

    @@ -129,7 +151,9 @@ exports[` renders correctly does not render mobile links 1`] = ` className="area-4" >

    - Connect + + Connect +

    @@ -288,7 +322,7 @@ exports[` renders correctly does not render mobile links 1`] = ` renders correctly does not render mobile links 1`] = `

    - © 2012– - 2019 - - example - Inc. + + © 2012–2019 example Inc. +
    - EdX, Open edX, and MicroMasters are registered trademarks of edX Inc. | 粤ICP备17044299号-2 + + EdX, Open edX, and MicroMasters are registered trademarks of edX Inc. | 粤ICP备17044299号-2 +

    @@ -349,44 +383,54 @@ exports[` renders correctly does not render social links 1`] = ` - About + + About +
  • - example - for Business + + example for Business +
  • - Affiliates + + Affiliates +
  • - Open - example + + Open example +
  • - Careers + + Careers +
  • - News + + News +
  • @@ -395,7 +439,9 @@ exports[` renders correctly does not render social links 1`] = ` className="area-3" >

    - Legal + + Legal +

    @@ -441,7 +497,9 @@ exports[` renders correctly does not render social links 1`] = ` className="area-4" >

    - Connect + + Connect +

    @@ -519,13 +587,13 @@ exports[` renders correctly does not render social links 1`] = `

    - © 2012– - 2019 - - example - Inc. + + © 2012–2019 example Inc. +
    - EdX, Open edX, and MicroMasters are registered trademarks of edX Inc. | 粤ICP备17044299号-2 + + EdX, Open edX, and MicroMasters are registered trademarks of edX Inc. | 粤ICP备17044299号-2 +

    @@ -569,44 +637,54 @@ exports[` renders correctly renders with social and mobile links 1 - About + + About +
  • - example - for Business + + example for Business +
  • - Affiliates + + Affiliates +
  • - Open - example + + Open example +
  • - Careers + + Careers +
  • - News + + News +
  • @@ -615,7 +693,9 @@ exports[` renders correctly renders with social and mobile links 1 className="area-3" >

    - Legal + + Legal +

    @@ -661,7 +751,9 @@ exports[` renders correctly renders with social and mobile links 1 className="area-4" >

    - Connect + + Connect +

    @@ -820,7 +922,7 @@ exports[` renders correctly renders with social and mobile links 1 renders correctly renders with social and mobile links 1

    - © 2012– - 2019 - - example - Inc. + + © 2012–2019 example Inc. +
    - EdX, Open edX, and MicroMasters are registered trademarks of edX Inc. | 粤ICP备17044299号-2 + + EdX, Open edX, and MicroMasters are registered trademarks of edX Inc. | 粤ICP备17044299号-2 +

    diff --git a/src/lib/components/SiteFooter/index.jsx b/src/lib/components/SiteFooter/index.jsx index 60ebfa61f..e91f994ea 100644 --- a/src/lib/components/SiteFooter/index.jsx +++ b/src/lib/components/SiteFooter/index.jsx @@ -1,6 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { FormattedMessage, injectIntl, intlShape } from 'react-intl'; import { Hyperlink, Icon } from '@edx/paragon'; +import messages from './SiteFooter.messages'; const EVENT_NAMES = { FOOTER_LINK: 'edx.bi.footer.link', @@ -24,7 +26,13 @@ class SiteFooter extends React.Component { renderSiteLogo() { return ( - {`${this.props.siteName} + {this.props.intl.formatMessage( ); } @@ -34,6 +42,7 @@ class SiteFooter extends React.Component { renderSocialLinks() { const { + intl, siteName, showSocialLinks, facebookUrl, @@ -54,69 +63,109 @@ class SiteFooter extends React.Component {
  • - +
  • - -
  • - +
  • - +
  • - +
  • - +
  • - +
  • @@ -127,6 +176,7 @@ class SiteFooter extends React.Component { renderMobileLinks() { const { + intl, siteName, showMobileLinks, appleAppStoreUrl, @@ -140,7 +190,10 @@ class SiteFooter extends React.Component { {`Download @@ -149,7 +202,10 @@ class SiteFooter extends React.Component { {`Download @@ -162,6 +218,7 @@ class SiteFooter extends React.Component { render() { const { + intl, siteName, openSourceUrl, termsOfServiceUrl, @@ -172,52 +229,200 @@ class SiteFooter extends React.Component { return (