Skip to content
This repository has been archived by the owner on Feb 12, 2024. It is now read-only.

Commit

Permalink
Add functionality for i18n within application (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
zplata authored Oct 16, 2017
1 parent 58146e0 commit dbf3aa5
Show file tree
Hide file tree
Showing 83 changed files with 10,900 additions and 442 deletions.
6 changes: 4 additions & 2 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
"FHIR": true,
"document": true,
"Canadarm": true,
"canadarmConfig": true
"canadarmConfig": true,
"navigator": true
},
"rules": {
"class-methods-use-this": ["error", {"exceptMethods": ["handleSubmit", "handleKeyPress", "shouldComponentUpdate", "render"]}],
"no-console": 0,
"jsx-a11y/no-static-element-interactions": 0
"jsx-a11y/no-static-element-interactions": 0,
"react/prefer-stateless-function": [0, { "ignorePureComponents": true }]
}
}
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CHANGELOG

* 1.4.0
- Add i18n functionality to allow for specific language/locale
- Replaced fhir-client library with the latest version
- Allow for unknown gender option to be pulled in via FHIR
* 1.3.0
- Update UI for the risk factors view to be print-friendly
* 1.2.0
Expand Down
1 change: 1 addition & 0 deletions app/__mocks__/fileMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'test-file-stub';
7 changes: 6 additions & 1 deletion app/__mocks__/load_fhir_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,16 @@ ASCVDRisk.computeLifetimeRisk = jest.fn(() => 35);
ASCVDRisk.computeLowestLifetime = jest.fn(() => 5);
ASCVDRisk.computePotentialRisk = jest.fn(() => 20);
ASCVDRisk.canCalculateScore = jest.fn(() => true);
ASCVDRisk.missingFields = jest.fn();
ASCVDRisk.missingFields = jest.fn(() => []);
ASCVDRisk.isValidAge = jest.fn(() => true);
ASCVDRisk.isValidTotalCholesterol = jest.fn(() => false);
ASCVDRisk.isValidSysBP = jest.fn(() => true);
ASCVDRisk.isValidHDL = jest.fn(() => true);
ASCVDRisk.computeBirthDateFromAge = jest.fn();

// Test function to change gender in Jest component tests.
ASCVDRisk.changeGender = (gender) => {
ASCVDRisk.patientInfo.gender = gender;
};

module.exports = ASCVDRisk;
37 changes: 10 additions & 27 deletions app/load_fhir_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -673,50 +673,33 @@ window.ASCVDRisk = window.ASCVDRisk || {};
const missingFields = () => {
const needInput = [];
if (!ASCVDRisk.isValidTotalCholesterol(ASCVDRisk.patientInfo.totalCholesterol)) {
needInput.push('Total cholesterol');
needInput.push('formTotalCholesterol');
}
if (ASCVDRisk.patientInfo.relatedFactors.diabetic === undefined) {
needInput.push('Diabetes status');
needInput.push('formDiabetic');
}
if (!ASCVDRisk.isValidAge(ASCVDRisk.patientInfo.age)) {
needInput.push('Age');
needInput.push('formAge');
}
if (!ASCVDRisk.isValidHDL(ASCVDRisk.patientInfo.hdl)) {
needInput.push('HDL - cholesterol');
needInput.push('formHdl');
}
if (ASCVDRisk.patientInfo.relatedFactors.smoker === undefined) {
needInput.push('Current smoking status');
needInput.push('formSmoker');
}
if (ASCVDRisk.patientInfo.relatedFactors.race === undefined) {
needInput.push('Race');
needInput.push('formRace');
}
if (!ASCVDRisk.isValidSysBP(ASCVDRisk.patientInfo.systolicBloodPressure)) {
needInput.push('Systolic blood pressure');
needInput.push('formSysBP');
}
if (ASCVDRisk.patientInfo.relatedFactors.hypertensive === undefined) {
needInput.push('Hypertension status');
needInput.push('formHypertensive');
}
if (ASCVDRisk.patientInfo.gender === undefined) {
needInput.push('Gender');
needInput.push('formSex');
}
if (needInput.length === 9) {
return 'All fields required to compute ASCVD risk';
} else if (needInput.length > 2) {
let retStatement = '';
for (let i = 0; i < needInput.length; i += 1) {
if (i === needInput.length - 1) {
retStatement += `and ${needInput[i]} are all required to compute ASCVD risk`;
} else {
retStatement += `${needInput[i]}, `;
}
}
return retStatement;
} else if (needInput.length === 2) {
return `${needInput[0]} and ${needInput[1]} are both required to compute ASCVD risk`;
} else if (needInput.length === 1) {
return `${needInput[0]} is required to compute ASCVD risk`;
} else if (needInput.length === 0) { return ''; }
return '';
return needInput;
};
ASCVDRisk.missingFields = missingFields;
ASCVDRisk.patientInfo = PatientInfo;
Expand Down
2 changes: 1 addition & 1 deletion build/css/styles.css

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions build/images/Settings.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 15 additions & 12 deletions build/js/bundle.js

Large diffs are not rendered by default.

66 changes: 61 additions & 5 deletions components/App/app.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { intlShape } from 'react-intl';
import ASCVDRisk from '../../app/load_fhir_data';
import PatientBanner from '../../components/PatientBanner/banner';
import Header from '../../components/Header/header';
Expand All @@ -17,6 +18,7 @@ class App extends React.Component {
this.updateRiskScores = this.updateRiskScores.bind(this);
this.updateChangedProperty = this.updateChangedProperty.bind(this);
this.setView = this.setView.bind(this);
this.getBirthDateDisplay = this.getBirthDateDisplay.bind(this);
this.addOption = this.addOption.bind(this);
this.removeOption = this.removeOption.bind(this);
this.state = {
Expand Down Expand Up @@ -65,6 +67,8 @@ class App extends React.Component {
updateChangedProperty={this.updateChangedProperty}
options={this.state.options}
removeOption={this.removeOption}
intl={this.props.intl}
currentLocale={this.props.currentLocale}
/>
);
} else if (this.state.view === 'Risk Factors') {
Expand All @@ -77,13 +81,34 @@ class App extends React.Component {
lifetimeBest={ASCVDRisk.computeLowestLifetime()}
options={this.state.options}
removeOption={this.removeOption}
intl={this.props.intl}
currentLocale={this.props.currentLocale}
/>
);
}
return (
<Recommendations />
<Recommendations intl={this.props.intl} />
);
}
/**
* Displays a user-friendly date of birth
* @returns {string} - Internationalized string representing a possible date of birth
*/
getBirthDateDisplay() {
const propIntl = this.props.intl;
const messages = propIntl.messages;
let birthDateDisplay = '';
const date = ASCVDRisk.patientInfo.dateOfBirth;
if (Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date.getTime())) {
birthDateDisplay = propIntl.formatMessage(messages.bannerDate, {
date: (`0${date.getDate()}`).slice(-2),
month: (`0${date.getMonth() + 1}`).slice(-2),
year: date.getFullYear(),
});
}
return birthDateDisplay;
}


/**
* Adds a risk factor option to options property
Expand Down Expand Up @@ -137,16 +162,40 @@ class App extends React.Component {
}

render() {
const propIntl = this.props.intl;
const messages = propIntl.messages;
let gender;
if (ASCVDRisk.patientInfo.gender &&
ASCVDRisk.patientInfo.gender.toLowerCase() === 'female') {
gender = propIntl.formatMessage(messages.bannerFemale);
} else if (ASCVDRisk.patientInfo.gender &&
ASCVDRisk.patientInfo.gender.toLowerCase() === 'male') {
gender = propIntl.formatMessage(messages.bannerMale);
} else {
gender = propIntl.formatMessage(messages.bannerUnknownGender);
}
return (
<div>
<PatientBanner
hideBanner={ASCVDRisk.hideDemoBanner}
name={`${ASCVDRisk.patientInfo.firstName} ${ASCVDRisk.patientInfo.lastName}`}
age={ASCVDRisk.patientInfo.age}
gender={ASCVDRisk.patientInfo.gender}
dob={ASCVDRisk.patientInfo.dateOfBirth}
age={propIntl.formatMessage(messages.bannerYears,
{ age: propIntl.formatNumber(ASCVDRisk.patientInfo.age) })}
gender={gender}
dobPrompt={propIntl.formatMessage(messages.bannerDOB)}
dob={this.getBirthDateDisplay()}
/>
<Header
header={propIntl.formatMessage(messages.appHeader)}
languagePrompt={propIntl.formatMessage(messages.language)}
locales={[
{ name: propIntl.formatMessage(messages.enUS), val: 'en' },
{ name: propIntl.formatMessage(messages.enUK), val: 'en-GB' },
{ name: propIntl.formatMessage(messages.es), val: 'es' },
]}
updateLocale={this.props.updateLocale}
currentLocale={this.props.currentLocale}
/>
<Header header={'ASCVD Risk Calculator'} />
<Navbar
updateView={this.updateView}
tab_one={'Results'}
Expand All @@ -155,11 +204,18 @@ class App extends React.Component {
tabIndex={this.state.tabIndex}
hideNav={this.state.hideNav}
changedProperty={this.state.changedProperty}
intl={propIntl}
currentLocale={this.props.currentLocale}
/>
{ this.setView() }
</div>
);
}
}
App.propTypes = {
intl: intlShape,
currentLocale: React.PropTypes.string,
updateLocale: React.PropTypes.func.isRequired,
};

export default App;
3 changes: 3 additions & 0 deletions components/App/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { injectIntl } from 'react-intl';
import App from './app';
export default injectIntl(App);
53 changes: 53 additions & 0 deletions components/Entry/entry.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import { IntlProvider, addLocaleData } from 'react-intl';
import en from 'react-intl/locale-data/en';
import es from 'react-intl/locale-data/es';
import App from '../App/index';
import localeData from '../../locales/translations.json';

addLocaleData([...en, ...es]);

class Entry extends React.Component {
constructor(props) {
super(props);
this.updateLocale = this.updateLocale.bind(this);

// Define the user's language. Accounts for pulling this from different browsers
const language = (navigator.languages && navigator.languages[0]) ||
navigator.language || navigator.userLanguage;

this.state = {
locale: language,
};
}

updateLocale(locale) {
this.setState({
locale,
});
}

render() {
let languageWithoutRegionCode;
if (this.state.locale) {
// Split locales with a region code
languageWithoutRegionCode = this.state.locale.toLowerCase().split(/[_-]+/)[0];
}

const messages = localeData[this.state.locale]
|| localeData[languageWithoutRegionCode]
|| localeData.en;

return (
<IntlProvider
locale={this.state.locale}
defaultLocale={this.state.locale}
messages={messages}
>
<App updateLocale={this.updateLocale} currentLocale={this.state.locale} />
</IntlProvider>
);
}
}

export default Entry;
24 changes: 16 additions & 8 deletions components/Form/ButtonForm/button_form.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import cx from 'classnames';
import { intlShape } from 'react-intl';
import ASCVDRisk from '../../../app/load_fhir_data';
import styles from './button_form.css';

Expand Down Expand Up @@ -75,22 +76,28 @@ class ButtonForm extends React.Component {


render() {
const propIntl = this.props.intl;
const messages = propIntl.messages;
return (
<div className={styles.container}>
<div className={cx(styles.prompt, this.missingField())}>{this.props.prompt}</div>
<div className={styles.left}>
<input
<button
className={cx(styles.btn,
ButtonForm.isActive(this.props.option_one, this.props.property))}
type="button" onClick={this.handleSubmit} value={this.props.option_one}
/>
ButtonForm.isActive(this.props.option_one, this.props.property))}
onClick={this.handleSubmit} value={this.props.option_one}
>
{propIntl.formatMessage(messages[`form${this.props.option_one}`])}
</button>
</div>
<div className={styles.right}>
<input
<button
className={cx(styles.btn,
ButtonForm.isActive(this.props.option_two, this.props.property))}
type="button" onClick={this.handleSubmit} value={this.props.option_two}
/>
ButtonForm.isActive(this.props.option_two, this.props.property))}
onClick={this.handleSubmit} value={this.props.option_two}
>
{propIntl.formatMessage(messages[`form${this.props.option_two}`])}
</button>
</div>
</div>
);
Expand All @@ -105,6 +112,7 @@ ButtonForm.propTypes = {
prompt: React.PropTypes.string.isRequired,
property: React.PropTypes.string.isRequired,
removeOption: React.PropTypes.func,
intl: intlShape,
};

export default ButtonForm;
Loading

0 comments on commit dbf3aa5

Please sign in to comment.