Skip to content
Open

I18n #27

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
2,534 changes: 1,443 additions & 1,091 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"@material-ui/icons": "^5.0.0-alpha.24",
"axios": "^0.18.1",
"date-fns": "^2.16.1",
"i18next": "^19.8.7",
"i18next-icu": "^1.4.2",
"json-query": "^2.2.2",
"moment": "^2.24.0",
"postcss-import": "^12.0.1",
Expand All @@ -17,6 +19,7 @@
"react": "^16.10.1",
"react-dom": "^16.10.1",
"react-draggable": "^3.3.2",
"react-i18next": "^11.8.5",
"react-list": "^0.8.13",
"react-markdown": "^5.0.3",
"react-rendered-size": "^1.1.1",
Expand Down
78 changes: 78 additions & 0 deletions src/I18nProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import i18n from 'i18next';
import ICU from 'i18next-icu';
// import LanguageDetector from 'i18next-browser-languagedetector'; // for now: always en-US
import { initReactI18next, I18nextProvider } from 'react-i18next';

import enTranslations from './translations/en.json';
// translations of FHIR structures from machine to human
import fhirTranslations from './translations/fhir.json';

/**
* https://www.i18next.com/principles/namespaces
*/
const defaultNamespace = 'common';

const defaultLocale = 'en-US';

// https://react.i18next.com/latest/using-with-hooks

i18n.use(initReactI18next).init({
lng: defaultLocale,
fallbackLng: defaultLocale,
ns: [defaultNamespace, 'fhir'],
defaultNS: defaultNamespace,
debug: false,
resources: {
en: {
[defaultNamespace]: enTranslations,
fhir: fhirTranslations,
},
},
returnObjects: true, // support js Arrays and Objects in translation files
saveMissing: true, // enables missingKeyHandler
missingKeyHandler: (lng, ns, key, fallbackValue) => { // detect and fail specs, if missing key detected:
console.warn('warning: missing translation: ', JSON.stringify({ // eslint-disable-line no-console
lng, ns, key, fallbackValue,
}, null, ' '));
},
react: {
useSuspense: false,
transSupportBasicHtmlNodes: true,
transKeepBasicHtmlNodesFor: ['br', 'b', 'i', 'strong'], // limited support for attributes -- use ReactMarkdown for <a href="...">
},
});

const I18nContext = React.createContext({
language: defaultLocale,
setLanguage: () => {},
});

const I18nProvider = ({ children }) => {
const [language, setLanguage] = useState(defaultLocale);

useEffect(() => {
i18n.changeLanguage(language);
}, [language]);

return (
<I18nextProvider i18n={i18n}>
<I18nContext.Provider value={{
language,
setLanguage,
}}
>
{children}
</I18nContext.Provider>
</I18nextProvider>
);
};

I18nProvider.propTypes = {
children: PropTypes.node.isRequired,
};

export { i18n };

export default I18nProvider;
14 changes: 10 additions & 4 deletions src/components/ContentPanel/ContentRight.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
useRecoilValue,
} from 'recoil';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';

import { dotClickContextState } from '../StandardFilters';

Expand Down Expand Up @@ -661,13 +662,16 @@ class ContentPanel extends React.PureComponent {
// console.info('this.isVirtualDisplay: ', this.isVirtualDisplay());
const contents = !this.state.currResources || this.renderDotOrAll();
// console.info('contents: ', contents);

const subCount = this.state.currResources.length;
// const heading = `Displaying ${subCount} of ${this.props.totalResCount} record${this.props.totalResCount === 1 ? '' : 's'}`;
const { t, totalResCount } = this.props;
const heading = t('card-details.heading', { subCount, count: totalResCount });
return (
<div>
<div className="content-panel-inner-title">
<div className="content-panel-inner-title-left">
<div className="content-panel-item-count">
{ `Displaying ${this.state.currResources.length} of ${this.props.totalResCount} record${this.props.totalResCount === 1 ? '' : 's'}` }
{ heading }
</div>

{ config.enableContentPanelLeftRight && (
Expand Down Expand Up @@ -744,19 +748,21 @@ class ContentPanel extends React.PureComponent {
}
}

const ContentPanelHOC = React.memo((props) => {
const ContentPanelHOC = ((props) => {
const dotClickContext = useRecoilValue(dotClickContextState);
const patient = useRecoilValue(patientRecord);
const { t } = useTranslation();

return (
<ContentPanel
{...props} // eslint-disable-line react/jsx-props-no-spreading
context={dotClickContext}
patient={patient}
t={t}
/>
);
});

ContentPanelHOC.propTypes = ContentPanel.propTypes;

export default ContentPanelHOC;
export default (ContentPanelHOC);
23 changes: 15 additions & 8 deletions src/components/cards/MedicationCardBody.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import React from 'react';
import { useTranslation } from 'react-i18next';

import CARD_BODY_LABEL from './cardBodyLabel';
import CardBodyField from './CardBodyField';
import { formatDate } from './GenericCardBody';

const MedicationCardBody = ({ fieldsData }) => {
const { t } = useTranslation();

function formatDosageInstruction() {
if (fieldsData.dosageInstruction?.timing?.repeat) {
const asNeededText = fieldsData.dosageInstruction.asNeededBoolean
? 'as needed'
: 'as instructed'; // what the opposite of As Needed?
const { frequency } = fieldsData.dosageInstruction.timing.repeat;
const { period } = fieldsData.dosageInstruction.timing.repeat;
// DSTU2 / STU3 compatibility
const { periodUnit, periodUnits } = fieldsData.dosageInstruction.timing.repeat;
return `${frequency} every ${period} ${periodUnit || periodUnits} ${asNeededText}`; // need dynamic translation for
const o = fieldsData.dosageInstruction;
return t('fhir:dosageInstruction', { o });

// const asNeededText = fieldsData.dosageInstruction.asNeededBoolean
// ? 'as needed'
// : 'as instructed'; // what the opposite of As Needed?
// const { frequency } = fieldsData.dosageInstruction.timing.repeat;
// const { period } = fieldsData.dosageInstruction.timing.repeat;
// // DSTU2 / STU3 compatibility
// const { periodUnit, periodUnits } = fieldsData.dosageInstruction.timing.repeat;
// return `${frequency} every ${period} ${periodUnit || periodUnits} ${asNeededText}`; // need dynamic translation for
// }
}
return null;
}
Expand Down
26 changes: 16 additions & 10 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@ import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import {
RecoilRoot,
// atom,
// selector,
// useRecoilState,
// useRecoilValue,
} from 'recoil';

import { ThemeProvider, rootTheme } from './themes';
import I18nProvider from './I18nProvider';
import './css/Colors.css';
import './css/Fonts.css';

Expand All @@ -21,12 +18,21 @@ export const PATIENT_MODE_SEGMENT = '/:patientMode(participant|uploaded)';
ReactDOM.render(
<RecoilRoot>
<ThemeProvider theme={rootTheme}>
<Router>
<Switch>
<Route exact path="/" component={ParticipantList} />
<Route path={`${PATIENT_MODE_SEGMENT}/:participantId/:activeView?`} component={DiscoveryApp} />
</Switch>
</Router>
<I18nProvider>
<Router>
<Switch>
<Route
exact
path="/"
component={ParticipantList}
/>
<Route
path={`${PATIENT_MODE_SEGMENT}/:participantId/:activeView?`}
component={DiscoveryApp}
/>
</Switch>
</Router>
</I18nProvider>
</ThemeProvider>
</RecoilRoot>,
document.getElementById('root'),
Expand Down
9 changes: 9 additions & 0 deletions src/translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"glossary": {
"records": "record",
"records_plural": "records"
},
"card-details": {
"heading": "Displaying {{subCount}} of {{count}} $t(glossary.records, {{count}})"
}
}
5 changes: 5 additions & 0 deletions src/translations/fhir.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dosageInstruction": "{{o.timing.repeat.frequency}} every {{o.timing.repeat.period}} {{o.timing.repeat.periodUnits}} $t(fhir:asNeeded, o.asNeededBoolean)",
"asNeeded_true": "as needed",
"asNeeded_false": "as instructed"
}