diff --git a/portafly/package.json b/portafly/package.json index b36708eed4..3b47150307 100644 --- a/portafly/package.json +++ b/portafly/package.json @@ -10,10 +10,13 @@ "dotenv-expand": "5.1.0", "fs-extra": "^8.1.0", "history": "^4.10.1", + "i18next": "^19.3.3", + "i18next-browser-languagedetector": "^4.0.2", "react": "^16.12.0", "react-app-polyfill": "^1.0.6", "react-async": "^10.0.0", "react-dom": "^16.12.0", + "react-i18next": "^11.3.3", "react-router": "^5.1.2", "react-router-dom": "^5.1.2", "react-router-last-location": "^2.0.1", @@ -25,6 +28,8 @@ "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", + "@types/i18next": "^13.0.0", + "@types/i18next-browser-languagedetector": "^3.0.0", "@types/jest": "^24.0.0", "@types/node": "^12.0.0", "@types/react": "^16.9.0", @@ -126,7 +131,8 @@ "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/src/tests/__mocks__/fileMock.js", "@src": "/src/root.ts", "@src/(.*)": "/src/$1", - "@test/(.*)": "/src/tests/$1" + "@test/(.*)": "/src/tests/$1", + "i18next": "/src/tests/__mocks__/reacti18nextMock.js" }, "moduleFileExtensions": [ "web.js", diff --git a/portafly/src/App.tsx b/portafly/src/App.tsx index db54de9146..ea51d749c4 100644 --- a/portafly/src/App.tsx +++ b/portafly/src/App.tsx @@ -1,53 +1,12 @@ import 'react-app-polyfill/ie11' -import { Brand } from '@patternfly/react-core' import React from 'react' -import { BrowserRouter as Router, Redirect, useHistory } from 'react-router-dom' -import { AppLayout, SwitchWith404, LazyRoute } from 'components' +import { BrowserRouter as Router, Redirect } from 'react-router-dom' +import { SwitchWith404, LazyRoute, Root } from 'components' import { LastLocationProvider } from 'react-router-last-location' -import logo from 'assets/logo.svg' - -const Logo = -const navItems = [ - { - title: 'Overview', - to: '/', - exact: true - }, - { - title: 'Analytics', - to: '/analytics', - items: [ - { to: '/analytics/usage', title: 'Usage' } - ] - }, - { - title: 'Applications', - to: '/applications', - items: [ - { to: '/applications', title: 'Listing' }, - { to: '/applications/plans', title: 'Application Plans' } - ] - }, - { - title: 'Integration', - to: '/integration', - items: [ - { to: '/integration/configuration', title: 'Configuration' } - ] - } -] const getOverviewPage = () => import('pages/Overview') const getApplicationsPage = () => import('pages/Applications') -const App = () => ( - - - - - -) - const PagesSwitch = () => ( @@ -56,25 +15,14 @@ const PagesSwitch = () => ( ) -const Root = () => { - const history = useHistory() - const logoProps = React.useMemo( - () => ({ - onClick: () => history.push('/') - }), - [history] - ) - return ( - - - - ) -} +const App = () => ( + + + + + + + +) export { App } diff --git a/portafly/src/components/Root.tsx b/portafly/src/components/Root.tsx new file mode 100644 index 0000000000..7f7e7835a3 --- /dev/null +++ b/portafly/src/components/Root.tsx @@ -0,0 +1,62 @@ +import React from 'react' +import { Brand } from '@patternfly/react-core' +import { useTranslation } from 'i18n/useTranslation' +import { useHistory } from 'react-router-dom' +import { AppLayout } from 'components' +import logo from 'assets/logo.svg' + +const Root: React.FunctionComponent = ({ children }) => { + const { t } = useTranslation('shared') + const Logo = + + const navItems = [ + { + title: t('nav_items.overview'), + to: '/', + exact: true + }, + { + title: t('nav_items.analytics'), + to: '/analytics', + items: [ + { to: '/analytics/usage', title: t('nav_items.analytics_usage') } + ] + }, + { + title: t('nav_items.applications'), + to: '/applications', + items: [ + { to: '/applications', title: t('nav_items.applications_listing') }, + { to: '/applications/plans', title: t('nav_items.applications_app_plans') } + ] + }, + { + title: t('nav_items.integration'), + to: '/integration', + items: [ + { to: '/integration/configuration', title: t('nav_items.integration_configuration') } + ] + } + ] + + const history = useHistory() + const logoProps = React.useMemo( + () => ({ + onClick: () => history.push('/') + }), + [history] + ) + return ( + + {children} + + ) +} + +export { Root } diff --git a/portafly/src/components/index.tsx b/portafly/src/components/index.tsx index 1f15f55dc7..890563ecc1 100644 --- a/portafly/src/components/index.tsx +++ b/portafly/src/components/index.tsx @@ -8,3 +8,4 @@ export * from './Loading' export * from './NotFound' export * from './SwitchWith404' export * from './util' +export * from './Root' diff --git a/portafly/src/i18n/i18n.tsx b/portafly/src/i18n/i18n.tsx new file mode 100644 index 0000000000..170a1bef87 --- /dev/null +++ b/portafly/src/i18n/i18n.tsx @@ -0,0 +1,34 @@ +import i18n, { FormatFunction } from 'i18next' +import LanguageDetector from 'i18next-browser-languagedetector' +import { initReactI18next } from 'react-i18next' +import { ITranslationsPages, Translations, EN } from 'i18n' + +const formatFn: FormatFunction = (value, format) => { + if (format === 'uppercase') return value.toUpperCase() + if (format === 'lowercase') return value.toLowerCase() + return value +} + +const sections: Array = ['shared', 'overview', 'analytics', 'applications', 'integration'] + +const options = { + lng: EN, + fallbackLng: [EN], + debug: false, + interpolation: { + format: formatFn, + escapeValue: false + }, + ns: sections, + defaultNS: 'shared', + react: { + transKeepBasicHtmlNodesFor: ['br', 'strong', 'i'] + }, + resources: Translations +} + +i18n.use(LanguageDetector) + .use(initReactI18next) + .init(options) + +export { i18n } diff --git a/portafly/src/i18n/index.ts b/portafly/src/i18n/index.ts new file mode 100644 index 0000000000..9e2061bec9 --- /dev/null +++ b/portafly/src/i18n/index.ts @@ -0,0 +1,4 @@ +export * from './i18n' +export * from './locales' +export * from './supported-languages' +export * from './useTranslation' diff --git a/portafly/src/i18n/locales/en/analytics.json b/portafly/src/i18n/locales/en/analytics.json new file mode 100644 index 0000000000..1e77acc37c --- /dev/null +++ b/portafly/src/i18n/locales/en/analytics.json @@ -0,0 +1,8 @@ +{ + "page_title": "Analytics | Portafly", + "page_title_desc": "Page title of the Analytics page", + "body_title": "Analytics", + "body_title_desc": "Body title of the Analytics page", + "subtitle": "This is the Analytics page", + "subtitle_desc": "Subtitle of the Analytics page" +} diff --git a/portafly/src/i18n/locales/en/applications.json b/portafly/src/i18n/locales/en/applications.json new file mode 100644 index 0000000000..b35e13716b --- /dev/null +++ b/portafly/src/i18n/locales/en/applications.json @@ -0,0 +1,8 @@ +{ + "page_title": "Applications | Portafly", + "page_title_desc": "Page title of the Applications page", + "body_title": "Applications", + "body_title_desc": "Body title of the Applications page", + "subtitle": "This is the Applications page", + "subtitle_desc": "Subtitle of the Applications page" +} diff --git a/portafly/src/i18n/locales/en/index.ts b/portafly/src/i18n/locales/en/index.ts new file mode 100644 index 0000000000..b4a96ac22a --- /dev/null +++ b/portafly/src/i18n/locales/en/index.ts @@ -0,0 +1,13 @@ +import shared from 'i18n/locales/en/shared.json' +import overview from 'i18n/locales/en/overview.json' +import analytics from 'i18n/locales/en/analytics.json' +import applications from 'i18n/locales/en/applications.json' +import integration from 'i18n/locales/en/integration.json' + +export { + shared, + overview, + analytics, + applications, + integration +} diff --git a/portafly/src/i18n/locales/en/integration.json b/portafly/src/i18n/locales/en/integration.json new file mode 100644 index 0000000000..94254a550b --- /dev/null +++ b/portafly/src/i18n/locales/en/integration.json @@ -0,0 +1,8 @@ +{ + "page_title": "Integration | Portafly", + "page_title_desc": "Page title of the Integration page", + "body_title": "Integration", + "body_title_desc": "Body title of the Integration page", + "subtitle": "This is the Integration page", + "subtitle_desc": "Subtitle of the Integration page" +} diff --git a/portafly/src/i18n/locales/en/overview.json b/portafly/src/i18n/locales/en/overview.json new file mode 100644 index 0000000000..6b93d7d212 --- /dev/null +++ b/portafly/src/i18n/locales/en/overview.json @@ -0,0 +1,8 @@ +{ + "page_title": "Overview | Portafly", + "page_title_desc": "Page title of the Overview page", + "body_title": "Overview", + "body_title_desc": "Body title of the Overview page", + "subtitle": "This is the Overview page", + "subtitle_desc": "Subtitle of the Overview page" +} diff --git a/portafly/src/i18n/locales/en/shared.json b/portafly/src/i18n/locales/en/shared.json new file mode 100644 index 0000000000..40f96bacdd --- /dev/null +++ b/portafly/src/i18n/locales/en/shared.json @@ -0,0 +1,30 @@ +{ + "logo_alt_text": "Red Hat Integration logo", + "logo_alt_text_desc": "Alt attribute of the main logo", + "nav_items": { + "overview": "Overview", + "overview_desc": "Title of Overview menu item", + "analytics": "Analytics", + "analytics_desc": "Title of Analytics menu item", + "analytics_usage": "Usage", + "analytics_usage_desc": "Title of Analytics > Usage menu item", + "applications": "Applications", + "applications_desc": "Title of Applications menu item", + "applications_listing": "Listing", + "applications_listing_desc": "Title of Applications > Listing menu item", + "applications_app_plans": "Applications Plans", + "applications_app_plans_desc": "Title of Applications > Applications Plans menu item", + "integration": "Integration", + "integration_desc": "Title of Integration menu item", + "integration_configuration": "Configuration", + "integration_configuration_desc": "Title of Integration > Configuration menu item" + }, + "navigation_items_desc": "Titles of navigation items", + "format": { + "uppercase": "{{text, uppercase}}", + "uppercase_desc": "Formats a text as uppercase", + "lowercase": "{{text, lowercase}}", + "lowercase_desc": "Formats a text as lowercase" + }, + "format_desc": "Utilities to format text" +} diff --git a/portafly/src/i18n/locales/index.ts b/portafly/src/i18n/locales/index.ts new file mode 100644 index 0000000000..98947e0631 --- /dev/null +++ b/portafly/src/i18n/locales/index.ts @@ -0,0 +1,11 @@ +import { ISupportedLanguages } from 'i18n' +import * as en from 'i18n/locales/en' + +export type ITranslationsPages = keyof typeof en +export type ITranslations = { [P in ITranslationsPages]: any } + +const Translations: Record = { + en +} + +export { Translations } diff --git a/portafly/src/i18n/supported-languages.ts b/portafly/src/i18n/supported-languages.ts new file mode 100644 index 0000000000..c4bc0f4505 --- /dev/null +++ b/portafly/src/i18n/supported-languages.ts @@ -0,0 +1,3 @@ +export const EN = 'en' + +export type ISupportedLanguages = typeof EN // | typeof ... diff --git a/portafly/src/i18n/useTranslation.ts b/portafly/src/i18n/useTranslation.ts new file mode 100644 index 0000000000..dc3d3a0165 --- /dev/null +++ b/portafly/src/i18n/useTranslation.ts @@ -0,0 +1,14 @@ +import { useTranslation, UseTranslationOptions } from 'react-i18next' +import { ITranslationsPages } from 'i18n' + +/** + * This wrapper mostly allow us to control what strings we pass + * into useTranslation and provides the IDE with intellisense for + * that matter. + */ +const useTranslationWrapper = ( + ns?: ITranslationsPages | Array, + options?: UseTranslationOptions +) => useTranslation(ns, options) + +export { useTranslationWrapper as useTranslation } diff --git a/portafly/src/index.tsx b/portafly/src/index.tsx index 141f9ccbd6..bcc096a405 100644 --- a/portafly/src/index.tsx +++ b/portafly/src/index.tsx @@ -1,6 +1,7 @@ import React from 'react' import ReactDOM from 'react-dom' import '@patternfly/react-core/dist/styles/base.css' +import 'i18n/i18n' import { App } from 'App' ReactDOM.render(, document.getElementById('root')) diff --git a/portafly/src/pages/Applications.tsx b/portafly/src/pages/Applications.tsx index b80a753746..cf81a632ed 100644 --- a/portafly/src/pages/Applications.tsx +++ b/portafly/src/pages/Applications.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { useTranslation } from 'i18n/useTranslation' import { useFetch } from 'react-async' import { useDocumentTitle } from 'components' import { @@ -11,7 +12,8 @@ import { import { Table, TableHeader, TableBody } from '@patternfly/react-table' const Applications: React.FunctionComponent = () => { - useDocumentTitle('Applications') + const { t } = useTranslation('applications') + useDocumentTitle(t('page_title')) const columns = [ 'Name', @@ -48,11 +50,11 @@ const Applications: React.FunctionComponent = () => { <> - Applications + {t('body_title')} - This is the applications screen. + {t('subtitle')} @@ -60,10 +62,10 @@ const Applications: React.FunctionComponent = () => { {rows && ( - - - -
+ + + +
)}
diff --git a/portafly/src/pages/Overview.tsx b/portafly/src/pages/Overview.tsx index 58fffef11e..db2c6bf42c 100644 --- a/portafly/src/pages/Overview.tsx +++ b/portafly/src/pages/Overview.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { useTranslation } from 'i18n/useTranslation' import { useA11yRouteChange, useDocumentTitle } from 'components' import { PageSection, @@ -10,17 +11,16 @@ import { } from '@patternfly/react-core' const Overview: React.FunctionComponent = () => { + const { t } = useTranslation('overview') useA11yRouteChange() - useDocumentTitle('Overview') + useDocumentTitle(t('page_title')) return ( <> - Overview + {t('body_title')} - PortaFly - {' '} - the next gen UI for Porta API MGMT App + {t('subtitle')} @@ -28,7 +28,7 @@ const Overview: React.FunctionComponent = () => { -

OHAI

+

{t('shared:format.uppercase', { text: 'Ohai' })}

diff --git a/portafly/src/tests/__mocks__/reacti18nextMock.js b/portafly/src/tests/__mocks__/reacti18nextMock.js new file mode 100644 index 0000000000..a29ada48a9 --- /dev/null +++ b/portafly/src/tests/__mocks__/reacti18nextMock.js @@ -0,0 +1,3 @@ +module.exports = { + useTranslation: () => ({ t: (key) => key }) +} diff --git a/portafly/yarn.lock b/portafly/yarn.lock index 190c63e89f..fac25fffad 100644 --- a/portafly/yarn.lock +++ b/portafly/yarn.lock @@ -1248,7 +1248,7 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/runtime@^7.8.4": +"@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4": version "7.9.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06" integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q== @@ -1910,6 +1910,20 @@ resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.5.tgz#527d20ef68571a4af02ed74350164e7a67544860" integrity sha512-wLD/Aq2VggCJXSjxEwrMafIP51Z+13H78nXIX0ABEuIGhmB5sNGbR113MOKo+yfw+RDo1ZU3DM6yfnnRF/+ouw== +"@types/i18next-browser-languagedetector@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/i18next-browser-languagedetector/-/i18next-browser-languagedetector-3.0.0.tgz#5117c813488ec5515f316fd0bbc32e4d03dbaf86" + integrity sha512-jCIazV+0MyFB/re4i+HdqkNLNIWahcVztIPyDoBM2KjrFIhzGyvpclel7ma6xhbm+PvidTDY0eXcFRCO+2QOhQ== + dependencies: + i18next-browser-languagedetector "*" + +"@types/i18next@^13.0.0": + version "13.0.0" + resolved "https://registry.yarnpkg.com/@types/i18next/-/i18next-13.0.0.tgz#403ef338add0104e74d9759f1b39217e7c5d4084" + integrity sha512-gp/SIShAuf4WOqi8ey0nuI7qfWaVpMNCcs/xLygrh/QTQIXmlDC1E0TtVejweNW+7SGDY7g0lyxyKZIJuCKIJw== + dependencies: + i18next "*" + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" @@ -6417,6 +6431,13 @@ html-minifier-terser@^5.0.1: relateurl "^0.2.7" terser "^4.3.9" +html-parse-stringify2@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz#dc5670b7292ca158b7bc916c9a6735ac8872834a" + integrity sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o= + dependencies: + void-elements "^2.0.1" + html-tags@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140" @@ -6532,6 +6553,20 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= +i18next-browser-languagedetector@*, i18next-browser-languagedetector@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-4.0.2.tgz#eb02535cc5e57dd534fc60abeede05a3823a8551" + integrity sha512-AK4IZ3XST4HIKShgpB2gOFeDPrMOnZx56GLA6dGo/8rvkiczIlq05lV8w77c3ShEZxtTZeUVRI4Q/cBFFVXS/w== + dependencies: + "@babel/runtime" "^7.5.5" + +i18next@*, i18next@^19.3.3: + version "19.3.3" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-19.3.3.tgz#04bd79b315e5fe2c87ab8f411e5d55eda0a17bd8" + integrity sha512-CnuPqep5/JsltkGvQqzYN4d79eCe0TreCBRF3a8qHHi8x4SON1qqZ/pvR2X7BfNkNqpA5HXIqw0E731H+VsgSg== + dependencies: + "@babel/runtime" "^7.3.1" + iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -10372,6 +10407,14 @@ react-error-overlay@^6.0.7: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108" integrity sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA== +react-i18next@^11.3.3: + version "11.3.3" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.3.3.tgz#a84dcc32e3ad013012964d836790d8c6afac8e88" + integrity sha512-sGnPwJ0Kf8qTRLTnTRk030KiU6WYEZ49rP9ILPvCnsmgEKyucQfTxab+klSYnCSKYija+CWL+yo+c9va9BmJeg== + dependencies: + "@babel/runtime" "^7.3.1" + html-parse-stringify2 "2.0.1" + react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4: version "16.12.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" @@ -12632,6 +12675,11 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== +void-elements@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" + integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w= + w3c-hr-time@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045"