Skip to content

Commit

Permalink
Revert "feat: move i18n to use edx package (openedx#33)" (openedx#34)
Browse files Browse the repository at this point in the history
This reverts commit aca507d.
  • Loading branch information
abutterworth committed May 31, 2019
1 parent aca507d commit 9eb84c2
Show file tree
Hide file tree
Showing 25 changed files with 243 additions and 118 deletions.
3 changes: 1 addition & 2 deletions .babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@
"i18n": {
"plugins": [
["react-intl", {
"messagesDir": "./temp",
"moduleSourceName": "@edx/frontend-i18n"
"messagesDir": "./temp"
}]
]
}
Expand Down
64 changes: 0 additions & 64 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
"@edx/frontend-auth": "^5.2.0",
"@edx/frontend-component-footer": "^5.0.0",
"@edx/frontend-component-site-header": "^2.1.4",
"@edx/frontend-i18n": "^1.0.4",
"@edx/frontend-logging": "^2.0.2",
"@edx/paragon": "^4.2.4",
"@fortawesome/fontawesome-svg-core": "^1.2.14",
Expand Down
3 changes: 2 additions & 1 deletion src/components/App.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React, { Component } from 'react';
import { connect, Provider } from 'react-redux';
import PropTypes from 'prop-types';
import { IntlProvider, injectIntl, intlShape } from 'react-intl';
import { Route, Switch } from 'react-router-dom';
import { ConnectedRouter } from 'connected-react-router';
import { sendTrackEvent } from '@edx/frontend-analytics';
import SiteHeader from '@edx/frontend-component-site-header';
import SiteFooter from '@edx/frontend-component-footer';
import { IntlProvider, injectIntl, intlShape, getLocale, getMessages } from '@edx/frontend-i18n';
import { getLocale, getMessages } from '@edx/frontend-i18n'; // eslint-disable-line
import {
faFacebookSquare,
faTwitterSquare,
Expand Down
2 changes: 1 addition & 1 deletion src/components/App.messages.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineMessages } from '@edx/frontend-i18n';
import { defineMessages } from 'react-intl';

const messages = defineMessages({
'siteheader.links.courses': {
Expand Down
2 changes: 1 addition & 1 deletion src/components/ErrorPage.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-i18n';
import { FormattedMessage } from 'react-intl';

export default function ErrorPage() {
return (
Expand Down
2 changes: 1 addition & 1 deletion src/components/NotFoundPage.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-i18n';
import { FormattedMessage } from 'react-intl';

export default function NotFoundPage() {
return (
Expand Down
165 changes: 165 additions & 0 deletions src/i18n/i18n-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/**
* For each locale we want to support, react-intl needs 1) the locale-data, which includes
* information about how to format numbers, handle plurals, etc., and 2) the translations, as an
* object holding message id / translated string pairs. A locale string and the messages object are
* passed into the IntlProvider element that wraps your element hierarchy.
*
* Note that react-intl has no way of checking if the translations you give it actually have
* anything to do with the locale you pass it; it will happily use whatever messages object you pass
* in. However, if the locale data for the locale you passed into the IntlProvider was not
* correctly installed with addLocaleData, all of your translations will fall back to the default
* (in our case English), *even if you gave IntlProvider the correct messages object for that
* locale*.
*/

import { addLocaleData } from 'react-intl';
import Cookies from 'universal-cookie';

import arLocale from 'react-intl/locale-data/ar';
import enLocale from 'react-intl/locale-data/en';
import es419Locale from 'react-intl/locale-data/es';
import frLocale from 'react-intl/locale-data/fr';
import zhcnLocale from 'react-intl/locale-data/zh';

import COUNTRIES, { langs as countryLangs } from 'i18n-iso-countries';
import LANGUAGES, { langs as languageLangs } from '@cospired/i18n-iso-languages';

import arMessages from './messages/ar.json';
// no need to import en messages-- they are in the defaultMessage field
import es419Messages from './messages/es_419.json';
import frMessages from './messages/fr.json';
import zhcnMessages from './messages/zh_CN.json';

addLocaleData([...arLocale, ...enLocale, ...es419Locale, ...frLocale, ...zhcnLocale]);

// TODO: When we start dynamically loading translations only for the current locale, change this.
COUNTRIES.registerLocale(require('i18n-iso-countries/langs/ar.json'));
COUNTRIES.registerLocale(require('i18n-iso-countries/langs/en.json'));
COUNTRIES.registerLocale(require('i18n-iso-countries/langs/es.json'));
COUNTRIES.registerLocale(require('i18n-iso-countries/langs/fr.json'));
COUNTRIES.registerLocale(require('i18n-iso-countries/langs/zh.json'));

// TODO: When we start dynamically loading translations only for the current locale, change this.
// TODO: Also note that Arabic (ar) and Chinese (zh) are missing here. That's because they're
// not implemented in this library. If you read this and it's been a while, go check and see
// if that's changed!
LANGUAGES.registerLocale(require('@cospired/i18n-iso-languages/langs/en.json'));
LANGUAGES.registerLocale(require('@cospired/i18n-iso-languages/langs/es.json'));
LANGUAGES.registerLocale(require('@cospired/i18n-iso-languages/langs/fr.json'));

const messages = { // current fallback strategy is to use the first two letters of the locale code
ar: arMessages,
es: es419Messages,
fr: frMessages,
zh: zhcnMessages,
};

const cookies = new Cookies();

const getTwoLetterLanguageCode = code => code.substr(0, 2);

// Get the locale by setting priority. Skip if we don't support that language.
const getLocale = (localeStr) => {
// 1. Explicit application request
if (localeStr && messages[localeStr] !== undefined) {
return localeStr;
}
// 2. User setting in cookie
const cookieLangPref = cookies.get(process.env.LANGUAGE_PREFERENCE_COOKIE_NAME);
if (cookieLangPref && messages[getTwoLetterLanguageCode(cookieLangPref)] !== undefined) {
return getTwoLetterLanguageCode(cookieLangPref);
}
// 3. Browser language (default)
return getTwoLetterLanguageCode(window.navigator.language);
};

const getMessages = (locale = getLocale()) => messages[locale];

const rtlLocales = ['ar', 'he', 'fa', 'ur'];
const isRtl = locale => rtlLocales.includes(locale);

const handleRtl = () => {
if (isRtl(getLocale())) {
document.getElementsByTagName('html')[0].setAttribute('dir', 'rtl');
document.styleSheets[0].disabled = true;
} else {
document.styleSheets[1].disabled = true;
}
};

/**
* Provides a lookup table of country IDs to country names for the current locale.
*/
const getCountryMessages = (locale) => {
const finalLocale = countryLangs().includes(locale) ? locale : 'en';

return COUNTRIES.getNames(finalLocale);
};

/**
* Provides a lookup table of language IDs to language names for the current locale.
*/
const getLanguageMessages = (locale) => {
const finalLocale = languageLangs().includes(locale) ? locale : 'en';

return LANGUAGES.getNames(finalLocale);
};

const sortFunction = (a, b) => {
// If localeCompare exists, use that. (Not supported in some older browsers)
if (typeof String.prototype.localeCompare === 'function') {
return a[1].localeCompare(b[1], getLocale());
}
if (a[1] === b[1]) {
return 0;
}
// Otherwise make a best effort.
return a[1] > b[1] ? 1 : -1;
};

/**
* Provides a list of countries represented as objects of the following shape:
*
* {
* key, // The ID of the country
* name // The localized name of the country
* }
*
* The list is sorted alphabetically in the current locale.
* This is useful for select dropdowns primarily.
*/
const getCountryList = (locale) => {
const countryMessages = getCountryMessages(locale);
return Object.entries(countryMessages)
.sort(sortFunction)
.map(([code, name]) => ({ code, name }));
};

/**
* Provides a list of languages represented as objects of the following shape:
*
* {
* key, // The ID of the language
* name // The localized name of the language
* }
*
* The list is sorted alphabetically in the current locale.
* This is useful for select dropdowns primarily.
*/
const getLanguageList = (locale) => {
const languageMessages = getLanguageMessages(locale);
return Object.entries(languageMessages)
.sort(sortFunction)
.map(([code, name]) => ({ code, name }));
};

export {
getCountryList,
getCountryMessages,
getLanguageList,
getLanguageMessages,
getLocale,
getMessages,
handleRtl,
isRtl,
};
53 changes: 23 additions & 30 deletions src/i18n/index.jsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,25 @@
import arMessages from './messages/ar.json';
import caMessages from './messages/ca.json';
// no need to import en messages-- they are in the defaultMessage field
import es419Messages from './messages/es_419.json';
import frMessages from './messages/fr.json';
import zhcnMessages from './messages/zh_CN.json';
import heMessages from './messages/he.json';
import idMessages from './messages/id.json';
import kokrMessages from './messages/ko_kr.json';
import plMessages from './messages/pl.json';
import ptbrMessages from './messages/pt_br.json';
import ruMessages from './messages/ru.json';
import thMessages from './messages/th.json';
import ukMessages from './messages/uk.json';
import { intlShape } from 'react-intl';
import injectIntlWithShim from './injectIntlWithShim';
import {
getCountryList,
getCountryMessages,
getLanguageList,
getLanguageMessages,
getLocale,
getMessages,
handleRtl,
isRtl,
} from './i18n-loader';

const messages = {
ar: arMessages,
'es-419': es419Messages,
fr: frMessages,
'zh-cn': zhcnMessages,
ca: caMessages,
he: heMessages,
id: idMessages,
'ko-kr': kokrMessages,
pl: plMessages,
'pt-br': ptbrMessages,
ru: ruMessages,
th: thMessages,
uk: ukMessages,
export {
injectIntlWithShim as injectIntl,
getCountryList,
getCountryMessages,
getLanguageList,
getLanguageMessages,
getLocale,
getMessages,
handleRtl,
isRtl,
intlShape,
};

export default messages;
41 changes: 41 additions & 0 deletions src/i18n/injectIntlWithShim.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import { injectIntl, intlShape } from 'react-intl';
import { logError } from '@edx/frontend-logging';


const injectIntlWithShim = (WrappedComponent) => {
class ShimmedIntlComponent extends React.Component {
static propTypes = {
intl: intlShape.isRequired,
};

constructor(props) {
super(props);
this.shimmedIntl = Object.create(this.props.intl, {
formatMessage: {
value: (definition) => {
if (definition === undefined || definition.id === undefined) {
const error = new Error('i18n error: An undefined message was supplied to intl.formatMessage.');
if (process.env.NODE_ENV !== 'production') {
console.error(error); // eslint-disable-line no-console
return '!!! Missing message supplied to intl.formatMessage !!!';
}
logError(error);
return ''; // Fail silent in production
}
return this.props.intl.formatMessage(definition);
},
},
});
}

render() {
return <WrappedComponent {...this.props} intl={this.shimmedIntl} />;
}
}

return injectIntl(ShimmedIntlComponent);
};


export default injectIntlWithShim;
1 change: 0 additions & 1 deletion src/i18n/messages/ca.json

This file was deleted.

1 change: 0 additions & 1 deletion src/i18n/messages/he.json

This file was deleted.

1 change: 0 additions & 1 deletion src/i18n/messages/id.json

This file was deleted.

1 change: 0 additions & 1 deletion src/i18n/messages/ko_kr.json

This file was deleted.

Loading

0 comments on commit 9eb84c2

Please sign in to comment.