From 17404a9fea3d3b2e8f799e89cf00c55e8fe02f85 Mon Sep 17 00:00:00 2001 From: Fabio Bonelli Date: Tue, 16 Jan 2024 09:14:43 +0100 Subject: [PATCH] feat(l10n): refactor the localization code (#296) * App localization is now based on the detected language from the browser at runtime * Spell the whole language name in the UI * Change the UI for multiple languages in the publiccode.yml file, removing the tabs * Use a library for the list of languages, so we don't have to maintain our own Obsoletes #247 Related #234 Fixes #123, Fixes #26 --- package-lock.json | 105 +- package.json | 2 + src/app/components/Editor.jsx | 10 +- src/app/components/EditorForm.jsx | 4 +- src/app/components/LanguageSwitcher.tsx | 63 -- src/app/components/PubliccodeYmlLanguages.tsx | 60 ++ src/app/contents/constants.ts | 2 +- src/app/contents/fields/generic.ts | 4 +- src/app/contents/langs.ts | 959 ------------------ src/app/form/widgets/BaseInputWidget.js | 10 +- src/app/store/index.ts | 2 +- src/app/store/language.ts | 24 - src/app/store/publiccodeYmlLanguages.ts | 28 + src/asset/language_switcher.scss | 17 +- src/i18n/index.ts | 66 +- src/i18n/translations.json | 6 +- webpack.config.ts | 2 + 17 files changed, 267 insertions(+), 1097 deletions(-) delete mode 100644 src/app/components/LanguageSwitcher.tsx create mode 100644 src/app/components/PubliccodeYmlLanguages.tsx delete mode 100644 src/app/contents/langs.ts delete mode 100644 src/app/store/language.ts create mode 100644 src/app/store/publiccodeYmlLanguages.ts diff --git a/package-lock.json b/package-lock.json index 2a534c47..fff2c717 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,8 +22,10 @@ "dotenv": "^16.3.1", "draft-js": "^0.11.7", "i18next": "^23.2.2", + "i18next-browser-languagedetector": "^7.2.0", "immutable": "^4.3.0", "js-yaml": "3.14.0", + "locale-codes": "^1.3.1", "lodash": "^4.17.21", "prop-type": "^0.0.1", "rc-collapse": "^3.7.0", @@ -642,11 +644,11 @@ } }, "node_modules/@babel/runtime": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", - "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.7.tgz", + "integrity": "sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" @@ -9648,6 +9650,14 @@ "@babel/runtime": "^7.22.5" } }, + "node_modules/i18next-browser-languagedetector": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.0.tgz", + "integrity": "sha512-U00DbDtFIYD3wkWsr2aVGfXGAj2TgnELzOX9qv8bT0aJtvPV9CRO77h+vgmHFBMe7LAxdwvT/7VkCWGya6L3tA==", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -10693,6 +10703,14 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/iso639-codes": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/iso639-codes/-/iso639-codes-1.0.1.tgz", + "integrity": "sha512-jdTSv8yn6D7GODDrRtuWG7y3du3aoa+ki5H8h/Y48/NleNAd7Fw/M2niTTLXGH4QnqhJ98hg1JMQtP9csQ31Lg==", + "engines": { + "node": ">=8" + } + }, "node_modules/isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", @@ -12852,6 +12870,11 @@ "node": ">=6" } }, + "node_modules/langs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/langs/-/langs-2.0.0.tgz", + "integrity": "sha512-v4pxOBEQVN1WBTfB1crhTtxzNLZU9HPWgadlwzWKISJtt6Ku/CnpBrwVy+jFv8StjxsPfwPFzO0CMwdZLJ0/BA==" + }, "node_modules/latest-version": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", @@ -12995,6 +13018,19 @@ "node": ">=8.9.0" } }, + "node_modules/locale-codes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/locale-codes/-/locale-codes-1.3.1.tgz", + "integrity": "sha512-C7fxGkU4jAuHqavtKj4IhSD2yPEzChFMRfNHjzwIAz9JTbYHtBJDcQQgmJDezBogk9/vvgS7chKMhpVEKavk5A==", + "dependencies": { + "iso639-codes": "^1.0.1", + "langs": "^2.0.0", + "windows-locale": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -16360,9 +16396,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regex-not": { "version": "1.0.2", @@ -20499,6 +20535,14 @@ "integrity": "sha512-qNXwI591Z88c8bWxp+yjV60Ch4F8Riawe3iGxbzquhy8Xs9m+0+SLFBGb/0yCTIDElawtaImC37fYZ+dr32KqQ==", "dev": true }, + "node_modules/windows-locale": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/windows-locale/-/windows-locale-1.1.3.tgz", + "integrity": "sha512-0OlMOPNGj7GTB6C7WmqS3o4eydjnoYj0uwot2KJf7E0JUucwYwzkcvCWQwnuOV60WqDMeGJpSankgveNMj5r0g==", + "engines": { + "node": ">=v10.24.1" + } + }, "node_modules/windows-release": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-5.1.1.tgz", @@ -21163,11 +21207,11 @@ } }, "@babel/runtime": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", - "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.7.tgz", + "integrity": "sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==", "requires": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" } }, "@babel/template": { @@ -28090,6 +28134,14 @@ "@babel/runtime": "^7.22.5" } }, + "i18next-browser-languagedetector": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.0.tgz", + "integrity": "sha512-U00DbDtFIYD3wkWsr2aVGfXGAj2TgnELzOX9qv8bT0aJtvPV9CRO77h+vgmHFBMe7LAxdwvT/7VkCWGya6L3tA==", + "requires": { + "@babel/runtime": "^7.23.2" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -28816,6 +28868,11 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "iso639-codes": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/iso639-codes/-/iso639-codes-1.0.1.tgz", + "integrity": "sha512-jdTSv8yn6D7GODDrRtuWG7y3du3aoa+ki5H8h/Y48/NleNAd7Fw/M2niTTLXGH4QnqhJ98hg1JMQtP9csQ31Lg==" + }, "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", @@ -30626,6 +30683,11 @@ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true }, + "langs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/langs/-/langs-2.0.0.tgz", + "integrity": "sha512-v4pxOBEQVN1WBTfB1crhTtxzNLZU9HPWgadlwzWKISJtt6Ku/CnpBrwVy+jFv8StjxsPfwPFzO0CMwdZLJ0/BA==" + }, "latest-version": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", @@ -30739,6 +30801,16 @@ "json5": "^2.1.2" } }, + "locale-codes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/locale-codes/-/locale-codes-1.3.1.tgz", + "integrity": "sha512-C7fxGkU4jAuHqavtKj4IhSD2yPEzChFMRfNHjzwIAz9JTbYHtBJDcQQgmJDezBogk9/vvgS7chKMhpVEKavk5A==", + "requires": { + "iso639-codes": "^1.0.1", + "langs": "^2.0.0", + "windows-locale": "^1.1.0" + } + }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -33230,9 +33302,9 @@ "requires": {} }, "regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "regex-not": { "version": "1.0.2", @@ -36304,6 +36376,11 @@ "integrity": "sha512-qNXwI591Z88c8bWxp+yjV60Ch4F8Riawe3iGxbzquhy8Xs9m+0+SLFBGb/0yCTIDElawtaImC37fYZ+dr32KqQ==", "dev": true }, + "windows-locale": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/windows-locale/-/windows-locale-1.1.3.tgz", + "integrity": "sha512-0OlMOPNGj7GTB6C7WmqS3o4eydjnoYj0uwot2KJf7E0JUucwYwzkcvCWQwnuOV60WqDMeGJpSankgveNMj5r0g==" + }, "windows-release": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-5.1.1.tgz", diff --git a/package.json b/package.json index 52bf1196..4b50aab2 100644 --- a/package.json +++ b/package.json @@ -89,8 +89,10 @@ "dotenv": "^16.3.1", "draft-js": "^0.11.7", "i18next": "^23.2.2", + "i18next-browser-languagedetector": "^7.2.0", "immutable": "^4.3.0", "js-yaml": "3.14.0", + "locale-codes": "^1.3.1", "lodash": "^4.17.21", "prop-type": "^0.0.1", "rc-collapse": "^3.7.0", diff --git a/src/app/components/Editor.jsx b/src/app/components/Editor.jsx index 6c0ce480..6d9a1ef2 100644 --- a/src/app/components/Editor.jsx +++ b/src/app/components/Editor.jsx @@ -1,7 +1,7 @@ import { Fragment, useCallback, useEffect, useState } from "react"; import PropTypes from "prop-types"; import Head from "./Head"; -import { LanguageSwitcher } from "./LanguageSwitcher"; +import { PubliccodeYmlLanguages } from "./PubliccodeYmlLanguages"; import { useAppDispatch, useAppSelector } from "../store"; import EditorForm from "./EditorForm"; import InfoBox from "./InfoBox"; @@ -27,7 +27,7 @@ import { toFlatPropertyMap, transformSimpleStringArrays, } from "../utils/transform"; -import { setLanguages, resetLanguages } from "../store/language"; +import { setPubliccodeYmlLanguages, resetPubliccodeYmlLanguages } from "../store/publiccodeYmlLanguages"; import useDebounce from "../hooks/useDebounce"; import { DevTool } from '@hookform/devtools'; @@ -166,7 +166,7 @@ export const Editor = (props) => { notify(t("editor.errors.yamlloading"), { state: "error" }); return; } - dispatch(setLanguages(extractLanguages(data))); + dispatch(setPubliccodeYmlLanguages(extractLanguages(data))); return data; }; @@ -204,7 +204,7 @@ export const Editor = (props) => { setYaml(null); clearErrors(); setFlatErrors(null); - dispatch(resetLanguages()); + dispatch(resetPubliccodeYmlLanguages()); }; const handleValidationErrors = useCallback((validator) => { @@ -276,7 +276,7 @@ export const Editor = (props) => {
- +
{blocks && allFields && ( {
- {languages && languages.length > 0 ? ( + {languages && ( { t )} /> - ) : ( -
{t("editor.nolanguage")}
)}
diff --git a/src/app/components/LanguageSwitcher.tsx b/src/app/components/LanguageSwitcher.tsx deleted file mode 100644 index 9566bbe2..00000000 --- a/src/app/components/LanguageSwitcher.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { useAppSelector, useAppDispatch } from "../store"; -import available_languages from "../contents/langs"; -import CloseButton from "./CloseButton"; -import { setLanguages } from "../store/language"; -import { DropdownList } from "react-widgets"; - -export const LanguageSwitcher = (): JSX.Element => { - const [selectedLanguage, setSelectedLanguage] = useState(); - const [dropdownVisible, setDropdownVisible] = useState(false); - const dispatch = useAppDispatch(); - const languages = useAppSelector((state) => state.language.languages); - const { t } = useTranslation(); - - const handleChange = (l?: { text: string; value: string }) => { - if (l !== undefined) { - const v = l.value; - if (!languages.includes(v)) { - dispatch(setLanguages([...languages, v])); - } - setSelectedLanguage(undefined); - } - setDropdownVisible(false); - }; - - const removeLanguage = (v: string) => { - dispatch(setLanguages([...languages.filter((x) => x !== v)])); - }; - - return ( -
- {languages.map((lng) => { - return ( -
-
{lng}
- removeLanguage(lng)} /> -
- ); - })} - {!dropdownVisible && ( -
-
setDropdownVisible(true)}> - {t("editor.addlanguage")} -
-
- )} - {dropdownVisible && ( - - )} -
- ); -}; -export default LanguageSwitcher; diff --git a/src/app/components/PubliccodeYmlLanguages.tsx b/src/app/components/PubliccodeYmlLanguages.tsx new file mode 100644 index 00000000..ee8bcc19 --- /dev/null +++ b/src/app/components/PubliccodeYmlLanguages.tsx @@ -0,0 +1,60 @@ +import { useEffect } from 'react'; +import { useTranslation } from "react-i18next"; +import { useAppDispatch } from "../store"; +import { getPubliccodeYmlLanguages, setPubliccodeYmlLanguages } from "../store/publiccodeYmlLanguages"; +import { Multiselect } from "react-widgets"; +import { useSelector } from 'react-redux'; +import { allLangs } from '../../i18n'; +import { upperFirst } from 'lodash'; + +interface Language { + value: string; + text: string; +} + +const renderListItem = (item: Language) => ( + + {upperFirst(item.text)} + +) + +const renderTagValue = ({ item }: { item: Language }) => ( + + {upperFirst(item.text)} + +) + +export const PubliccodeYmlLanguages = (): JSX.Element => { + const dispatch = useAppDispatch(); + const { i18n, t } = useTranslation(); + const publiccodeYmlLanguages = useSelector(getPubliccodeYmlLanguages) + + useEffect(() => { + dispatch(setPubliccodeYmlLanguages([i18n.language])); + console.log(i18n.language) + }, [dispatch, i18n.language]); + + const handleChange = (newSelection: Language[]) => { + if (newSelection.length != 0){ + dispatch(setPubliccodeYmlLanguages(newSelection.map(l => l.value))); + } + }; + + return ( +
+ +
+ ); +}; +export default PubliccodeYmlLanguages; diff --git a/src/app/contents/constants.ts b/src/app/contents/constants.ts index 96e840bf..a1ad4732 100644 --- a/src/app/contents/constants.ts +++ b/src/app/contents/constants.ts @@ -4,7 +4,7 @@ export const { VALIDATOR_URL, VALIDATOR_REMOTE_URL, DEFAULT_COUNTRY, - DEFAULT_LANGUAGE = "it" || navigator.language, + FALLBACK_LANGUAGE = "en", // eslint-disable-next-line no-undef } = process.env; diff --git a/src/app/contents/fields/generic.ts b/src/app/contents/fields/generic.ts index bb3d6569..0ded8dd5 100644 --- a/src/app/contents/fields/generic.ts +++ b/src/app/contents/fields/generic.ts @@ -1,7 +1,7 @@ import categories from "../categories"; import scopes from "../scopes"; import licenses from "../../../generated/licenses.json"; -import { langs } from "../langs"; +import { allLangs } from '../../../i18n'; import countries from "../countries"; const developmentStatus_list = [ @@ -359,7 +359,7 @@ const fields = (): Array => { items: { title: "item", type: "string", - enum: langs, + enum: allLangs(), }, widget: "tags", section: 4, diff --git a/src/app/contents/langs.ts b/src/app/contents/langs.ts deleted file mode 100644 index 91e92f17..00000000 --- a/src/app/contents/langs.ts +++ /dev/null @@ -1,959 +0,0 @@ -export const langs = [ - "aa", - "ab", - "ae", - "af", - "ak", - "am", - "an", - "ar", - "as", - "av", - "ay", - "az", - "ba", - "be", - "bg", - "bh", - "bi", - "bm", - "bn", - "bo", - "br", - "bs", - "ca", - "ce", - "ch", - "co", - "cr", - "cs", - "cu", - "cv", - "cy", - "da", - "de", - "dv", - "dz", - "ee", - "el", - "en", - "eo", - "es", - "et", - "eu", - "fa", - "ff", - "fi", - "fj", - "fo", - "fr", - "fy", - "ga", - "gd", - "gl", - "gn", - "gu", - "gv", - "ha", - "he", - "hi", - "ho", - "hr", - "ht", - "hu", - "hy", - "hz", - "ia", - "id", - "ie", - "ig", - "ii", - "ik", - "io", - "is", - "it", - "iu", - "ja", - "jv", - "ka", - "kg", - "ki", - "kj", - "kk", - "kl", - "km", - "kn", - "ko", - "kr", - "ks", - "ku", - "kv", - "kw", - "ky", - "la", - "lb", - "lg", - "li", - "ln", - "lo", - "lt", - "lu", - "lv", - "mg", - "mh", - "mi", - "mk", - "ml", - "mn", - "mr", - "ms", - "mt", - "my", - "na", - "nb", - "nd", - "ne", - "ng", - "nl", - "nn", - "no", - "nr", - "nv", - "ny", - "oc", - "oj", - "om", - "or", - "os", - "pa", - "pi", - "pl", - "ps", - "pt", - "qu", - "rm", - "rn", - "ro", - "ru", - "rw", - "sa", - "sc", - "sd", - "se", - "sg", - "si", - "sk", - "sl", - "sm", - "sn", - "so", - "sq", - "sr", - "ss", - "st", - "su", - "sv", - "sw", - "ta", - "te", - "tg", - "th", - "ti", - "tk", - "tl", - "tn", - "to", - "tr", - "ts", - "tt", - "tw", - "ty", - "ug", - "uk", - "ur", - "uz", - "ve", - "vi", - "vo", - "wa", - "wo", - "xh", - "yi", - "yo", - "za", - "zh", - "zu", -]; - -/** - * @author Phil Teare - * using wikipedia data - */ -const languages: Record< - string, - { name: string; nativeName: string; count?: number; isRTL?: boolean } -> = { - it: { - name: "Italian", - nativeName: "Italiano", - }, - en: { - name: "English", - nativeName: "English", - count: 1, - }, - ab: { - name: "Abkhaz", - nativeName: "аҧсуа", - }, - aa: { - name: "Afar", - nativeName: "Afaraf", - }, - af: { - name: "Afrikaans", - nativeName: "Afrikaans", - }, - ak: { - name: "Akan", - nativeName: "Akan", - }, - sq: { - name: "Albanian", - nativeName: "Shqip", - }, - am: { - name: "Amharic", - nativeName: "አማርኛ", - }, - ar: { - name: "Arabic", - nativeName: "العربية", - isRTL: true, - }, - an: { - name: "Aragonese", - nativeName: "Aragonés", - }, - hy: { - name: "Armenian", - nativeName: "Հայերեն", - }, - as: { - name: "Assamese", - nativeName: "অসমীয়া", - }, - av: { - name: "Avaric", - nativeName: "авар мацӀ", - }, - ae: { - name: "Avestan", - nativeName: "avesta", - }, - ay: { - name: "Aymara", - nativeName: "aymar aru", - }, - az: { - name: "Azerbaijani", - nativeName: "azərbaycan dili", - }, - bm: { - name: "Bambara", - nativeName: "bamanankan", - }, - ba: { - name: "Bashkir", - nativeName: "башҡорт теле", - }, - eu: { - name: "Basque", - nativeName: "euskara", - }, - be: { - name: "Belarusian", - nativeName: "Беларуская", - }, - bn: { - name: "Bengali", - nativeName: "বাংলা", - }, - bh: { - name: "Bihari", - nativeName: "भोजपुरी", - }, - bi: { - name: "Bislama", - nativeName: "Bislama", - }, - bs: { - name: "Bosnian", - nativeName: "bosanski jezik", - }, - br: { - name: "Breton", - nativeName: "brezhoneg", - }, - bg: { - name: "Bulgarian", - nativeName: "български език", - }, - my: { - name: "Burmese", - nativeName: "ဗမာစာ", - }, - ca: { - name: "Catalan", - nativeName: "Català", - }, - ch: { - name: "Chamorro", - nativeName: "Chamoru", - }, - ce: { - name: "Chechen", - nativeName: "нохчийн мотт", - }, - ny: { - name: "Chichewa", - nativeName: "chiCheŵa", - }, - zh: { - name: "Chinese", - nativeName: "中文", - }, - cv: { - name: "Chuvash", - nativeName: "чӑваш чӗлхи", - }, - kw: { - name: "Cornish", - nativeName: "Kernewek", - }, - co: { - name: "Corsican", - nativeName: "corsu", - }, - cr: { - name: "Cree", - nativeName: "ᓀᐦᐃᔭᐍᐏᐣ", - }, - hr: { - name: "Croatian", - nativeName: "hrvatski", - }, - cs: { - name: "Czech", - nativeName: "česky", - }, - da: { - name: "Danish", - nativeName: "dansk", - }, - dv: { - name: "Dhivehi", - nativeName: "Maldivian", - }, - nl: { - name: "Dutch", - nativeName: "Nederlands", - }, - eo: { - name: "Esperanto", - nativeName: "Esperanto", - }, - et: { - name: "Estonian", - nativeName: "eesti", - }, - ee: { - name: "Ewe", - nativeName: "Eʋegbe", - }, - fo: { - name: "Faroese", - nativeName: "føroyskt", - }, - fj: { - name: "Fijian", - nativeName: "vosa Vakaviti", - }, - fi: { - name: "Finnish", - nativeName: "suomi", - }, - fr: { - name: "French", - nativeName: "français", - }, - ff: { - name: "Fula", - nativeName: "Fulfulde", - }, - gl: { - name: "Galician", - nativeName: "Galego", - }, - ka: { - name: "Georgian", - nativeName: "ქართული", - }, - de: { - name: "German", - nativeName: "Deutsch", - }, - el: { - name: "Greek", - nativeName: "Ελληνικά", - }, - gn: { - name: "Guaraní", - nativeName: "Avañeẽ", - }, - gu: { - name: "Gujarati", - nativeName: "ગુજરાતી", - }, - ht: { - name: "Haitian", - nativeName: "Kreyòl ayisyen", - }, - ha: { - name: "Hausa", - nativeName: "هَوُسَ", - }, - he: { - name: "Hebrew", - nativeName: "עברית", - isRTL: true, - }, - hz: { - name: "Herero", - nativeName: "Otjiherero", - }, - hi: { - name: "Hindi", - nativeName: "हिन्दी, हिंदी", - }, - ho: { - name: "Hiri Motu", - nativeName: "Hiri Motu", - }, - hu: { - name: "Hungarian", - nativeName: "Magyar", - }, - ia: { - name: "Interlingua", - nativeName: "Interlingua", - }, - id: { - name: "Indonesian", - nativeName: "Bahasa Indonesia", - }, - ie: { - name: "Interlingue", - nativeName: "Interlingue", - }, - ga: { - name: "Irish", - nativeName: "Gaeilge", - }, - ig: { - name: "Igbo", - nativeName: "Asụsụ Igbo", - }, - ik: { - name: "Inupiaq", - nativeName: "Iñupiaq", - }, - io: { - name: "Ido", - nativeName: "Ido", - }, - is: { - name: "Icelandic", - nativeName: "Íslenska", - }, - iu: { - name: "Inuktitut", - nativeName: "ᐃᓄᒃᑎᑐᑦ", - }, - ja: { - name: "Japanese", - nativeName: "日本語", - }, - jv: { - name: "Javanese", - nativeName: "basa Jawa", - }, - kl: { - name: "Kalaallisut", - nativeName: "kalaallisut", - }, - kn: { - name: "Kannada", - nativeName: "ಕನ್ನಡ", - }, - kr: { - name: "Kanuri", - nativeName: "Kanuri", - }, - ks: { - name: "Kashmiri", - nativeName: " كشميري‎", - count: 0, - isRTL: true, - }, - kk: { - name: "Kazakh", - nativeName: "Қазақ тілі", - }, - km: { - name: "Khmer", - nativeName: "ភាសាខ្មែរ", - }, - ki: { - name: "Kikuyu", - nativeName: "Gĩkũyũ", - }, - rw: { - name: "Kinyarwanda", - nativeName: "Ikinyarwanda", - }, - ky: { - name: "Kirghiz", - nativeName: "кыргыз тили", - }, - kv: { - name: "Komi", - nativeName: "коми кыв", - }, - kg: { - name: "Kongo", - nativeName: "KiKongo", - }, - ko: { - name: "Korean", - nativeName: "한국어", - }, - ku: { - name: "Kurdish", - nativeName: "كوردی‎", - count: 0, - isRTL: true, - }, - kj: { - name: "Kwanyama", - nativeName: "Kuanyama", - }, - la: { - name: "Latin", - nativeName: "latine", - }, - lb: { - name: "Luxembourgish", - nativeName: "Lëtzebuergesch", - }, - lg: { - name: "Luganda", - nativeName: "Luganda", - }, - li: { - name: "Limburgish", - nativeName: "Limburgs", - }, - ln: { - name: "Lingala", - nativeName: "Lingála", - }, - lo: { - name: "Lao", - nativeName: "ພາສາລາວ", - }, - lt: { - name: "Lithuanian", - nativeName: "lietuvių kalba", - }, - lu: { - name: "Luba-Katanga", - nativeName: "Luba", - }, - lv: { - name: "Latvian", - nativeName: "latviešu valoda", - }, - gv: { - name: "Manx", - nativeName: "Gaelg", - }, - mk: { - name: "Macedonian", - nativeName: "македонски јазик", - }, - mg: { - name: "Malagasy", - nativeName: "Malagasy fiteny", - }, - ms: { - name: "Malay", - nativeName: "بهاس ملايو‎", - count: 0, - isRTL: true, - }, - ml: { - name: "Malayalam", - nativeName: "മലയാളം", - }, - mt: { - name: "Maltese", - nativeName: "Malti", - }, - mi: { - name: "Māori", - nativeName: "te reo Māori", - }, - mr: { - name: "Marathi", - nativeName: "मराठी", - }, - mh: { - name: "Marshallese", - nativeName: "Kajin M̧ajeļ", - }, - mn: { - name: "Mongolian", - nativeName: "монгол", - }, - na: { - name: "Nauru", - nativeName: "Ekakairũ Naoero", - }, - nv: { - name: "Navajo", - nativeName: "Dinékʼehǰí", - }, - nb: { - name: "Norwegian Bokmål", - nativeName: "Norsk bokmål", - }, - nd: { - name: "North Ndebele", - nativeName: "isiNdebele", - }, - ne: { - name: "Nepali", - nativeName: "नेपाली", - }, - ng: { - name: "Ndonga", - nativeName: "Owambo", - }, - nn: { - name: "Norwegian Nynorsk", - nativeName: "Norsk nynorsk", - }, - no: { - name: "Norwegian", - nativeName: "Norsk", - }, - ii: { - name: "Nuosu", - nativeName: "ꆈꌠ꒿ Nuosuhxop", - }, - nr: { - name: "South Ndebele", - nativeName: "isiNdebele", - }, - oc: { - name: "Occitan", - nativeName: "Occitan", - }, - oj: { - name: "Ojibwe, Ojibwa", - nativeName: "ᐊᓂᔑᓈᐯᒧᐎᓐ", - }, - cu: { - name: "Church Slavic", - nativeName: "ѩзыкъ словѣньскъ", - }, - om: { - name: "Oromo", - nativeName: "Afaan Oromoo", - }, - or: { - name: "Oriya", - nativeName: "ଓଡ଼ିଆ", - }, - os: { - name: "Ossetian", - nativeName: "ирон æвзаг", - }, - pa: { - name: "Panjabi", - nativeName: "پنجابی‎", - count: 0, - isRTL: true, - }, - pi: { - name: "Pāli", - nativeName: "पाऴि", - }, - fa: { - name: "Persian", - nativeName: "فارسی", - count: 0, - isRTL: true, - }, - pl: { - name: "Polish", - nativeName: "polski", - }, - ps: { - name: "Pashto", - nativeName: "پښتو", - }, - pt: { - name: "Portuguese", - nativeName: "Português", - }, - qu: { - name: "Quechua", - nativeName: "Kichwa", - }, - rm: { - name: "Romansh", - nativeName: "rumantsch", - }, - rn: { - name: "Kirundi", - nativeName: "kiRundi", - }, - ro: { - name: "Romanian", - nativeName: "română", - }, - ru: { - name: "Russian", - nativeName: "русский язык", - }, - sa: { - name: "Sanskrit", - nativeName: "संस्कृतम्", - }, - sc: { - name: "Sardinian", - nativeName: "sardu", - }, - sd: { - name: "Sindhi", - nativeName: " سنڌي، سندھی‎", - count: 0, - isRTL: true, - }, - se: { - name: "Northern Sami", - nativeName: "Davvisámegiella", - }, - sm: { - name: "Samoan", - nativeName: "gagana faa Samoa", - }, - sg: { - name: "Sango", - nativeName: "yângâ tî sängö", - }, - sr: { - name: "Serbian", - nativeName: "српски језик", - }, - gd: { - name: "Scottish Gaelic; Gaelic", - nativeName: "Gàidhlig", - }, - sn: { - name: "Shona", - nativeName: "chiShona", - }, - si: { - name: "Sinhala, Sinhalese", - nativeName: "සිංහල", - }, - sk: { - name: "Slovak", - nativeName: "slovenčina", - }, - sl: { - name: "Slovene", - nativeName: "slovenščina", - }, - so: { - name: "Somali", - nativeName: "Soomaaliga", - }, - st: { - name: "Southern Sotho", - nativeName: "Sesotho", - }, - es: { - name: "Spanish", - nativeName: "español", - }, - su: { - name: "Sundanese", - nativeName: "Basa Sunda", - }, - sw: { - name: "Swahili", - nativeName: "Kiswahili", - }, - ss: { - name: "Swati", - nativeName: "SiSwati", - }, - sv: { - name: "Swedish", - nativeName: "svenska", - }, - ta: { - name: "Tamil", - nativeName: "தமிழ்", - }, - te: { - name: "Telugu", - nativeName: "తెలుగు", - }, - tg: { - name: "Tajik", - nativeName: "تاجیکی‎", - count: 0, - isRTL: true, - }, - th: { - name: "Thai", - nativeName: "ไทย", - }, - ti: { - name: "Tigrinya", - nativeName: "ትግርኛ", - }, - bo: { - name: "Tibetan", - nativeName: "བོད་ཡིག", - }, - tk: { - name: "Turkmen", - nativeName: "Türkmen, Түркмен", - }, - tl: { - name: "Tagalog", - nativeName: "ᜏᜒᜃᜅ᜔ ᜆᜄᜎᜓᜄ᜔", - }, - tn: { - name: "Tswana", - nativeName: "Setswana", - }, - to: { - name: "Tonga", - nativeName: "faka Tonga", - }, - tr: { - name: "Turkish", - nativeName: "Türkçe", - }, - ts: { - name: "Tsonga", - nativeName: "Xitsonga", - }, - tt: { - name: "Tatar", - nativeName: "تاتارچا‎", - count: 0, - isRTL: true, - }, - tw: { - name: "Twi", - nativeName: "Twi", - }, - ty: { - name: "Tahitian", - nativeName: "Reo Tahiti", - }, - ug: { - name: "Uighur", - nativeName: "ئۇيغۇرچە‎", - }, - uk: { - name: "Ukrainian", - nativeName: "українська", - }, - ur: { - name: "Urdu", - nativeName: "اردو", - count: 0, - isRTL: true, - }, - uz: { - name: "Uzbek", - nativeName: "أۇزبېك‎", - count: 0, - isRTL: true, - }, - ve: { - name: "Venda", - nativeName: "Tshivenḓa", - }, - vi: { - name: "Vietnamese", - nativeName: "Tiếng Việt", - }, - vo: { - name: "Volapük", - nativeName: "Volapük", - }, - wa: { - name: "Walloon", - nativeName: "Walon", - }, - cy: { - name: "Welsh", - nativeName: "Cymraeg", - }, - wo: { - name: "Wolof", - nativeName: "Wollof", - }, - fy: { - name: "Western Frisian", - nativeName: "Frysk", - }, - xh: { - name: "Xhosa", - nativeName: "isiXhosa", - }, - yi: { - name: "Yiddish", - nativeName: "ייִדיש", - isRTL: true, - }, - yo: { - name: "Yoruba", - nativeName: "Yorùbá", - }, - za: { - name: "Zhuang", - nativeName: "Saɯ cueŋƅ", - }, -}; - -export const nameLangs = Object.keys(languages).map((x) => ({ - text: languages[x].nativeName, - value: x, -})); -export const nativeNameLangs = Object.keys(languages).map((x) => ({ - text: languages[x].name, - value: x, -})); - -export default nameLangs; diff --git a/src/app/form/widgets/BaseInputWidget.js b/src/app/form/widgets/BaseInputWidget.js index 69ec3d89..2c285a4e 100644 --- a/src/app/form/widgets/BaseInputWidget.js +++ b/src/app/form/widgets/BaseInputWidget.js @@ -2,6 +2,7 @@ import { useState } from "react"; import PropTypes from "prop-types"; import Info from "../../components/Info"; import { useController, useFormContext } from "react-hook-form"; +import { useTranslation } from "react-i18next"; import { get } from "lodash"; import { Input } from "design-react-kit"; @@ -9,6 +10,7 @@ const BaseInputWidget = (props) => { const name = props.fieldName; const id = `field-${name}`; const { control, formState } = useFormContext(); + const { i18n } = useTranslation(); const propertyNames = name.split(/\./); const propertyName = propertyNames[propertyNames.length - 1]; @@ -44,15 +46,17 @@ const BaseInputWidget = (props) => { }); const [count, setCount] = useState(0); + const inLang = props.schema.language + ? ` (in ${new Intl.DisplayNames([i18n.language], { type: 'language' }).of(props.schema.lang)})` + : ''; + return ( <> ; -} - -const initialState: LanguageState = { - languages: [DEFAULT_LANGUAGE], -}; - -export const languageSlice = createSlice({ - name: "language", - initialState, - reducers: { - setLanguages: (_, action: PayloadAction>) => ({ - languages: action.payload, - }), - resetLanguages: () => initialState, - }, -}); -export const { setLanguages, resetLanguages } = languageSlice.actions; - -export default languageSlice.reducer; diff --git a/src/app/store/publiccodeYmlLanguages.ts b/src/app/store/publiccodeYmlLanguages.ts new file mode 100644 index 00000000..928cdf5d --- /dev/null +++ b/src/app/store/publiccodeYmlLanguages.ts @@ -0,0 +1,28 @@ +import { PayloadAction, createSlice } from "@reduxjs/toolkit"; +import { FALLBACK_LANGUAGE } from "../contents/constants"; + +interface PubliccodeYmlLanguages { + languages: Array; +} + +const initialState: PubliccodeYmlLanguages = { + // Fallback to the UI language as last resort + languages: [FALLBACK_LANGUAGE], +}; + +export const languageSlice = createSlice({ + name: "language", + initialState, + reducers: { + setPubliccodeYmlLanguages: (_, action: PayloadAction>) => ({ + languages: action.payload, + }), + resetPubliccodeYmlLanguages: () => initialState, + }, +}); + +export const getPubliccodeYmlLanguages = (state) => state.language.languages; + +export const { setPubliccodeYmlLanguages, resetPubliccodeYmlLanguages } = languageSlice.actions; + +export default languageSlice.reducer; diff --git a/src/asset/language_switcher.scss b/src/asset/language_switcher.scss index 5c6068bd..f165388b 100644 --- a/src/asset/language_switcher.scss +++ b/src/asset/language_switcher.scss @@ -10,6 +10,21 @@ flex-flow: row wrap; padding: 0 $content-pad-horiz; background: $head-background-color; + min-width: 50rem; +} + +.rw-multiselect-input { + min-width: 50rem; + margin-left: 0.5rem; + margin-right: 0.5rem; +} + +.rw-multiselect-tag-btn { + opacity: 60%; +} + +.rw-multiselect-tag-btn:hover { + opacity: 100%; } .language-switcher__link, @@ -59,7 +74,7 @@ input[type="text"].language-filter__input { margin-top: -5px; - border-bottom: none; + border-bottom: 3px; font-size: 18px; color: #5c6f82; border-bottom: 1px solid #e8e8e8; diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 49de24f3..fe0a813e 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -1,24 +1,56 @@ import i18n from "i18next"; import { initReactI18next } from "react-i18next"; -import { DEFAULT_LANGUAGE } from "../app/contents/constants"; +import locales from "locale-codes"; +import LanguageDetector from 'i18next-browser-languagedetector'; + +import { FALLBACK_LANGUAGE } from "../app/contents/constants"; import translation from "./translations.json"; import pc from "./publiccode.json"; -i18n.use(initReactI18next).init({ - ns: ["translation", "pc"], - defaultNS: "translation", - resources: { - en: { - pc: pc.en.pc, - translation: translation.en.translation, +i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + ns: ["translation", "pc"], + defaultNS: "translation", + resources: { + en: { + pc: pc.en.pc, + translation: translation.en.translation, + }, + it: { + pc: pc.it.pc, + translation: translation.it.translation, + }, }, - it: { - pc: pc.it.pc, - translation: translation.it.translation, + supportedLngs: ["en", "it"], + nonExplicitSupportedLngs: true, // make pass eg. "en-US" if "en" is in supportedLngs + fallbackLng: FALLBACK_LANGUAGE, + interpolation: { + escapeValue: false, }, - }, - lng: DEFAULT_LANGUAGE, - interpolation: { - escapeValue: false, - }, -}); + detection: { + order: ['querystring', 'navigator', 'htmlTag', 'path', 'subdomain'], + + lookupQuerystring: 'lang', + lookupCookie: 'lang', + lookupLocalStorage: 'lang', + lookupSessionStorage: 'lang', + + caches: [], + } + }); + +export const displayName = (tag: string, locale: string = i18n.language) => { + try { + return new Intl.DisplayNames([locale], { type: 'language' }).of(tag); + } catch (e) { + return null; + } +} + +export const allLangs = (locale: string = i18n.language) => { + return locales.all + .map(l => ({ text: displayName(l.tag, locale), value: l.tag })) + .filter(e => e !== null) +} diff --git a/src/i18n/translations.json b/src/i18n/translations.json index 1a6c9fcf..007a2ecd 100644 --- a/src/i18n/translations.json +++ b/src/i18n/translations.json @@ -6,9 +6,8 @@ "title": "publiccode.yml Editor", "needhelp": "Need Help?", "lastgeneration": "Last generation", - "addlanguage": "+ Add language", + "addlanguage": "add language to the publiccode.yml file...", "readmore": "Read more", - "nolanguage": "No language selected", "nocodegenerated": "No code generated", "copy": "Copia", "copytext": "Copied to clipboard", @@ -55,9 +54,8 @@ "title": "publiccode.yml Editor", "needhelp": "Aiuto?", "lastgeneration": "Ultimo aggiornamento", - "addlanguage": "+ Aggiungi lingua", + "addlanguage": "aggiungi lingua al file publiccode.yml...", "readmore": "Leggi di più", - "nolanguage": "Nessuna lingua selezionata", "nocodegenerated": "Nessun publiccode generato", "copy": "Copia", "copytext": "Copiato negli appunti", diff --git a/webpack.config.ts b/webpack.config.ts index 651ef67e..0a9a80e1 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -19,6 +19,8 @@ const config = ( ELASTIC_URL: JSON.stringify(process.env.ELASTIC_URL), VALIDATOR_URL: JSON.stringify(process.env.VALIDATOR_URL), VALIDATOR_REMOTE_URL: JSON.stringify(process.env.VALIDATOR_REMOTE_URL), + FALLBACK_LANGUAGE: JSON.stringify(process.env.FALLBACK_LANGUAGE), + DEFAULT_COUNTRY: JSON.stringify(process.env.DEFAULT_COUNTRY), }, }), new HtmlWebpackPlugin({