From a7e59c6b6029fb7678c7a80b23b5390720fd782d Mon Sep 17 00:00:00 2001 From: Alexander Burmatov Date: Mon, 21 Oct 2024 22:43:20 +0300 Subject: [PATCH] Adding localization - Replaced the text for localization. - Added Select to select the language in the Header. - Localization of the DateTime format. - Added Russian language. --- package-lock.json | 136 +++++++++++ package.json | 4 + public/locales/en/translation.json | 227 ++++++++++++++++++ public/locales/ru/translation.json | 227 ++++++++++++++++++ src/App.js | 38 +-- src/components/Explore/Explore.jsx | 21 +- src/components/Explore/FilterDialog.jsx | 18 +- src/components/Header/ExploreHeader.jsx | 5 +- src/components/Header/Header.jsx | 41 +++- src/components/Header/SearchSuggestion.jsx | 11 +- src/components/Header/UserAccountMenu.jsx | 7 +- src/components/Home/Home.jsx | 21 +- src/components/Login/SignIn.jsx | 19 +- src/components/Login/SignInPresentation.jsx | 4 +- .../Login/ThirdPartyLoginComponents.jsx | 13 +- src/components/Repo/RepoDetails.jsx | 18 +- src/components/Repo/RepoDetailsMetadata.jsx | 33 ++- src/components/Repo/Tabs/Tags.jsx | 13 +- src/components/Shared/DeleteTag.jsx | 5 +- .../Shared/DeleteTagConfirmDialog.jsx | 6 +- src/components/Shared/FilterCard.jsx | 7 +- src/components/Shared/LayerCard.jsx | 9 +- src/components/Shared/NoDataComponent.jsx | 4 +- src/components/Shared/PullCommandButton.jsx | 7 +- src/components/Shared/ReferrerCard.jsx | 12 +- src/components/Shared/RepoCard.jsx | 26 +- src/components/Shared/SignatureTooltip.jsx | 12 +- src/components/Shared/TagCard.jsx | 27 ++- src/components/Shared/VulnerabilityCard.jsx | 17 +- .../Shared/VulnerabilityCountCard.jsx | 39 ++- .../Shared/VulnerabilityPackageSection.jsx | 9 +- src/components/Tag/Tabs/DependsOn.jsx | 7 +- src/components/Tag/Tabs/HistoryLayers.jsx | 7 +- src/components/Tag/Tabs/IsDependentOn.jsx | 7 +- src/components/Tag/Tabs/ReferredBy.jsx | 7 +- .../Tag/Tabs/VulnerabilitiesDetails.jsx | 21 +- src/components/Tag/TagDetails.jsx | 19 +- src/components/Tag/TagDetailsMetadata.jsx | 22 +- src/components/User/ApiKeys/ApiKeyCard.jsx | 7 +- .../User/ApiKeys/ApiKeyConfirmDialog.jsx | 11 +- src/components/User/ApiKeys/ApiKeyDialog.jsx | 23 +- .../User/ApiKeys/ApiKeyRevokeDialog.jsx | 13 +- src/components/User/ApiKeys/ApiKeys.jsx | 7 +- src/index.js | 1 + src/utilities/filterConstants.js | 6 +- src/utilities/i18n.js | 23 ++ src/utilities/sortCriteria.js | 20 +- .../vulnerabilityAndSignatureComponents.jsx | 9 +- 48 files changed, 1031 insertions(+), 215 deletions(-) create mode 100644 public/locales/en/translation.json create mode 100644 public/locales/ru/translation.json create mode 100644 src/utilities/i18n.js diff --git a/package-lock.json b/package-lock.json index 31e852ab..8fb247e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,11 +21,15 @@ "axios": "^1.7.4", "downshift": "^6.1.12", "export-from-json": "^1.7.3", + "i18next": "^23.16.4", + "i18next-browser-languagedetector": "^8.0.0", + "i18next-http-backend": "^2.6.2", "lodash": "^4.17.21", "luxon": "^3.4.4", "markdown-to-jsx": "^7.5.0", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-i18next": "^15.1.0", "react-router-dom": "^6.27.0", "react-sticky-el": "^2.0.9", "web-vitals": "^2.1.3", @@ -7266,6 +7270,15 @@ "node": ">=0.8" } }, + "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==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -10207,6 +10220,15 @@ "node": ">=12" } }, + "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==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-webpack-plugin": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.3.tgz", @@ -10356,6 +10378,47 @@ "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" }, + "node_modules/i18next": { + "version": "23.16.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.4.tgz", + "integrity": "sha512-9NIYBVy9cs4wIqzurf7nLXPyf3R78xYbxExVqHLK9od3038rjpyOEzW+XB130kZ1N4PZ9inTtJ471CRJ4Ituyg==", + "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" + } + ], + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-http-backend": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.6.2.tgz", + "integrity": "sha512-Hp/kd8/VuoxIHmxsknJXjkTYYHzivAyAF15pzliKzk2TiXC25rZCEerb1pUFoxz4IVrG3fCvQSY51/Lu4ECV4A==", + "license": "MIT", + "dependencies": { + "cross-fetch": "4.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -13741,6 +13804,48 @@ "tslib": "^2.0.3" } }, + "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==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/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==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/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==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -16088,6 +16193,28 @@ "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==", "dev": true }, + "node_modules/react-i18next": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.1.0.tgz", + "integrity": "sha512-zj3nJynMnZsy2gPZiOTC7XctCY5eQGqT3tcKMmfJWC9FMvgd+960w/adq61j8iPzpwmsXejqID9qC3Mqu1Xu2Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "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-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -18509,6 +18636,15 @@ "node": ">= 0.8" } }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/package.json b/package.json index 59c91cb5..7537a592 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,16 @@ "axios": "^1.7.4", "downshift": "^6.1.12", "export-from-json": "^1.7.3", + "i18next": "^23.16.2", + "i18next-browser-languagedetector": "^8.0.0", + "i18next-http-backend": "^2.6.2", "lodash": "^4.17.21", "luxon": "^3.4.4", "markdown-to-jsx": "^7.5.0", "react": "^17.0.2", "react-dom": "^17.0.2", "react-router-dom": "^6.27.0", + "react-i18next": "^15.1.0", "react-sticky-el": "^2.0.9", "web-vitals": "^2.1.3", "xlsx": "^0.18.5" diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json new file mode 100644 index 00000000..c30f360c --- /dev/null +++ b/public/locales/en/translation.json @@ -0,0 +1,227 @@ +{ + "main": { + "cancel": "Cancel", + "close": "Close", + "created": "Created", + "days": "days", + "description": "Description", + "descriptionNA": "Description not available", + "digest": "DIGEST", + "hours": "hours", + "layers": "Layers", + "license": "License", + "licenseNA": "License info not available", + "loading": "Loading...", + "minutes": "minutes", + "NA": "not available", + "nothingFound": "Nothing found", + "osOrArch": "OS/Arch", + "published": "published", + "referredBy": "Referred By", + "revoke": "Revoke", + "signIn": "Sign in", + "sort": "Sort", + "stars": "Stars", + "timestampNA": "Timestamp N/A", + "totalSize": "Total size", + "unknown": "Unknown", + "usedBy": "Used by", + "uses": "Uses", + "vendorNA": "Vendor not available", + "vulnerabilities": "Vulnerabilities", + "weeks": "weeks" + }, + "explore": { + "OS": "Operating system", + "architectures": "Architectures", + "additionalFilters": "Additional filters", + "showing": "Showing", + "resultsOutOf": "results out of", + "filterResults": "Filter results", + "noResults": "Looks like we don't have anything matching that search. Try searching something else." + }, + "filterDialog": { + "filter": "Filter", + "sortResults": "Sort results", + "confirm": "Confirm" + }, + "header": { + "product": "Product", + "docs": "Docs" + }, + "exploreHeader": { + "home": "Home" + }, + "searchSuggestion": { + "search": "Search for content...", + "advancedSearch": "Press Enter for advanced search", + "useAposChar": "Use the ':' character to search for tags" + }, + "userAccountMenu": { + "APIKeys": "API Keys", + "logOut": "Log out" + }, + "home": { + "noImages": "No images", + "viewAll": "View all", + "mostPopularImages": "Most popular images", + "recentlyUpdatedImages": "Recently updated images", + "bookmarks": "Bookmarks" + }, + "signIn": { + "welcomeBack": "Welcome back! Please login.", + "or": "or", + "username": "Username", + "enterPassword": "Enter password", + "authFailed": "Authentication Failed. Please try again.", + "continue": "Continue", + "continueAsGuest": "Continue as guest" + }, + "signInPresentation": { + "description": "OCI-native container image registry, simplified" + }, + "thirdPartyLoginComponents": { + "continueWith": "Continue with", + "signInWith": "Sign in with" + }, + "tags": { + "tagsHistory": "Tags History", + "searchTags": "Search tags..." + }, + "repoDetails": { + "titleNA": "Title not available" + }, + "repoDetailsMetadata": { + "repository": "Repository", + "totalDownloads": "Total downloads", + "lastPublish": "Last publish" + }, + "deleteTag": { + "deleteImage": "Permanently delete image" + }, + "deleteTagConfirmDialog": { + "delete": "Delete" + }, + "filterCard": { + "filterTitle": "Filter Title" + }, + "layerCard": { + "details": "DETAILS", + "command": "Command" + }, + "noData": { + "noData": "No Data" + }, + "pullCommandButton": { + "copiedPullCommand": "Copied Pull Command", + "pull": "Pull" + }, + "referrerCard": { + "type": "Type:", + "mediaType": "Media type:", + "size": "Size:", + "annotations": "ANNOTATIONS" + }, + "repoCard": { + "downloads": "Downloads" + }, + "signatureTooltip": { + "notSigned": "Not signed", + "tool": "Tool", + "signedBy": "Signed-by" + }, + "tagCard": { + "tag": "Tag", + "by": "by", + "showMore": "Show more", + "showLess": "Show less", + "compressedSize": "COMPRESSED SIZE" + }, + "vulnerabilityCard": { + "notFixed": "Not fixed", + "loadMore": "Load more", + "externalReference": "External reference", + "packages": "Packages", + "fixedIn": "Fixed in" + }, + "vulnerabilityCountCard": { + "total": "Total", + "critical": "Critical", + "criticalShort": "C", + "high": "High", + "highShort": "H", + "medium": "Medium", + "mediumShort": "M", + "low": "Low", + "lowShort": "L", + "unknown": "Unknown", + "unknownShort": "U" + }, + "vulnerabilityPackageSection": { + "packagePath": "Package Path", + "installedVersion": "Installed Version", + "fixedVersion": "Fixed Version" + }, + "dependsOn": {}, + "historyLayers": { + "noLayers": "No Layer data available" + }, + "IsDependentOn": {}, + "ReferredBy": {}, + "VulnerabilitiesDetails": { + "noVulnerabilities": "No Vulnerabilities", + "gettingYourDataReady": "Getting your data ready for export", + "collapseListView": "Collapse list view", + "expandListView": "Expand list view", + "search": "Search", + "exclude": "Exclude" + }, + "TagDetails": { + "digest": "Digest" + }, + "tagDetailsMetadata": { + "lastPublished": "Last Published" + }, + "apiKeyCard": { + "key": "KEY" + }, + "apiKeyConfirmDialog": { + "apiKey": "Api Key", + "plsCopy": "Please copy the api key, you will not be able to see it once the page is refreshed" + }, + "apiKeyDialog": { + "createApiKey": "Create Api Key", + "expDate": "Expiration date", + "expTime": "Expiration time", + "custom": "custom", + "create": "Create" + }, + "apiKeyRevokeDialog": { + "key": "key", + "areYouSure": "Are you sure you want to revoke this api key?" + }, + "apiKeys": { + "manageYourKeys": "Manage your API Keys", + "createNewKey": "Create new API key" + }, + "sortCriteria": { + "relevance": "Relevance", + "recent": "Recent", + "alphabetical": "Alphabetical", + "alphabeticalDesc": "Alphabetical desc", + "mostStarred": "Most starred", + "mostDownloaded": "Most downloaded", + "newest": "Newest", + "oldest": "Oldest", + "AZ": "A - Z", + "ZA": "Z - A" + }, + "filterConstants": { + "signedImages": "Signed Images", + "bookmarks": "Bookmarks", + "starredRepositories": "Starred Repositories" + }, + "vulnerabilityAndSignatureComponents": { + "failed2scan": "Failed to scan" + } +} diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json new file mode 100644 index 00000000..a0e4c235 --- /dev/null +++ b/public/locales/ru/translation.json @@ -0,0 +1,227 @@ +{ + "main": { + "cancel": "Закрыть", + "close": "Закрыть", + "created": "Создано", + "days": "дней", + "description": "Описание", + "descriptionNA": "Описание недоступно", + "digest": "ДАЙДЖЕСТ", + "hours": "часов", + "layers": "Слои", + "license": "Лицензия", + "licenseNA": "Информация о лицензии недоступна", + "loading": "Загрузка...", + "minutes": "минут", + "NA": "недоступно", + "nothingFound": "Ничего не найдено", + "osOrArch": "ОС/Архитектура", + "published": "опубликовано", + "referredBy": "Упомянутый в", + "revoke": "Отозвать", + "signIn": "Войти", + "sort": "Сортировать по", + "stars": "Звезд", + "timestampNA": "Время НД", + "totalSize": "Общий размер", + "unknown": "Неизвестен", + "usedBy": "Используется", + "uses": "Использует", + "vendorNA": "Автор неизвестен", + "vulnerabilities": "Уязвимости", + "weeks": "недель" + }, + "explore": { + "OS": "Операционная система", + "architectures": "Архитектуры", + "additionalFilters": "Дополнительные фильтры", + "showing": "Показано", + "resultsOutOf": "результатов из", + "filterResults": "Отфильтровать результаты", + "noResults": "Похоже, у нас нет ничего подходящего для этого поиска. Попробуйте поискать что-нибудь еще." + }, + "filterDialog": { + "filter": "Фильтр", + "sortResults": "Сортировать результаты", + "confirm": "Подтвердить" + }, + "header": { + "product": "Проект", + "docs": "Документация" + }, + "exploreHeader": { + "home": "Домой" + }, + "searchSuggestion": { + "search": "Поиск по содержимому сайта...", + "advancedSearch": "Нажмите Enter для продвинутого поиска", + "useAposChar": "Используйте символ ':' для поиска по тегам" + }, + "userAccountMenu": { + "APIKeys": "API Ключи", + "logOut": "Выйти" + }, + "home": { + "noImages": "Нет образов", + "viewAll": "Посмотреть все", + "mostPopularImages": "Наиболее популярные образы", + "recentlyUpdatedImages": "Недавно обновленные образы", + "bookmarks": "Закладки" + }, + "signIn": { + "welcomeBack": "Добро пожаловать! Пожалуйста, войдите в систему.", + "or": "или", + "username": "Имя пользователя", + "enterPassword": "Введите пароль", + "authFailed": "Не удалось выполнить аутентификацию. Пожалуйста, попробуйте снова.", + "continue": "Продолжить", + "continueAsGuest": "Продолжить как гость" + }, + "signInPresentation": { + "description": "OCI-native реестр образов контейнеров, упрощенный" + }, + "thirdPartyLoginComponents": { + "continueWith": "Продолжить с", + "signInWith": "Войти при помощи" + }, + "tags": { + "tagsHistory": "История тегов", + "searchTags": "Поиск тегов..." + }, + "repoDetails": { + "titleNA": "Название недоступно" + }, + "repoDetailsMetadata": { + "repository": "Репозиторий", + "totalDownloads": "Количество скачиваний", + "lastPublish": "Последняя публикация" + }, + "deleteTag": { + "deleteImage": "Безвозвратно удалить изображение" + }, + "deleteTagConfirmDialog": { + "delete": "Удалить" + }, + "filterCard": { + "filterTitle": "Название фильтра" + }, + "layerCard": { + "details": "ДЕТАЛИ", + "command": "Команда" + }, + "noData": { + "noData": "Нет информации" + }, + "pullCommandButton": { + "copiedPullCommand": "Команда Pull скопирована", + "pull": "Pull" + }, + "referrerCard": { + "type": "Тип:", + "mediaType": "Тип медиа:", + "size": "Размер:", + "annotations": "АННОТАЦИИ" + }, + "repoCard": { + "downloads": "Скачиваний" + }, + "signatureTooltip": { + "notSigned": "Не подписан", + "tool": "Инструмент", + "signedBy": "Подписан" + }, + "tagCard": { + "tag": "Тег", + "by": "", + "showMore": "Показать больше", + "showLess": "Скрыть", + "compressedSize": "РАЗМЕР СЖАТОГО" + }, + "vulnerabilityCard": { + "notFixed": "Не исправлено", + "loadMore": "Показать больше", + "externalReference": "Внешняя ссылка", + "packages": "Пакеты", + "fixedIn": "Исправлено в" + }, + "vulnerabilityCountCard": { + "total": "Всего", + "critical": "Критических", + "criticalShort": "К", + "high": "Серьезных", + "highShort": "С", + "medium": "Средних", + "mediumShort": "Ср", + "low": "Небольших", + "lowShort": "Н", + "unknown": "Неизвестных", + "unknownShort": "Не" + }, + "vulnerabilityPackageSection": { + "packagePath": "Путь пакета", + "installedVersion": "Установленная версия", + "fixedVersion": "Исправленная версия" + }, + "dependsOn": {}, + "historyLayers": { + "noLayers": "Нет информаци о слое" + }, + "IsDependentOn": {}, + "ReferredBy": {}, + "VulnerabilitiesDetails": { + "noVulnerabilities": "Никаких уязвимостей", + "gettingYourDataReady": "Подготовка ваших данных к экспорту", + "collapseListView": "Свернуть представление списка", + "expandListView": "Развернуть представление списка", + "search": "Поиск", + "exclude": "Исключить" + }, + "TagDetails": { + "digest": "Дайджест" + }, + "tagDetailsMetadata": { + "lastPublished": "Последняя публикация" + }, + "apiKeyCard": { + "key": "КЛЮЧ" + }, + "apiKeyConfirmDialog": { + "apiKey": "Api Ключ", + "plsCopy": "Пожалуйста, скопируйте данный API ключ, вы не сможете увидеть его после обновления страницы" + }, + "apiKeyDialog": { + "createApiKey": "Создать API ключ", + "expDate": "Дата окончания", + "expTime": "Время окончания", + "custom": "пользовательский", + "create": "Создать" + }, + "apiKeyRevokeDialog": { + "key": "ключ", + "areYouSure": "Вы уверены, что хотите отозвать этот API-ключ?" + }, + "apiKeys": { + "manageYourKeys": "Управление вашими API ключами", + "createNewKey": "Создать новый API ключ" + }, + "sortCriteria": { + "relevance": "По релевантности", + "recent": "Последние", + "alphabetical": "По алфавиту (А - Я)", + "alphabeticalDesc": "По алфавиту (Я - А)", + "mostStarred": "Наиболее любимые", + "mostDownloaded": "Наиболее скачиваемые", + "newest": "От новых к старым", + "oldest": "От старых к новым", + "AZ": "А- Я", + "ZA": "Я - А" + }, + "filterConstants": { + "signedImages": "Подписанные образы", + "bookmarks": "Закладки", + "starredRepositories": "Понравившиеся" + }, + "vulnerabilityAndSignatureComponents": { + "failed2scan": "Не удалось выполнить сканирование" + } +} diff --git a/src/App.js b/src/App.js index 6fdd96cd..1a42d1b9 100644 --- a/src/App.js +++ b/src/App.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, Suspense } from 'react'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import { isAuthenticated, isApiKeyEnabled } from 'utilities/authUtilities'; @@ -18,23 +18,25 @@ function App() { return (
- - - }> - } /> - } /> - } /> - } /> - } /> - {isApiKeyEnabled() && } />} - } /> - - }> - } /> - } /> - - - + + + + }> + } /> + } /> + } /> + } /> + } /> + {isApiKeyEnabled() && } />} + } /> + + }> + } /> + } /> + + + +
); } diff --git a/src/components/Explore/Explore.jsx b/src/components/Explore/Explore.jsx index ccaf3931..e3000076 100644 --- a/src/components/Explore/Explore.jsx +++ b/src/components/Explore/Explore.jsx @@ -1,5 +1,6 @@ // react global import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; // components import RepoCard from '../Shared/RepoCard.jsx'; @@ -210,6 +211,8 @@ function Explore({ searchInputValue }) { setFilterDialogOpen(true); }; + const { t } = useTranslation(); + const renderRepoCards = () => { return ( exploreData && @@ -243,21 +246,21 @@ function Explore({ searchInputValue }) { return ( - Showing {exploreData?.length} results out of {totalItems} + {t('explore.showing')} {exploreData?.length} {t('explore.resultsOutOf')} {totalItems} {!isLoading && ( )} - Sort + {t('main.sort')} @@ -324,7 +327,7 @@ function Explore({ searchInputValue }) {
- Looks like we don't have anything matching that search. Try searching something else. + {t('explore.noResults')}
diff --git a/src/components/Explore/FilterDialog.jsx b/src/components/Explore/FilterDialog.jsx index 56249828..93fd31ca 100644 --- a/src/components/Explore/FilterDialog.jsx +++ b/src/components/Explore/FilterDialog.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; import { makeStyles } from '@mui/styles'; import { Dialog, @@ -28,16 +29,23 @@ function FilterDialog(props) { setOpen(false); }; + const { t } = useTranslation(); + return ( - Filter + {t('filterDialog.filter')} - Sort results + {t('filterDialog.sortResults')} - {Object.values(sortByCriteria).map((el) => ( - {el.label} + {t(el.label)} ))} @@ -45,7 +53,7 @@ function FilterDialog(props) { {renderFilterCards()} - + ); diff --git a/src/components/Header/ExploreHeader.jsx b/src/components/Header/ExploreHeader.jsx index e76c0008..e17f6a12 100644 --- a/src/components/Header/ExploreHeader.jsx +++ b/src/components/Header/ExploreHeader.jsx @@ -1,5 +1,7 @@ // react global import { Link, useLocation, useNavigate } from 'react-router-dom'; +// localization +import { useTranslation } from 'react-i18next'; // components import { Typography, Breadcrumbs } from '@mui/material'; @@ -51,6 +53,7 @@ function ExploreHeader() { const pathToBeDisplayed = pathWithoutImage.replace('/image/', ''); const pathHeader = pathToBeDisplayed.replace('/', ' / ').replace(/%2F/g, '/'); const pathWithTag = path.substring(0, path.lastIndexOf('/')); + const { t } = useTranslation(); return (
@@ -58,7 +61,7 @@ function ExploreHeader() { - Home + {t('exploreHeader.home')} diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx index ad7e70fd..86fe8d9c 100644 --- a/src/components/Header/Header.jsx +++ b/src/components/Header/Header.jsx @@ -1,11 +1,12 @@ // react global import React, { useState, useEffect } from 'react'; import { Link, useLocation } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import { isAuthenticated, isAuthenticationEnabled, logoutUser } from '../../utilities/authUtilities'; // components -import { AppBar, Toolbar, Grid, Button } from '@mui/material'; +import { AppBar, Toolbar, Grid, Button, FormControl, Select, MenuItem } from '@mui/material'; import SearchSuggestion from './SearchSuggestion'; import UserAccountMenu from './UserAccountMenu'; // styling @@ -95,6 +96,10 @@ const useStyles = makeStyles((theme) => ({ fontSize: '1rem', textTransform: 'none', fontWeight: 600 + }, + selectLanguage: { + width: 'max-content', + backgroundColor: '#ffffff' } })); @@ -131,6 +136,18 @@ function Header({ setSearchCurrentValue = () => {} }) { const classes = useStyles(); const path = useLocation().pathname; + const locales = { + en: { title: 'English' }, + ru: { title: 'Русский' } + }; + + const { t, i18n } = useTranslation(); + const [selectedLanguage, setSelectedLanguage] = useState(i18n.language); + const handleLanguageChange = (event) => { + i18n.changeLanguage(event.target.value); + setSelectedLanguage(event.target.value); + }; + const handleSignInClick = () => { logoutUser(); }; @@ -150,7 +167,7 @@ function Header({ setSearchCurrentValue = () => {} }) { - Product + {t('header.product')} @@ -160,7 +177,7 @@ function Header({ setSearchCurrentValue = () => {} }) { target="_blank" rel="noreferrer" > - Docs + {t('header.docs')} @@ -181,10 +198,26 @@ function Header({ setSearchCurrentValue = () => {} }) { {!isAuthenticated() && isAuthenticationEnabled() && ( )} + + + + + diff --git a/src/components/Header/SearchSuggestion.jsx b/src/components/Header/SearchSuggestion.jsx index ddb68913..accafaaa 100644 --- a/src/components/Header/SearchSuggestion.jsx +++ b/src/components/Header/SearchSuggestion.jsx @@ -3,6 +3,7 @@ import { makeStyles } from '@mui/styles'; import PhotoIcon from '@mui/icons-material/Photo'; import SearchIcon from '@mui/icons-material/Search'; import React, { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { api, endpoints } from 'api'; import { host } from 'host'; import { mapToImage, mapToRepo } from 'utilities/objectModels'; @@ -274,6 +275,8 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) { )); }; + const { t } = useTranslation(); + return (
{} }) { {...getComboboxProps()} > {} }) { spacing={2} > - Loading... + {t('main.loading')} @@ -331,7 +334,7 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) { onClick={() => {}} > - Press Enter for advanced search + {t('searchSuggestion.advancedSearch')} {} }) { onClick={() => {}} > - Use the ':' character to search for tags + {t('searchSuggestion.useAposChar')} diff --git a/src/components/Header/UserAccountMenu.jsx b/src/components/Header/UserAccountMenu.jsx index 5223dd65..a7fd3f3d 100644 --- a/src/components/Header/UserAccountMenu.jsx +++ b/src/components/Header/UserAccountMenu.jsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Menu, MenuItem, IconButton, Avatar, Divider } from '@mui/material'; @@ -22,6 +23,8 @@ function UserAccountMenu() { setAnchorEl(null); }; + const { t } = useTranslation(); + return ( <> {isApiKeyEnabled() && ( - API Keys + {t('userAccountMenu.APIKeys')} )} {isApiKeyEnabled() && } - Log out + {t('userAccountMenu.logOut')} ); diff --git a/src/components/Home/Home.jsx b/src/components/Home/Home.jsx index c36424d5..b11fa637 100644 --- a/src/components/Home/Home.jsx +++ b/src/components/Home/Home.jsx @@ -3,6 +3,7 @@ import { makeStyles } from '@mui/styles'; import { api, endpoints } from 'api'; import { host } from '../../host'; import React, { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import RepoCard from '../Shared/RepoCard'; import { mapToRepo } from 'utilities/objectModels'; import Loading from '../Shared/Loading'; @@ -250,6 +251,8 @@ function Home() { popularData.length === 0 && recentData.length === 0; + const { t } = useTranslation(); + const renderCards = (cardArray) => { return ( cardArray && @@ -281,18 +284,18 @@ function Home() { const renderContent = () => { return isNoData() === true ? ( - + ) : (
- Most popular images + {t('home.mostPopularImages')}
handleClickViewAll('sortby', sortByCriteria.downloads.value)}> - View all + {t('home.viewAll')}
@@ -301,7 +304,7 @@ function Home() {
- Recently updated images + {t('home.recentlyUpdatedImages')}
@@ -310,7 +313,7 @@ function Home() { className={classes.viewAll} onClick={() => handleClickViewAll('sortby', sortByCriteria.updateTime.value)} > - View all + {t('home.viewAll')}
@@ -320,7 +323,7 @@ function Home() {
- Bookmarks + {t('home.bookmarks')}
@@ -329,7 +332,7 @@ function Home() { className={classes.viewAll} onClick={() => handleClickViewAll('filter', 'IsBookmarked')} > - View all + {t('home.viewAll')}
@@ -341,7 +344,7 @@ function Home() {
- Stars + {t('main.stars')}
@@ -350,7 +353,7 @@ function Home() { className={classes.viewAll} onClick={() => handleClickViewAll('filter', 'IsStarred')} > - View all + {t('home.viewAll')}
diff --git a/src/components/Login/SignIn.jsx b/src/components/Login/SignIn.jsx index 761ed8c1..da9d12a7 100644 --- a/src/components/Login/SignIn.jsx +++ b/src/components/Login/SignIn.jsx @@ -1,6 +1,7 @@ // react global import React, { useEffect, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; // utility import { api, endpoints } from '../../api'; @@ -324,6 +325,8 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading = ); }; + const { t } = useTranslation(); + return (
{isLoading ? ( @@ -333,17 +336,17 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading = - Sign In + {t('main.signIn')} - Welcome back! Please login. + {t('signIn.welcomeBack')} {renderThirdPartyLoginMethods()} {Object.keys(authMethods).length > 1 && Object.keys(authMethods).includes('openid') && Object.keys(authMethods.openid.providers).length > 0 && ( - or + {t('signIn.or')} )} {Object.keys(authMethods).includes('htpasswd') && ( @@ -353,7 +356,7 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading = required fullWidth id="username" - label="Username" + label={t('signIn.username')} name="username" className={classes.textField} inputProps={{ className: classes.textColor }} @@ -368,7 +371,7 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading = required fullWidth name="password" - label="Enter password" + label={t('signIn.enterPassword')} type="password" id="password" className={classes.textField} @@ -382,7 +385,7 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading = {requestProcessing && } {requestError && ( - Authentication Failed. Please try again. + {t('signIn.authFailed')} )}
@@ -393,7 +396,7 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading = onClick={handleClick} data-testid="basic-auth-submit-btn" > - Continue + {t('signIn.continue')}
@@ -405,7 +408,7 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading = className={classes.continueAsGuestButton} onClick={handleGuestClick} > - Continue as guest + {t('signIn.continueAsGuest')} )}
diff --git a/src/components/Login/SignInPresentation.jsx b/src/components/Login/SignInPresentation.jsx index 2158b32c..6cab4bdb 100644 --- a/src/components/Login/SignInPresentation.jsx +++ b/src/components/Login/SignInPresentation.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; import { Stack, Typography } from '@mui/material'; import { makeStyles } from '@mui/styles'; @@ -37,6 +38,7 @@ const useStyles = makeStyles((theme) => ({ export default function SigninPresentation() { const classes = useStyles(); + const { t } = useTranslation(); return (
@@ -44,7 +46,7 @@ export default function SigninPresentation() { zot logo
- OCI-native container image registry, simplified + {t('signInPresentation.description')}
diff --git a/src/components/Login/ThirdPartyLoginComponents.jsx b/src/components/Login/ThirdPartyLoginComponents.jsx index ead49dc2..c0d33dd3 100644 --- a/src/components/Login/ThirdPartyLoginComponents.jsx +++ b/src/components/Login/ThirdPartyLoginComponents.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; import Button from '@mui/material/Button'; import SvgIcon from '@mui/material/SvgIcon'; @@ -46,6 +47,7 @@ const useStyles = makeStyles(() => ({ function GithubLoginButton({ handleClick }) { const classes = useStyles(); + const { t } = useTranslation(); return ( ); } function GoogleLoginButton({ handleClick }) { const classes = useStyles(); + const { t } = useTranslation(); return ( ); } function GitlabLoginButton({ handleClick }) { const classes = useStyles(); + const { t } = useTranslation(); return ( ); } @@ -83,10 +87,11 @@ function GitlabLoginButton({ handleClick }) { function OIDCLoginButton({ handleClick, oidcName }) { const classes = useStyles(); const loginWithName = oidcName || 'OIDC'; + const { t } = useTranslation(); return ( ); } diff --git a/src/components/Repo/RepoDetails.jsx b/src/components/Repo/RepoDetails.jsx index d04b76b1..c5a738bd 100644 --- a/src/components/Repo/RepoDetails.jsx +++ b/src/components/Repo/RepoDetails.jsx @@ -1,5 +1,6 @@ // react global import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; // external import { DateTime } from 'luxon'; @@ -248,16 +249,23 @@ function RepoDetails() { }); }; + const { t, i18n } = useTranslation(); + const [selectedLanguage] = useState(i18n.language); + const getVendor = () => { - return `${repoDetailData.newestTag?.Vendor || 'Vendor not available'} •`; + return `${repoDetailData.newestTag?.Vendor || t('main.vendorNA')} •`; }; const getVersion = () => { - return `published ${repoDetailData.newestTag?.Tag} •`; + return `${t('main.published')} ${repoDetailData.newestTag?.Tag} •`; }; const getLast = () => { const lastDate = repoDetailData.lastUpdated - ? DateTime.fromISO(repoDetailData.lastUpdated).toRelative({ unit: ['weeks', 'days', 'hours', 'minutes'] }) - : `Timestamp N/A`; + ? DateTime.fromISO(repoDetailData.lastUpdated) + .setLocale(selectedLanguage) + .toRelative({ + unit: ['weeks', 'days', 'hours', 'minutes'] + }) + : `${t('main.timestampNA')}`; return lastDate; }; @@ -342,7 +350,7 @@ function RepoDetails() {
- {repoDetailData?.title || 'Title not available'} + {repoDetailData?.title || t('repoDetails.titleNA')} {platformChips()} diff --git a/src/components/Repo/RepoDetailsMetadata.jsx b/src/components/Repo/RepoDetailsMetadata.jsx index 9b4901e4..330a0b02 100644 --- a/src/components/Repo/RepoDetailsMetadata.jsx +++ b/src/components/Repo/RepoDetailsMetadata.jsx @@ -2,7 +2,8 @@ import { Card, CardContent, Grid, Typography, Tooltip } from '@mui/material'; import makeStyles from '@mui/styles/makeStyles'; import { DateTime } from 'luxon'; import { Markdown } from 'utilities/MarkdowntojsxWrapper'; -import React from 'react'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import transform from '../../utilities/transform'; const useStyles = makeStyles((theme) => ({ @@ -44,20 +45,26 @@ const useStyles = makeStyles((theme) => ({ function RepoDetailsMetadata(props) { const classes = useStyles(); const { repoURL, totalDownloads, lastUpdated, size, license, description } = props; + const { t, i18n } = useTranslation(); + const [selectedLanguage] = useState(i18n.language); const lastDate = lastUpdated - ? DateTime.fromISO(lastUpdated).toRelative({ unit: ['weeks', 'days', 'hours', 'minutes'] }) - : `Timestamp N/A`; + ? DateTime.fromISO(lastUpdated) + .setLocale(selectedLanguage) + .toRelative({ + unit: ['weeks', 'days', 'hours', 'minutes'] + }) + : `${t('main.timestampNA')}`; return ( - Repository + {t('repoDetailsMetadata.repository')} - {repoURL || `not available`} + {repoURL || `${t('main.NA')}`} @@ -66,10 +73,10 @@ function RepoDetailsMetadata(props) { - Total downloads + {t('repoDetailsMetadata.totalDownloads')} - {!isNaN(totalDownloads) ? totalDownloads : `not available`} + {!isNaN(totalDownloads) ? totalDownloads : `${t('main.NA')}`} @@ -79,7 +86,7 @@ function RepoDetailsMetadata(props) { - Last publish + {t('repoDetailsMetadata.lastPublish')} @@ -93,7 +100,7 @@ function RepoDetailsMetadata(props) { - Total size + {t('main.totalSize')} {transform.formatBytes(size) || `----`} @@ -107,11 +114,11 @@ function RepoDetailsMetadata(props) { - License + {t('main.license')} - {license ? {license} : `License info not available`} + {license ? {license} : `${t('main.licenseNA')}`} @@ -123,10 +130,10 @@ function RepoDetailsMetadata(props) { - Description + {t('main.description')} - {description ? {description} : `Description not available`} + {description ? {description} : `${t('main.descriptionNA')}`} diff --git a/src/components/Repo/Tabs/Tags.jsx b/src/components/Repo/Tabs/Tags.jsx index 6abb6f38..b6633f85 100644 --- a/src/components/Repo/Tabs/Tags.jsx +++ b/src/components/Repo/Tabs/Tags.jsx @@ -1,5 +1,6 @@ // react global import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; // components import Typography from '@mui/material/Typography'; @@ -82,6 +83,8 @@ export default function Tags(props) { setSortFilter(value); }; + const { t } = useTranslation(); + return ( @@ -92,19 +95,19 @@ export default function Tags(props) { align="left" style={{ color: 'rgba(0, 0, 0, 0.87)', fontSize: '1.5rem', fontWeight: '600' }} > - Tags History + {t('tags.tagsHistory')} - Sort + {t('main.sort')} @@ -112,7 +115,7 @@ export default function Tags(props) { @@ -46,7 +49,7 @@ export default function DeleteTag(props) { diff --git a/src/components/Shared/DeleteTagConfirmDialog.jsx b/src/components/Shared/DeleteTagConfirmDialog.jsx index 4964c9a0..90860095 100644 --- a/src/components/Shared/DeleteTagConfirmDialog.jsx +++ b/src/components/Shared/DeleteTagConfirmDialog.jsx @@ -1,17 +1,19 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; // components import { Button, Dialog, DialogTitle, DialogActions } from '@mui/material'; export default function DeleteTagConfirmDialog(props) { const { onClose, open, title, onConfirm } = props; + const { t } = useTranslation(); return ( {title} diff --git a/src/components/Shared/FilterCard.jsx b/src/components/Shared/FilterCard.jsx index ffa60140..474094d0 100644 --- a/src/components/Shared/FilterCard.jsx +++ b/src/components/Shared/FilterCard.jsx @@ -2,6 +2,7 @@ import { Card, CardContent, Checkbox, FormControlLabel, Stack, Tooltip, Typograp import { makeStyles } from '@mui/styles'; import { isArray, isNil } from 'lodash'; import React from 'react'; +import { useTranslation } from 'react-i18next'; const useStyles = makeStyles((theme) => ({ card: { @@ -70,6 +71,8 @@ function FilterCard(props) { return filterValue[filter.value] || false; }; + const { t } = useTranslation(); + const getFilterRows = () => { const filterRows = filters; return filterRows.map((filter, index) => { @@ -79,7 +82,7 @@ function FilterCard(props) { className={classes.formControl} componentsProps={{ typography: { variant: 'body2', className: classes.cardContentText } }} control={} - label={filter.label} + label={t(filter.label)} id={title} checked={getCheckboxStatus(filter)} onChange={() => handleFilterClicked(event, filter.value)} @@ -93,7 +96,7 @@ function FilterCard(props) { return ( - {title || 'Filter Title'} + {title || t('filterCard.filterTitle')} {getFilterRows()} diff --git a/src/components/Shared/LayerCard.jsx b/src/components/Shared/LayerCard.jsx index 133cadc8..77e157b2 100644 --- a/src/components/Shared/LayerCard.jsx +++ b/src/components/Shared/LayerCard.jsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import transform from 'utilities/transform'; @@ -99,6 +100,8 @@ function LayerCard(props) { else return layer.Size; }; + const { t } = useTranslation(); + return ( @@ -123,17 +126,17 @@ function LayerCard(props) { ) : ( )} - DETAILS + {t('layerCard.details')} - Command + {t('layerCard.command')} {historyDescription.CreatedBy} {!historyDescription.EmptyLayer && ( <> - DIGEST + {t('main.digest')} {layer.Digest} diff --git a/src/components/Shared/NoDataComponent.jsx b/src/components/Shared/NoDataComponent.jsx index d6290816..9203928c 100644 --- a/src/components/Shared/NoDataComponent.jsx +++ b/src/components/Shared/NoDataComponent.jsx @@ -1,5 +1,6 @@ // react global import React from 'react'; +import { useTranslation } from 'react-i18next'; // components import { Stack, Typography } from '@mui/material'; @@ -28,11 +29,12 @@ const useStyles = makeStyles((theme) => ({ function NoDataComponent({ text }) { const classes = useStyles(); + const { t } = useTranslation(); return ( - {text ? text : 'No Data'} + {text ? text : t('noData.noData')} ); } diff --git a/src/components/Shared/PullCommandButton.jsx b/src/components/Shared/PullCommandButton.jsx index aff8fb40..da964a97 100644 --- a/src/components/Shared/PullCommandButton.jsx +++ b/src/components/Shared/PullCommandButton.jsx @@ -1,4 +1,5 @@ import React, { useState, useEffect, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; import makeStyles from '@mui/styles/makeStyles'; import { Grid, Button, FormControl, Menu, MenuItem, Box, Tab, InputBase, IconButton, ButtonBase } from '@mui/material'; @@ -162,9 +163,11 @@ function PullCommandButton(props) { } }, [isCopied]); + const { t } = useTranslation(); + return isCopied ? ( ) : ( @@ -174,7 +177,7 @@ function PullCommandButton(props) { className={`${classes.copyStringSelect} ${open && classes.copyStringSelectOpened}`} disableRipple > - Pull {imageName} + {t('pullCommandButton.pull')} {imageName} {getButtonIcon()} - Type: {artifactType && `${artifactType}`} + {t('referrerCard.type')} {artifactType && `${artifactType}`} - Media type: {mediaType && `${mediaType}`} + {t('referrerCard.mediaType')} {mediaType && `${mediaType}`} - Size: {size && `${size}`} + {t('referrerCard.size')} {size && `${size}`} setDigestDropdownOpen(!digestDropdownOpen)}> {!digestDropdownOpen ? ( @@ -96,7 +98,7 @@ export default function ReferrerCard(props) { cursor: 'pointer' }} > - DIGEST + {t('main.digest')} @@ -124,7 +126,7 @@ export default function ReferrerCard(props) { cursor: 'pointer' }} > - ANNOTATIONS + {t('referrerCard.annotations')} diff --git a/src/components/Shared/RepoCard.jsx b/src/components/Shared/RepoCard.jsx index e196a092..ab5c86dd 100644 --- a/src/components/Shared/RepoCard.jsx +++ b/src/components/Shared/RepoCard.jsx @@ -1,6 +1,7 @@ // react global import React, { useRef, useMemo, useState } from 'react'; import { useNavigate, createSearchParams } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; // utility import { DateTime } from 'luxon'; @@ -259,16 +260,21 @@ function RepoCard(props) { )); }; + const { t, i18n } = useTranslation(); + const [selectedLanguage] = useState(i18n.language); + const getVendor = () => { - return `${vendor || 'Vendor not available'} •`; + return `${vendor || t('main.vendorNA')} •`; }; const getVersion = () => { - return `published ${version} •`; + return `${t('main.published')} ${version} •`; }; const getLast = () => { const lastDate = lastUpdated - ? DateTime.fromISO(lastUpdated).toRelative({ unit: ['weeks', 'days', 'hours', 'minutes'] }) - : `Timestamp N/A`; + ? DateTime.fromISO(lastUpdated) + .setLocale(selectedLanguage) + .toRelative({ unit: ['weeks', 'days', 'hours', 'minutes'] }) + : `${t('main.timestampNA')}`; return lastDate; }; @@ -346,9 +352,9 @@ function RepoCard(props) {
{getSignatureChips()} - + - {description || 'Description not available'} + {description || t('main.descriptionNA')} @@ -375,10 +381,10 @@ function RepoCard(props) { - Downloads • + {t('repoCard.downloads')} • - {!isNaN(downloads) ? downloads : `not available`} + {!isNaN(downloads) ? downloads : `${t('main.NA')}`} {/* @@ -392,10 +398,10 @@ function RepoCard(props) { {renderStar()} - Stars • + {t('main.stars')} • - {!isNaN(currentStarCount) ? currentStarCount : `not available`} + {!isNaN(currentStarCount) ? currentStarCount : `${t('main.NA')}`} diff --git a/src/components/Shared/SignatureTooltip.jsx b/src/components/Shared/SignatureTooltip.jsx index bde8ae17..aec97d1e 100644 --- a/src/components/Shared/SignatureTooltip.jsx +++ b/src/components/Shared/SignatureTooltip.jsx @@ -1,17 +1,23 @@ import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import { Typography, Stack } from '@mui/material'; import { isEmpty } from 'lodash'; import { getStrongestSignature, getAllAuthorsOfSignatures } from 'utilities/vulnerabilityAndSignatureCheck'; function SignatureTooltip({ signatureInfo }) { const strongestSignature = useMemo(() => getStrongestSignature(signatureInfo)); + const { t } = useTranslation(); return isEmpty(strongestSignature) ? ( - Not signed + {t('signatureTooltip.notSigned')} ) : ( - Tool: {strongestSignature?.tool || 'Unknown'} - Signed-by: {getAllAuthorsOfSignatures(signatureInfo) || 'Unknown'} + + {t('signatureTooltip.tool')}: {strongestSignature?.tool || t('main.unknown')} + + + {t('signatureTooltip.signedBy')}: {getAllAuthorsOfSignatures(signatureInfo) || t('main.unknown')} + ); } diff --git a/src/components/Shared/TagCard.jsx b/src/components/Shared/TagCard.jsx index 91fb0be9..3935b703 100644 --- a/src/components/Shared/TagCard.jsx +++ b/src/components/Shared/TagCard.jsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { makeStyles } from '@mui/styles'; import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import { Box, Card, CardContent, Collapse, Grid, Stack, Tooltip, Typography, Divider } from '@mui/material'; import { Markdown } from 'utilities/MarkdowntojsxWrapper'; import transform from 'utilities/transform'; @@ -84,9 +85,14 @@ export default function TagCard(props) { const classes = useStyles(); + const { t, i18n } = useTranslation(); + const [selectedLanguage] = useState(i18n.language); + const lastDate = lastUpdated - ? DateTime.fromISO(lastUpdated).toRelative({ unit: ['weeks', 'days', 'hours', 'minutes'] }) - : `Timestamp N/A`; + ? DateTime.fromISO(lastUpdated) + .setLocale(selectedLanguage) + .toRelative({ unit: ['weeks', 'days', 'hours', 'minutes'] }) + : `${t('main.timestampNA')}`; const navigate = useNavigate(); const goToTags = (digest = null) => { @@ -102,7 +108,7 @@ export default function TagCard(props) { - Tag + {t('tagCard.tag')} {isDeletable && } @@ -113,11 +119,12 @@ export default function TagCard(props) { - Created + {t('main.created')} - {lastDate} by {vendor || 'Vendor not available'} + {lastDate} {t('tagCard.by')} + {vendor || t('main.vendorNA')} @@ -128,18 +135,20 @@ export default function TagCard(props) { ) : ( )} - {!open ? `Show more` : `Show less`} + + {!open ? `${t('tagCard.showMore')}` : `${t('tagCard.showLess')}`} + - DIGEST + {t('main.digest')} - OS/Arch + {t('main.osOrArch')} - COMPRESSED SIZE + {t('tagCard.compressedSize')} diff --git a/src/components/Shared/VulnerabilityCard.jsx b/src/components/Shared/VulnerabilityCard.jsx index 976913bc..2d69adb7 100644 --- a/src/components/Shared/VulnerabilityCard.jsx +++ b/src/components/Shared/VulnerabilityCard.jsx @@ -1,4 +1,5 @@ import React, { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; // utility import { api, endpoints } from '../../api'; @@ -177,6 +178,8 @@ function VulnerabilitiyCard(props) { setPageNumber((pageNumber) => pageNumber + 1); }; + const { t } = useTranslation(); + const renderFixedVer = () => { if (!isEmpty(fixedInfo)) { return fixedInfo.map((tag, index) => { @@ -187,7 +190,7 @@ function VulnerabilitiyCard(props) { ); }); } else { - return 'Not fixed'; + return t('vulnerabilityCard.notFixed'); } }; @@ -206,7 +209,7 @@ function VulnerabilitiyCard(props) { onClick={loadMore} component="div" > - Load more + {t('vulnerabilityCard.loadMore')} ) ); @@ -243,7 +246,7 @@ function VulnerabilitiyCard(props) { - External reference + {t('vulnerabilityCard.externalReference')} - Packages + {t('vulnerabilityCard.packages')} - Fixed in + {t('vulnerabilityCard.fixedIn')} {loadingFixed ? ( - 'Loading...' + t('main.loading') ) : ( {renderFixedVer()} @@ -283,7 +286,7 @@ function VulnerabilitiyCard(props) { )} - Description + {t('main.description')} diff --git a/src/components/Shared/VulnerabilityCountCard.jsx b/src/components/Shared/VulnerabilityCountCard.jsx index be308cbb..87e992cb 100644 --- a/src/components/Shared/VulnerabilityCountCard.jsx +++ b/src/components/Shared/VulnerabilityCountCard.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; import makeStyles from '@mui/styles/makeStyles'; import { Stack, Tooltip } from '@mui/material'; @@ -71,26 +72,40 @@ function VulnerabilitiyCountCard(props) { const classes = useStyles(); const { total, critical, high, medium, low, unknown, filterBySeverity } = props; + const { t } = useTranslation(); + return ( - filterBySeverity('')}> -
Total {total}
+ filterBySeverity('')}> +
+ {t('vulnerabilityCountCard.total')} {total} +
- filterBySeverity('CRITICAL')}> -
C {critical}
+ filterBySeverity('CRITICAL')}> +
+ {t('vulnerabilityCountCard.criticalShort')} {critical} +
- filterBySeverity('HIGH')}> -
H {high}
+ filterBySeverity('HIGH')}> +
+ {t('vulnerabilityCountCard.highShort')} {high} +
- filterBySeverity('MEDIUM')}> -
M {medium}
+ filterBySeverity('MEDIUM')}> +
+ {t('vulnerabilityCountCard.mediumShort')} {medium} +
- filterBySeverity('LOW')}> -
L {low}
+ filterBySeverity('LOW')}> +
+ {t('vulnerabilityCountCard.lowShort')} {low} +
- filterBySeverity('UNKNOWN')}> -
U {unknown}
+ filterBySeverity('UNKNOWN')}> +
+ {t('vulnerabilityCountCard.unknownShort')} {unknown} +
diff --git a/src/components/Shared/VulnerabilityPackageSection.jsx b/src/components/Shared/VulnerabilityPackageSection.jsx index 4ddae292..e118ca88 100644 --- a/src/components/Shared/VulnerabilityPackageSection.jsx +++ b/src/components/Shared/VulnerabilityPackageSection.jsx @@ -1,4 +1,6 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; + import { Divider, Grid, Stack, Typography } from '@mui/material'; import makeStyles from '@mui/styles/makeStyles'; @@ -26,6 +28,7 @@ const useStyles = makeStyles(() => ({ function VulnerabilityPackageSection(props) { const { cve } = props; const classes = useStyles(); + const { t } = useTranslation(); return ( - Package Path + {t('vulnerabilityPackageSection.packagePath')} {cve.packagePath} @@ -46,7 +49,7 @@ function VulnerabilityPackageSection(props) { - Installed Version + {t('vulnerabilityPackageSection.installedVersion')} {cve.packageInstalledVersion} @@ -54,7 +57,7 @@ function VulnerabilityPackageSection(props) { - Fixed Version + {t('vulnerabilityPackageSection.fixedVersion')} {cve.packageFixedVersion} diff --git a/src/components/Tag/Tabs/DependsOn.jsx b/src/components/Tag/Tabs/DependsOn.jsx index 1cfafb37..208c0c81 100644 --- a/src/components/Tag/Tabs/DependsOn.jsx +++ b/src/components/Tag/Tabs/DependsOn.jsx @@ -1,4 +1,5 @@ import React, { useEffect, useState, useMemo, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; import { isEmpty } from 'lodash'; // utility @@ -99,6 +100,8 @@ function DependsOn(props) { }; }, [isLoading, isEndOfList]); + const { t } = useTranslation(); + const renderDependencies = () => { return !isEmpty(images) ? ( images.map((dependence, index) => { @@ -115,7 +118,7 @@ function DependsOn(props) { ); }) ) : ( -
{!isLoading && Nothing found }
+
{!isLoading && {t('main.nothingFound')} }
); }; @@ -132,7 +135,7 @@ function DependsOn(props) { return (
- Uses + {t('main.uses')} diff --git a/src/components/Tag/Tabs/HistoryLayers.jsx b/src/components/Tag/Tabs/HistoryLayers.jsx index c37146e6..d9b9f9d7 100644 --- a/src/components/Tag/Tabs/HistoryLayers.jsx +++ b/src/components/Tag/Tabs/HistoryLayers.jsx @@ -1,4 +1,5 @@ import React, { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; // components import { Stack, Typography } from '@mui/material'; @@ -35,10 +36,12 @@ function HistoryLayers(props) { }; }, [name, history]); + const { t } = useTranslation(); + return ( <> - Layers + {t('main.layers')} {isLoading ? ( @@ -57,7 +60,7 @@ function HistoryLayers(props) { }) ) : (
- No Layer data available + {t('historyLayers.noLayers')}
)}
diff --git a/src/components/Tag/Tabs/IsDependentOn.jsx b/src/components/Tag/Tabs/IsDependentOn.jsx index 03ec2b3d..9eed00c4 100644 --- a/src/components/Tag/Tabs/IsDependentOn.jsx +++ b/src/components/Tag/Tabs/IsDependentOn.jsx @@ -1,4 +1,5 @@ import React, { useEffect, useMemo, useState, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; import { isEmpty } from 'lodash'; // utility @@ -99,6 +100,8 @@ function IsDependentOn(props) { }; }, [isLoading, isEndOfList]); + const { t } = useTranslation(); + const renderDependents = () => { return !isEmpty(images) ? ( images?.map((dependence, index) => { @@ -115,7 +118,7 @@ function IsDependentOn(props) { ); }) ) : ( -
{!isLoading && Nothing found }
+
{!isLoading && {t('main.nothingFound')} }
); }; @@ -132,7 +135,7 @@ function IsDependentOn(props) { return (
- Used by + {t('main.usedBy')} diff --git a/src/components/Tag/Tabs/ReferredBy.jsx b/src/components/Tag/Tabs/ReferredBy.jsx index 6a38fc3a..12331d61 100644 --- a/src/components/Tag/Tabs/ReferredBy.jsx +++ b/src/components/Tag/Tabs/ReferredBy.jsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { makeStyles } from '@mui/styles'; import { isEmpty } from 'lodash'; import { Typography, Stack } from '@mui/material'; @@ -36,6 +37,8 @@ function ReferredBy(props) { setIsLoading(false); }, []); + const { t } = useTranslation(); + const renderReferrers = () => { return !isEmpty(referrersData) ? ( referrersData.map((referrer, index) => { @@ -51,14 +54,14 @@ function ReferredBy(props) { ); }) ) : ( -
{!isLoading && Nothing found }
+
{!isLoading && {t('main.nothingFound')} }
); }; return (
- Referred By + {t('main.referredBy')} diff --git a/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx b/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx index a7d8f8a7..be5370dd 100644 --- a/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx +++ b/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx @@ -1,4 +1,5 @@ import React, { useEffect, useMemo, useState, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; // utility import { api, endpoints } from '../../../api'; @@ -339,13 +340,19 @@ function VulnerabilitiesDetails(props) { } }, [openExport]); + const { t } = useTranslation(); + const renderCVEs = () => { return !isEmpty(cveData) ? ( cveData.map((cve, index) => { return ; }) ) : ( -
{!isLoading && No Vulnerabilities }
+
+ {!isLoading && ( + {t('VulnerabilitiesDetails.noVulnerabilities')} + )} +
); }; @@ -383,7 +390,7 @@ function VulnerabilitiesDetails(props) { - Vulnerabilities + {t('main.vulnerabilities')} @@ -391,12 +398,12 @@ function VulnerabilitiesDetails(props) { } /> @@ -475,7 +482,7 @@ function VulnerabilitiesDetails(props) { diff --git a/src/components/Tag/TagDetails.jsx b/src/components/Tag/TagDetails.jsx index abe90d48..04fa80a6 100644 --- a/src/components/Tag/TagDetails.jsx +++ b/src/components/Tag/TagDetails.jsx @@ -1,5 +1,6 @@ import { useLocation, useNavigate, useParams } from 'react-router-dom'; import React, { useEffect, useMemo, useState, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; // utility import { api, endpoints } from '../../api'; @@ -247,6 +248,8 @@ function TagDetails() { )); }; + const { t } = useTranslation(); + return ( <> {isLoading ? ( @@ -289,10 +292,10 @@ function TagDetails() { - OS/Arch + {t('main.osOrArch')} {!isEmpty(selectedManifest) && ( - 7 days - 30 days - 60 days - 90 days - custom + 7 {t('main.days')} + 30 {t('main.days')} + 60 {t('main.days')} + 90 {t('main.days')} + {t('apiKeyDialog.custom')} @@ -159,10 +162,10 @@ function ApiKeyDialog(props) { onClick={handleSubmit} disabled={expirationDateOffset === 'custom' && isNil(selectedExpirationDate)} > - Create + {t('apiKeyDialog.create')} diff --git a/src/components/User/ApiKeys/ApiKeyRevokeDialog.jsx b/src/components/User/ApiKeys/ApiKeyRevokeDialog.jsx index d5dcb5d7..ef540b04 100644 --- a/src/components/User/ApiKeys/ApiKeyRevokeDialog.jsx +++ b/src/components/User/ApiKeys/ApiKeyRevokeDialog.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; import { api, endpoints } from 'api'; import { host } from 'host'; @@ -45,23 +46,27 @@ function ApiKeyRevokeDialog(props) { }); }; + const { t } = useTranslation(); + return ( - Revoke "{apiKey?.label}" key + + {t('main.revoke')} "{apiKey?.label}" {t('apiKeyRevokeDialog.key')} + - Are you sure you want to revoke this api key? + {t('apiKeyRevokeDialog.areYouSure')} diff --git a/src/components/User/ApiKeys/ApiKeys.jsx b/src/components/User/ApiKeys/ApiKeys.jsx index 9117111b..3e146e33 100644 --- a/src/components/User/ApiKeys/ApiKeys.jsx +++ b/src/components/User/ApiKeys/ApiKeys.jsx @@ -1,4 +1,5 @@ import React, { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { isEmpty, isNil } from 'lodash'; import { api, endpoints } from 'api'; @@ -98,6 +99,8 @@ function ApiKeys() { )); }; + const { t } = useTranslation(); + return ( <> {isLoading ? ( @@ -111,10 +114,10 @@ function ApiKeys() { - Manage your API Keys + {t('apiKeys.manageYourKeys')} diff --git a/src/index.js b/src/index.js index c21d1b97..502a89c1 100644 --- a/src/index.js +++ b/src/index.js @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; +import './utilities/i18n'; import { createTheme, ThemeProvider, StyledEngineProvider, adaptV4Theme } from '@mui/material/styles'; import { LocalizationProvider } from '@mui/x-date-pickers'; diff --git a/src/utilities/filterConstants.js b/src/utilities/filterConstants.js index 25d0501e..28152b80 100644 --- a/src/utilities/filterConstants.js +++ b/src/utilities/filterConstants.js @@ -15,17 +15,17 @@ const osFilters = [ const imageFilters = [ { - label: 'Signed Images', + label: 'filterConstants.signedImages', value: 'HasToBeSigned', type: 'boolean' }, { - label: 'Bookmarks', + label: 'filterConstants.bookmarks', value: 'IsBookmarked', type: 'boolean' }, { - label: 'Starred Repositories', + label: 'filterConstants.starredRepositories', value: 'IsStarred', type: 'boolean' } diff --git a/src/utilities/i18n.js b/src/utilities/i18n.js new file mode 100644 index 00000000..4467b194 --- /dev/null +++ b/src/utilities/i18n.js @@ -0,0 +1,23 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import Backend from 'i18next-http-backend'; + +i18n + .use(Backend) + .use(LanguageDetector) + .use(initReactI18next) + .init({ + fallbackLng: { + ru_RU: 'ru', + default: ['en'] + }, + backend: { + loadPath: '/locales/{{lng}}/{{ns}}.json' + }, + interpolation: { + escapeValue: false + } + }); + +export default i18n; diff --git a/src/utilities/sortCriteria.js b/src/utilities/sortCriteria.js index 25f660da..95cc3def 100644 --- a/src/utilities/sortCriteria.js +++ b/src/utilities/sortCriteria.js @@ -3,55 +3,55 @@ import { DateTime } from 'luxon'; export const sortByCriteria = { relevance: { value: 'RELEVANCE', - label: 'Relevance' + label: 'sortCriteria.relevance' }, updateTime: { value: 'UPDATE_TIME', - label: 'Recent' + label: 'sortCriteria.recent' }, alphabetic: { value: 'ALPHABETIC_ASC', - label: 'Alphabetical' + label: 'sortCriteria.alphabetical' }, alphabeticDesc: { value: 'ALPHABETIC_DSC', - label: 'Alphabetical desc' + label: 'sortCriteria.alphabeticalDesc' }, // stars: { // value: 'STARS', - // label: 'Most starred' + // label: 'sortCriteria.mostStarred' // }, downloads: { value: 'DOWNLOADS', - label: 'Most downloaded' + label: 'sortCriteria.mostDownloaded' } }; export const tagsSortByCriteria = { updateTimeDesc: { value: 'UPDATETIME_DESC', - label: 'Newest', + label: 'sortCriteria.newest', func: (a, b) => { return DateTime.fromISO(b.lastUpdated).diff(DateTime.fromISO(a.lastUpdated)); } }, updateTime: { value: 'UPDATETIME', - label: 'Oldest', + label: 'sortCriteria.oldest', func: (a, b) => { return DateTime.fromISO(a.lastUpdated).diff(DateTime.fromISO(b.lastUpdated)); } }, alphabetic: { value: 'ALPHABETIC', - label: 'A - Z', + label: 'sortCriteria.AZ', func: (a, b) => { return a.tag?.localeCompare(b.tag); } }, alphabeticDesc: { value: 'ALPHABETIC_DESC', - label: 'Z - A', + label: 'sortCriteria.ZA', func: (a, b) => { return b.tag?.localeCompare(a.tag); } diff --git a/src/utilities/vulnerabilityAndSignatureComponents.jsx b/src/utilities/vulnerabilityAndSignatureComponents.jsx index 1ec4e344..21c47173 100644 --- a/src/utilities/vulnerabilityAndSignatureComponents.jsx +++ b/src/utilities/vulnerabilityAndSignatureComponents.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; import { Chip, Tooltip, Badge } from '@mui/material'; import SvgIcon from '@mui/material/SvgIcon'; import { ReactComponent as failedScanBug } from '../assets/failedScan.svg'; @@ -62,8 +63,10 @@ const UnknownVulnerabilityIcon = ({ vulnerabilityStringTitle }) => { ); }; const FailedScanIcon = () => { + const { t } = useTranslation(); + return ( - + { ); }; const FailedScanChip = () => { + const { t } = useTranslation(); + return ( }