diff --git a/features/internationalized_ui.feature b/features/internationalized_ui.feature new file mode 100644 index 00000000..e03abb0e --- /dev/null +++ b/features/internationalized_ui.feature @@ -0,0 +1,55 @@ +#language: fr + +Fonctionnalité: Avoir une interface traduite dans la langue favorite + +Scénario: en français sur un document sans ses gloses ouvertes + + Soit "en" la langue préférée configurée dans le navigateur + Et "Vidéo Sherlock Jr. (Buster Keaton)" le document principal + Et un texte flottant avec la balise title "Create a document as a glose" est présent + Et un texte flottant avec la balise title "Create a collection from this document" est présent + Quand je choisis "fr" comme langue préférée configurée dans le navigateur et que je rafraichisse la page + Alors un texte flottant avec la balise title "Créer un document en tant que glose" est présent + Et un texte flottant avec la balise title "Créer une collection à partir de ce document" est présent + +Scénario: en français sur la page principale + + Soit "en" la langue préférée configurée dans le navigateur + Et la liste des documents affichée + Et la page contient + |Sign in| + |referenced by 1 document(s)| + Et un texte flottant avec la balise title "Create a document from scratch" est présent + Et un placeholder contenant "Username" est présent + Et un placeholder contenant "Password" est présent + Quand je choisis "fr" comme langue préférée configurée dans le navigateur et que je rafraichisse la page + Alors la page contient + |Se connecter| + |référencé par 1 document(s)| + Et un texte flottant avec la balise title "Créer un document à partir de zéro" est présent + Et un placeholder contenant "Nom d’utilisateur" est présent + Et un placeholder contenant "Mot de passe" est présent + +Scénario: en français sur la page de création de document + + Soit "fr" la langue préférée configurée dans le navigateur + Et la liste des documents affichée + Et une session active avec mon compte + Quand j'essaie de créer un nouveau document + Alors la page contient + | ()| + |Tous droits réservés| + || + +Scénario: en anglais sur un document avec une de ses gloses ouverte + + Soit "fr" la langue préférée configurée dans le navigateur + Et "Vidéo Sherlock Jr. (Buster Keaton)" le document principal + Et "Note rire Buster Keaton (Antoine-Valentin Charpentier)" une des gloses ouverte + Et un texte flottant avec l’attribut title "Éditer les métadonnées..." est présent + Et un texte flottant avec l’attribut title "Éditer le contenu..." est présent + Et un texte flottant avec la balise title "Éditer le type" est présent + Quand je choisis "en" comme langue préférée configurée dans le navigateur, que je rafraichisse la page et que "Note rire Buster Keaton (Antoine-Valentin Charpentier)" une des gloses ouverte + Alors un texte flottant avec l’attribut title "Edit metadata..." est présent + Et un texte flottant avec l’attribut title "Edit content..." est présent + Et un texte flottant avec la balise title "Edit type" est présent \ No newline at end of file diff --git a/features/step_definitions/context.rb b/features/step_definitions/context.rb index 2ab48061..52adc0f3 100644 --- a/features/step_definitions/context.rb +++ b/features/step_definitions/context.rb @@ -131,3 +131,19 @@ Soit("un document que l'on consulte") do visit '/146e6e8442f0405b721b79357d00d0a1' end + +Soit("{string} la langue préférée configurée dans le navigateur") do |language| + page.driver.add_headers("Accept-Language" => language) +end + +Soit('un texte flottant avec la balise title {string} est présent') do |text| + expect(page).to have_selector("title", text: /#{text}/i, visible: false) +end + +Soit('un texte flottant avec l’attribut title {string} est présent') do |text| + expect(page).to have_selector("[title='#{text}']") +end + +Soit('un placeholder contenant {string} est présent') do |text| + expect(page).to have_selector("input[placeholder='#{text}']") +end \ No newline at end of file diff --git a/features/step_definitions/event.rb b/features/step_definitions/event.rb index 958907f7..aa606f5f 100644 --- a/features/step_definitions/event.rb +++ b/features/step_definitions/event.rb @@ -56,3 +56,15 @@ click_on_icon('sources .create-document') end +Quand("je choisis {string} comme langue préférée configurée dans le navigateur et que je rafraichisse la page") do |language| + page.driver.add_headers("Accept-Language" => language) + visit current_path + sleep 2 +end + +Quand("je choisis {string} comme langue préférée configurée dans le navigateur, que je rafraichisse la page et que {string} une des gloses ouverte") do |language, title| + page.driver.add_headers("Accept-Language" => language) + visit current_path + sleep 2 + click_on_icon_next_to('open', title) +end diff --git a/features/step_definitions/outcome.rb b/features/step_definitions/outcome.rb index f9c945cc..14a69dbd 100644 --- a/features/step_definitions/outcome.rb +++ b/features/step_definitions/outcome.rb @@ -85,4 +85,10 @@ Alors("je ne vois pas le document intitulé {string}") do |title| expect(page).not_to have_content(title) +end + +Soit('la page contient') do |table| + table.raw.flatten.each do |localization| + expect(page).to have_content localization + end end \ No newline at end of file diff --git a/features/support/env.rb b/features/support/env.rb index e25f6f62..07f20a6b 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -2,7 +2,7 @@ require 'capybara/cuprite' Before do - Capybara.current_session.driver.add_headers("Accept-Language" => "fr") + Capybara.current_session.driver.add_headers("Accept-Language" => "en") end Capybara.run_server = false Capybara.default_driver = :cuprite @@ -36,10 +36,17 @@ def have_image(alternative_text) end def sign_in(username, password) - fill_in placeholder: "Username", with: username - fill_in placeholder: 'Password', with: password - click_on 'Sign in' - expect(page).to have_content username + if page.has_content?('Sign in') + fill_in placeholder: "Username", with: username + fill_in placeholder: 'Password', with: password + click_on 'Sign in' + expect(page).to have_content username + else + fill_in placeholder: "Nom d’utilisateur", with: username + fill_in placeholder: 'Mot de passe', with: password + click_on 'Se connecter' + expect(page).to have_content username + end end def sign_out diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0d98eda1..da7b250c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,10 +14,14 @@ "bootstrap": "^5.2.2", "buffer": "^6.0.3", "events": "^3.3.0", + "i18next": "^23.11.5", + "i18next-browser-languagedetector": "^8.0.0", + "i18next-http-backend": "^2.5.2", "react": "^18.2.0", "react-bootstrap": "^2.5.0", "react-bootstrap-icons": "^1.10.2", "react-dom": "^18.2.0", + "react-i18next": "^14.1.2", "react-image-crop": "^10.0.9", "react-markdown": "^8.0.4", "react-notifications": "^1.7.4", @@ -711,6 +715,14 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -1564,6 +1576,14 @@ "@types/unist": "*" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-void-elements": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", @@ -1573,6 +1593,44 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/i18next": { + "version": "23.11.5", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.5.tgz", + "integrity": "sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz", + "integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-http-backend": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.5.2.tgz", + "integrity": "sha512-+K8HbDfrvc1/2X8jpb7RLhI9ZxBDpx3xogYkQwGKlWAUXLSEGXzgdt3EcUjLlBCdMwdQY+K+EUF6oh8oB6rwHw==", + "dependencies": { + "cross-fetch": "4.0.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -2635,6 +2693,25 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2838,6 +2915,27 @@ "react": "^18.2.0" } }, + "node_modules/react-i18next": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.2.tgz", + "integrity": "sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-image-crop": { "version": "10.1.8", "resolved": "https://registry.npmjs.org/react-image-crop/-/react-image-crop-10.1.8.tgz", @@ -3850,6 +3948,11 @@ "node": ">=8" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -4206,6 +4309,14 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", @@ -4228,6 +4339,20 @@ "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-3.5.2.tgz", "integrity": "sha512-c0rhqNcHXRkY/ogGDJQxZ9Im9D19hDihbzSQJrsioex+KnFgmMzBiy57Z1EjkhX/+OjyBpclDCzz2ITtjokFmg==" }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which-boxed-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index c9308a53..1d65cc2b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,10 +9,14 @@ "bootstrap": "^5.2.2", "buffer": "^6.0.3", "events": "^3.3.0", + "i18next": "^23.11.5", + "i18next-browser-languagedetector": "^8.0.0", + "i18next-http-backend": "^2.5.2", "react": "^18.2.0", "react-bootstrap": "^2.5.0", "react-bootstrap-icons": "^1.10.2", "react-dom": "^18.2.0", + "react-i18next": "^14.1.2", "react-image-crop": "^10.0.9", "react-markdown": "^8.0.4", "react-notifications": "^1.7.4", diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json new file mode 100644 index 00000000..68f833bb --- /dev/null +++ b/frontend/public/locales/en/translation.json @@ -0,0 +1,21 @@ +{ + "document": "Create a document", + "glose": "as a glose", + "source": "as a source", + "scratch": "from scratch", + "collection": "Create a collection from this document", + "signin": "Sign in", + "reference": "referenced by", + "username": "Username", + "password": "Password", + "title": "", + "creator": "<CREATOR>", + "rights": "All rights reserved", + "text": "<TEXT>", + "metadata": "Edit metadata...", + "content": "Edit content...", + "type": "Edit type", + "doc_close": "Close this document", + "doc_open": "Open this document", + "doc_focus": "Focus on this document" +} \ No newline at end of file diff --git a/frontend/public/locales/fr/translation.json b/frontend/public/locales/fr/translation.json new file mode 100644 index 00000000..06513b6d --- /dev/null +++ b/frontend/public/locales/fr/translation.json @@ -0,0 +1,21 @@ +{ + "document": "Créer un document", + "glose": "en tant que glose", + "source": "en tant que source", + "scratch": "à partir de zéro", + "collection": "Créer une collection à partir de ce document", + "signin": "Se connecter", + "reference": "référencé par", + "username": "Nom d’utilisateur", + "password": "Mot de passe", + "title": "<TITRE>", + "creator": "<CRÉATEUR>", + "rights": "Tous droits réservés", + "text": "<TEXTE>", + "metadata": "Éditer les métadonnées...", + "content": "Éditer le contenu...", + "type": "Éditer le type", + "doc_close": "Fermer ce document", + "doc_open": "Ouvrir ce document", + "doc_focus": "Se concentrer sur ce document" +} \ No newline at end of file diff --git a/frontend/src/components/BrowseTools.js b/frontend/src/components/BrowseTools.js index 0130af09..8737f10a 100644 --- a/frontend/src/components/BrowseTools.js +++ b/frontend/src/components/BrowseTools.js @@ -1,21 +1,23 @@ import { Link } from 'react-router-dom'; import { Bookmark, ChevronBarDown, ChevronExpand } from 'react-bootstrap-icons'; +import { useTranslation } from 'react-i18next'; function BrowseTools({id, closable, openable }) { + const { t } = useTranslation(); return ( <> {closable && <Link to="#" className="icon"> - <ChevronBarDown title="Close this document" /> + <ChevronBarDown title={`${t('doc_close')}`} /> </Link> } {openable && <Link to={`#${id}`} className="icon open"> - <ChevronExpand title="Open this document" /> + <ChevronExpand title={`${t('doc_open')}`} /> </Link> } <Link to={`../${id}`} className="icon focus"> - <Bookmark title="Focus on this document" /> + <Bookmark title={`${t('doc_focus')}`} /> </Link> </> ); diff --git a/frontend/src/components/DocumentsCards.js b/frontend/src/components/DocumentsCards.js index c07de29d..2dd81eee 100644 --- a/frontend/src/components/DocumentsCards.js +++ b/frontend/src/components/DocumentsCards.js @@ -6,6 +6,7 @@ import Metadata from './Metadata'; import BrowseTools from './BrowseTools'; import FutureDocument from './FutureDocument'; import { TypeBadge } from './Type'; +import { useTranslation } from 'react-i18next'; // asSource is a flag that indicates whether to create a parent (left) or a glose (right) function DocumentsCards({docs, expandable, byRow, createOn, setLastUpdate, backend, asSource = false}) { @@ -48,9 +49,10 @@ function DocumentCard({doc, expandable}) { } function References({doc}) { + const { t } = useTranslation(); if (doc.referenced) return ( <Card.Footer> - referenced by {doc.referenced} document(s) + {t('reference')} {doc.referenced} document(s) </Card.Footer> ); } diff --git a/frontend/src/components/EditableText.js b/frontend/src/components/EditableText.js index e1958dcd..56018520 100644 --- a/frontend/src/components/EditableText.js +++ b/frontend/src/components/EditableText.js @@ -3,8 +3,10 @@ import '../styles/EditableText.css'; import { useState, useEffect } from 'react'; import FormattedText from './FormattedText'; import {v4 as uuid} from 'uuid'; +import { useTranslation } from 'react-i18next'; function EditableText({id, text, rubric, isPartOf, links, backend, setLastUpdate}) { + const { t } = useTranslation(); const [beingEdited, setBeingEdited] = useState(false); const [editedDocument, setEditedDocument] = useState(); const [editedText, setEditedText] = useState(); @@ -57,7 +59,7 @@ function EditableText({id, text, rubric, isPartOf, links, backend, setLastUpdate }; if (!beingEdited) return ( - <div className="editable content" onClick={handleClick} title="Edit content..."> + <div className="editable content" onClick={handleClick} title={`${t('content')}`}> <FormattedText> {editedText || text} </FormattedText> diff --git a/frontend/src/components/FutureDocument.js b/frontend/src/components/FutureDocument.js index b9868fbc..02a8f562 100644 --- a/frontend/src/components/FutureDocument.js +++ b/frontend/src/components/FutureDocument.js @@ -1,4 +1,5 @@ import '../styles/FutureDocument.css'; +import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import Card from 'react-bootstrap/Card'; @@ -17,6 +18,7 @@ function FutureDocument({relatedTo, verb = 'refersTo', setLastUpdate, backend, a function FutureDocumentIcon({relatedTo, verb, setLastUpdate, backend, asSource = false}) { const navigate = useNavigate(); + const { t } = useTranslation(); let handleClick = async () => { let _id = uuid(); @@ -27,11 +29,11 @@ function FutureDocumentIcon({relatedTo, verb, setLastUpdate, backend, asSource = backend.putDocument({ _id, editors, - dc_creator: '<CREATOR>', - dc_title: '<TITLE>', + dc_creator: t('creator'), + dc_title: t('title'), dc_issued: new Date(), dc_license: '', - text: '<TEXT>', + text: t('text'), }).then(() => { setLastUpdate(_id); backend.getDocument(documentId) @@ -55,11 +57,11 @@ function FutureDocumentIcon({relatedTo, verb, setLastUpdate, backend, asSource = backend.putDocument({ _id, editors, - dc_creator: '<CREATOR>', - dc_title: '<TITLE>', + dc_creator: t('creator'), + dc_title: t('title'), dc_issued: new Date(), dc_license: '', - text: '<TEXT>', + text: t('text'), links }).then((x) => { setLastUpdate(_id); @@ -71,14 +73,13 @@ function FutureDocumentIcon({relatedTo, verb, setLastUpdate, backend, asSource = switch (verb) { case 'includes': return ( - <FolderPlus title="Create a collection from this document" + <FolderPlus title= {`${t('collection')}`} className="icon create-collection" onClick={handleClick} /> ); default: return ( - <PlusLg title={`Create a document ${asSource ? 'as a source' : relatedTo.length ? 'as a glose' : 'from scratch'}`} - className="icon create-document" onClick={handleClick} + <PlusLg title={`${t('document')} ${asSource ? t('source') : relatedTo.length ? t('glose') : t('scratch')}`} className="icon create-document" onClick={handleClick} /> ); } diff --git a/frontend/src/components/License.js b/frontend/src/components/License.js index 4f25b87c..6b92e636 100644 --- a/frontend/src/components/License.js +++ b/frontend/src/components/License.js @@ -1,6 +1,8 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; function License({ license }) { + const { t } = useTranslation(); let license_uri = license; let [license_name] = /BY[\w-]+/i.exec(license_uri) || []; @@ -13,7 +15,7 @@ function License({ license }) { ); return ( <span className="license"> - All rights reserved + {t('rights')} </span> ); } diff --git a/frontend/src/components/Menu.js b/frontend/src/components/Menu.js index d33e88a7..e5561c8b 100644 --- a/frontend/src/components/Menu.js +++ b/frontend/src/components/Menu.js @@ -7,6 +7,7 @@ import Row from 'react-bootstrap/Row'; import Form from 'react-bootstrap/Form'; import { Link } from 'react-router-dom'; import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; function Menu({backend, user, setUser}) { return ( @@ -28,6 +29,7 @@ function Menu({backend, user, setUser}) { } function Authentication({backend, user, setUser}) { + const { t } = useTranslation(); let handleSubmit = (e) => { e.preventDefault(); @@ -46,18 +48,18 @@ function Authentication({backend, user, setUser}) { <Form onSubmit={handleSubmit}> <Row className="g-1"> <Col> - <input placeholder="Username" name="name" + <input placeholder={`${t('username')}`} name="name" className="form-control-sm" /> </Col> <Col> - <input placeholder="Password" name="password" type="password" + <input placeholder={`${t('password')}`} name="password" type="password" className="form-control-sm" /> </Col> <Col> <button className="btn btn-outline-light" type="submit"> - Sign in + {t('signin')} </button> </Col> </Row> diff --git a/frontend/src/components/Metadata.js b/frontend/src/components/Metadata.js index 0d714752..ba355ba2 100644 --- a/frontend/src/components/Metadata.js +++ b/frontend/src/components/Metadata.js @@ -2,8 +2,10 @@ import '../styles/Metadata.css'; import { useEffect, useState } from 'react'; import yaml from 'yaml'; +import { useTranslation } from 'react-i18next'; function Metadata({metadata = {}, editable, backend}) { + const { t } = useTranslation(); const [beingEdited, setBeingEdited] = useState(false); const [editedDocument, setEditedDocument] = useState(metadata); @@ -45,7 +47,7 @@ function Metadata({metadata = {}, editable, backend}) { if (!beingEdited) { let {dc_title, dc_spatial, dc_creator, dc_translator, dc_isPartOf, dc_issued} = editedMetadata; let attributes = (editable) - ? {className: 'editable metadata', onClick: handleClick, title: 'Edit metadata...'} + ? {className: 'editable metadata', onClick: handleClick, title: t('metadata')} : {}; return ( <span {...attributes}> diff --git a/frontend/src/components/Type.js b/frontend/src/components/Type.js index 42e24134..9ffa2deb 100644 --- a/frontend/src/components/Type.js +++ b/frontend/src/components/Type.js @@ -5,6 +5,7 @@ import { TagFill } from 'react-bootstrap-icons'; import { useState, useContext } from 'react'; import { ListGroup } from 'react-bootstrap'; import { TypesContext } from './TypesContext.js'; +import { useTranslation } from 'react-i18next'; export function TypeBadge({ type, addClassName }) { if (!type) return null; @@ -44,6 +45,7 @@ function TypeList({ typeSelected, handleUpdate }) { } function Type({ metadata, editable, backend }) { + const { t } = useTranslation(); const [ beingEdited, setBeingEdited ] = useState(false); const [ typeSelected, setTypeSelected ] = useState(metadata.type); const [ editedDocument, setEditedDocument ] = useState(metadata); @@ -68,7 +70,7 @@ function Type({ metadata, editable, backend }) { <div style={{ paddingTop: 10, paddingBottom: 30 }}> <div style={{ paddingTop: 0, justifyContent: 'flex-end' }}> <TypeBadge addClassName="typeSelected" type={typeSelected}/> - {editable ? <TagFill onClick={handleEdit} className="icon typeIcon" title="Edit type"/> : null} + {editable ? <TagFill onClick={handleEdit} className="icon typeIcon" title={`${t('type')}`}/> : null} </div> {beingEdited ? <TypeList typeSelected={typeSelected} handleUpdate={handleUpdate}/> diff --git a/frontend/src/i18n.js b/frontend/src/i18n.js new file mode 100644 index 00000000..505de7e2 --- /dev/null +++ b/frontend/src/i18n.js @@ -0,0 +1,26 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import HttpBackend from 'i18next-http-backend'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +i18n + .use(HttpBackend) // charge les traductions à partir de fichiers JSON + .use(LanguageDetector) // détecte la langue de l'utilisateur + .use(initReactI18next) // initie la bibliothèque react-i18next + .init({ + fallbackLng: 'en', // langue par défaut + supportedLngs: ['en', 'fr'], // Liste des langues supportées + debug: true, // active le débogage pour voir les messages dans la console + interpolation: { + escapeValue: false, // réglez sur false si vous utilisez React pour éviter l'échappement double + }, + backend: { + loadPath: '/locales/{{lng}}/translation.json', // chemin des fichiers de traduction + }, + detection: { + order: ['navigator', 'cookie', 'localStorage', 'sessionStorage', 'querystring', 'htmlTag'], // Ordre des méthodes de détection de la langue + caches: ['cookie'], // Méthodes de mise en cache de la langue choisie + }, + }); + +export default i18n; \ No newline at end of file diff --git a/frontend/src/index.js b/frontend/src/index.js index c5b76695..6f13bf3d 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -11,6 +11,8 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { NotificationContainer, NotificationManager } from 'react-notifications'; import 'react-notifications/lib/notifications.css'; import { TypesContext } from './components/TypesContext.js'; +import { I18nextProvider } from 'react-i18next'; +import i18n from './i18n'; const backend = new Hyperglosae( x => NotificationManager.warning(x, '', 2000) @@ -45,8 +47,11 @@ function App() { ); } -root.render(<App/>); - +root.render( + <I18nextProvider i18n={i18n}> + <App /> + </I18nextProvider> +); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals