Allow languages and countries to be localized. (#125)
And also sort them by their localized values.
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user