Skip to content

Commit

Permalink
React i18n (ifmeorg#836)
Browse files Browse the repository at this point in the history
* wip(react-i18n): setup linters for react ci, fix lint errors, update flow-typed, mount path for i18n libs absolute import.

* feat(i18n): display storybook example of a dynamically changing I18n Component.

* feat(i18n): complete the integration of React-i18n to components in Storybook.

* refactor(react): make all components contributed adhere to i18n specifications.

* fix(webpack): fix react-on-rails plugin not understanding resolve.modules in webpack.

* fix(styles): fix up linting rules modification.

* style(Dropdown): add explicit case when locale is null or undefined.

* style(client): clean up styles for code climate #1

* fix(webpack): add glob module to automatically extract all register points in client, make client resolve js and jsx.

* style(codeclimate): adjust javascript mass threshold for duplication.

* chore(codeclimate): pass lint and adjust code climate js duplcation mass weights
  • Loading branch information
nma authored and julianguyen committed Mar 10, 2018
1 parent 89a2ba2 commit 28fd58e
Show file tree
Hide file tree
Showing 29 changed files with 584 additions and 249 deletions.
5 changes: 3 additions & 2 deletions .codeclimate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ engines:
enabled: true
config:
languages:
- ruby
- javascript
ruby: {}
javascript:
mass_threshold: 80
exclude_paths:
- "client/**/__tests__/"
eslint:
Expand Down
2 changes: 1 addition & 1 deletion circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ test:
- RAILS_ENV=test bundle exec rake db:create db:schema:load
- cd client &&
RAILS_ENV=test bundle exec rake react_on_rails:locale &&
yarn run build:test
yarn run lint:setup && yarn run lint && yarn run build:test
override:
- bundle exec rspec --format progress --format RspecJunitFormatter -o
$CIRCLE_TEST_REPORTS/rspec.xml
Expand Down
2 changes: 2 additions & 0 deletions client/.eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
flow-typed/
karma.conf.js
translations.js
default.js
15 changes: 14 additions & 1 deletion client/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@
"/^render.+$/",
"render"
]
}]
}],
"react/prefer-stateless-function": ["off"]
},
"settings": {
"import/resolver": {
"node": {
"paths": [
// allow absolute import from "app/libs/.." as "libs/.."
"./app",
// allow absolute import from "../configs/locales.." as "configs/locales.."
".."
]
}
}
}
}
1 change: 1 addition & 0 deletions client/.flowconfig
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@

[options]
module.name_mapper='.*\(.s?css\)' -> 'empty/object'
module.name_mapper='^libs\(.*\)$' -> '<PROJECT_ROOT>/app/libs\1'
3 changes: 2 additions & 1 deletion client/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.iml
flow-typed/

translations.js
default.js
7 changes: 4 additions & 3 deletions client/.storybook/webpack.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const path = require('path');
const baseConfig = require('../webpack.config.base');

module.exports = Object.assign(baseConfig, {

module.exports = {
module: {
rules: [
{
Expand All @@ -23,4 +24,4 @@ module.exports = {
},
],
},
};
});
1 change: 0 additions & 1 deletion client/app/bundles/momentDashboards/components/Chart.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ const colorSchemes = ['#6D0839', '#66118', '#7F503F', '#775577', '#CCAADD'];
* We wrap the element here in case we want to replace ChartKick with another library.
*/
// We keep the class otherwise our enzyme tests can't reference this component by name
// eslint-disable-next-line react/prefer-stateless-function
export default class Chart extends React.Component<chartShape, {}> {
props: chartShape;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export default class ChartControl extends React.Component<chartControlProp, char

onSelectType(value: string) {
return () => {
this.setState({type: value});
this.setState({ type: value });
};
}

Expand Down
44 changes: 31 additions & 13 deletions client/app/bundles/shared/components/Dropdown/Dropdown.jsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,39 @@
// @flow
import React from 'react';
import shortid from 'shortid';
import isNil from 'lodash/isNil';
import { getAvailableLocales } from 'libs/i18n/I18nUtils';

import css from './Dropdown.scss';

import enYML from '../../../../../../config/locales/en.yml';
const defaultLocales = getAvailableLocales();

type Props = {
onChange: (e: string) => void,
locale: string,
localeList: { [key: string]: string },
};

const options = Object.keys(enYML.en.languages).map(key =>
(<option value={key} key={shortid.generate()} >
{enYML.en.languages[key]}
</option>),
);
export default (variationClassName: string) =>
({ onChange, locale, localeList }: Props) => {
const localeOptions = isNil(localeList) ? defaultLocales : localeList;

export default variationClassName => () => (
<div className={`${css.select_dropdown} ${variationClassName}`}>
<select>
{options}
</select>
</div>
);
const options = Object.keys(localeOptions).map(key =>
(<option value={key} key={shortid.generate()} >
{localeOptions[key]}
</option>),
);

return (
<div className={`${css.select_dropdown} ${variationClassName}`}>
{!isNil(locale) ? (
<select onChange={e => onChange(e.target.value)} value={locale || null}>
{options}
</select>
) : (
<select onChange={e => onChange(e.target.value)}>
{options}
</select>
)}
</div>);
};
52 changes: 33 additions & 19 deletions client/app/bundles/shared/components/Footer/Connect.jsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,39 @@
// @flow
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl } from 'react-intl';
import { defaultMessages } from 'libs/i18n/default';
import css from './Footer.scss';

const Connect = props => (
<ul>
<h6 className={css.footer_header}>Connect</h6>
<li><a href="mailto:join.ifme@gmail.com" target="blank">{props.common.form.email}</a></li>
<li><a href="http://facebook.com/ifmeorg" target="blank">{props.navigation.facebook}</a></li>
<li><a href="https://github.com/ifmeorg/ifme" target="blank">{props.navigation.github}</a></li>
<li><a href="https://www.instagram.com/ifmeorg" target="blank">{props.navigation.instagram}</a></li>
<li><a href="https://medium.com/ifme" target="blank">{props.navigation.medium}</a></li>
<li><a href="https://opencollective.com/ifme" target="blank">{props.navigation.opencollective}</a></li>
<li><a href="http://patreon.com/ifme" target="blank">{props.navigation.patreon}</a></li>
<li><a href="https://medium.com/feed/ifme" target="blank">{props.navigation.rss}</a></li>
<li><a href="http://twitter.com/ifmeorg" target="blank">{props.navigation.twitter}</a></li>
</ul>
);

Connect.propTypes = {
navigation: PropTypes.string.isRequired,
common: PropTypes.string.isRequired,
type Prop = {
intl: Object
};

const NEW_WINDOW_NAME = '_blank';

const links = [
[defaultMessages.commonFormEmail, 'mailto:join.ifme@gmail.com'],
[defaultMessages.navigationFacebook, 'http://facebook.com/ifmeorg'],
[defaultMessages.navigationGithub, 'http://facebook.com/ifmeorg'],
[defaultMessages.navigationInstagram, 'https://www.instagram.com/ifmeorg'],
[defaultMessages.navigationMedium, 'https://medium.com/ifme'],
[defaultMessages.navigationOpencollective, 'https://opencollective.com/ifme'],
[defaultMessages.navigationPatreon, 'http://patreon.com/ifme'],
[defaultMessages.navigationRss, 'https://medium.com/feed/ifme'],
[defaultMessages.navigationTwitter, 'http://twitter.com/ifmeorg'],
];

const Connect = injectIntl(({ intl }: Prop) => {
const { formatMessage } = intl;
return (
<ul>
<h6 className={css.footer_header}>Connect</h6>
{
links.map(([label, url]) => (
<li><a href={url} target={NEW_WINDOW_NAME}>{formatMessage(label)}</a></li>
))
}
</ul>
);
});

export default Connect;
83 changes: 63 additions & 20 deletions client/app/bundles/shared/components/Footer/Footer.jsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,86 @@
// @flow
import React from 'react';
import { IntlProvider, injectIntl } from 'react-intl';
import { defaultMessages, defaultLocale } from 'libs/i18n/default';
import { getMessages } from 'libs/i18n/I18nUtils';
import css from './Footer.scss';
import Resources from './Resources';
import Connect from './Connect';
import Ifme from './Ifme';
import DropdownGhostSmall from '../Dropdown/DropdownGhostSmall';

import enYML from '../../../../../../config/locales/en.yml';
const we = defaultMessages.sharedFooterLicenseWe;
const foss = defaultMessages.sharedFooterLicenseFoss;
const licenseSubtitle = defaultMessages.sharedFooterLicenseSubtitle;
const licenseName = defaultMessages.sharedFooterLicenceName;

const navProp = enYML.en.navigation;
const we = enYML.en.shared.footer.license_we;
const foss = enYML.en.shared.footer.license_foss;
type FooterProps = {
intl: Object,
onChange: (locale: string) => void,
}

const Footer = () => (
<div className={css.footer}>
<div id={css.footer_contest}>
const TableCell = (props: { children: any }) => (<div className={`${css.table_cell}`}>{props.children}</div>);

const InjectedFooter = injectIntl(({ intl, onChange }: FooterProps) => {
const { formatMessage } = intl;
return (
<div className={css.footer}>
<div className={css.table}>
<div className={css.row}>
<div className={`${css.table_cell} ${css.if_me}`}>
<Ifme app_name={enYML.en.app_name} navigation={navProp} />
</div>
<div className={`${css.table_cell} ${css.connect}`}>
<Connect navigation={navProp} common={enYML.en.common} />
</div>
<TableCell className={css.if_me}>
<Ifme />
</TableCell>
<TableCell className={css.connect}>
<Connect />
</TableCell>
<div className={`${css.table_cell} ${css.resources}`}>
<Resources pages={enYML.en.pages.resources} navigation={navProp} />
<Resources />
</div>
<div className={`${css.table_cell} ${css.dropdown}`}>
<DropdownGhostSmall />
<DropdownGhostSmall
onChange={onChange}
locale={intl.locale}
/>
</div>
<div className={`${css.table_cell} ${css.love_foss}`}>
<h4>{we} &hearts; {foss}</h4>
<h4>{formatMessage(we)} &hearts; {formatMessage(foss)}</h4>
<a className={css.license} href="https://github.com/ifmeorg/ifme/blob/master/LICENSE.txt" target="blank" >
{enYML.en.shared.footer.license_subtitle} {enYML.en.shared.footer.licence_name}
{formatMessage(licenseSubtitle, { license: formatMessage(licenseName) })}
</a>
</div>
</div>
</div>
</div>
</div>
);
);
});

type Props = {
locale: string
};

type State = {
locale: string
}

export default class Footer extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
locale: props.locale || defaultLocale,
};
}

export default Footer;
render() {
return (
<IntlProvider
locale={this.state.locale}
key={this.state.locale}
messages={getMessages(this.state.locale)}
>
<InjectedFooter
onChange={selected => this.setState({ locale: selected })}
/>
</IntlProvider>
);
}
}
5 changes: 1 addition & 4 deletions client/app/bundles/shared/components/Footer/Footer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@
width: 100%;
background: rgba(0, 0, 0, 0.5);
font-family: $font-family;

.footer_content {
position: relative;
}
position: relative;

.row {
display: flex;
Expand Down
38 changes: 21 additions & 17 deletions client/app/bundles/shared/components/Footer/Ifme.jsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
// @flow
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl } from 'react-intl';
import { defaultMessages } from 'libs/i18n/default';
import css from './Footer.scss';

const Ifme = props => (
<ul>
<h6 className={css.footer_header}>{props.app_name}</h6>
<li><a href="/about">{props.navigation.about}</a></li>
<li><a href="/blog">{props.navigation.blog}</a></li>
<li><a href="/contribute">{props.navigation.contribute}</a></li>
<li><a href="/faq">{props.navigation.faq}</a></li>
<li><a href="/partners">{props.navigation.partners}</a></li>
<li><a href="/press">{props.navigation.press}</a></li>
<li><a href="/privacy">{props.navigation.privacy}</a></li>
</ul>
);

Ifme.propTypes = {
navigation: PropTypes.string.isRequired,
app_name: PropTypes.string.isRequired,
type Prop = {
intl: Object
};

const Ifme = injectIntl(({ intl }: Prop) => {
const { formatMessage } = intl;
return (
<ul>
<h6 className={css.footer_header}>{formatMessage(defaultMessages.appName)}</h6>
<li><a href="/about">{formatMessage(defaultMessages.navigationAbout)}</a></li>
<li><a href="/blog">{formatMessage(defaultMessages.navigationBlog)}</a></li>
<li><a href="/contribute">{formatMessage(defaultMessages.navigationContribute)}</a></li>
<li><a href="/faq">{formatMessage(defaultMessages.navigationFaq)}</a></li>
<li><a href="/partners">{formatMessage(defaultMessages.navigationPartners)}</a></li>
<li><a href="/press">{formatMessage(defaultMessages.navigationPress)}</a></li>
<li><a href="/privacy">{formatMessage(defaultMessages.navigationPrivacy)}</a></li>
</ul>
);
});

export default Ifme;
Loading

0 comments on commit 28fd58e

Please sign in to comment.