diff --git a/package-lock.json b/package-lock.json index 97a80293..8507a6f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,11 +22,15 @@ "axios": "^1.7.7", "downshift": "^6.1.12", "export-from-json": "^1.7.4", + "i18next": "^23.16.4", + "i18next-browser-languagedetector": "^8.0.0", + "i18next-http-backend": "^2.6.2", "lodash": "^4.17.21", "luxon": "^3.5.0", "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.1.1", "web-vitals": "^2.1.3", @@ -6550,6 +6554,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", @@ -9421,6 +9434,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", @@ -9570,6 +9592,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.5", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.5.tgz", + "integrity": "sha512-KTlhE3EP9x6pPTAW7dy0WKIhoCpfOGhRQlO+jttQLgzVaoOjWwBWramu7Pp0i+8wDNduuzXfe3kkVbzrKyrbTA==", + "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", @@ -11854,6 +11917,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", @@ -14201,6 +14306,28 @@ "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==", "dev": true }, + "node_modules/react-i18next": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.1.1.tgz", + "integrity": "sha512-R/Vg9wIli2P3FfeI8o1eNJUJue5LWpFsQePCHdQDmX0Co3zkr6kdT8gAseb/yGeWbNz1Txc4bKDQuZYsC0kQfw==", + "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", @@ -17650,6 +17777,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 34c0eb46..ef5e96d1 100644 --- a/package.json +++ b/package.json @@ -17,12 +17,16 @@ "axios": "^1.7.7", "downshift": "^6.1.12", "export-from-json": "^1.7.4", + "i18next": "^23.16.4", + "i18next-browser-languagedetector": "^8.0.0", + "i18next-http-backend": "^2.6.2", "lodash": "^4.17.21", "luxon": "^3.5.0", "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.1.1", "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..b9a74e97 --- /dev/null +++ b/public/locales/en/translation.json @@ -0,0 +1,228 @@ +{ + "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", + "no": "No" + }, + "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..8c81e84b --- /dev/null +++ b/public/locales/ru/translation.json @@ -0,0 +1,229 @@ +{ + "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": "НИ", + "no": "Нет", + "none": "Отсутствует" + }, + "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/__tests__/Explore/Explore.test.js b/src/__tests__/Explore/Explore.test.js index 8568cd73..ac8a0013 100644 --- a/src/__tests__/Explore/Explore.test.js +++ b/src/__tests__/Explore/Explore.test.js @@ -332,7 +332,7 @@ describe('Explore component', () => { it('displays the no data message if no data is received', async () => { jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: { GlobalSearch: { Repos: [] } } } }); render(); - expect(await screen.findByText(/Looks like/i)).toBeInTheDocument(); + expect(await screen.findByText(/explore.noResults/i)).toBeInTheDocument(); }); it('renders signature icons', async () => { @@ -344,17 +344,17 @@ describe('Explore component', () => { const allUntrustedSignaturesIcons = await screen.findAllByTestId("untrusted-icon"); fireEvent.mouseOver(allUntrustedSignaturesIcons[0]); - expect(await screen.findByText("Signed-by: Unknown")).toBeInTheDocument(); + expect(await screen.findByText("signatureTooltip.signedBy: main.unknown")).toBeInTheDocument(); const allTrustedSignaturesIcons = await screen.findAllByTestId("verified-icon"); fireEvent.mouseOver(allTrustedSignaturesIcons[8]); - expect(await screen.findByText("Tool: cosign")).toBeInTheDocument(); - expect(await screen.findByText("Signed-by: author1")).toBeInTheDocument(); + expect(await screen.findByText("signatureTooltip.tool: cosign")).toBeInTheDocument(); + expect(await screen.findByText("signatureTooltip.signedBy: author1")).toBeInTheDocument(); fireEvent.mouseOver(allTrustedSignaturesIcons[9]); - expect(await screen.findByText("Tool: notation")).toBeInTheDocument(); - expect(await screen.findByText("Signed-by: author2")).toBeInTheDocument(); + expect(await screen.findByText("signatureTooltip.tool: notation")).toBeInTheDocument(); + expect(await screen.findByText("signatureTooltip.signedBy: author2")).toBeInTheDocument(); const allNoSignedIcons = await screen.findAllByTestId("unverified-icon"); fireEvent.mouseOver(allNoSignedIcons[0]); - expect(await screen.findByText("Not signed")).toBeInTheDocument(); + expect(await screen.findByText("signatureTooltip.notSigned")).toBeInTheDocument(); }); it('renders vulnerability icons', async () => { @@ -379,12 +379,12 @@ describe('Explore component', () => { it("should render the sort filter and be able to change it's value", async () => { jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } }); render(); - const selectFilter = await screen.findByText('Relevance'); + const selectFilter = await screen.findByText('sortCriteria.relevance'); expect(selectFilter).toBeInTheDocument(); userEvent.click(selectFilter); - const newOption = await screen.findByText('Alphabetical'); + const newOption = await screen.findByText('sortCriteria.alphabetical'); userEvent.click(newOption); - expect(await screen.findByText('Alphabetical')).toBeInTheDocument(); + expect(await screen.findByText('sortCriteria.alphabetical')).toBeInTheDocument(); }); it('should get preselected filters and sorting order from query params', async () => { @@ -412,7 +412,7 @@ describe('Explore component', () => { await userEvent.click(windowsCheckbox); expect(windowsCheckbox).toBeChecked(); expect(await screen.findAllByTestId('repo-card')).toHaveLength(1); - const signedCheckboxLabel = await screen.findByText(/signed images/i); + const signedCheckboxLabel = await screen.findByText(/filterConstants.signedImages/i); jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: filteredMockImageListSigned() } }); await userEvent.click(signedCheckboxLabel); expect(await screen.findAllByTestId('repo-card')).toHaveLength(6); diff --git a/src/__tests__/Header/Header.test.js b/src/__tests__/Header/Header.test.js new file mode 100644 index 00000000..183a1a1d --- /dev/null +++ b/src/__tests__/Header/Header.test.js @@ -0,0 +1,29 @@ +import { render, screen } from '@testing-library/react'; +import Header from 'components/Header/Header'; +import React from 'react'; +import MockThemeProvider from '__mocks__/MockThemeProvider'; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => {} +})); + +const HeaderWrapper = () => { + return ( + + + + } /> + + + + ); +}; + +describe('Account Menu', () => { + it('is language select renders in header component', async () => { + render(); + expect(await screen.queryByTestId('select-language')).toBeInTheDocument(); + }); +}); diff --git a/src/__tests__/HomePage/Home.test.js b/src/__tests__/HomePage/Home.test.js index 3a5073f0..29c60d43 100644 --- a/src/__tests__/HomePage/Home.test.js +++ b/src/__tests__/HomePage/Home.test.js @@ -288,7 +288,7 @@ describe('Home component', () => { jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListBookmarks } }); jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListStars } }); render(); - const viewAllButtons = await screen.findAllByText(/view all/i); + const viewAllButtons = await screen.findAllByText(/home.viewAll/i); expect(viewAllButtons).toHaveLength(4); jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: [] } }); fireEvent.click(viewAllButtons[0]); diff --git a/src/__tests__/LoginPage/SignIn.test.js b/src/__tests__/LoginPage/SignIn.test.js index 7052c078..c611d6f4 100644 --- a/src/__tests__/LoginPage/SignIn.test.js +++ b/src/__tests__/LoginPage/SignIn.test.js @@ -49,8 +49,8 @@ describe('Sign in form', () => { it('should change username and password values on user input', async () => { render( {}} />); - const usernameInput = await screen.findByLabelText(/^Username/i); - const passwordInput = await screen.findByLabelText(/^Enter Password/i); + const usernameInput = await screen.findByLabelText(/^signIn.username/i); + const passwordInput = await screen.findByLabelText(/^signIn.enterPassword/i); fireEvent.change(usernameInput, { target: { value: 'test' } }); fireEvent.change(passwordInput, { target: { value: 'test' } }); expect(usernameInput).toHaveValue('test'); @@ -60,8 +60,8 @@ describe('Sign in form', () => { it('should display error if username and password values are empty after change', async () => { render( {}} />); - const usernameInput = await screen.findByLabelText(/^Username/i); - const passwordInput = await screen.findByLabelText(/^Enter Password/i); + const usernameInput = await screen.findByLabelText(/^signIn.username/i); + const passwordInput = await screen.findByLabelText(/^signIn.enterPassword/i); userEvent.click(usernameInput); userEvent.type(usernameInput, 't'); userEvent.type(usernameInput, '{backspace}'); @@ -77,13 +77,13 @@ describe('Sign in form', () => { it('should log in the user and navigate to homepage if login is successful using button', async () => { render( {}} />); - const usernameInput = await screen.findByLabelText(/^Username/i); - const passwordInput = await screen.findByLabelText(/^Enter Password/i); + const usernameInput = await screen.findByLabelText(/^signIn.username/i); + const passwordInput = await screen.findByLabelText(/^signIn.enterPassword/i); userEvent.type(usernameInput, 'test'); userEvent.type(passwordInput, 'test'); jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: {} } }); - const submitButton = await screen.findByText('Continue'); + const submitButton = await screen.findByText('signIn.continue'); fireEvent.click(submitButton); await waitFor(() => { expect(mockedUsedNavigate).toHaveBeenCalledWith('/home'); @@ -93,7 +93,7 @@ describe('Sign in form', () => { it('should display an error if username is blank and login is attempted using button', async () => { render( {}} />); - const passwordInput = await screen.findByLabelText(/^Enter Password/i); + const passwordInput = await screen.findByLabelText(/^signIn.enterPassword/i); userEvent.type(passwordInput, 'test'); const submitButton = await screen.findByTestId('basic-auth-submit-btn'); fireEvent.click(submitButton); @@ -108,7 +108,7 @@ describe('Sign in form', () => { it('should display an error if password is blank and login is attempted using button', async () => { render( {}} />); - const usernameInput = await screen.findByLabelText(/^Username/i); + const usernameInput = await screen.findByLabelText(/^signIn.username/i); userEvent.type(usernameInput, 'test'); const submitButton = await screen.findByTestId('basic-auth-submit-btn'); fireEvent.click(submitButton); @@ -136,8 +136,8 @@ describe('Sign in form', () => { it('should log in the user and navigate to homepage if login is successful using enter key on username field', async () => { render( {}} />); - const usernameInput = await screen.findByLabelText(/^Username/i); - const passwordInput = await screen.findByLabelText(/^Enter Password/i); + const usernameInput = await screen.findByLabelText(/^signIn.username/i); + const passwordInput = await screen.findByLabelText(/^signIn.enterPassword/i); userEvent.type(usernameInput, 'test'); userEvent.type(passwordInput, 'test'); @@ -151,8 +151,8 @@ describe('Sign in form', () => { it('should log in the user and navigate to homepage if login is successful using enter key on password field', async () => { render( {}} />); - const usernameInput = await screen.findByLabelText(/^Username/i); - const passwordInput = await screen.findByLabelText(/^Enter Password/i); + const usernameInput = await screen.findByLabelText(/^signIn.username/i); + const passwordInput = await screen.findByLabelText(/^signIn.enterPassword/i); userEvent.type(usernameInput, 'test'); userEvent.type(passwordInput, 'test'); @@ -166,7 +166,7 @@ describe('Sign in form', () => { it('should display an error if username is blank and login is attempted using enter key', async () => { render( {}} />); - const passwordInput = await screen.findByLabelText(/^Enter Password/i); + const passwordInput = await screen.findByLabelText(/^signIn.enterPassword/i); userEvent.type(passwordInput, 'test'); userEvent.type(passwordInput, '{enter}'); @@ -180,7 +180,7 @@ describe('Sign in form', () => { it('should display an error if password is blank and login is attempted using enter key', async () => { render( {}} />); - const usernameInput = await screen.findByLabelText(/^Username/i); + const usernameInput = await screen.findByLabelText(/^signIn.username/i); userEvent.type(usernameInput, 'test'); userEvent.type(usernameInput, '{enter}'); @@ -194,7 +194,7 @@ describe('Sign in form', () => { it('should display an error if username and password are both blank and login is attempted using enter key', async () => { render( {}} />); - const passwordInput = await screen.findByLabelText(/^Enter Password/i); + const passwordInput = await screen.findByLabelText(/^signIn.enterPassword/i); userEvent.type(passwordInput, '{enter}'); await waitFor(() => expect(screen.queryByText(/enter a username/i)).toBeInTheDocument()); @@ -207,18 +207,18 @@ describe('Sign in form', () => { it('should should display login error if login not successful', async () => { render( {}} />); - const usernameInput = await screen.findByLabelText(/^Username/i); - const passwordInput = await screen.findByLabelText(/^Enter Password/i); + const usernameInput = await screen.findByLabelText(/^signIn.username/i); + const passwordInput = await screen.findByLabelText(/^signIn.enterPassword/i); userEvent.type(usernameInput, 'test'); userEvent.type(passwordInput, 'test'); jest.spyOn(api, 'get').mockRejectedValue({ status: 401, data: {} }); - const submitButton = await screen.findByText('Continue'); + const submitButton = await screen.findByText('signIn.continue'); fireEvent.click(submitButton); await waitFor(() => { - expect(screen.queryByText(/Authentication Failed/i)).toBeInTheDocument(); + expect(screen.queryByText(/signIn.authFailed/i)).toBeInTheDocument(); }); await waitFor(() => { expect(mockedUsedNavigate).not.toHaveBeenCalled(); diff --git a/src/__tests__/RepoPage/Repo.test.js b/src/__tests__/RepoPage/Repo.test.js index 98f5b175..c5dfd6e4 100644 --- a/src/__tests__/RepoPage/Repo.test.js +++ b/src/__tests__/RepoPage/Repo.test.js @@ -270,7 +270,7 @@ describe('Repo details component', () => { jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockRepoDetailsWithMissingData } }); render(); expect(await screen.findByText('test')).toBeInTheDocument(); - expect((await screen.findAllByText(/timestamp n\/a/i)).length).toBeGreaterThan(0); + expect((await screen.findAllByText(/main.timestampNA/i)).length).toBeGreaterThan(0); }); it('renders vulnerability icons', async () => { @@ -302,13 +302,13 @@ describe('Repo details component', () => { render(); expect(await screen.findAllByTestId('verified-icon')).toHaveLength(2); - const allTrustedSignaturesIcons = await screen.findAllByTestId("verified-icon"); + const allTrustedSignaturesIcons = await screen.findAllByTestId('verified-icon'); fireEvent.mouseOver(allTrustedSignaturesIcons[0]); - expect(await screen.findByText("Tool: cosign")).toBeInTheDocument(); - expect(await screen.findByText("Signed-by: author1")).toBeInTheDocument(); + expect(await screen.findByText('signatureTooltip.tool: cosign')).toBeInTheDocument(); + expect(await screen.findByText('signatureTooltip.signedBy: author1')).toBeInTheDocument(); fireEvent.mouseOver(allTrustedSignaturesIcons[1]); - expect(await screen.findByText("Tool: notation")).toBeInTheDocument(); - expect(await screen.findByText("Signed-by: author2")).toBeInTheDocument(); + expect(await screen.findByText('signatureTooltip.tool: notation')).toBeInTheDocument(); + expect(await screen.findByText('signatureTooltip.signedBy: author2')).toBeInTheDocument(); }); it("should log error if data can't be fetched", async () => { diff --git a/src/__tests__/RepoPage/Tags.test.js b/src/__tests__/RepoPage/Tags.test.js index 4d574000..91ba8a51 100644 --- a/src/__tests__/RepoPage/Tags.test.js +++ b/src/__tests__/RepoPage/Tags.test.js @@ -74,9 +74,9 @@ describe('Tags component', () => { render(); const openBtn = screen.getAllByText(/show/i); fireEvent.click(openBtn[0]); - expect(screen.getByText(/OS\/ARCH/i)).toBeInTheDocument(); + expect(screen.getByText(/main.osOrArch/i)).toBeInTheDocument(); fireEvent.click(openBtn[0]); - await waitFor(() => expect(screen.queryByText(/OS\/ARCH/i)).not.toBeInTheDocument()); + await waitFor(() => expect(screen.queryByText(/main.osOrArch/i)).not.toBeInTheDocument()); }); // it('should see delete tag button and its dialog', async () => { @@ -115,7 +115,7 @@ describe('Tags component', () => { it('should filter tag list based on user input', async () => { render(); - const tagFilterInput = await screen.findByPlaceholderText(/Search Tags/i); + const tagFilterInput = await screen.findByPlaceholderText(/tags.searchTags/i); expect(await screen.findByText(/latest/i)).toBeInTheDocument(); expect(await screen.findByText(/bullseye/i)).toBeInTheDocument(); userEvent.type(tagFilterInput, 'bull'); @@ -125,12 +125,12 @@ describe('Tags component', () => { it('should sort tags based on the picked sort criteria', async () => { render(); - const selectFilter = await screen.findByText('Newest'); + const selectFilter = await screen.findByText('sortCriteria.newest'); expect(selectFilter).toBeInTheDocument(); userEvent.click(selectFilter); - const newOption = await screen.findByText('A - Z'); + const newOption = await screen.findByText('sortCriteria.AZ'); userEvent.click(newOption); - expect(await screen.findByText('A - Z')).toBeInTheDocument(); - expect(await screen.queryByText('Newest')).not.toBeInTheDocument(); + expect(await screen.findByText('sortCriteria.AZ')).toBeInTheDocument(); + expect(await screen.queryByText('sortCriteria.newest')).not.toBeInTheDocument(); }); }); diff --git a/src/__tests__/Shared/RepoCard.test.js b/src/__tests__/Shared/RepoCard.test.js index 2aa201e6..c18adc33 100644 --- a/src/__tests__/Shared/RepoCard.test.js +++ b/src/__tests__/Shared/RepoCard.test.js @@ -87,7 +87,7 @@ describe('Repo card component', () => { expect(cardTitle).toBeInTheDocument(); userEvent.click(cardTitle); expect(mockedUsedNavigate).toBeCalledWith(`/image/${mockImage.name}`); - expect(await screen.findByText(/timestamp n\/a/i)).toBeInTheDocument(); + expect(await screen.findByText(/main.timestampNA/i)).toBeInTheDocument(); }); it('navigates to explore page when platform chip is clicked', async () => { diff --git a/src/__tests__/Shared/SearchSuggestion.test.js b/src/__tests__/Shared/SearchSuggestion.test.js index 6456e30f..ae05f3b0 100644 --- a/src/__tests__/Shared/SearchSuggestion.test.js +++ b/src/__tests__/Shared/SearchSuggestion.test.js @@ -82,7 +82,7 @@ describe('Search component', () => { it('should display suggestions when user searches', async () => { jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } }); render(); - const searchInput = screen.getByPlaceholderText(/search for content/i); + const searchInput = screen.getByPlaceholderText(/searchSuggestion.search/i); expect(searchInput).toBeInTheDocument(); userEvent.type(searchInput, 'test'); expect(await screen.findByText(/alpine/i)).toBeInTheDocument(); @@ -91,7 +91,7 @@ describe('Search component', () => { it('should navigate to repo page when a repo suggestion is clicked', async () => { jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } }); render(); - const searchInput = screen.getByPlaceholderText(/search for content/i); + const searchInput = screen.getByPlaceholderText(/searchSuggestion.search/i); userEvent.type(searchInput, 'test'); const suggestionItemRepo = await screen.findByText(/alpine/i); userEvent.click(suggestionItemRepo); @@ -101,7 +101,7 @@ describe('Search component', () => { it('should navigate to repo page when a image suggestion is clicked', async () => { jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } }); render(); - const searchInput = screen.getByPlaceholderText(/search for content/i); + const searchInput = screen.getByPlaceholderText(/searchSuggestion.search/i); userEvent.type(searchInput, 'debian:test'); const suggestionItemImage = await screen.findByText(/debian:testTag/i); userEvent.click(suggestionItemImage); @@ -112,7 +112,7 @@ describe('Search component', () => { jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: {} }); const error = jest.spyOn(console, 'error').mockImplementation(() => {}); render(); - const searchInput = screen.getByPlaceholderText(/search for content/i); + const searchInput = screen.getByPlaceholderText(/searchSuggestion.search/i); userEvent.type(searchInput, 'debian'); await waitFor(() => expect(error).toBeCalledTimes(1)); }); @@ -121,7 +121,7 @@ describe('Search component', () => { jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: {} }); const error = jest.spyOn(console, 'error').mockImplementation(() => {}); render(); - const searchInput = screen.getByPlaceholderText(/search for content/i); + const searchInput = screen.getByPlaceholderText(/searchSuggestion.search/i); userEvent.type(searchInput, 'debian:test'); await waitFor(() => expect(error).toBeCalledTimes(1)); }); diff --git a/src/__tests__/TagPage/DependsOn.test.js b/src/__tests__/TagPage/DependsOn.test.js index f510609a..cf668327 100644 --- a/src/__tests__/TagPage/DependsOn.test.js +++ b/src/__tests__/TagPage/DependsOn.test.js @@ -90,7 +90,7 @@ describe('Dependencies tab', () => { it('should render the dependencies if there are any', async () => { jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: mockDependenciesList }); render(); - expect(await screen.findAllByText(/Tag/i)).toHaveLength(8); + expect(await screen.findAllByText(/Tag/i)).toHaveLength(16); }); it('renders no dependencies if there are not any', async () => { @@ -99,7 +99,7 @@ describe('Dependencies tab', () => { data: { data: { BaseImageList: { Results: [], Page: {} } } } }); render(); - expect(await screen.findByText(/Nothing found/i)).toBeInTheDocument(); + expect(await screen.findByText(/main.nothingFound/i)).toBeInTheDocument(); }); it("should log an error when data can't be fetched", async () => { @@ -113,6 +113,6 @@ describe('Dependencies tab', () => { jest.spyOn(api, 'get').mockResolvedValue({ status: 500, data: { errors: ['test error'] } }); jest.spyOn(console, 'error').mockImplementation(() => {}); render(); - expect(await screen.findByText(/Nothing found/i)).toBeInTheDocument(); + expect(await screen.findByText(/main.nothingFound/i)).toBeInTheDocument(); }); }); diff --git a/src/__tests__/TagPage/HistoryLayers.test.js b/src/__tests__/TagPage/HistoryLayers.test.js index 8baea02e..e44b5c9c 100644 --- a/src/__tests__/TagPage/HistoryLayers.test.js +++ b/src/__tests__/TagPage/HistoryLayers.test.js @@ -38,7 +38,7 @@ describe('Layers page', () => { it('renders no layers if there are not any', async () => { render(); - await waitFor(() => expect(screen.getAllByText(/No Layer data available/i)).toHaveLength(1)); + await waitFor(() => expect(screen.getAllByText(/historyLayers.noLayers/i)).toHaveLength(1)); }); it('opens dropdown and renders layer command and digest', async () => { diff --git a/src/__tests__/TagPage/IsDependentOn.test.js b/src/__tests__/TagPage/IsDependentOn.test.js index 6131579a..47397775 100644 --- a/src/__tests__/TagPage/IsDependentOn.test.js +++ b/src/__tests__/TagPage/IsDependentOn.test.js @@ -90,7 +90,7 @@ describe('Dependents tab', () => { it('should render the dependents if there are any', async () => { jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: mockDependentsList }); render(); - expect(await screen.findAllByText(/tag/i)).toHaveLength(8); + expect(await screen.findAllByText(/tag/i)).toHaveLength(16); }); it('renders no dependents if there are not any', async () => { @@ -99,7 +99,7 @@ describe('Dependents tab', () => { data: { data: { DerivedImageList: { Results: [], Page: {} } } } }); render(); - expect(await screen.findByText(/Nothing found/i)).toBeInTheDocument(); + expect(await screen.findByText(/main.nothingFound/i)).toBeInTheDocument(); }); it("should log an error when data can't be fetched", async () => { @@ -113,6 +113,6 @@ describe('Dependents tab', () => { jest.spyOn(api, 'get').mockResolvedValue({ status: 500, data: { errors: ['test error'] } }); jest.spyOn(console, 'error').mockImplementation(() => {}); render(); - expect(await screen.findByText(/Nothing found/i)).toBeInTheDocument(); + expect(await screen.findByText(/main.nothingFound/i)).toBeInTheDocument(); }); }); diff --git a/src/__tests__/TagPage/ReferredBy.test.js b/src/__tests__/TagPage/ReferredBy.test.js index 1d7a76c4..fe50e94c 100644 --- a/src/__tests__/TagPage/ReferredBy.test.js +++ b/src/__tests__/TagPage/ReferredBy.test.js @@ -53,12 +53,12 @@ afterEach(() => { describe('Referred by tab', () => { it('should render referrers if there are any', async () => { render(); - expect(await screen.findAllByText('Media type: application/vnd.oci.artifact.manifest.v1+json')).toHaveLength(2); + expect(await screen.findAllByText('referrerCard.mediaType application/vnd.oci.artifact.manifest.v1+json')).toHaveLength(2); }); it("renders no referrers if there aren't any", async () => { render(); - expect(await screen.findByText(/Nothing found/i)).toBeInTheDocument(); + expect(await screen.findByText(/main.nothingFound/i)).toBeInTheDocument(); }); it('should display the digest when clicking the dropdowns', async () => { diff --git a/src/__tests__/TagPage/TagDetails.test.js b/src/__tests__/TagPage/TagDetails.test.js index 9de14cf6..1b30507c 100644 --- a/src/__tests__/TagPage/TagDetails.test.js +++ b/src/__tests__/TagPage/TagDetails.test.js @@ -895,14 +895,14 @@ describe('Tags details', () => { fireEvent.click(dependenciesTab); expect(await screen.findByTestId('depends-on-container')).toBeInTheDocument(); jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: mockDependentsList }); - const dependentsTab = await screen.findByText(/used by/i); + const dependentsTab = await screen.findByText(/main.usedBy/i); fireEvent.click(dependentsTab); expect(await screen.findByTestId('dependents-container')).toBeInTheDocument(); jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: mockCVEList }); - const vulnerabilityTab = await screen.findByText(/vulnerabilities/i); + const vulnerabilityTab = await screen.findByText(/main.vulnerabilities/i); fireEvent.click(vulnerabilityTab); expect(await screen.findByTestId('vulnerability-container')).toBeInTheDocument(); - const referrersTab = await screen.findByText(/referred by/i); + const referrersTab = await screen.findByText(/main.referredBy/i); fireEvent.click(referrersTab); jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: [] }); expect(await screen.findByTestId('referred-by-container')).toBeInTheDocument(); @@ -983,17 +983,17 @@ describe('Tags details', () => { const allTrustedSignaturesIcons = await screen.findAllByTestId('verified-icon'); fireEvent.mouseOver(allTrustedSignaturesIcons[0]); - expect(await screen.findByText('Tool: cosign')).toBeInTheDocument(); - expect(await screen.findByText('Signed-by: author1')).toBeInTheDocument(); + expect(await screen.findByText('signatureTooltip.tool: cosign')).toBeInTheDocument(); + expect(await screen.findByText('signatureTooltip.signedBy: author1')).toBeInTheDocument(); fireEvent.mouseOver(allTrustedSignaturesIcons[1]); - expect(await screen.findByText('Tool: notation')).toBeInTheDocument(); - expect(await screen.findByText('Signed-by: author2')).toBeInTheDocument(); + expect(await screen.findByText('signatureTooltip.tool: notation')).toBeInTheDocument(); + expect(await screen.findByText('signatureTooltip.signedBy: author2')).toBeInTheDocument(); }); it('should copy the docker pull string to clipboard', async () => { jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } }); render(); - const dropdown = await screen.findByText(`Pull ${mockImage.Image.RepoName}:${mockImage.Image.Tag}`); + const dropdown = await screen.findByText(`pullCommandButton.pull ${mockImage.Image.RepoName}:${mockImage.Image.Tag}`); expect(dropdown).toBeInTheDocument(); userEvent.click(dropdown); await waitFor(() => expect(screen.queryAllByTestId('pull-menuItem')).toHaveLength(1)); @@ -1009,7 +1009,7 @@ describe('Tags details', () => { it('should copy the podman pull string to clipboard', async () => { jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } }); render(); - const dropdown = await screen.findByText(`Pull ${mockImage.Image.RepoName}:${mockImage.Image.Tag}`); + const dropdown = await screen.findByText(`pullCommandButton.pull ${mockImage.Image.RepoName}:${mockImage.Image.Tag}`); expect(dropdown).toBeInTheDocument(); userEvent.click(dropdown); await waitFor(() => expect(screen.queryAllByTestId('pull-menuItem')).toHaveLength(1)); @@ -1026,7 +1026,7 @@ describe('Tags details', () => { it('should copy the skopeo copy string to clipboard', async () => { jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } }); render(); - const dropdown = await screen.findByText(`Pull ${mockImage.Image.RepoName}:${mockImage.Image.Tag}`); + const dropdown = await screen.findByText(`pullCommandButton.pull ${mockImage.Image.RepoName}:${mockImage.Image.Tag}`); expect(dropdown).toBeInTheDocument(); userEvent.click(dropdown); await waitFor(() => expect(screen.queryAllByTestId('pull-menuItem')).toHaveLength(1)); @@ -1043,7 +1043,7 @@ describe('Tags details', () => { it('should show pull tabs in dropdown and allow nagivation between them', async () => { jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } }); render(); - const dropdown = await screen.findByText(`Pull ${mockImage.Image.RepoName}:${mockImage.Image.Tag}`); + const dropdown = await screen.findByText(`pullCommandButton.pull ${mockImage.Image.RepoName}:${mockImage.Image.Tag}`); expect(dropdown).toBeInTheDocument(); userEvent.click(dropdown); await waitFor(() => expect(screen.queryAllByTestId('pull-menuItem')).toHaveLength(1)); @@ -1056,7 +1056,7 @@ describe('Tags details', () => { it('should show the copied successfully button for 3 seconds', async () => { jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } }); render(); - const dropdown = await screen.findByText(`Pull ${mockImage.Image.RepoName}:${mockImage.Image.Tag}`); + const dropdown = await screen.findByText(`pullCommandButton.pull ${mockImage.Image.RepoName}:${mockImage.Image.Tag}`); expect(dropdown).toBeInTheDocument(); await userEvent.click(dropdown); await waitFor(() => expect(screen.queryAllByTestId('pull-dropdown')).toHaveLength(1)); diff --git a/src/__tests__/TagPage/VulnerabilitiesDetails.test.js b/src/__tests__/TagPage/VulnerabilitiesDetails.test.js index 8c4cab67..b7bf8b08 100644 --- a/src/__tests__/TagPage/VulnerabilitiesDetails.test.js +++ b/src/__tests__/TagPage/VulnerabilitiesDetails.test.js @@ -641,46 +641,46 @@ describe('Vulnerabilties page', () => { it('renders the vulnerabilities if there are any', async () => { jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } }); render(); - await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); - await waitFor(() => expect(screen.getAllByText('Total 5')).toHaveLength(1)); + await waitFor(() => expect(screen.getAllByText('main.vulnerabilities')).toHaveLength(1)); + await waitFor(() => expect(screen.getAllByText('vulnerabilityCountCard.total 5')).toHaveLength(1)); await waitFor(() => expect(screen.getAllByText(/CVE/)).toHaveLength(20)); }); it('renders the vulnerabilities by severity', async () => { jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }); render(); - await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); - await waitFor(() => expect(screen.getAllByText('Total 5')).toHaveLength(1)); + await waitFor(() => expect(screen.getAllByText('main.vulnerabilities')).toHaveLength(1)); + await waitFor(() => expect(screen.getAllByText('vulnerabilityCountCard.total 5')).toHaveLength(1)); await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(20)); - expect(screen.getByLabelText('Medium')).toBeInTheDocument(); - const mediumSeverity = await screen.getByLabelText('Medium'); + expect(screen.getByLabelText('vulnerabilityCountCard.medium')).toBeInTheDocument(); + const mediumSeverity = await screen.getByLabelText('vulnerabilityCountCard.medium'); jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('MEDIUM') } }); fireEvent.click(mediumSeverity); await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(6)); - expect(screen.getByLabelText('High')).toBeInTheDocument(); - const highSeverity = await screen.getByLabelText('High'); + expect(screen.getByLabelText('vulnerabilityCountCard.high')).toBeInTheDocument(); + const highSeverity = await screen.getByLabelText('vulnerabilityCountCard.high'); jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('HIGH') } }); fireEvent.click(highSeverity); await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(1)); - expect(screen.getByLabelText('Critical')).toBeInTheDocument(); - const criticalSeverity = await screen.getByLabelText('Critical'); + expect(screen.getByLabelText('vulnerabilityCountCard.critical')).toBeInTheDocument(); + const criticalSeverity = await screen.getByLabelText('vulnerabilityCountCard.critical'); jest .spyOn(api, 'get') .mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('CRITICAL') } }); fireEvent.click(criticalSeverity); await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(1)); - expect(screen.getByLabelText('Low')).toBeInTheDocument(); - const lowSeverity = await screen.getByLabelText('Low'); + expect(screen.getByLabelText('vulnerabilityCountCard.low')).toBeInTheDocument(); + const lowSeverity = await screen.getByLabelText('vulnerabilityCountCard.low'); jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('LOW') } }); fireEvent.click(lowSeverity); await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(10)); - expect(screen.getByLabelText('Unknown')).toBeInTheDocument(); - const unknownSeverity = await screen.getByLabelText('Unknown'); + expect(screen.getByLabelText('main.unknown')).toBeInTheDocument(); + const unknownSeverity = await screen.getByLabelText('main.unknown'); jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('UNKNOWN') } }); fireEvent.click(unknownSeverity); await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(1)); - expect(screen.getByText('Total 5')).toBeInTheDocument(); - const totalSeverity = await screen.getByText('Total 5'); + expect(screen.getByText('vulnerabilityCountCard.total 5')).toBeInTheDocument(); + const totalSeverity = await screen.getByText('vulnerabilityCountCard.total 5'); jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('') } }); fireEvent.click(totalSeverity); await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(20)); @@ -707,8 +707,8 @@ describe('Vulnerabilties page', () => { const cveSearchInput = screen.getByPlaceholderText(/search/i); const expandSearch = cveSearchInput.parentElement.parentElement.parentElement.parentElement.childNodes[0]; await fireEvent.click(expandSearch); - await waitFor(() => expect(screen.getAllByPlaceholderText('Exclude')).toHaveLength(1)); - const excludeInput = screen.getByPlaceholderText('Exclude'); + await waitFor(() => expect(screen.getAllByPlaceholderText('VulnerabilitiesDetails.exclude')).toHaveLength(1)); + const excludeInput = screen.getByPlaceholderText('VulnerabilitiesDetails.exclude'); userEvent.type(excludeInput, '2022'); expect(excludeInput).toHaveValue('2022'); await waitFor(() => expect(screen.queryAllByText(/2022/i)).toHaveLength(0)); @@ -721,7 +721,7 @@ describe('Vulnerabilties page', () => { data: { data: { CVEListForImage: { Tag: '', Page: {}, CVEList: [], Summary: {} } } } }); render(); - await waitFor(() => expect(screen.getAllByText('No Vulnerabilities')).toHaveLength(1)); + await waitFor(() => expect(screen.getAllByText(/VulnerabilitiesDetails.noVulnerabilities/i)).toHaveLength(1)); }); it('should show description for vulnerabilities', async () => { @@ -732,7 +732,7 @@ describe('Vulnerabilties page', () => { render(); const expandListBtn = await screen.findAllByTestId('ViewAgendaIcon'); fireEvent.click(expandListBtn[0]); - await waitFor(() => expect(screen.getAllByText(/Description/)).toHaveLength(20)); + await waitFor(() => expect(screen.getAllByText(/main.description/)).toHaveLength(20)); await waitFor(() => expect(screen.getAllByText(/CPAN 2.28 allows Signature Verification Bypass./i)).toHaveLength(1) ); @@ -752,12 +752,12 @@ describe('Vulnerabilties page', () => { .mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageOne } }) .mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageTwo } }); render(); - await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); + await waitFor(() => expect(screen.getAllByText('main.vulnerabilities')).toHaveLength(1)); const expandListBtn = await screen.findAllByTestId('KeyboardArrowRightIcon'); fireEvent.click(expandListBtn[1]); await waitFor(() => expect(screen.getByText('1.0.16')).toBeInTheDocument()); - await waitFor(() => expect(screen.getAllByText(/Load more/).length).toBe(1)); - const loadMoreBtn = screen.getAllByText(/Load more/)[0]; + await waitFor(() => expect(screen.getAllByText(/vulnerabilityCard.loadMore/).length).toBe(1)); + const loadMoreBtn = screen.getAllByText(/vulnerabilityCard.loadMore/)[0]; await fireEvent.click(loadMoreBtn); await waitFor(() => expect(loadMoreBtn).not.toBeInTheDocument()); expect(await screen.findByText('latest')).toBeInTheDocument(); @@ -813,7 +813,7 @@ describe('Vulnerabilties page', () => { jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } }); render(); - await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); + await waitFor(() => expect(screen.getAllByText('main.vulnerabilities')).toHaveLength(1)); const downloadBtn = await screen.findAllByTestId('DownloadIcon'); fireEvent.click(downloadBtn[0]); expect(await screen.findByTestId('export-csv-menuItem')).toBeInTheDocument(); @@ -840,7 +840,7 @@ describe('Vulnerabilties page', () => { .mockRejectedValue({ status: 500, data: {} }); const error = jest.spyOn(console, 'error').mockImplementation(() => {}); render(); - await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); + await waitFor(() => expect(screen.getAllByText('main.vulnerabilities')).toHaveLength(1)); const downloadBtn = await screen.findAllByTestId('DownloadIcon'); fireEvent.click(downloadBtn[0]); expect(await screen.findByTestId('export-csv-menuItem')).toBeInTheDocument(); @@ -851,14 +851,14 @@ describe('Vulnerabilties page', () => { it('should expand/collapse the list of CVEs', async () => { jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }); render(); - await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); + await waitFor(() => expect(screen.getAllByText('main.vulnerabilities')).toHaveLength(1)); jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEFixed.pageOne } }); const expandListBtn = await screen.findAllByTestId('ViewAgendaIcon'); fireEvent.click(expandListBtn[0]); - await waitFor(() => expect(screen.getAllByText('Fixed in')).toHaveLength(20)); + await waitFor(() => expect(screen.getAllByText('vulnerabilityCard.fixedIn')).toHaveLength(20)); const collapseListBtn = await screen.findAllByTestId('ViewHeadlineIcon'); fireEvent.click(collapseListBtn[0]); - expect(await screen.findByText('Fixed in')).not.toBeVisible(); + expect(await screen.findByText('vulnerabilityCard.fixedIn')).not.toBeVisible(); }); it('should handle fixed CVE query errors', async () => { @@ -867,11 +867,11 @@ describe('Vulnerabilties page', () => { .mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }) .mockRejectedValue({ status: 500, data: {} }); render(); - await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); + await waitFor(() => expect(screen.getAllByText('main.vulnerabilities')).toHaveLength(1)); const error = jest.spyOn(console, 'error').mockImplementation(() => {}); const expandListBtn = await screen.findAllByTestId('KeyboardArrowRightIcon'); fireEvent.click(expandListBtn[1]); - await waitFor(() => expect(screen.getByText(/not fixed/i)).toBeInTheDocument()); + await waitFor(() => expect(screen.getByText(/vulnerabilityCard.notFixed/i)).toBeInTheDocument()); await waitFor(() => expect(error).toBeCalledTimes(1)); }); }); 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..d55f0442 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,27 @@ 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..8881fe91 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,16 +71,18 @@ function FilterCard(props) { return filterValue[filter.value] || false; }; + const { t } = useTranslation(); + const getFilterRows = () => { const filterRows = filters; return filterRows.map((filter, index) => { return ( - + } - 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 c9c8c373..6388338f 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/vulnerabilityAndSignatureCheck.jsx b/src/utilities/vulnerabilityAndSignatureCheck.jsx index dff55ef0..c66b594e 100644 --- a/src/utilities/vulnerabilityAndSignatureCheck.jsx +++ b/src/utilities/vulnerabilityAndSignatureCheck.jsx @@ -1,5 +1,6 @@ import { isEmpty } from 'lodash'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { NoneVulnerabilityIcon, LowVulnerabilityIcon, @@ -35,13 +36,37 @@ const getAllAuthorsOfSignatures = (signatureInfo) => { const VulnerabilityIconCheck = ({ vulnerabilitySeverity }) => { let result; + + const { t } = useTranslation(); + let vulnerabilityStringTitle = ''; if (vulnerabilitySeverity) { vulnerabilityStringTitle = vulnerabilitySeverity.charAt(0) + vulnerabilitySeverity.substring(1).toLowerCase(); + switch (vulnerabilityStringTitle) { + case 'None': + vulnerabilityStringTitle = 'vulnerabilityCountCard.no'; + break; + case 'Low': + vulnerabilityStringTitle = 'vulnerabilityCountCard.low'; + break; + case 'Medium': + vulnerabilityStringTitle = 'vulnerabilityCountCard.medium'; + break; + case 'High': + vulnerabilityStringTitle = 'vulnerabilityCountCard.high'; + break; + case 'Critical': + vulnerabilityStringTitle = 'vulnerabilityCountCard.critical'; + break; + case 'Unknown': + vulnerabilityStringTitle = 'vulnerabilityCountCard.unknown'; + break; + } + vulnerabilityStringTitle = t(vulnerabilityStringTitle); } switch (vulnerabilitySeverity) { case 'NONE': - result = ; + result = ; break; case 'LOW': result = ; 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 ( }