diff --git a/src/components/ProfilePage/Country.jsx b/src/components/ProfilePage/Country.jsx
index beeac65..b9acebd 100644
--- a/src/components/ProfilePage/Country.jsx
+++ b/src/components/ProfilePage/Country.jsx
@@ -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 => (
-
+ {sortedCountries.map(({ code, name }) => (
+
))}
{ALL_COUNTRIES[country]}
+{countryMessages[country]}
), empty: ( @@ -119,7 +124,7 @@ class Country extends React.Component {{ALL_COUNTRIES[country]}
+{countryMessages[country]}
), }} @@ -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)); diff --git a/src/components/ProfilePage/PreferredLanguage.jsx b/src/components/ProfilePage/PreferredLanguage.jsx index e58fbd2..b9cebda 100644 --- a/src/components/ProfilePage/PreferredLanguage.jsx +++ b/src/components/ProfilePage/PreferredLanguage.jsx @@ -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 }) => ( ))} @@ -117,7 +116,7 @@ class PreferredLanguage extends React.Component { showVisibility={visibilityLanguageProficiencies !== null} visibility={visibilityLanguageProficiencies} /> -{ALL_LANGUAGES[value]}
+{languageMessages[value]}
), empty: ( @@ -135,7 +134,7 @@ class PreferredLanguage extends React.Component {{ALL_LANGUAGES[value]}
+{languageMessages[value]}
), }} @@ -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)); diff --git a/src/constants/countries.js b/src/constants/countries.js deleted file mode 100644 index eade656..0000000 --- a/src/constants/countries.js +++ /dev/null @@ -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 }; diff --git a/src/constants/languages.js b/src/constants/languages.js deleted file mode 100644 index 9a51877..0000000 --- a/src/constants/languages.js +++ /dev/null @@ -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 }; diff --git a/src/i18n/i18n-loader.js b/src/i18n/i18n-loader.js index 5668d20..f8dd2f2 100644 --- a/src/i18n/i18n-loader.js +++ b/src/i18n/i18n-loader.js @@ -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, +}; diff --git a/src/selectors/ProfilePageSelector.js b/src/selectors/ProfilePageSelector.js index 0206ca0..95b01c5 100644 --- a/src/selectors/ProfilePageSelector.js +++ b/src/selectors/ProfilePageSelector.js @@ -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,