diff --git a/package-lock.json b/package-lock.json index b8684f8..7e32d17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,9 +17,12 @@ "bootstrap": "^4.4.1", "excerpts": "^0.0.3", "html-to-react": "1.7.0", + "i18next": "^23.11.4", + "i18next-browser-languagedetector": "^8.0.0", "react": "18.2.0", "react-dom": "18.2.0", "react-helmet": "^6.1.0", + "react-i18next": "^12.2.0", "react-js-pagination": "^3.0.3", "react-router-dom": "^6.10.0", "swagger-ui-react": "^4.15.5" @@ -3997,6 +4000,14 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "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-to-react": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/html-to-react/-/html-to-react-1.7.0.tgz", @@ -4122,6 +4133,36 @@ "node": ">=8.12.0" } }, + "node_modules/i18next": { + "version": "23.11.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.4.tgz", + "integrity": "sha512-CCUjtd5TfaCl+mLUzAA0uPSN+AVn4fP/kWCYt/hocPUwusTpMVczdrRyOBUwk6N05iH40qiKx6q1DoNJtBIwdg==", + "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/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -6066,6 +6107,27 @@ "react": ">=16.3.0" } }, + "node_modules/react-i18next": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.3.1.tgz", + "integrity": "sha512-5v8E2XjZDFzK7K87eSwC7AJcAkcLt5xYZ4+yTPDAW1i7C93oOY1dnr4BaQM7un4Hm+GmghuiPvevWwlca5PwDA==", + "dependencies": { + "@babel/runtime": "^7.20.6", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 19.0.0", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-immutable-proptypes": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/react-immutable-proptypes/-/react-immutable-proptypes-2.2.0.tgz", @@ -7528,6 +7590,14 @@ } } }, + "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", diff --git a/package.json b/package.json index ba12050..7e72e8e 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,12 @@ "bootstrap": "^4.4.1", "excerpts": "^0.0.3", "html-to-react": "1.7.0", + "i18next": "^23.11.4", + "i18next-browser-languagedetector": "^8.0.0", "react": "18.2.0", "react-dom": "18.2.0", "react-helmet": "^6.1.0", + "react-i18next": "^12.2.0", "react-js-pagination": "^3.0.3", "react-router-dom": "^6.10.0", "swagger-ui-react": "^4.15.5" diff --git a/src/components/FeaturedDatasets/index.jsx b/src/components/FeaturedDatasets/index.jsx index 845d4c8..4fd02da 100644 --- a/src/components/FeaturedDatasets/index.jsx +++ b/src/components/FeaturedDatasets/index.jsx @@ -3,13 +3,17 @@ import PropTypes from "prop-types"; import excerpts from 'excerpts'; import { Link } from 'react-router-dom'; import { Text } from "@civicactions/data-catalog-components"; +import '../../i18n'; +import { useTranslation } from 'react-i18next'; import config from "../../assets/config"; import './featureddatasets.scss'; const FeaturedDatasets = ({ datasets }) => { + const { t, i18n } = useTranslation(); + return (
-

Featured Datasets

+

{t('home.featured')}

    {datasets.map((item) => (
  1. diff --git a/src/i18n.js b/src/i18n.js new file mode 100644 index 0000000..5d3c4a9 --- /dev/null +++ b/src/i18n.js @@ -0,0 +1,59 @@ +import i18n from 'i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import { initReactI18next } from 'react-i18next'; + +const resources = { + en: { + translation: { + 'about': { + 'title': 'About this site' + }, + 'home': { + 'featured': 'Featured Datasets', + 'topics': 'Dataset Topics' + }, + 'not_found': { + 'title': 'Page not found.' + }, + 'publishers': { + 'title': 'Dataset Publishers' + }, + 'search': { + 'title': 'Datasets' + } + }, + }, + es: { + translation: { + 'about': { + title: 'Sobre este sitio' + }, + 'home': { + 'featured': 'Conjuntos de Datos Destacados', + 'topics': 'Temas de Conjuntos de Datos' + }, + 'not_found': { + 'title': 'Página no encontrada.' + }, + 'publishers': { + 'title': 'Publicadores de Conjuntos de Datos', + }, + 'search': { + 'title': 'Conjuntos de Datos' + } + }, + }, +}; + +i18n + .use(LanguageDetector) + .use(initReactI18next) // passes i18n down to react-i18next + .init({ + resources, + fallbackLng: 'en', + interpolation: { + escapeValue: false, // react already safes from xss + }, + }); + +export default i18n; diff --git a/src/templates/about/index.jsx b/src/templates/about/index.jsx index 9a73483..8823f79 100644 --- a/src/templates/about/index.jsx +++ b/src/templates/about/index.jsx @@ -1,36 +1,45 @@ import React from "react"; import { Announcement } from "@civicactions/data-catalog-components"; -import Layout from '../../components/Layout'; +import "../../i18n"; +import { useTranslation } from "react-i18next"; +import Layout from "../../components/Layout"; import config from "../../assets/config"; -import { version, dependencies } from '../../../package.json'; +import { version, dependencies } from "../../../package.json"; -const About = () => ( +const About = () => { + const { t, i18n } = useTranslation(); + + return ( -
    -

    About this site

    -
    -
    -

    This is the default state of the DKAN data catalog.

    -

    - This tool helps create open data catalogs using React and other - libraries. -

    -
    -
    - -

    Update this about page before publishing.

    -
    -
    -
    -

    App version:

    -
    -
    -

    data-catalog-app: {version}

    -

    data-catalog-components: {dependencies["@civicactions/data-catalog-components"]}

    -
    -
    +
    +

    {t("about.title")}

    +
    +
    +

    This is the default state of the DKAN data catalog.

    +

    + This tool helps create open data catalogs using React and other + libraries. +

    +
    +
    + +

    Update this about page before publishing.

    +
    +
    +
    +

    App version:

    +
    +
    +

    data-catalog-app: {version}

    +

    + data-catalog-components:{" "} + {dependencies["@civicactions/data-catalog-components"]} +

    +
    +
    -); + ); +}; export default About; diff --git a/src/templates/home/index.jsx b/src/templates/home/index.jsx index 55044cb..1d2eafc 100644 --- a/src/templates/home/index.jsx +++ b/src/templates/home/index.jsx @@ -1,74 +1,96 @@ -import React, {useEffect} from "react"; -import { useQuery } from '@tanstack/react-query'; +import React, { useEffect } from "react"; +import { useQuery } from "@tanstack/react-query"; import { Blocks, Hero, IconList, IconListItem, - StatBlock + StatBlock, } from "@civicactions/data-catalog-components"; -import Layout from '../../components/Layout'; -import FeaturedDatasets from '../../components/FeaturedDatasets'; +import "../../i18n"; +import { useTranslation } from "react-i18next"; +import Layout from "../../components/Layout"; +import FeaturedDatasets from "../../components/FeaturedDatasets"; import copy from "../../assets/copy.json"; const Home = () => { + const { t, i18n } = useTranslation(); const datasets = useQuery({ - queryKey: ['datasets'], + queryKey: ["datasets"], queryFn: () => { - return fetch(`${import.meta.env.VITE_REACT_APP_ROOT_URL}/metastore/schemas/dataset/items?show-reference-ids`).then( - (res) => res.json(), - ) - } + return fetch( + `${ + import.meta.env.VITE_REACT_APP_ROOT_URL + }/metastore/schemas/dataset/items?show-reference-ids` + ).then((res) => res.json()); + }, }).data; const themes = useQuery({ - queryKey: ['themes'], + queryKey: ["themes"], queryFn: () => { - return fetch(`${import.meta.env.VITE_REACT_APP_ROOT_URL}/metastore/schemas/theme/items`).then( - (res) => res.json(), - ) - } + return fetch( + `${ + import.meta.env.VITE_REACT_APP_ROOT_URL + }/metastore/schemas/theme/items` + ).then((res) => res.json()); + }, }).data; - const orderedDatasets = (datasets && datasets.length) ? datasets.sort(function(a,b) { - return a.title - b.title; - }): []; + const orderedDatasets = + datasets && datasets.length + ? datasets.sort(function (a, b) { + return a.title - b.title; + }) + : []; - const fDatasets = orderedDatasets.length > 3 ? orderedDatasets.slice(orderedDatasets.length -3, orderedDatasets.length) : orderedDatasets; + const fDatasets = + orderedDatasets.length > 3 + ? orderedDatasets.slice( + orderedDatasets.length - 3, + orderedDatasets.length + ) + : orderedDatasets; - const items = (themes && themes.length) ? - themes.map(x => { - let item = { - identifier: x.identifier, - ref: `search?theme=${x.data}`, - title: x.data, - size: "100" - }; - return item; - }) : []; + const items = + themes && themes.length + ? themes.map((x) => { + let item = { + identifier: x.identifier, + ref: `search?theme=${x.data}`, + title: x.data, + size: "100", + }; + return item; + }) + : []; return ( -
    - +
    +
    - +
    -
    +
    ); -} +}; export default Home; diff --git a/src/templates/publishers/index.jsx b/src/templates/publishers/index.jsx index a109038..5c8ba49 100644 --- a/src/templates/publishers/index.jsx +++ b/src/templates/publishers/index.jsx @@ -1,14 +1,18 @@ import React from 'react'; import { PublisherList } from "@civicactions/data-catalog-components"; +import '../../i18n'; +import { useTranslation } from 'react-i18next'; import config from "../../assets/config"; import Layout from '../../components/Layout'; import orgs from "../../assets/publishers"; const Publishers = () => { + const { t, i18n } = useTranslation(); + return (
    -

    Dataset Publishers

    +

    {t('publishers.title')}

    If you need to separate users and datasets into smaller collections, diff --git a/src/templates/search/index.jsx b/src/templates/search/index.jsx index f4f36e0..9659cca 100644 --- a/src/templates/search/index.jsx +++ b/src/templates/search/index.jsx @@ -2,16 +2,20 @@ import React from 'react'; import { useLocation } from "react-router-dom" import { defaultFacets, sortOptions } from '../../config/search'; import { Search } from "@civicactions/data-catalog-components"; +import '../../i18n'; +import { useTranslation } from 'react-i18next'; import Layout from "../../components/Layout"; import config from "../../assets/config"; const SearchTemplate = () => { const location = useLocation(); + const { t, i18n } = useTranslation(); + return (

    -

    Datasets

    +

    {t('search.title')}