Allow languages and countries to be localized. (#125)

And also sort them by their localized values.
This commit is contained in:
David Joy
2019-04-05 10:42:36 -04:00
committed by GitHub
parent 4277c742c9
commit 6ab28a8306
6 changed files with 178 additions and 34 deletions

View File

@@ -12,11 +12,8 @@ import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
// Constants
import { ALL_COUNTRIES } from '../../constants/countries';
// Selectors
import { editableFormSelector } from '../../selectors/ProfilePageSelector';
import { countrySelector } from '../../selectors/ProfilePageSelector';
class Country extends React.Component {
constructor(props) {
@@ -51,7 +48,15 @@ class Country extends React.Component {
render() {
const {
formId, country, visibilityCountry, editMode, saveState, error, intl,
formId,
country,
visibilityCountry,
editMode,
saveState,
error,
intl,
sortedCountries,
countryMessages,
} = this.props;
return (
@@ -76,8 +81,8 @@ class Country extends React.Component {
onChange={this.handleChange}
aria-describedby={`${formId}-error-feedback`}
>
{Object.keys(ALL_COUNTRIES).map(key => (
<option key={key} value={key}>{ALL_COUNTRIES[key]}</option>
{sortedCountries.map(({ code, name }) => (
<option key={code} value={code}>{name}</option>
))}
</Input>
<FormFeedback id={`${formId}-error-feedback`}>{error}</FormFeedback>
@@ -101,7 +106,7 @@ class Country extends React.Component {
showVisibility={visibilityCountry !== null}
visibility={visibilityCountry}
/>
<p className="h5">{ALL_COUNTRIES[country]}</p>
<p className="h5">{countryMessages[country]}</p>
</React.Fragment>
),
empty: (
@@ -119,7 +124,7 @@ class Country extends React.Component {
<EditableItemHeader
content={intl.formatMessage(messages['profile.country.label'])}
/>
<p className="h5">{ALL_COUNTRIES[country]}</p>
<p className="h5">{countryMessages[country]}</p>
</React.Fragment>
),
}}
@@ -141,6 +146,11 @@ Country.propTypes = {
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
error: PropTypes.string,
sortedCountries: PropTypes.arrayOf(PropTypes.shape({
code: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
})).isRequired,
countryMessages: PropTypes.objectOf(PropTypes.string).isRequired,
// Actions
changeHandler: PropTypes.func.isRequired,
@@ -161,6 +171,6 @@ Country.defaultProps = {
};
export default connect(
editableFormSelector,
countrySelector,
{},
)(injectIntl(Country));

View File

@@ -12,11 +12,8 @@ import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
// Constants
import { ALL_LANGUAGES } from '../../constants/languages';
// Selectors
import { editableFormSelector } from '../../selectors/ProfilePageSelector';
import { preferredLanguageSelector } from '../../selectors/ProfilePageSelector';
class PreferredLanguage extends React.Component {
constructor(props) {
@@ -66,6 +63,8 @@ class PreferredLanguage extends React.Component {
saveState,
error,
intl,
sortedLanguages,
languageMessages,
} = this.props;
const value = languageProficiencies.length ? languageProficiencies[0].code : '';
@@ -92,7 +91,7 @@ class PreferredLanguage extends React.Component {
onChange={this.handleChange}
aria-describedby={`${formId}-error-feedback`}
>
{Object.entries(ALL_LANGUAGES).map(([code, name]) => (
{sortedLanguages.map(({ code, name }) => (
<option key={code} value={code}>{name}</option>
))}
</Input>
@@ -117,7 +116,7 @@ class PreferredLanguage extends React.Component {
showVisibility={visibilityLanguageProficiencies !== null}
visibility={visibilityLanguageProficiencies}
/>
<p className="h5">{ALL_LANGUAGES[value]}</p>
<p className="h5">{languageMessages[value]}</p>
</React.Fragment>
),
empty: (
@@ -135,7 +134,7 @@ class PreferredLanguage extends React.Component {
<EditableItemHeader
content={intl.formatMessage(messages['profile.preferredlanguage.label'])}
/>
<p className="h5">{ALL_LANGUAGES[value]}</p>
<p className="h5">{languageMessages[value]}</p>
</React.Fragment>
),
}}
@@ -162,6 +161,11 @@ PreferredLanguage.propTypes = {
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
error: PropTypes.string,
sortedLanguages: PropTypes.arrayOf(PropTypes.shape({
code: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
})).isRequired,
languageMessages: PropTypes.objectOf(PropTypes.string).isRequired,
// Actions
changeHandler: PropTypes.func.isRequired,
@@ -182,6 +186,6 @@ PreferredLanguage.defaultProps = {
};
export default connect(
editableFormSelector,
preferredLanguageSelector,
{},
)(injectIntl(PreferredLanguage));

View File

@@ -1,7 +0,0 @@
import COUNTRIES from 'i18n-iso-countries';
COUNTRIES.registerLocale(require('i18n-iso-countries/langs/en.json'));
const ALL_COUNTRIES = COUNTRIES.getNames('en');
export { ALL_COUNTRIES, COUNTRIES };

View File

@@ -1,7 +0,0 @@
import LANGUAGES from '@cospired/i18n-iso-languages';
LANGUAGES.registerLocale(require('@cospired/i18n-iso-languages/langs/en.json'));
const ALL_LANGUAGES = LANGUAGES.getNames('en');
export { ALL_LANGUAGES, LANGUAGES };

View File

@@ -20,6 +20,9 @@ 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';
@@ -28,6 +31,21 @@ 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'));
// TODO (ARCH-563): this should ultimately come from the user's settings, but for now, set your
// browser language to the language you want to see
const getLocale = (localeStr = window.navigator.language) => localeStr.substr(0, 2);
@@ -41,8 +59,11 @@ const messages = { // current fallback strategy is to use the first two letters
const getMessages = (locale = getLocale()) => messages[locale];
const rtlLocales = ['ar', 'he', 'fa', 'ur'];
const isRtl = locale => rtlLocales.includes(locale);
const handleRtl = () => {
if (getLocale() === 'ar') {
if (isRtl(getLocale())) {
document.getElementsByTagName('html')[0].setAttribute('dir', 'rtl');
document.styleSheets[0].disabled = true;
} else {
@@ -50,4 +71,79 @@ const handleRtl = () => {
}
};
export { getLocale, getMessages, handleRtl };
/**
* 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,
};

View File

@@ -1,4 +1,5 @@
import { createSelector } from 'reselect';
import { getLocale, getLanguageList, getCountryList, getCountryMessages, getLanguageMessages } from '../i18n/i18n-loader';
export const formIdSelector = (state, props) => props.formId;
export const authenticationUsernameSelector = state => state.authentication.username;
@@ -84,6 +85,53 @@ export const editableFormSelector = createSelector(
}),
);
// Because this selector has no input selectors, it will only be evaluated once. This is fine
// for now because we don't allow users to change the locale after page load.
// Once we DO allow this, we should create an actual action which dispatches the locale into redux,
// then we can modify this to get the locale from state rather than from getLocale() directly.
// Once we do that, this will work as expected and be re-evaluated when the locale changes.
export const localeSelector = () => getLocale();
export const countryMessagesSelector = createSelector(
localeSelector,
locale => getCountryMessages(locale),
);
export const languageMessagesSelector = createSelector(
localeSelector,
locale => getLanguageMessages(locale),
);
export const sortedLanguagesSelector = createSelector(
localeSelector,
locale => getLanguageList(locale),
);
export const sortedCountriesSelector = createSelector(
localeSelector,
locale => getCountryList(locale),
);
export const preferredLanguageSelector = createSelector(
editableFormSelector,
sortedLanguagesSelector,
languageMessagesSelector,
(editableForm, sortedLanguages, languageMessages) => ({
...editableForm,
sortedLanguages,
languageMessages,
}),
);
export const countrySelector = createSelector(
editableFormSelector,
sortedCountriesSelector,
countryMessagesSelector,
(editableForm, sortedCountries, countryMessages) => ({
...editableForm,
sortedCountries,
countryMessages,
}),
);
export const certificatesSelector = createSelector(
editableFormSelector,
profileCourseCertificatesSelector,