diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..0e6d835a --- /dev/null +++ b/.babelrc @@ -0,0 +1,11 @@ +{ + "plugins": [ + [ + "formatjs", + { + "idInterpolationPattern": "[sha512:contenthash:base64:6]", + "ast": true + } + ] + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d3b007bf..f5ee0581 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Add filter schema to registry to create dynamic filters in items tab - Render text value instead of key value in vocabulary select - Create scale images in IImageAttachment behavior + - Add i18n ( english, catalan, spanish) 0.23.1 diff --git a/package.json b/package.json index d555b919..bd3559ea 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "jwt-decode": "3.1.2", "prop-types": "15.7.2", "react-beautiful-dnd": "13.1.1", + "react-intl": "6.5.5", "react-useportal": "1.0.19", "uuid": "9.0.1" }, @@ -29,9 +30,11 @@ "devDependencies": { "@babel/cli": "7.12.10", "@babel/core": "7.12.10", + "@formatjs/cli": "^6.2.4", "@testing-library/jest-dom": "5.11.6", "@testing-library/react": "11.2.2", "@testing-library/user-event": "12.6.0", + "babel-plugin-formatjs": "^10.5.10", "husky": "4.3.6", "microbundle": "0.13.0", "prettier": "2.2.1", @@ -47,7 +50,13 @@ "build:js": "rm -rf ./dist && microbundle --jsx React.createElement --no-compress --sourcemap", "build:css": "rm -rf ./dist/css && mkdir ./dist/css && sass ./src/guillo-gmi/scss/styles.sass ./dist/css/style.css", "prepublish": "yarn build", - "test": "vitest run" + "test": "vitest run", + "intl-extract": "formatjs extract 'src/**/*.js' --out-file src/guillo-gmi/locales/en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'", + "intl-compile-en": "formatjs compile src/guillo-gmi/locales/en.json --ast --out-file src/guillo-gmi/locales/compiled/en.json", + "intl-compile-ca": "formatjs compile src/guillo-gmi/locales/ca.json --ast --out-file src/guillo-gmi/locales/compiled/ca.json", + "intl-compile-es": "formatjs compile src/guillo-gmi/locales/es.json --ast --out-file src/guillo-gmi/locales/compiled/es.json", + "intl-compile": "npm run intl-compile-en && npm run intl-compile-es && npm run intl-compile-ca" + }, "eslintConfig": { "extends": "react-app" diff --git a/src/guillo-gmi/components/behavior_view.js b/src/guillo-gmi/components/behavior_view.js index 0dbe18b5..8a673d49 100644 --- a/src/guillo-gmi/components/behavior_view.js +++ b/src/guillo-gmi/components/behavior_view.js @@ -1,6 +1,8 @@ import React from 'react' import { useTraversal } from '../contexts' import { get } from '../lib/utils' +import { useIntl } from 'react-intl' +import { genericMessages } from '../locales/generic_messages' export function BehaviorsView({ context, schema }) { const Ctx = useTraversal() @@ -34,9 +36,10 @@ export function BehaviorsView({ context, schema }) { } export function BehaviorNotImplemented() { + const intl = useIntl() return ( - Not Implemented + {intl.formatMessage(genericMessages.not_implemented)} ) } diff --git a/src/guillo-gmi/components/behaviors/iimageattachment.js b/src/guillo-gmi/components/behaviors/iimageattachment.js index 328e587c..76f9e07f 100644 --- a/src/guillo-gmi/components/behaviors/iimageattachment.js +++ b/src/guillo-gmi/components/behaviors/iimageattachment.js @@ -7,10 +7,16 @@ import { Delete } from '../ui' import { Button } from '../input/button' import { FileUpload } from '../input/upload' import { Confirm } from '../../components/modal' +import { useIntl } from 'react-intl' +import { + genericFileMessages, + genericMessages, +} from '../../locales/generic_messages' const _sizesImages = ['large', 'preview', 'mini', 'thumb'] export function IImageAttachment({ properties, values }) { + const intl = useIntl() const cfg = useConfig() const Ctx = useTraversal() const modifyContent = Ctx.hasPerm('guillotina.ModifyContent') @@ -22,13 +28,12 @@ export function IImageAttachment({ properties, values }) { const uploadFile = async (ev) => { ev.preventDefault() - setLoading(true) setError(undefined) const endpoint = `${Ctx.path}@upload/image` const req = await Ctx.client.upload(endpoint, file) if (req.status !== 200) { - setError('Failed to upload file') + setError(intl.formatMessage(genericFileMessages.error_upload_file)) setLoading(false) return } @@ -44,7 +49,11 @@ export function IImageAttachment({ properties, values }) { } if (hasError) { - setError(`Failed to upload file ${endpointSize}`) + setError( + intl.formatMessage(genericFileMessages.error_upload_file_size, { + size: sizesImages[i], + }) + ) setLoading(false) return } @@ -52,7 +61,7 @@ export function IImageAttachment({ properties, values }) { setFile(undefined) setLoading(false) - Ctx.flash(`Image uploaded!`, 'success') + Ctx.flash(intl.formatMessage(genericFileMessages.image_uploaded), 'success') Ctx.refresh() } @@ -62,12 +71,12 @@ export function IImageAttachment({ properties, values }) { const endpoint = `${Ctx.path}@delete/image` const req = await Ctx.client.delete(endpoint, file) if (req.status !== 200) { - setError('Failed to delete file') + setError(intl.formatMessage(genericFileMessages.failed_delete_file)) setLoading(false) return } setLoading(false) - Ctx.flash(`Image deleted!`, 'success') + Ctx.flash(intl.formatMessage(genericFileMessages.image_deleted), 'success') Ctx.refresh() } @@ -81,12 +90,14 @@ export function IImageAttachment({ properties, values }) { loading={loading} onCancel={() => setShowConfirmToDelete(false)} onConfirm={() => deleteFile()} - message={`Are you sure to remove the image?`} + message={intl.formatMessage( + genericFileMessages.confirm_message_delete_image + )} /> )} {values['image'] && ( - Image + {intl.formatMessage(genericMessages.image)}
- +
- Upload + {intl.formatMessage(genericMessages.upload)}
diff --git a/src/guillo-gmi/components/behaviors/imultiattachment.js b/src/guillo-gmi/components/behaviors/imultiattachment.js index b50cbcdd..2fa9863b 100644 --- a/src/guillo-gmi/components/behaviors/imultiattachment.js +++ b/src/guillo-gmi/components/behaviors/imultiattachment.js @@ -9,8 +9,14 @@ import { EditableField } from '../fields/editableField' import { Delete } from '../ui' import { Confirm } from '../../components/modal' import { Table } from '../ui' +import { useIntl } from 'react-intl' +import { + genericFileMessages, + genericMessages, +} from '../../locales/generic_messages' export function IMultiAttachment({ properties, values }) { + const intl = useIntl() const [fileKey, setFileKey] = useState('') const [file, setFile] = useState() const [fileKeyToDelete, setFileKeyToDelete] = useState(undefined) @@ -23,7 +29,7 @@ export function IMultiAttachment({ properties, values }) { const uploadFile = async (ev) => { ev.preventDefault() if (!fileKey && !file) { - setError('Provide a file and a key name') + setError(intl.formatMessage(genericFileMessages.error_file_key_name)) return } setLoading(true) @@ -31,14 +37,14 @@ export function IMultiAttachment({ properties, values }) { const endpoint = `${Ctx.path}@upload/files/${fileKey}` const req = await Ctx.client.upload(endpoint, file) if (req.status !== 200) { - setError('Failed to upload file') + setError(intl.formatMessage(genericFileMessages.error_upload_file)) setLoading(false) return } setFileKey('') setFile(undefined) setLoading(false) - Ctx.flash(`${fileKey} uploaded!`, 'success') + Ctx.flash(intl.formatMessage(genericFileMessages.file_uploaded), 'success') Ctx.refresh() } @@ -48,12 +54,12 @@ export function IMultiAttachment({ properties, values }) { const endpoint = `${Ctx.path}@delete/files/${fileKeyToDelete}` const req = await Ctx.client.delete(endpoint, file) if (req.status !== 200) { - setError('Failed to delete file') + setError(intl.formatMessage(genericFileMessages.failed_delete_file)) setLoading(false) return } setLoading(false) - Ctx.flash(`${fileKeyToDelete} delete!`, 'success') + setError(intl.formatMessage(genericFileMessages.failed_delete_file)) Ctx.refresh() } @@ -67,7 +73,12 @@ export function IMultiAttachment({ properties, values }) { loading={loading} onCancel={() => setFileKeyToDelete(undefined)} onConfirm={() => deleteFile(fileKeyToDelete)} - message={`Are you sure to remove: ${fileKeyToDelete}?`} + message={ + (intl.formatMessage( + genericFileMessages.confirm_message_delete_file + ), + { fileKeyToDelete }) + } /> )} @@ -97,13 +108,17 @@ export function IMultiAttachment({ properties, values }) { ))} {Object.keys(values['files']).length === 0 && ( - No files uploaded + + {intl.formatMessage(genericFileMessages.no_files_uploaded)} + )} {modifyContent && ( - +
- Upload + {intl.formatMessage(genericMessages.upload)}
diff --git a/src/guillo-gmi/components/behaviors/imultiimageattachment.js b/src/guillo-gmi/components/behaviors/imultiimageattachment.js index 06145283..2c466b8f 100644 --- a/src/guillo-gmi/components/behaviors/imultiimageattachment.js +++ b/src/guillo-gmi/components/behaviors/imultiimageattachment.js @@ -9,10 +9,16 @@ import { EditableField } from '../fields/editableField' import { useConfig } from '../../hooks/useConfig' import { Table } from '../ui' import { Input } from '../input/input' +import { useIntl } from 'react-intl' +import { + genericFileMessages, + genericMessages, +} from '../../locales/generic_messages' const _sizesImages = ['large', 'preview', 'mini', 'thumb'] export function IMultiImageAttachment({ properties, values }) { + const intl = useIntl() const cfg = useConfig() const [fileKey, setFileKey] = useState('') const [file, setFile] = useState(null) @@ -27,7 +33,7 @@ export function IMultiImageAttachment({ properties, values }) { const uploadFile = async (ev) => { ev.preventDefault() if (!fileKey && !file) { - setError('Provide a file and a key name') + setError(intl.formatMessage(genericFileMessages.error_file_key_name)) return } setLoading(true) @@ -35,7 +41,7 @@ export function IMultiImageAttachment({ properties, values }) { const endpoint = `${Ctx.path}@upload/images/${fileKey}` const req = await Ctx.client.upload(endpoint, file) if (req.status !== 200) { - setError('Failed to upload file') + setError(intl.formatMessage(genericFileMessages.error_upload_file)) setLoading(false) return } @@ -51,7 +57,11 @@ export function IMultiImageAttachment({ properties, values }) { } if (hasError) { - setError(`Failed to upload file ${endpointSize}`) + setError( + intl.formatMessage(genericFileMessages.error_upload_file_size, { + size: sizesImages[i], + }) + ) setLoading(false) return } @@ -60,7 +70,7 @@ export function IMultiImageAttachment({ properties, values }) { setFileKey('') setFile(undefined) setLoading(false) - Ctx.flash(`${fileKey} uploaded!`, 'success') + Ctx.flash(intl.formatMessage(genericFileMessages.image_uploaded), 'success') Ctx.refresh() } @@ -70,12 +80,12 @@ export function IMultiImageAttachment({ properties, values }) { const endpoint = `${Ctx.path}@delete/images/${fileKeyToDelete}` const req = await Ctx.client.delete(endpoint, file) if (req.status !== 200) { - setError('Failed to delete file') + setError(intl.formatMessage(genericFileMessages.failed_delete_file)) setLoading(false) return } setLoading(false) - Ctx.flash(`${fileKeyToDelete} delete!`, 'success') + Ctx.flash(intl.formatMessage(genericFileMessages.image_deleted), 'success') Ctx.refresh() } @@ -89,7 +99,12 @@ export function IMultiImageAttachment({ properties, values }) { loading={loading} onCancel={() => setFileKeyToDelete(undefined)} onConfirm={() => deleteFile()} - message={`Are you sure to remove: ${fileKeyToDelete}?`} + message={ + (intl.formatMessage( + genericFileMessages.confirm_message_delete_file + ), + { fileKeyToDelete }) + } /> )} @@ -120,13 +135,18 @@ export function IMultiImageAttachment({ properties, values }) { ))} {Object.keys(values['images']).length === 0 && ( - No images uploaded + + {intl.formatMessage(genericFileMessages.no_images_uploaded)} + )} {modifyContent && ( - +
- Upload + {intl.formatMessage(genericMessages.upload)}
diff --git a/src/guillo-gmi/components/behaviors/imultiimageorderedattachment.js b/src/guillo-gmi/components/behaviors/imultiimageorderedattachment.js index 33620e01..d4100c5f 100644 --- a/src/guillo-gmi/components/behaviors/imultiimageorderedattachment.js +++ b/src/guillo-gmi/components/behaviors/imultiimageorderedattachment.js @@ -8,6 +8,11 @@ import { useConfig } from '../../hooks/useConfig' import React, { useEffect, useState } from 'react' import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd' import { v4 as uuidv4 } from 'uuid' +import { defineMessages, useIntl } from 'react-intl' +import { + genericFileMessages, + genericMessages, +} from '../../locales/generic_messages' const StrictModeDroppable = ({ children, ...props }) => { const [enabled, setEnabled] = useState(false) @@ -33,8 +38,18 @@ const reorder = (list, startIndex, endIndex) => { } const _sizesImages = ['large', 'preview', 'mini', 'thumb'] - +const messages = defineMessages({ + failed_to_sort_images: { + id: 'failed_to_sort_images', + defaultMessage: 'Failed to sort images', + }, + images_sorted: { + id: 'images_sorted', + defaultMessage: 'Images sorted', + }, +}) export function IMultiImageOrderedAttachment({ properties, values }) { + const intl = useIntl() const cfg = useConfig() const [sortedList, setSortedList] = useState(Object.keys(values['images'])) const [file, setFile] = useState(null) @@ -67,20 +82,20 @@ export function IMultiImageOrderedAttachment({ properties, values }) { const endpoint = `${Ctx.path}@sort/images/` const req = await Ctx.client.patch(endpoint, resultSorted) if (req.status !== 200) { - setError('Failed to sorted images') + setError(intl.formatMessage(messages.failed_to_sort_images)) setLoading(false) return } setLoading(false) - Ctx.flash(`Images sorted`, 'success') + Ctx.flash(intl.formatMessage(messages.images_sorted), 'success') Ctx.refresh() } const uploadFile = async (ev) => { ev.preventDefault() if (!file) { - setError('Provide a file and a key name') + setError(intl.formatMessage(genericFileMessages.error_file_key_name)) return } setLoading(true) @@ -89,7 +104,7 @@ export function IMultiImageOrderedAttachment({ properties, values }) { const endpoint = `${Ctx.path}@upload/images/${fileKey}` const req = await Ctx.client.upload(endpoint, file) if (req.status !== 200) { - setError('Failed to upload file') + setError(intl.formatMessage(genericFileMessages.error_upload_file)) setLoading(false) return } @@ -105,7 +120,11 @@ export function IMultiImageOrderedAttachment({ properties, values }) { } if (hasError) { - setError(`Failed to upload file ${endpointSize}`) + setError( + intl.formatMessage(genericFileMessages.error_upload_file_size, { + size: sizesImages[i], + }) + ) setLoading(false) return } @@ -113,7 +132,7 @@ export function IMultiImageOrderedAttachment({ properties, values }) { setFile(undefined) setLoading(false) - Ctx.flash(`Image uploaded!`, 'success') + Ctx.flash(intl.formatMessage(genericFileMessages.image_uploaded), 'success') Ctx.refresh() } @@ -123,12 +142,12 @@ export function IMultiImageOrderedAttachment({ properties, values }) { const endpoint = `${Ctx.path}@delete/images/${fileKeyToDelete}` const req = await Ctx.client.delete(endpoint, file) if (req.status !== 200) { - setError('Failed to delete file') + setError(intl.formatMessage(genericFileMessages.failed_delete_file)) setLoading(false) return } setLoading(false) - Ctx.flash(`Image deleted!`, 'success') + Ctx.flash(intl.formatMessage(genericFileMessages.image_deleted), 'success') Ctx.refresh() } @@ -139,7 +158,12 @@ export function IMultiImageOrderedAttachment({ properties, values }) { loading={loading} onCancel={() => setFileKeyToDelete(undefined)} onConfirm={() => deleteFile()} - message={`Are you sure to remove the image?`} + message={ + (intl.formatMessage( + genericFileMessages.confirm_message_delete_file + ), + { fileKeyToDelete }) + } /> )}
@@ -189,11 +213,15 @@ export function IMultiImageOrderedAttachment({ properties, values }) {
{Object.keys(values['images']).length === 0 && ( -
No images uploaded
+
+ {intl.formatMessage(genericFileMessages.no_images_uploaded)} +
)} {modifyContent && (
- +
- Upload + {intl.formatMessage(genericMessages.upload)}
diff --git a/src/guillo-gmi/components/behaviors/iworkflow.js b/src/guillo-gmi/components/behaviors/iworkflow.js index c89de017..aab7c61a 100644 --- a/src/guillo-gmi/components/behaviors/iworkflow.js +++ b/src/guillo-gmi/components/behaviors/iworkflow.js @@ -3,8 +3,33 @@ import { useTraversal } from '../../contexts' import { Confirm } from '../modal' import { useCrudContext } from '../../hooks/useCrudContext' import { ItemModel } from '../../models' +import { defineMessages, useIntl } from 'react-intl' + +const messages = defineMessages({ + status_changed_ok: { + id: 'status_changed_ok', + defaultMessage: 'Great status changed!', + }, + status_changed_error: { + id: 'status_changed_error', + defaultMessage: 'Failed to status changed!: {error}', + }, + confirm_message: { + id: 'confirm_message', + defaultMessage: 'Are you sure to change state: {title}?', + }, + current_state: { + id: 'current_state', + defaultMessage: 'Current state: {state}', + }, + actions: { + id: 'actions', + defaultMessage: 'Actions:', + }, +}) export function IWorkflow() { + const intl = useIntl() const Ctx = useTraversal() const { post, loading } = useCrudContext() const modifyContent = Ctx.hasPerm('guillotina.ModifyContent') @@ -34,9 +59,14 @@ export function IWorkflow() { ) await loadDefinition() if (!isError) { - Ctx.flash(`Great status changed!`, 'success') + Ctx.flash(intl.formatMessage(messages.status_changed_ok), 'success') } else { - Ctx.flash(`Failed to status changed!: ${errorMessage}`, 'danger') + Ctx.flash( + intl.formatMessage(messages.status_changed_error, { + error: errorMessage, + }), + 'danger' + ) } Ctx.refresh() @@ -51,9 +81,9 @@ export function IWorkflow() { loading={loading} onCancel={() => setWorkflowAction(null)} onConfirm={doWorkflowAction} - message={`Are you sure to change state: ${ - Ctx.context.title || Ctx.context['@name'] - }?`} + message={intl.formatMessage(messages.confirm_message, { + title: Ctx.context.title || Ctx.context['@name'], + })} /> )} @@ -62,7 +92,7 @@ export function IWorkflow() { className="has-text-weight-bold" data-test={`textInfoStatus-${currentState}`} > - Current state: {currentState} + {intl.formatMessage(messages.current_state, { state: currentState })} {modifyContent && ( @@ -70,7 +100,7 @@ export function IWorkflow() { className=" is-flex is-align-items-center has-text-weight-bold" data-test={`textInfoStatus-${currentState}`} > -    +    {definition.transitions.map((transition) => { return ( ) } @@ -63,6 +66,7 @@ export function CreateButton() { } export function ContextToolbar({ AddButton, ...props }) { + const intl = useIntl() const [state, setState] = useSetState(initialState) const [location, setLocation, del] = useLocation() const traversal = useTraversal() @@ -110,7 +114,7 @@ export function ContextToolbar({ AddButton, ...props }) { onChange={(ev) => setSearchValue(ev.target.value)} type="text" className="input is-size-7" - placeholder="Search..." + placeholder={intl.formatMessage(genericMessages.search)} data-test="inputFilterTest" /> diff --git a/src/guillo-gmi/components/error_boundary.js b/src/guillo-gmi/components/error_boundary.js index 543a82c3..dcfffe1f 100644 --- a/src/guillo-gmi/components/error_boundary.js +++ b/src/guillo-gmi/components/error_boundary.js @@ -1,8 +1,9 @@ import React from 'react' +import { injectIntl } from 'react-intl' const style = { color: '#F44336', fontSize: 20, paddingBottom: 20 } -export default class ErrorBoundary extends React.Component { +class ErrorBoundaryComponent extends React.Component { state = {} componentDidCatch(error, errorInfo) { @@ -19,7 +20,12 @@ export default class ErrorBoundary extends React.Component { if (hasError) { return (
-
Something went wrong.
+
+ {this.props.intl.formatMessage({ + id: 'something_went_wrong', + defaultMessage: 'Something went wrong.', + })} +
{errorMsg && (

{errorMsg} @@ -33,3 +39,5 @@ export default class ErrorBoundary extends React.Component { return this.props.children } } + +export default injectIntl(ErrorBoundaryComponent) diff --git a/src/guillo-gmi/components/fields/downloadField.js b/src/guillo-gmi/components/fields/downloadField.js index 25306374..faf44f89 100644 --- a/src/guillo-gmi/components/fields/downloadField.js +++ b/src/guillo-gmi/components/fields/downloadField.js @@ -1,7 +1,10 @@ import * as React from 'react' import { useTraversal } from '../../contexts' +import { useIntl } from 'react-intl' +import { genericMessages } from '../../locales/generic_messages' export const DownloadField = ({ value }) => { + const intl = useIntl() const Ctx = useTraversal() const { data, field } = value @@ -45,7 +48,7 @@ export const DownloadField = ({ value }) => { getField(false) }} > - Open + {intl.formatMessage(genericMessages.open)}

@@ -57,7 +60,7 @@ export const DownloadField = ({ value }) => { getField(true) }} > - Download + {intl.formatMessage(genericMessages.download)}
diff --git a/src/guillo-gmi/components/fields/editableField.js b/src/guillo-gmi/components/fields/editableField.js index ac4e2375..5e63b7bb 100644 --- a/src/guillo-gmi/components/fields/editableField.js +++ b/src/guillo-gmi/components/fields/editableField.js @@ -7,6 +7,11 @@ import { useRef } from 'react' import { useEffect } from 'react' import { Icon } from '../ui' import { get } from '../../lib/utils' +import { useIntl } from 'react-intl' +import { + genericFileMessages, + genericMessages, +} from '../../locales/generic_messages' export function EditableField({ field, @@ -16,6 +21,7 @@ export function EditableField({ modifyContent, required, }) { + const intl = useIntl() const ref = useRef() const [isEdit, setEdit] = useState(false) const [val, setValue] = useState(value) @@ -40,12 +46,18 @@ export function EditableField({ if (ev) ev.preventDefault() if (!field) { - Ctx.flash(`Provide a key name!`, 'danger') + Ctx.flash( + intl.formatMessage(genericMessages.error_provide_key_name), + 'danger' + ) return } if (!val && required) { - Ctx.flash(`${field} is mandatory!`, 'danger') + Ctx.flash( + intl.formatMessage(genericMessages.mandatory_field, { field }), + 'danger' + ) return } @@ -57,17 +69,29 @@ export function EditableField({ const endpoint = `${Ctx.path}@upload/${field}` const req = await Ctx.client.upload(endpoint, value) if (req.status !== 200) { - Ctx.flash(`Failed to upload file ${field}!`, 'danger') + Ctx.flash( + intl.formatMessage(genericFileMessages.error_upload_file), + 'danger' + ) } else { - Ctx.flash(`${field} uploaded!`, 'success') + Ctx.flash( + intl.formatMessage(genericFileMessages.file_uploaded), + 'success' + ) } } else { const data = ns ? { [ns]: { [field]: val } } : { [field]: val } const dataPatch = await patch(data) if (dataPatch.isError) { - Ctx.flash(`Error in field ${field}!`, 'danger') + Ctx.flash( + intl.formatMessage(genericMessages.error_in_field, { field }), + 'danger' + ) } else { - Ctx.flash(`Field ${field}, updated!`, 'success') + Ctx.flash( + intl.formatMessage(genericMessages.field_updated, { field }), + 'success' + ) } } @@ -79,16 +103,25 @@ export function EditableField({ if (ev) ev.preventDefault() if (schema?.widget === 'file') { if (!field || (!val && required)) { - Ctx.flash(`You can't delete ${field}!`, 'danger') + Ctx.flash( + intl.formatMessage(genericMessages.can_not_delete_field, { field }), + 'danger' + ) return } const data = ns ? { [ns]: { [field]: null } } : { [field]: null } const dataPatch = await patch(data) if (dataPatch.isError) { - Ctx.flash(`Error in field ${field}!`, 'danger') + Ctx.flash( + intl.formatMessage(genericMessages.error_in_field, { field }), + 'danger' + ) } else { - Ctx.flash(`Field ${field}, deleted!`, 'success') + Ctx.flash( + intl.formatMessage(genericMessages.field_deleted, { field }), + 'success' + ) } setEdit(false) @@ -139,7 +172,7 @@ export function EditableField({ onClick={saveField} dataTest="editableFieldBtnSaveTest" > - Save + {intl.formatMessage(genericMessages.save)}
@@ -148,7 +181,7 @@ export function EditableField({ onClick={() => setEdit(false)} dataTest="editableFieldBtnCancelTest" > - Cancel + {intl.formatMessage(genericMessages.cancel)}
{!required && fieldHaveDeleteButton(schema) && ( @@ -158,7 +191,7 @@ export function EditableField({ onClick={deleteField} dataTest="editableFieldBtnDeleteTest" > - Delete + {intl.formatMessage(genericMessages.delete)} )} diff --git a/src/guillo-gmi/components/fields/renderField.js b/src/guillo-gmi/components/fields/renderField.js index 8afab0b5..75173889 100644 --- a/src/guillo-gmi/components/fields/renderField.js +++ b/src/guillo-gmi/components/fields/renderField.js @@ -2,6 +2,7 @@ import React from 'react' import { DownloadField } from './downloadField' import { useVocabulary } from '../../hooks/useVocabulary' import { get } from '../../lib/utils' +import { useIntl } from 'react-intl' const plain = ['string', 'number', 'boolean'] export function RenderField({ value, Widget }) { @@ -39,10 +40,14 @@ const FieldValue = ({ field, value }) => ( ) -export const DEFAULT_VALUE_EDITABLE_FIELD = 'Click to edit' -export const DEFAULT_VALUE_NO_EDITABLE_FIELD = ' -- ' - export function RenderFieldComponent({ schema, field, val, modifyContent }) { + const intl = useIntl() + const DEFAULT_VALUE_EDITABLE_FIELD = intl.formatMessage({ + id: 'default_value_editable_field', + defaultMessage: 'Click to edit', + }) + const DEFAULT_VALUE_NO_EDITABLE_FIELD = ' -- ' + const getRenderProps = () => { const renderProps = { value: diff --git a/src/guillo-gmi/components/guillotina.js b/src/guillo-gmi/components/guillotina.js index 224ad7bc..9b0d59c2 100644 --- a/src/guillo-gmi/components/guillotina.js +++ b/src/guillo-gmi/components/guillotina.js @@ -9,8 +9,24 @@ import { useLocation } from '../hooks/useLocation' import { guillotinaReducer } from '../reducers/guillotina' import { initialState } from '../reducers/guillotina' import { Loading } from './ui/loading' +import { IntlProvider } from 'react-intl' +import langCA from '../locales/compiled/ca.json' +import langES from '../locales/compiled/es.json' +import langEN from '../locales/compiled/en.json' -export function Guillotina({ auth, ...props }) { +function loadLocaleData(locale) { + switch (locale) { + case 'ca': + return langCA + case 'es': + return langES + default: + return langEN + } +} + +export function Guillotina({ auth, locale, ...props }) { + const messages = loadLocaleData(locale) const url = props.url || 'http://localhost:8080' // without trailing slash const config = props.config || {} const client = useGuillotinaClient() @@ -71,35 +87,37 @@ export function Guillotina({ auth, ...props }) { const Action = action.action ? registry.getAction(action.action) : null return ( - - {!errorStatus && ( - - {permissions && ( - - {action.action && } -
-
-
- + + + {!errorStatus && ( + + {permissions && ( + + {action.action && } +
+
+
+ +
-
- - {Main && ( - -
- {state.loading && } - {!state.loading &&
} -
-
- )} - {/*

Guillotina {JSON.stringify(state.context)}

*/} - - )} - - )} - {errorStatus === 'notallowed' && } - {errorStatus === 'notfound' && } - + + {Main && ( + +
+ {state.loading && } + {!state.loading &&
} +
+
+ )} + {/*

Guillotina {JSON.stringify(state.context)}

*/} + + )} + + )} + {errorStatus === 'notallowed' && } + {errorStatus === 'notfound' && } + + ) } diff --git a/src/guillo-gmi/components/input/email.js b/src/guillo-gmi/components/input/email.js index 62e0e021..13803ca2 100644 --- a/src/guillo-gmi/components/input/email.js +++ b/src/guillo-gmi/components/input/email.js @@ -2,13 +2,18 @@ import React from 'react' import { Input } from './input' import { isEmail } from '../../lib/validators' import { Icon } from '../ui/icon' +import { useIntl } from 'react-intl' export const EmailInput = ({ value = '', dataTest, ...rest }) => { + const intl = useIntl() return ( } diff --git a/src/guillo-gmi/components/input/input_list.js b/src/guillo-gmi/components/input/input_list.js index 6cfc4b51..9a2831a8 100644 --- a/src/guillo-gmi/components/input/input_list.js +++ b/src/guillo-gmi/components/input/input_list.js @@ -1,8 +1,10 @@ import * as React from 'react' import { Input } from './input' +import { useIntl } from 'react-intl' export const InputList = React.forwardRef( ({ value, onChange, dataTest }, ref) => { + const intl = useIntl() const [inputValue, setInputValue] = React.useState('') const addTags = (event) => { if (event.key === 'Enter' && event.target.value !== '') { @@ -33,7 +35,10 @@ export const InputList = React.forwardRef(
addTags(event)} value={inputValue} ref={ref} diff --git a/src/guillo-gmi/components/input/search_input.js b/src/guillo-gmi/components/input/search_input.js index f9a4fc90..9beadacd 100644 --- a/src/guillo-gmi/components/input/search_input.js +++ b/src/guillo-gmi/components/input/search_input.js @@ -8,6 +8,8 @@ import ErrorZone from '../error_zone' import { Loading } from '../ui' import { generateUID } from '../../lib/helpers' import { useConfig } from '../../hooks/useConfig' +import { useIntl } from 'react-intl' +import { genericMessages } from '../../locales/generic_messages' function debounce(func, wait) { let timeout return function () { @@ -44,6 +46,7 @@ export const SearchInput = ({ dataTestItem = 'searchInputItemTest', renderTextItemOption = null, }) => { + const intl = useIntl() const [options, setOptions] = useSetState(initialState) const [isOpen, setIsOpen] = React.useState(false) const [searchTerm, setSearchTerm] = React.useState('') @@ -169,7 +172,7 @@ export const SearchInput = ({ data-test={dataTestSearchInput} className="input" type="text" - placeholder="Search..." + placeholder={intl.formatMessage(genericMessages.search)} value={searchTerm} onChange={(ev) => { delayedQuery(ev.target.value) @@ -200,7 +203,9 @@ export const SearchInput = ({ })} {options.items && options.items.length === 0 && ( -
No results
+
+ {intl.formatMessage(genericMessages.no_results)} +
)} {options.items && options.items_total > options.items.length && ( @@ -214,7 +219,7 @@ export const SearchInput = ({ handleSearch(options.page + 1, true) }} > - Load more... + {intl.formatMessage(genericMessages.load_more)}
)} diff --git a/src/guillo-gmi/components/input/select.js b/src/guillo-gmi/components/input/select.js index 25661913..07a28909 100644 --- a/src/guillo-gmi/components/input/select.js +++ b/src/guillo-gmi/components/input/select.js @@ -3,6 +3,8 @@ import PropTypes from 'prop-types' import ErrorZone from '../error_zone' import { classnames, generateUID } from '../../lib/helpers' import { get } from '../../lib/utils' +import { useIntl } from 'react-intl' +import { genericMessages } from '../../locales/generic_messages' // @ TODO implement hasErrors /** @type any */ @@ -29,6 +31,7 @@ export const Select = React.forwardRef( }, ref ) => { + const intl = useIntl() const [uid] = useState(generateUID('select')) const onUpdate = (ev) => { @@ -45,7 +48,9 @@ export const Select = React.forwardRef( } if (appendDefault) { - options = [{ text: 'Choose...', value: '' }].concat(options) + options = [ + { text: intl.formatMessage(genericMessages.choose), value: '' }, + ].concat(options) } const statusClasses = error ? 'is-danger' : '' diff --git a/src/guillo-gmi/components/input/upload.js b/src/guillo-gmi/components/input/upload.js index 92de3e8b..8269656d 100644 --- a/src/guillo-gmi/components/input/upload.js +++ b/src/guillo-gmi/components/input/upload.js @@ -1,7 +1,9 @@ import React from 'react' import { lightFileReader } from '../../lib/client' +import { useIntl } from 'react-intl' export function FileUpload({ label, onChange, ...props }) { + const intl = useIntl() const changed = async (event) => { const file = await lightFileReader(event.target.files[0]) onChange(file) @@ -21,7 +23,13 @@ export function FileUpload({ label, onChange, ...props }) { - {label || 'Choose a file…'} + + {label || + intl.formatMessage({ + id: 'choose_file', + defaultMessage: 'Choose a file', + })} + diff --git a/src/guillo-gmi/components/modal.js b/src/guillo-gmi/components/modal.js index d7f348eb..3fb543e5 100644 --- a/src/guillo-gmi/components/modal.js +++ b/src/guillo-gmi/components/modal.js @@ -1,6 +1,8 @@ import React from 'react' import usePortal from 'react-useportal' import { Button } from './input/button' +import { useIntl } from 'react-intl' +import { genericMessages } from '../locales/generic_messages' export function Modal(props) { const { isActive, setActive, children } = props @@ -25,6 +27,7 @@ export function Modal(props) { } export function Confirm({ message, onCancel, onConfirm, loading }) { + const intl = useIntl() const setActive = () => onCancel() return ( @@ -38,7 +41,7 @@ export function Confirm({ message, onCancel, onConfirm, loading }) { onClick={() => onCancel()} data-test="btnCancelModalTest" > - Cancel + {intl.formatMessage(genericMessages.cancel)}    @@ -64,6 +67,7 @@ export function PathTree({ onConfirm, onCancel, }) { + const intl = useIntl() return (

{title}

@@ -90,7 +94,7 @@ export function PathTree({ onClick={onCancel} data-test="btnCancelModalTest" > - Cancel + {intl.formatMessage(genericMessages.cancel)}    diff --git a/src/guillo-gmi/components/notallowed.js b/src/guillo-gmi/components/notallowed.js index 6e4e2867..b32d4ba1 100644 --- a/src/guillo-gmi/components/notallowed.js +++ b/src/guillo-gmi/components/notallowed.js @@ -1,13 +1,20 @@ import React from 'react' import { Icon } from './ui/icon' +import { useIntl } from 'react-intl' export function NotAllowed() { + const intl = useIntl() return (

-

Not Allowed

+

+ {intl.formatMessage({ + id: 'not_allowed', + defaultMessage: 'Not Allowed', + })} +

) } diff --git a/src/guillo-gmi/components/notfound.js b/src/guillo-gmi/components/notfound.js index a70d524b..0526dd9c 100644 --- a/src/guillo-gmi/components/notfound.js +++ b/src/guillo-gmi/components/notfound.js @@ -1,13 +1,20 @@ import React from 'react' import { Icon } from './ui/icon' +import { useIntl } from 'react-intl' export function NotFound() { + const intl = useIntl() return (

-

404. Not Found

+

+ {intl.formatMessage({ + id: 'not_found', + defaultMessage: '404. Not Found', + })} +

) } diff --git a/src/guillo-gmi/components/pagination.js b/src/guillo-gmi/components/pagination.js index d14d0717..8deeaba2 100644 --- a/src/guillo-gmi/components/pagination.js +++ b/src/guillo-gmi/components/pagination.js @@ -1,7 +1,9 @@ import React from 'react' +import { useIntl } from 'react-intl' /* eslint jsx-a11y/anchor-is-valid: "off" */ export function Pagination({ current, total, doPaginate, pager }) { + const intl = useIntl() const maxPages = Math.ceil(total / pager) if (maxPages <= 1) { return null @@ -10,9 +12,20 @@ export function Pagination({ current, total, doPaginate, pager }) { return (

- {current + 1}/ - {maxPages} of  - {`${total} items`} + + {intl.formatMessage( + { + id: 'pagination', + defaultMessage: + '{currentPage} / {totalPages} of {totalItems} items', + }, + { + currentPage: current + 1, + totalPages: maxPages, + totalItems: total, + } + )} +