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 (
);
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() && (
)}
{isApiKeyEnabled() && }
-
+
>
);
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() {
- 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 (
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()}
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) && (
@@ -328,19 +331,19 @@ function TagDetails() {
disabled={isLoading}
>
- Layers
+ {t('main.layers')}
- Uses
+ {t('main.uses')}
- Used by
+ {t('main.usedBy')}
- Vulnerabilities
+ {t('main.vulnerabilities')}
- Referred by
+ {t('main.referredBy')}
diff --git a/src/components/Tag/TagDetailsMetadata.jsx b/src/components/Tag/TagDetailsMetadata.jsx
index 3da99cd3..9378182d 100644
--- a/src/components/Tag/TagDetailsMetadata.jsx
+++ b/src/components/Tag/TagDetailsMetadata.jsx
@@ -1,4 +1,5 @@
-import React from 'react';
+import React, { useState } from 'react';
+import { useTranslation } from 'react-i18next';
import transform from '../../utilities/transform';
import { DateTime } from 'luxon';
@@ -54,9 +55,14 @@ function TagDetailsMetadata(props) {
const classes = useStyles();
const { platform, lastUpdated, size, license, imageName } = 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 (
@@ -71,7 +77,7 @@ function TagDetailsMetadata(props) {
- OS/Arch
+ {t('main.osOrArch')}
{platform?.Os || `----`} / {platform?.Arch || `----`}
@@ -83,7 +89,7 @@ function TagDetailsMetadata(props) {
- Total Size
+ {t('main.totalSize')}
{transform.formatBytes(size) || `----`}
@@ -96,7 +102,7 @@ function TagDetailsMetadata(props) {
- Last Published
+ {t('tagDetailsMetadata.lastPublished')}
@@ -112,11 +118,11 @@ function TagDetailsMetadata(props) {
- License
+ {t('main.license')}
- {license ? {license} : `License info not available`}
+ {license ? {license} : `${t('main.licenseNA')}`}
diff --git a/src/components/User/ApiKeys/ApiKeyCard.jsx b/src/components/User/ApiKeys/ApiKeyCard.jsx
index 02b663bf..ea142a32 100644
--- a/src/components/User/ApiKeys/ApiKeyCard.jsx
+++ b/src/components/User/ApiKeys/ApiKeyCard.jsx
@@ -1,4 +1,5 @@
import React, { useState } from 'react';
+import { useTranslation } from 'react-i18next';
import { DateTime } from 'luxon';
import { isNil } from 'lodash';
@@ -105,6 +106,8 @@ function ApiKeyCard(props) {
setApiKeyRevokeOpen(true);
};
+ const { t } = useTranslation();
+
return (
@@ -121,7 +124,7 @@ function ApiKeyCard(props) {
{!isNil(apiKey.apiKey) && (
@@ -136,7 +139,7 @@ function ApiKeyCard(props) {
) : (
)}
- KEY
+ {t('apiKeyCard.key')}
diff --git a/src/components/User/ApiKeys/ApiKeyConfirmDialog.jsx b/src/components/User/ApiKeys/ApiKeyConfirmDialog.jsx
index ec51930e..7678168a 100644
--- a/src/components/User/ApiKeys/ApiKeyConfirmDialog.jsx
+++ b/src/components/User/ApiKeys/ApiKeyConfirmDialog.jsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { useTranslation } from 'react-i18next';
import { Dialog, DialogContent, DialogTitle, DialogActions, Button, Typography, Grid } from '@mui/material';
@@ -30,13 +31,17 @@ function ApiKeyConfirmDialog(props) {
setOpen(false);
};
+ const { t } = useTranslation();
+
return (
diff --git a/src/components/User/ApiKeys/ApiKeyDialog.jsx b/src/components/User/ApiKeys/ApiKeyDialog.jsx
index f6fc1a06..48f4a89a 100644
--- a/src/components/User/ApiKeys/ApiKeyDialog.jsx
+++ b/src/components/User/ApiKeys/ApiKeyDialog.jsx
@@ -1,4 +1,5 @@
import React, { useState } from 'react';
+import { useTranslation } from 'react-i18next';
import { isNil, isNumber } from 'lodash';
import { DateTime } from 'luxon';
@@ -102,9 +103,11 @@ function ApiKeyDialog(props) {
return `Expires on ${expDateTime.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY)}`;
};
+ const { t } = useTranslation();
+
return (
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 (
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 (
}