Commit 571e1eaa authored by Matija Obreza's avatar Matija Obreza

Merge branch '402-i18n' into 'master'

Resolve "i18n"

Closes #402

See merge request !318
parents dab5265b eab4fc9f
Pipeline #7459 passed with stages
in 7 minutes and 59 seconds
const fg = require('fast-glob');
const _ = require('lodash');
const fs = require('fs');
const getPrefix = (path) => path.substring(path.indexOf('./src/') + './src/'.length, path.indexOf('/translations.json'));
fg([`./src/**/translations.json`, `./src/translations.json`])
.then((entries) => entries.sort((a, b) => getPrefix(a).localeCompare(getPrefix(b))))
.then((entries) => entries.map((path) => getTranslations(path)))
.then((content) => fs.writeFileSync(`locales/en/translations.json`, `{\n${content}}`));
const getTranslations = (path) => {
const prefix = getPrefix(path);
console.log('Loading translations of module', prefix);
const fileContent = fs.readFileSync(path, 'utf8');
return prefix !== '/' ? _(`"${prefix}": ${fileContent}\n`).value() : fileContent.substr(1, fileContent.length - 2);
};
{ {
"action": { "action": {
"add": "Add {{what, lowercase}}", "add": "Add {{what, lowercase}}",
"approve": "Approve",
"backTo": "Back to {{where, lowercase}}", "backTo": "Back to {{where, lowercase}}",
"backToDashboard": "Back to dashboard",
"cancel": "Cancel", "cancel": "Cancel",
"close": "Close",
"collapse": "Collapse",
"delete": "Delete", "delete": "Delete",
"download": "Download",
"edit": "Edit", "edit": "Edit",
"login": "Login", "login": "Login",
"logout": "Logout", "logout": "Logout",
"publish": "Publish",
"saveChanges": "Save changes",
"search": "Search", "search": "Search",
"viewDetails": "View details" "unpublish": "Unpublish",
"un-publish": "Un-publish",
"viewDetails": "View details",
"applyFilters": "Apply filters",
"back": "Back",
"backToList": "Back to list",
"create": "Create",
"deleteData": "Delete data",
"manage": "Manage",
"nextStep": "Next step",
"openSidebar": "Open sidebar",
"read": "Read",
"reject": "Reject",
"remove": "Remove",
"reset": "Reset",
"return": "Return",
"save": "Save",
"show": "Show",
"sendToReview": "Send to review",
"view": "View",
"write": "Write",
"acceptAndPublish": "ACCEPT AND PUBLISH"
},
"csv": {
"autoDetect": "Auto-detect ",
"autoDetectLabel": "Auto-detect CSV settings",
"comma": "Comma",
"configuration": "CSV configuration",
"escapeCharacter": "Escape character",
"other": "Other",
"quoteCharacter": "Quote character",
"semicolon": "Semicolon",
"separator": "Separator character",
"space": "Space",
"tab": "Tab",
"typeYourEscapeCharacter": "Type your escape character",
"typeYourQuoteCharacter": "Type your quote character",
"typeYourSeparator": "Type your separator"
},
"f": {
"dateSearch": "Date search",
"keyword": "Keyword search",
"fromIncluding": "From including",
"lastModifiedDate": "Last modified date",
"textSearch": "Text search"
}, },
"label": { "label": {
"item": "Item", "item": "Item",
"item_plural": "Items", "item_plural": "Items",
"list": "List", "list": "List",
"list_plural": "Lists" "list_plural": "Lists",
"created": "Created",
"dateNotProvided": "Date not provided",
"description": "Description",
"either": "Either",
"filters": "Filters",
"from": "From",
"itemEditorWarn": "Don't use the ItemsEditor for more than 100 items!",
"lastModified": "Last modified",
"lastUpdated": "Last updated",
"modified": "Modified",
"no": "No",
"prettyNumber": "{{value, number}}",
"registered": "Registered",
"sortBy": "Sort By",
"status": "Status",
"stepsForDataPublication": "Steps for data publication completion",
"title": "Title",
"to": "To",
"untitled": "Untitled",
"UUID": "UUID",
"version": "Version {{version, numeric}}",
"yes": "Yes",
"basicMarkdown": "Basic markdown supported",
"fullMarkdown": "Full markdown supported",
"previewMarkdown": "Preview markdown"
}, },
"message": { "message": {
"confirmDelete": "Deleting the {{what, lowercase}} record is only possible when there is no associated data." "confirmDelete": "Deleting the {{what, lowercase}} record is only possible when there is no associated data."
}, },
"paginate": { "paginate": {
"numberOfItems": "{{count}} {{what, lowercase}}" "numberOfItems": "{{count}} {{what, lowercase}}",
"estimatedNumberOfItems": "About {{count, number}} {{what, lowercase}}"
},
"permissions": {
"label": "Permissions",
"managePermissions": "Manage permissions",
"class": "Class",
"objectID": "Object ID",
"owner": "Owner",
"inheritsPermissions": "inheritsPermissions",
"effectivePermissions": "Effective permissions",
"sid": "Sid"
},
"snackbar": {
"resettingFilters": "Resetting filters...",
"applyingFilters": "Applying filters..."
},
"sort": {
"title": "Title",
"version": "Version",
"owner": "Owner",
"createdDate": "Created date",
"latestEdit": "Latest edit",
"lastModifiedDateAsc": "Last modified date (oldest first)",
"lastModifiedDateDesc": "Last update (newest first)"
},
"status": {
"published": "Published",
"inReview": "In review",
"inProgress": "In progress"
} }
} }
This diff is collapsed.
This diff is collapsed.
...@@ -19,17 +19,18 @@ ...@@ -19,17 +19,18 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"clean": "rimraf target", "clean": "rimraf target",
"build": "webpack --config config/webpack-development.config.js", "build": "npm run generateI18n && webpack --config config/webpack-development.config.js",
"build:production": "cross-env NODE_ENV=production SSR=true webpack --config config/webpack-production.config.js", "build:production": "cross-env NODE_ENV=production SSR=true webpack --config config/webpack-production.config.js",
"build:server": "cross-env NODE_ENV=production SSR=true webpack --config config/server.config.js", "build:server": "cross-env NODE_ENV=production SSR=true webpack --config config/server.config.js",
"build:serverdebug": "cross-env SSR=true webpack --config config/server.config.js", "build:serverdebug": "cross-env SSR=true webpack --config config/server.config.js",
"serve": "webpack-dev-server --config config/webpack-development.config.js", "serve": "npm run generateI18n && webpack-dev-server --config config/webpack-development.config.js",
"serve:production": "cross-env NODE_ENV=production webpack-dev-server --config config/webpack-production.config.js", "serve:production": "cross-env NODE_ENV=production webpack-dev-server --config config/webpack-production.config.js",
"build:ssr": "rimraf target && npm run build:production && npm run build:server", "build:ssr": "npm run generateI18n && rimraf target && npm run build:production && npm run build:server",
"serve:ssr": "npm run build:ssr && cd target/app/server && cross-env SSR=true node server.js", "serve:ssr": "npm run build:ssr && cd target/app/server && cross-env SSR=true node server.js",
"build:ssrdebug": "rimraf target && npm run build && npm run build:serverdebug", "build:ssrdebug": "rimraf target && npm run build && npm run build:serverdebug",
"debug:ssr": "npm run build:ssrdebug && cd target/app/server && cross-env SSR=true node server.js", "debug:ssr": "npm run build:ssrdebug && cd target/app/server && cross-env SSR=true node server.js",
"ssr": "cd target/app/server && cross-env SSR=true node server.js", "ssr": "cd target/app/server && cross-env SSR=true node server.js",
"generateI18n": "node i18n/generateI18n.ts",
"i18nscan": "i18next-scanner --config i18next-scanner.config.js 'src/**/*.tsx'" "i18nscan": "i18next-scanner --config i18next-scanner.config.js 'src/**/*.tsx'"
}, },
"dependencies": { "dependencies": {
...@@ -47,6 +48,7 @@ ...@@ -47,6 +48,7 @@
"es-cookie": "^1.2.0", "es-cookie": "^1.2.0",
"express": "^4.16.3", "express": "^4.16.3",
"express-http-proxy": "^1.2.0", "express-http-proxy": "^1.2.0",
"fast-glob": "^2.2.3",
"flattenjs": "^1.0.4", "flattenjs": "^1.0.4",
"form-data": "^2.3.2", "form-data": "^2.3.2",
"history": "^4.7.2", "history": "^4.7.2",
......
...@@ -25,11 +25,11 @@ export const loadUserProfile = () => (dispatch, getState) => { ...@@ -25,11 +25,11 @@ export const loadUserProfile = () => (dispatch, getState) => {
export const changePassword = (newPassword: string, oldPassword: string) => (dispatch, getState) => { export const changePassword = (newPassword: string, oldPassword: string) => (dispatch, getState) => {
return UserProfileService.changePassword(newPassword, oldPassword) return UserProfileService.changePassword(newPassword, oldPassword)
.then(() => { .then(() => {
dispatch(showSnackbar('Password was changed successfully')); dispatch(showSnackbar('user.common.passChangeSuccess'));
}).catch((error) => { }).catch((error) => {
const data = _.get(error, 'response.data'); const data = _.get(error, 'response.data');
log('Save error', data.error); log('Save error', data.error);
dispatch(showSnackbar(data.error || 'Password change error')); dispatch(showSnackbar(data.error || 'user.common.passChangeError'));
throw new SubmissionError({ title: 'Password change error', _error: data.error }); throw new SubmissionError({ title: 'Password change error', _error: data.error });
}); });
}; };
......
{
"admin": {
"c": {
"cropForm": {
"cropCode": "Crop code",
"cropTitle": "Crop title",
"back": "Back to crop list"
}
},
"p": {
"edit": {
"onDelete": {
"message": "Delete crop '{{cropName, lowercase}}'?",
"description": "Note, deleting any crop causes mayhem."
}
}
}
},
"common": {
"modelName": "Crop",
"modelName_plural": "Crops",
"subtitle": "List of crops registered with Genesys",
"title": "Crops"
},
"public": {
"c": {
"cropSelector": {
"helper": "Contact helpdesk@genesys-pgr.org to register additional crops."
}
},
"p": {
"browse": {
"title": "What do you want to do?"
}
}
}
}
\ No newline at end of file
...@@ -36,7 +36,7 @@ class BrowsePage extends React.Component<IBrowsePageProps & any, any> { ...@@ -36,7 +36,7 @@ class BrowsePage extends React.Component<IBrowsePageProps & any, any> {
return ( return (
<PageLayout> <PageLayout>
<Authorize role="ROLE_ADMINISTRATOR"> <Authorize role="ROLE_ADMINISTRATOR">
<ContentHeaderWithButton title="What do you want to do?" buttons={ <Button variant="raised" onClick={ createCrop }>{ t('common:action.add', { what: t('label.crop') }) }</Button> } /> <ContentHeaderWithButton title={ t('crop.public.p.browse.title') } buttons={ <Button variant="raised" onClick={ createCrop }>{ t('common:action.add', { what: t('crop.common.modelName') }) }</Button> } />
</Authorize> </Authorize>
<Grid container spacing={ 0 } className="back-gray"> <Grid container spacing={ 0 } className="back-gray">
{ crops && crops.sort((a, b) => a.name.localeCompare(b.name)).map((crop: Crop) => ( { crops && crops.sort((a, b) => a.name.localeCompare(b.name)).map((crop: Crop) => (
......
...@@ -3,6 +3,7 @@ import { connect } from 'react-redux'; ...@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import Grid from '@material-ui/core/Grid'; import Grid from '@material-ui/core/Grid';
import Paper from '@material-ui/core/Paper'; import Paper from '@material-ui/core/Paper';
import { translate } from 'react-i18next';
import {log} from 'utilities/debug'; import {log} from 'utilities/debug';
...@@ -20,6 +21,7 @@ interface IDescriptorEditPageProps extends React.ClassAttributes<any> { ...@@ -20,6 +21,7 @@ interface IDescriptorEditPageProps extends React.ClassAttributes<any> {
saveCrop: (crop: Crop) => void; saveCrop: (crop: Crop) => void;
deleteCrop: (crop: Crop) => void; deleteCrop: (crop: Crop) => void;
crop: Crop; crop: Crop;
t: any;
} }
class CropEditPage extends React.Component<IDescriptorEditPageProps, any> { class CropEditPage extends React.Component<IDescriptorEditPageProps, any> {
...@@ -42,12 +44,12 @@ class CropEditPage extends React.Component<IDescriptorEditPageProps, any> { ...@@ -42,12 +44,12 @@ class CropEditPage extends React.Component<IDescriptorEditPageProps, any> {
} }
private onDelete = () => { private onDelete = () => {
const { crop, deleteCrop } = this.props; const { crop, deleteCrop, t } = this.props;
confirm(`Delete crop '${crop.name}'?`, { confirm(t('crops.admin.p.edit.onDelete.message', {cropName: crop.name}), {
description: `Note, deleting any crop causes mayhem.`, description: t('crops.admin.p.edit.onDelete.description'),
confirmLabel: 'Delete', confirmLabel: t('common:action.delete'),
abortLabel: 'Cancel', abortLabel: t('common:action.cancel'),
}).then(() => { }).then(() => {
deleteCrop(crop); deleteCrop(crop);
}).catch(() => { }).catch(() => {
...@@ -56,7 +58,7 @@ class CropEditPage extends React.Component<IDescriptorEditPageProps, any> { ...@@ -56,7 +58,7 @@ class CropEditPage extends React.Component<IDescriptorEditPageProps, any> {
} }
public render() { public render() {
const {shortName} = this.props; const {shortName, t} = this.props;
let {crop} = this.props; let {crop} = this.props;
if (!crop && !shortName) { if (!crop && !shortName) {
...@@ -72,7 +74,7 @@ class CropEditPage extends React.Component<IDescriptorEditPageProps, any> { ...@@ -72,7 +74,7 @@ class CropEditPage extends React.Component<IDescriptorEditPageProps, any> {
<PageLayout> <PageLayout>
<Grid item xs={ 12 }> <Grid item xs={ 12 }>
<Paper className="p-20 mb-10"> <Paper className="p-20 mb-10">
<CropForm initialValues={ crop } onSubmit={ this.onSave } onDelete={ this.onDelete } /> <CropForm initialValues={ crop } onSubmit={ this.onSave } onDelete={ this.onDelete } t={ t }/>
</Paper> </Paper>
</Grid> </Grid>
</PageLayout> </PageLayout>
...@@ -92,4 +94,4 @@ const mapDispatchToProps = (dispatch) => bindActionCreators({ ...@@ -92,4 +94,4 @@ const mapDispatchToProps = (dispatch) => bindActionCreators({
deleteCrop, deleteCrop,
}, dispatch); }, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(CropEditPage); export default translate()(connect(mapStateToProps, mapDispatchToProps)(CropEditPage));
...@@ -7,19 +7,19 @@ import {CROP_FORM} from 'constants/crop'; ...@@ -7,19 +7,19 @@ import {CROP_FORM} from 'constants/crop';
import {TextField} from 'ui/common/text-field'; import {TextField} from 'ui/common/text-field';
import Validators from 'utilities/Validators'; import Validators from 'utilities/Validators';
const CropForm = ({error, handleSubmit, initialValues, onDelete}) => { const CropForm = ({error, handleSubmit, initialValues, onDelete, t}) => {
return ( return (
<form onSubmit={ handleSubmit }> <form onSubmit={ handleSubmit }>
{ initialValues && initialValues.version && <div>Version: { initialValues.version }</div> } { initialValues && initialValues.version && <div> { t('common:label.version', {version: initialValues.version}) }</div> }
<Field required name="shortName" label="Crop code" component={ TextField } validate={ [ Validators.required, Validators.maxLength20 ] } /> <Field required name="shortName" label={ t('crop.admin.c.cropForm.cropCode') } component={ TextField } validate={ [ Validators.required, Validators.maxLength20 ] } />
<Field required name="name" label="Crop title" component={ TextField } validate={ [ Validators.required ] } /> <Field required name="name" label={ t('crop.admin.c.cropForm.cropTitle') } component={ TextField } validate={ [ Validators.required ] } />
<div>{ error && <strong>{ error }</strong> }</div> <div>{ error && <strong>{ error }</strong> }</div>
<Button variant="raised" type="submit">Save changes</Button> <Button variant="raised" type="submit">{ t('common:action.saveChanges') }</Button>
{ (initialValues._permissions && initialValues._permissions.delete) && <Button onClick={ onDelete } type="button">Delete</Button> } { (initialValues._permissions && initialValues._permissions.delete) && <Button onClick={ onDelete } type="button">{ t('common:action.delete') }</Button> }
<Link to="/crops"><Button type="button">Back to crop list</Button></Link> <Link to="/crops"><Button type="button">{ t('crop.admin.c.cropForm.back') }</Button></Link>
</form> </form>
); );
}; };
......
...@@ -54,7 +54,7 @@ const renderRadioButton = (input, code) => ( ...@@ -54,7 +54,7 @@ const renderRadioButton = (input, code) => (
const CropSelector = ({t, crops, label, single, fields, input}: ICropSelectorProps) => crops && ( const CropSelector = ({t, crops, label, single, fields, input}: ICropSelectorProps) => crops && (
<FormControl fullWidth> <FormControl fullWidth>
<FormLabel>{ label }</FormLabel> <FormLabel>{ label }</FormLabel>
<FormHelperText>{ t('m.crop.helper') }</FormHelperText> <FormHelperText>{ t('crop.public.c.cropSelector.helper') }</FormHelperText>
<Grid container> <Grid container>
{ {
crops.sort((a, b) => a.name.localeCompare(b.name)).map((crop: Crop) => ( crops.sort((a, b) => a.name.localeCompare(b.name)).map((crop: Crop) => (
......
...@@ -15,8 +15,8 @@ const publicRoutes = [ ...@@ -15,8 +15,8 @@ const publicRoutes = [
path: '/datasets', path: '/datasets',
component: Wrapper, component: Wrapper,
extraProps: { extraProps: {
title: 'p.datasets.title', title: 'datasets.common.modelName_plural',
subtitle: 'p.datasets.subtitle', subtitle: 'datasets.common.subtitle',
}, },
routes: [ routes: [
{ {
...@@ -44,7 +44,7 @@ const dashboardRoutes = [ ...@@ -44,7 +44,7 @@ const dashboardRoutes = [
component: DatasetStepper, component: DatasetStepper,
auth: [ROLE_USER, ROLE_ADMINISTRATOR], auth: [ROLE_USER, ROLE_ADMINISTRATOR],
extraProps: { extraProps: {
title: 'Phenotypic dataset publisher', title: 'datasets.dashboard.p.stepper.publisher',
}, },
routes: [ routes: [
{ {
...@@ -59,7 +59,7 @@ const dashboardRoutes = [ ...@@ -59,7 +59,7 @@ const dashboardRoutes = [
component: DatasetStepper, component: DatasetStepper,
auth: [ROLE_USER, ROLE_ADMINISTRATOR], auth: [ROLE_USER, ROLE_ADMINISTRATOR],
extraProps: { extraProps: {
title: 'Phenotypic dataset publisher', title: 'datasets.dashboard.p.stepper.publisher',
}, },
routes: [ routes: [
...datasetSteps, ...datasetSteps,
......
{
"common": {
"delete": "Deleting the dataset will remove all related data.",
"modelName": "Dataset",
"modelName_plural": "Datasets",
"menu": "Datasets",
"creator": {
"role": {
"MANAGER": "Data manager",
"COLLECTOR": "Data collector",
"DIGITIZER": "Data digitizer",
"CURATOR": "Data curator",
"null": "Not specified"
}
}
},
"dashboard": {
"p": {
"stepper": {
"basicInfo": {
"title": "Basic information",
"titleField": {
"label": "Dataset title",
"placeholder": "Name given to the dataset (e.g., Characterization of maize accessions in Kenya)"
},
"partner": {
"label": "Select Data provider",
"placeholder": "Data provider"
},
"version": {
"label": "Dataset version"
},
"description": {
"label": "Dataset description",
"placeholder": "An abstract, short or long description of the dataset. Descriptive details improves discoverability of the resource."
},
"created": {
"label": "Date of creation of the dataset",
"placeholder": "YYYYMMDD. If the month or day are missing, this should be indicated with ‘00’ [double zero] (e.g. 1975----, 19750000; 197506--, 19750600)"
},
"language": {
"label": "Language",
"placeholder": "Select language of the dataset"
},
"source": {
"label": "Source",
"placeholder": "A related resource from which the described dataset is derived (e.g., journal article, data article, conference proceedings). Use DOI, URL, Journal title, or Journal/conference title; vol., no. (year)"
},
"crops": {
"label": "Crops"
}
},
"attachments": {
"title": "Dataset attachments",
"name": "File name",
"label": "File label",
"description": "Description of file content",
"upload": "Upload file",
"datasetFile": "Dataset file"
},
"creators": {
"role": "Role",
"title": "Dataset creators",
"name": "Full name",
"namePlaceholder": "Jane A. Doe",