Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add foundational internationalization support via lingui #186

Merged
merged 18 commits into from
Nov 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions client/.env
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
REACT_APP_ROLLBAR_ACCESS_TOKEN=fef86ba10c2f4710925a5737badb6552
REACT_APP_STREETVIEW_API_KEY=AIzaSyCuf0Ca1EvxogvbZQKOBl_40y0UWm4Fk30

# Set this to a non-empty value in an `.env.local` file to actively
# "promote" locales other than English to users.
REACT_APP_ENABLE_PUBLIC_FACING_I18N=
3 changes: 3 additions & 0 deletions client/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*

/src/locales/_build
/src/locales/**/*.js
6 changes: 6 additions & 0 deletions client/.linguirc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"localeDir": "src/locales/",
"srcPathDirs": ["src/"],
"format": "po",
"sourceLocale": "en"
}
11 changes: 9 additions & 2 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@babel/core": "^7.7.2",
"@justfixnyc/geosearch-requester": "0.0.6",
"@lingui/cli": "^2.8.3",
"@lingui/macro": "^2.8.3",
"@lingui/react": "^2.8.3",
"@types/gtag.js": "^0.0.2",
"@types/jest": "^24.0.6",
"@types/lingui__react": "^2.8.1",
"@types/node": "^11.9.4",
"@types/react": "^16.8.3",
"@types/react-dom": "^16.8.2",
"@types/react-modal": "^3.8.2",
"@types/react-router-dom": "^4.3.4",
"babel-core": "^7.0.0-bridge.0",
"babel-polyfill": "^6.26.0",
"chart.js": "^2.8.0",
"chartjs-plugin-annotation": "^0.5.7",
Expand Down Expand Up @@ -41,11 +47,12 @@
"typescript": "^3.3.3"
},
"scripts": {
"lingui": "lingui",
"build-css": "node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/",
"watch-css": "yarn build-css && node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/ --watch --recursive",
"start-js": "cross-env NODE_PATH=src:src/components:src/containers:src/styles react-scripts start",
"start-js": "lingui compile && cross-env NODE_PATH=src:src/components:src/containers:src/styles react-scripts start",
"start": "npm-run-all -p watch-css start-js",
"build": "yarn build-css && cross-env NODE_PATH=src:src/components:src/containers:src/styles react-scripts build",
"build": "yarn build-css && lingui compile && cross-env NODE_PATH=src:src/components:src/containers:src/styles react-scripts build",
"test": "react-scripts test --env=jsdom --transformIgnorePatterns \"node_modules/(?!@justfixnyc)/\"",
"eject": "react-scripts eject"
},
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/AddressToolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { LocaleLink as Link } from '../i18n';
import Modal from './Modal';

import 'styles/AddressToolbar.css';
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/DetailView.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { CSSTransition } from 'react-transition-group';
import { Link } from 'react-router-dom';
import { LocaleLink as Link } from '../i18n';
import { StreetViewPanorama } from 'react-google-maps';
import Helpers from 'util/helpers';
import Browser from 'util/browser';
Expand Down
13 changes: 11 additions & 2 deletions client/src/components/IndicatorsViz.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import React, { Component } from 'react';
import { Bar } from 'react-chartjs-2';
import { I18n } from '@lingui/react';
import { t } from '@lingui/macro';

// reference: https://github.com/jerairrest/react-chartjs-2

import * as ChartAnnotation from 'chartjs-plugin-annotation';
Expand Down Expand Up @@ -57,7 +60,11 @@ export default class IndicatorsViz extends Component {
}

render() {
return <IndicatorsVizImplementation {...this.state} />;
return (
<I18n>
{({ i18n }) => <IndicatorsVizImplementation {...this.state} i18n={i18n} />}
</I18n>
);
}
}

Expand Down Expand Up @@ -128,6 +135,8 @@ class IndicatorsVizImplementation extends Component {
// Create "data" object according to Chart.js documentation
var datasets;

const { i18n } = this.props;

switch (this.props.activeVis) {
case 'viols':
datasets =
Expand Down Expand Up @@ -156,7 +165,7 @@ class IndicatorsVizImplementation extends Component {
case 'complaints':
datasets =
[{
label: 'Emergency',
label: i18n._(t`Emergency`),
data: this.groupData(this.props.complaintsData.values.emergency),
backgroundColor: 'rgba(227,74,51, 0.6)',
borderColor: 'rgba(227,74,51,1)',
Expand Down
11 changes: 5 additions & 6 deletions client/src/components/PropertiesSummary.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { Component } from 'react';
import { Trans, Plural } from '@lingui/macro';

import Loader from 'components/Loader';
import LegalFooter from 'components/LegalFooter';
Expand Down Expand Up @@ -34,6 +35,7 @@ export default class PropertiesSummary extends Component {

render() {
let agg = this.state.agg;
let {bldgs, units, age} = agg || {};

return (
<div className="Page PropertiesSummary">
Expand All @@ -44,12 +46,9 @@ export default class PropertiesSummary extends Component {
<div>
<h6>General info</h6>
<p>
There {parseInt(agg.bldgs) === 1 ?
<span>is <b>1</b> building </span> :
<span>are <b>{agg.bldgs}</b> buildings </span>}
in this portfolio with a total of {agg.units} unit{parseInt(agg.units) === 1 ? "" : "s"}.
The {parseInt(agg.bldgs) === 1 ? "" : "average"} age of {parseInt(agg.bldgs) === 1 ? "this building " : "these buildings "}
is <b>{agg.age}</b> years old.
<Trans>There <Plural value={bldgs} one={<span>is <b>1</b> building</span>} other={<span>are <b>{bldgs}</b> buildings</span>} /> in this portfolio with a total of <Plural value={units} one="1 unit" other="# units" />.</Trans>
{` `}
<Trans>The <Plural value={bldgs} one="" other="average" /> age of <Plural value={bldgs} one="this building" other="these buildings" /> is <b>{age}</b> years old.</Trans>
</p>
<aside>
{agg.violationsaddr && (
Expand Down
85 changes: 44 additions & 41 deletions client/src/containers/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import {
BrowserRouter as Router,
Route,
Switch,
NavLink,
Link
} from 'react-router-dom';
import { Trans } from '@lingui/macro';

import 'styles/App.css';

// import top-level containers (i.e. pages)
import { I18n, LocaleNavLink, LocaleLink as Link, LocaleSwitcher } from '../i18n';
import HomePage from 'HomePage';
import AddressPage from 'AddressPage';
import BBLPage from 'BBLPage';
Expand All @@ -34,46 +34,49 @@ constructor(props) {
render() {
return (
<Router>
<ScrollToTop>
<div className="App">
<div className="App__warning old_safari_only">
<h3>Warning! This site doesn't fully work on older versions of Safari. Try a <a href="http://outdatedbrowser.com/en">modern browser</a>.</h3>
<I18n>
<ScrollToTop>
<div className="App">
<div className="App__warning old_safari_only">
<h3>Warning! This site doesn't fully work on older versions of Safari. Try a <a href="http://outdatedbrowser.com/en">modern browser</a>.</h3>
</div>
<div className="App__header">
<Link onClick={() => {window.gtag('event', 'site-title');}} to="/">
<Trans render="h4">Who owns what in nyc?</Trans>
</Link>
<nav className="inline">
<LocaleNavLink exact to="/">Home</LocaleNavLink>
<LocaleNavLink to="/about">About</LocaleNavLink>
<LocaleNavLink to="/how-it-works">How it Works</LocaleNavLink>
<a href="https://www.justfix.nyc/donate">Donate</a>
<a href="#" // eslint-disable-line jsx-a11y/anchor-is-valid
onClick={() => this.setState({ showEngageModal: true })}>
Share
</a>
<LocaleSwitcher/>
</nav>
<Modal
showModal={this.state.showEngageModal}
onClose={() => this.setState({ showEngageModal: false })}>
<h5 className="first-header">Share this page with your neighbors:</h5>
<SocialShare location="share-modal" />
</Modal>
</div>
<div className="App__body">
<Switch>
<Route exact path="/:locale/" component={HomePage} />
<Route path="/:locale/address/:boro/:housenumber/:streetname" component={AddressPage} />
<Route path="/:locale/bbl/:boro/:block/:lot" component={BBLPage} />
<Route path="/:locale/bbl/:bbl" component={BBLPage} />
<Route path="/:locale/about" component={AboutPage} />
<Route path="/:locale/how-it-works" component={HowItWorksPage} />
<Route path="/:locale/terms-of-use" component={TermsOfUsePage} />
<Route path="/:locale/privacy-policy" component={PrivacyPolicyPage} />
</Switch>
</div>
</div>
<div className="App__header">
<Link onClick={() => {window.gtag('event', 'site-title');}} to="/">
<h4>Who owns what in nyc?</h4>
</Link>
<nav className="inline">
<NavLink exact to="/">Home</NavLink>
<NavLink to="/about">About</NavLink>
<NavLink to="/how-it-works">How it Works</NavLink>
<a href="https://www.justfix.nyc/donate">Donate</a>
<a href="#" // eslint-disable-line jsx-a11y/anchor-is-valid
onClick={() => this.setState({ showEngageModal: true })}>
Share
</a>
</nav>
<Modal
showModal={this.state.showEngageModal}
onClose={() => this.setState({ showEngageModal: false })}>
<h5 className="first-header">Share this page with your neighbors:</h5>
<SocialShare location="share-modal" />
</Modal>
</div>
<div className="App__body">
<Switch>
<Route exact path="/" component={HomePage} />
<Route path="/address/:boro/:housenumber/:streetname" component={AddressPage} />
<Route path="/bbl/:boro/:block/:lot" component={BBLPage} />
<Route path="/bbl/:bbl" component={BBLPage} />
<Route path="/about" component={AboutPage} />
<Route path="/how-it-works" component={HowItWorksPage} />
<Route path="/terms-of-use" component={TermsOfUsePage} />
<Route path="/privacy-policy" component={PrivacyPolicyPage} />
</Switch>
</div>
</div>
</ScrollToTop>
</ScrollToTop>
</I18n>
</Router>
);
}
Expand Down
2 changes: 1 addition & 1 deletion client/src/containers/BBLPage.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { Redirect } from 'react-router-dom';

import { LocaleRedirect as Redirect } from '../i18n';
import Loader from 'components/Loader';
import APIClient from 'components/APIClient';
import NotRegisteredPage from './NotRegisteredPage';
Expand Down
2 changes: 1 addition & 1 deletion client/src/containers/HomePage.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { Component } from 'react';
import { Redirect, Link } from 'react-router-dom';

import Loader from 'components/Loader';
import APIClient from 'components/APIClient';
Expand All @@ -8,6 +7,7 @@ import LegalFooter from 'components/LegalFooter';

import 'styles/HomePage.css';

import { LocaleLink as Link, LocaleRedirect as Redirect } from '../i18n';
import westminsterLogo from '../assets/img/westminster.svg';
import allyearLogo from '../assets/img/allyear.png';
import emLogo from '../assets/img/emassociates.jpg';
Expand Down
2 changes: 1 addition & 1 deletion client/src/containers/NotRegisteredPage.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { LocaleLink as Link } from '../i18n';
import Modal from 'components/Modal';
import LegalFooter from 'components/LegalFooter';
import Helpers from 'util/helpers';
Expand Down
2 changes: 1 addition & 1 deletion client/src/containers/NychaPage.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { LocaleLink as Link } from '../i18n';
import Browser from 'util/browser';
import LegalFooter from 'components/LegalFooter';
import Helpers from 'util/helpers';
Expand Down
48 changes: 48 additions & 0 deletions client/src/i18n.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { removeLocalePrefix, parseLocaleFromPath, isSupportedLocale, localeFromRouter, localePrefixPath } from "./i18n";

function routerProps(pathname: string): any {
return { location: { pathname }};
}

describe("i18n", () => {
it("removes locale prefixes from paths", () => {
expect(removeLocalePrefix("/en/boop")).toBe("/boop");
});

it("parses locales from paths when present", () => {
expect(parseLocaleFromPath("/es/blarf")).toBe('es');
});

it("parses nothing from paths when locale is not present", () => {
expect(parseLocaleFromPath("/blarf")).toBe(null);
});

it("tells us whether a locale is supported or not", () => {
expect(isSupportedLocale('en')).toBe(true);
expect(isSupportedLocale('zz')).toBe(false);
});

it("extracts locale from router path", () => {
expect(localeFromRouter(routerProps('/es/boop'))).toBe('es');
});

it("raises an assertion failure when locale is not present in router path", () => {
expect(() => localeFromRouter(routerProps('/boop'))).toThrow(
'"/boop" does not start with a valid locale!'
);
});

it("prefixes string paths with current locale", () => {
expect(localePrefixPath(routerProps('/es/narg'), '/boop/flarg')).toBe('/es/boop/flarg');
});

it("prefixes object paths with current locale", () => {
expect(localePrefixPath(routerProps('/es/narg'), {
pathname: '/boop/flarg',
search: '?foo'
})).toEqual({
pathname: '/es/boop/flarg',
search: '?foo'
});
});
});
Loading