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

(feat) Setup i18n for form engine lib #215

Merged
merged 8 commits into from
Apr 24, 2024
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
1 change: 0 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
dist/
node_modules/
**/*.md
**/*.json
**/*.yml
12 changes: 6 additions & 6 deletions __mocks__/react-i18next.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
const React = require('react');
const reactI18next = require('react-i18next');

const hasChildren = node => node && (node.children || (node.props && node.props.children));
const hasChildren = (node) => node && (node.children || (node.props && node.props.children));

const getChildren = node => (node && node.children ? node.children : node.props && node.props.children);
const getChildren = (node) => (node && node.children ? node.children : node.props && node.props.children);

const renderNodes = reactNodes => {
const renderNodes = (reactNodes) => {
if (typeof reactNodes === 'string') {
return reactNodes;
}
Expand All @@ -29,18 +29,18 @@ const renderNodes = reactNodes => {
});
};

const useMock = [k => k, {}];
const useMock = [(k) => k, {}];
useMock.t = (key, defaultValue) => defaultValue || key;
useMock.i18n = {};

module.exports = {
// this mock makes sure any components using the translate HoC receive the t function as a prop
Trans: ({ children }) => renderNodes(children),
Translation: ({ children }) => children(k => k, { i18n: {} }),
Translation: ({ children }) => children((k) => k, { i18n: {} }),
useTranslation: () => useMock,

// mock if needed
I18nextProvider: reactI18next.I18nextProvider,
I18nextProvider: jest.fn().mockImplementation(({ children }) => children),
initReactI18next: reactI18next.initReactI18next,
setDefaults: reactI18next.setDefaults,
getDefaults: reactI18next.getDefaults,
Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"build": "webpack --mode production",
"coverage": "yarn test --coverage",
"analyze": "webpack --mode=production --env.analyze=true",
"prepare": "husky install"
"prepare": "husky install",
"extract-translations": "i18next 'src/**/*.component.tsx' --config './tools/i18next-parser.config.js'"
},
"browserslist": [
"extends browserslist-config-openmrs"
Expand Down Expand Up @@ -42,6 +43,7 @@
"@openmrs/esm-framework": "5.x",
"@openmrs/esm-patient-common-lib": "7.x",
"dayjs": "1.x",
"i18next": "23.x",
"react": "18.x",
"react-i18next": "11.x",
"rxjs": "6.x",
Expand Down Expand Up @@ -75,7 +77,8 @@
"eslint-plugin-react-hooks": "^4.3.0",
"fork-ts-checker-webpack-plugin": "^6.3.3",
"husky": "^8.0.0",
"i18next": "^21.10.0",
"i18next": "^23.11.2",
"i18next-parser": "^8.13.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"jest-cli": "^29.7.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import { FieldValueView } from '../../value/view/field-value-view.component';
import { FormContext } from '../../../form-context';
import { FormFieldProps } from '../../../types';
import styles from './content-switcher.scss';
import { useTranslation } from 'react-i18next';

export const ContentSwitcher: React.FC<FormFieldProps> = ({ question, onChange, handler, previousValue }) => {
const { t } = useTranslation();
const [field, meta] = useField(question.id);
const { setFieldValue, encounterContext, layoutType, workspaceLayout } = React.useContext(FormContext);
const [errors, setErrors] = useState([]);
Expand Down Expand Up @@ -53,7 +55,7 @@ export const ContentSwitcher: React.FC<FormFieldProps> = ({ question, onChange,
isTrue(question.readonly) ? (
<div className={styles.formField}>
<FieldValueView
label={question.label}
label={t(question.label)}
value={field.value ? handler?.getDisplayValue(question, field.value) : field.value}
conceptName={question.meta?.concept?.display}
isInline={isInline}
Expand All @@ -62,16 +64,12 @@ export const ContentSwitcher: React.FC<FormFieldProps> = ({ question, onChange,
) : (
!question.isHidden && (
<FormGroup
legendText={question.label}
legendText={t(question.label)}
className={classNames({
[styles.errorLegend]: errors.length > 0,
[styles.boldedLegend]: errors.length === 0,
})}>
<Switcher
onChange={handleChange}
selectedIndex={selectedIndex}
className={styles.selectedOption}
size="md">
<Switcher onChange={handleChange} selectedIndex={selectedIndex} className={styles.selectedOption} size="md">
{question.questionOptions.answers.map((option, index) => (
<Switch
name={option.concept || option.value}
Expand Down
4 changes: 2 additions & 2 deletions src/components/inputs/date/date.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ const DateField: React.FC<FormFieldProps> = ({ question, onChange, handler, prev

return encounterContext.sessionMode == 'view' || encounterContext.sessionMode == 'embedded-view' ? (
<FieldValueView
label={question.label}
label={t(question.label)}
value={field.value instanceof Date ? getDisplay(field.value, question.questionOptions.rendering) : field.value}
conceptName={question.meta?.concept?.display}
isInline={isInline}
Expand All @@ -161,7 +161,7 @@ const DateField: React.FC<FormFieldProps> = ({ question, onChange, handler, prev
<DatePickerInput
id={question.id}
placeholder={placeholder}
labelText={question.label}
labelText={t(question.label)}
value={field.value instanceof Date ? field.value.toLocaleDateString(locale) : field.value}
// Added for testing purposes.
// Notes:
Expand Down
4 changes: 2 additions & 2 deletions src/components/inputs/file/file.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const File: React.FC<FileProps> = ({ question, handler }) => {

return encounterContext.sessionMode == 'view' || isTrue(question.readonly) ? (
<div>
<div className={styles.label}>{question.label}</div>
<div className={styles.label}>{t(question.label)}</div>
<div className={styles.uploadSelector}>
<div className={styles.selectorButton}>
<Button disabled={true} onClick={() => setUploadMode('uploader')}>
Expand All @@ -106,7 +106,7 @@ const File: React.FC<FileProps> = ({ question, handler }) => {
</div>
) : (
<div>
<div className={styles.label}>{question.label}</div>
<div className={styles.label}>{t(question.label)}</div>
<div className={styles.uploadSelector}>
<div className={styles.selectorButton}>
<Button onClick={() => setUploadMode('uploader')}>Upload file</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import { FormField } from '../../../types';
import { FormContext } from '../../../form-context';
import { FieldValueView } from '../../value/view/field-value-view.component';
import styles from './encounter-location.scss';
import { useTranslation } from 'react-i18next';

export const EncounterLocationPicker: React.FC<{ question: FormField; onChange: any }> = ({ question }) => {
const { t } = useTranslation();
const [field, meta] = useField(question.id);
const { setEncounterLocation, setFieldValue, layoutType, workspaceLayout, encounterContext } = useContext(FormContext);
const { setEncounterLocation, setFieldValue, layoutType, workspaceLayout, encounterContext } =
useContext(FormContext);
const [locations, setLocations] = useState([]);
const isProcessingSelection = useRef(false);

Expand Down Expand Up @@ -43,7 +46,7 @@ export const EncounterLocationPicker: React.FC<{ question: FormField; onChange:
return encounterContext.sessionMode == 'view' || encounterContext.sessionMode == 'embedded-view' ? (
<div className={styles.formField}>
<FieldValueView
label={question.label}
label={t(question.label)}
value={field.value ? field.value.display : field.value}
conceptName={question.meta?.concept?.display}
isInline={isInline}
Expand All @@ -55,7 +58,7 @@ export const EncounterLocationPicker: React.FC<{ question: FormField; onChange:
className={classNames(styles.boldedLabel, styles.formInputField, styles.multiSelectOverride, styles.flexRow)}>
<ComboBox
id={question.id}
titleText={question.label}
titleText={t(question.label)}
items={locations}
itemToString={(item) => item?.display}
selectedItem={locations.find((item) => item.uuid == field.value)}
Expand Down
10 changes: 5 additions & 5 deletions src/components/inputs/multi-select/multi-select.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const MultiSelect: React.FC<FormFieldProps> = ({ question, onChange, hand
}));

const initiallySelectedQuestionItems = [];
questionItems.forEach(item => {
questionItems.forEach((item) => {
if (field.value?.includes(item.concept)) {
initiallySelectedQuestionItems.push(item);
}
Expand Down Expand Up @@ -84,7 +84,7 @@ export const MultiSelect: React.FC<FormFieldProps> = ({ question, onChange, hand
return encounterContext.sessionMode == 'view' || encounterContext.sessionMode == 'embedded-view' ? (
<div className={styles.formField}>
<FieldValueView
label={question.label}
label={t(question.label)}
value={field.value ? handler?.getDisplayValue(question, field.value) : field.value}
conceptName={question.meta?.concept?.display}
isInline={isInline}
Expand All @@ -98,11 +98,11 @@ export const MultiSelect: React.FC<FormFieldProps> = ({ question, onChange, hand
<FilterableMultiSelect
placeholder={t('search', 'Search') + '...'}
onChange={handleSelectItemsChange}
id={question.label}
id={t(question.label)}
items={questionItems}
initialSelectedItems={initiallySelectedQuestionItems}
label={''}
titleText={question.label}
titleText={t(question.label)}
key={counter}
itemToString={(item) => (item ? item.label : ' ')}
disabled={question.disabled}
Expand All @@ -117,7 +117,7 @@ export const MultiSelect: React.FC<FormFieldProps> = ({ question, onChange, hand
<div className={styles.selectionDisplay}>
{field.value?.length ? (
<UnorderedList className={styles.list}>
{handler?.getDisplayValue(question, field.value)?.map(displayValue => displayValue + ', ')}
{handler?.getDisplayValue(question, field.value)?.map((displayValue) => displayValue + ', ')}
</UnorderedList>
) : (
<ValueEmpty />
Expand Down
6 changes: 4 additions & 2 deletions src/components/inputs/number/number.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import { FieldValueView } from '../../value/view/field-value-view.component';
import { FormFieldProps } from '../../../types';
import { FormContext } from '../../../form-context';
import styles from './number.scss';
import { useTranslation } from 'react-i18next';

const NumberField: React.FC<FormFieldProps> = ({ question, onChange, handler, previousValue }) => {
const [field, meta] = useField(question.id);
const { setFieldValue, encounterContext, layoutType, workspaceLayout, fields } = React.useContext(FormContext);
const [errors, setErrors] = useState([]);
const isFieldRequiredError = useMemo(() => errors[0]?.errCode == fieldRequiredErrCode, [errors]);
const [warnings, setWarnings] = useState([]);
const { t } = useTranslation();

useEffect(() => {
if (question['submission']) {
Expand Down Expand Up @@ -58,7 +60,7 @@ const NumberField: React.FC<FormFieldProps> = ({ question, onChange, handler, pr
return encounterContext.sessionMode == 'view' || encounterContext.sessionMode == 'embedded-view' ? (
<div className={styles.formField}>
<FieldValueView
label={question.label}
label={t(question.label)}
value={field.value ? handler?.getDisplayValue(question, field.value) : field.value}
conceptName={question.meta?.concept?.display}
isInline={isInline}
Expand All @@ -71,7 +73,7 @@ const NumberField: React.FC<FormFieldProps> = ({ question, onChange, handler, pr
id={question.id}
invalid={isFieldRequiredError && errors.length > 0}
invalidText={errors[0]?.message}
label={question.label}
label={t(question.label)}
max={Number(question.questionOptions.max) || undefined}
min={Number(question.questionOptions.min) || undefined}
name={question.id}
Expand Down
6 changes: 4 additions & 2 deletions src/components/inputs/radio/radio.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import { isInlineView } from '../../../utils/form-helper';
import { fieldRequiredErrCode, isEmpty } from '../../../validators/form-validator';
import { FieldValueView } from '../../value/view/field-value-view.component';
import styles from './radio.scss';
import { useTranslation } from 'react-i18next';

const Radio: React.FC<FormFieldProps> = ({ question, onChange, handler, previousValue }) => {
const [field, meta] = useField(question.id);
const { setFieldValue, encounterContext, layoutType, workspaceLayout, fields } = React.useContext(FormContext);
const [errors, setErrors] = useState([]);
const isFieldRequiredError = useMemo(() => errors[0]?.errCode == fieldRequiredErrCode, [errors]);
const [warnings, setWarnings] = useState([]);
const { t } = useTranslation();

useEffect(() => {
if (question['submission']) {
Expand Down Expand Up @@ -50,15 +52,15 @@ const Radio: React.FC<FormFieldProps> = ({ question, onChange, handler, previous
encounterContext.sessionMode == 'embedded-view' ||
isTrue(question.readonly) ? (
<FieldValueView
label={question.label}
label={t(question.label)}
value={field.value ? handler?.getDisplayValue(question, field.value) : field.value}
conceptName={question.meta?.concept?.display}
isInline={isInline}
/>
) : (
!question.isHidden && (
<FormGroup
legendText={question.label}
legendText={t(question.label)}
className={classNames({
[styles.errorLegend]: isFieldRequiredError,
[styles.boldedLegend]: !isFieldRequiredError,
Expand Down
5 changes: 2 additions & 3 deletions src/components/inputs/select/dropdown.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { FormContext } from '../../../form-context';
import { FormFieldProps } from '../../../types';
import styles from './dropdown.scss';


const Dropdown: React.FC<FormFieldProps> = ({ question, onChange, handler, previousValue }) => {
const { t } = useTranslation();
const [field, meta] = useField(question.id);
Expand Down Expand Up @@ -64,7 +63,7 @@ const Dropdown: React.FC<FormFieldProps> = ({ question, onChange, handler, previ

return encounterContext.sessionMode == 'view' || encounterContext.sessionMode == 'embedded-view' ? (
<FieldValueView
label={question.label}
label={t(question.label)}
value={field.value ? handler?.getDisplayValue(question, field.value) : field.value}
conceptName={question.meta?.concept?.display}
isInline={isInline}
Expand All @@ -75,7 +74,7 @@ const Dropdown: React.FC<FormFieldProps> = ({ question, onChange, handler, previ
<Layer>
<DropdownInput
id={question.id}
titleText={question.label}
titleText={t(question.label)}
label={t('chooseAnOption', 'Choose an option')}
items={question.questionOptions.answers
.filter((answer) => !answer.isHidden)
Expand Down
8 changes: 5 additions & 3 deletions src/components/inputs/text-area/text-area.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import { FieldValueView } from '../../value/view/field-value-view.component';
import { FormContext } from '../../../form-context';
import { FormFieldProps } from '../../../types';
import styles from './text-area.scss';
import { useTranslation } from 'react-i18next';

const TextArea: React.FC<FormFieldProps> = ({ question, onChange, handler, previousValue: previousValueProp, }) => {
const TextArea: React.FC<FormFieldProps> = ({ question, onChange, handler, previousValue: previousValueProp }) => {
const { t } = useTranslation();
const [field, meta] = useField(question.id);
const { setFieldValue, encounterContext, layoutType, workspaceLayout } = React.useContext(FormContext);
const [previousValue, setPreviousValue] = useState();
Expand Down Expand Up @@ -52,7 +54,7 @@ const TextArea: React.FC<FormFieldProps> = ({ question, onChange, handler, previ

return encounterContext.sessionMode == 'view' || encounterContext.sessionMode == 'embedded-view' ? (
<FieldValueView
label={question.label}
label={t(question.label)}
value={field.value}
conceptName={question.meta?.concept?.display}
isInline={isInline}
Expand All @@ -68,7 +70,7 @@ const TextArea: React.FC<FormFieldProps> = ({ question, onChange, handler, previ
<TextAreaInput
{...field}
id={question.id}
labelText={question.label}
labelText={t(question.label)}
name={question.id}
value={field.value || ''}
onFocus={() => setPreviousValue(field.value)}
Expand Down
11 changes: 9 additions & 2 deletions src/components/inputs/text/text.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import { isTrue } from '../../../utils/boolean-utils';
import { isInlineView } from '../../../utils/form-helper';
import { FieldValueView } from '../../value/view/field-value-view.component';
import styles from './text.scss';
import { useTranslation } from 'react-i18next';

const TextField: React.FC<FormFieldProps> = ({ question, onChange, handler, previousValue }) => {
const { t } = useTranslation();
const [field, meta] = useField(question.id);
const { setFieldValue, encounterContext, layoutType, workspaceLayout, fields } = React.useContext(FormContext);
const [errors, setErrors] = useState([]);
Expand Down Expand Up @@ -58,7 +60,12 @@ const TextField: React.FC<FormFieldProps> = ({ question, onChange, handler, prev
}, [encounterContext.sessionMode, question.readonly, question.inlineRendering, layoutType, workspaceLayout]);

return encounterContext.sessionMode == 'view' || encounterContext.sessionMode == 'embedded-view' ? (
<FieldValueView label={question.label} value={field.value} conceptName={question.meta?.concept?.display} isInline={isInline} />
<FieldValueView
label={t(question.label)}
value={field.value}
conceptName={question.meta?.concept?.display}
isInline={isInline}
/>
) : (
!question.isHidden && (
<>
Expand All @@ -67,7 +74,7 @@ const TextField: React.FC<FormFieldProps> = ({ question, onChange, handler, prev
<TextInput
{...field}
id={question.id}
labelText={question.label}
labelText={t(question.label)}
name={question.id}
value={field.value || ''}
disabled={question.disabled}
Expand Down
Loading
Loading