diff --git a/package-lock.json b/package-lock.json
index 31e852ab..8fb247e5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,11 +21,15 @@
"axios": "^1.7.4",
"downshift": "^6.1.12",
"export-from-json": "^1.7.3",
+ "i18next": "^23.16.4",
+ "i18next-browser-languagedetector": "^8.0.0",
+ "i18next-http-backend": "^2.6.2",
"lodash": "^4.17.21",
"luxon": "^3.4.4",
"markdown-to-jsx": "^7.5.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
+ "react-i18next": "^15.1.0",
"react-router-dom": "^6.27.0",
"react-sticky-el": "^2.0.9",
"web-vitals": "^2.1.3",
@@ -7266,6 +7270,15 @@
"node": ">=0.8"
}
},
+ "node_modules/cross-fetch": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
+ "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
+ "license": "MIT",
+ "dependencies": {
+ "node-fetch": "^2.6.12"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -10207,6 +10220,15 @@
"node": ">=12"
}
},
+ "node_modules/html-parse-stringify": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
+ "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
+ "license": "MIT",
+ "dependencies": {
+ "void-elements": "3.1.0"
+ }
+ },
"node_modules/html-webpack-plugin": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.3.tgz",
@@ -10356,6 +10378,47 @@
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
"integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ=="
},
+ "node_modules/i18next": {
+ "version": "23.16.4",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.4.tgz",
+ "integrity": "sha512-9NIYBVy9cs4wIqzurf7nLXPyf3R78xYbxExVqHLK9od3038rjpyOEzW+XB130kZ1N4PZ9inTtJ471CRJ4Ituyg==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://locize.com"
+ },
+ {
+ "type": "individual",
+ "url": "https://locize.com/i18next.html"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.23.2"
+ }
+ },
+ "node_modules/i18next-browser-languagedetector": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz",
+ "integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.23.2"
+ }
+ },
+ "node_modules/i18next-http-backend": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.6.2.tgz",
+ "integrity": "sha512-Hp/kd8/VuoxIHmxsknJXjkTYYHzivAyAF15pzliKzk2TiXC25rZCEerb1pUFoxz4IVrG3fCvQSY51/Lu4ECV4A==",
+ "license": "MIT",
+ "dependencies": {
+ "cross-fetch": "4.0.0"
+ }
+ },
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -13741,6 +13804,48 @@
"tslib": "^2.0.3"
}
},
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/node-fetch/node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
+ "node_modules/node-fetch/node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/node-fetch/node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
"node_modules/node-forge": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
@@ -16088,6 +16193,28 @@
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==",
"dev": true
},
+ "node_modules/react-i18next": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.1.0.tgz",
+ "integrity": "sha512-zj3nJynMnZsy2gPZiOTC7XctCY5eQGqT3tcKMmfJWC9FMvgd+960w/adq61j8iPzpwmsXejqID9qC3Mqu1Xu2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.25.0",
+ "html-parse-stringify": "^3.0.1"
+ },
+ "peerDependencies": {
+ "i18next": ">= 23.2.3",
+ "react": ">= 16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -18509,6 +18636,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/void-elements": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
+ "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/w3c-hr-time": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
diff --git a/package.json b/package.json
index 59c91cb5..7537a592 100644
--- a/package.json
+++ b/package.json
@@ -16,12 +16,16 @@
"axios": "^1.7.4",
"downshift": "^6.1.12",
"export-from-json": "^1.7.3",
+ "i18next": "^23.16.2",
+ "i18next-browser-languagedetector": "^8.0.0",
+ "i18next-http-backend": "^2.6.2",
"lodash": "^4.17.21",
"luxon": "^3.4.4",
"markdown-to-jsx": "^7.5.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^6.27.0",
+ "react-i18next": "^15.1.0",
"react-sticky-el": "^2.0.9",
"web-vitals": "^2.1.3",
"xlsx": "^0.18.5"
diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json
new file mode 100644
index 00000000..c30f360c
--- /dev/null
+++ b/public/locales/en/translation.json
@@ -0,0 +1,227 @@
+{
+ "main": {
+ "cancel": "Cancel",
+ "close": "Close",
+ "created": "Created",
+ "days": "days",
+ "description": "Description",
+ "descriptionNA": "Description not available",
+ "digest": "DIGEST",
+ "hours": "hours",
+ "layers": "Layers",
+ "license": "License",
+ "licenseNA": "License info not available",
+ "loading": "Loading...",
+ "minutes": "minutes",
+ "NA": "not available",
+ "nothingFound": "Nothing found",
+ "osOrArch": "OS/Arch",
+ "published": "published",
+ "referredBy": "Referred By",
+ "revoke": "Revoke",
+ "signIn": "Sign in",
+ "sort": "Sort",
+ "stars": "Stars",
+ "timestampNA": "Timestamp N/A",
+ "totalSize": "Total size",
+ "unknown": "Unknown",
+ "usedBy": "Used by",
+ "uses": "Uses",
+ "vendorNA": "Vendor not available",
+ "vulnerabilities": "Vulnerabilities",
+ "weeks": "weeks"
+ },
+ "explore": {
+ "OS": "Operating system",
+ "architectures": "Architectures",
+ "additionalFilters": "Additional filters",
+ "showing": "Showing",
+ "resultsOutOf": "results out of",
+ "filterResults": "Filter results",
+ "noResults": "Looks like we don't have anything matching that search. Try searching something else."
+ },
+ "filterDialog": {
+ "filter": "Filter",
+ "sortResults": "Sort results",
+ "confirm": "Confirm"
+ },
+ "header": {
+ "product": "Product",
+ "docs": "Docs"
+ },
+ "exploreHeader": {
+ "home": "Home"
+ },
+ "searchSuggestion": {
+ "search": "Search for content...",
+ "advancedSearch": "Press Enter for advanced search",
+ "useAposChar": "Use the ':' character to search for tags"
+ },
+ "userAccountMenu": {
+ "APIKeys": "API Keys",
+ "logOut": "Log out"
+ },
+ "home": {
+ "noImages": "No images",
+ "viewAll": "View all",
+ "mostPopularImages": "Most popular images",
+ "recentlyUpdatedImages": "Recently updated images",
+ "bookmarks": "Bookmarks"
+ },
+ "signIn": {
+ "welcomeBack": "Welcome back! Please login.",
+ "or": "or",
+ "username": "Username",
+ "enterPassword": "Enter password",
+ "authFailed": "Authentication Failed. Please try again.",
+ "continue": "Continue",
+ "continueAsGuest": "Continue as guest"
+ },
+ "signInPresentation": {
+ "description": "OCI-native container image registry, simplified"
+ },
+ "thirdPartyLoginComponents": {
+ "continueWith": "Continue with",
+ "signInWith": "Sign in with"
+ },
+ "tags": {
+ "tagsHistory": "Tags History",
+ "searchTags": "Search tags..."
+ },
+ "repoDetails": {
+ "titleNA": "Title not available"
+ },
+ "repoDetailsMetadata": {
+ "repository": "Repository",
+ "totalDownloads": "Total downloads",
+ "lastPublish": "Last publish"
+ },
+ "deleteTag": {
+ "deleteImage": "Permanently delete image"
+ },
+ "deleteTagConfirmDialog": {
+ "delete": "Delete"
+ },
+ "filterCard": {
+ "filterTitle": "Filter Title"
+ },
+ "layerCard": {
+ "details": "DETAILS",
+ "command": "Command"
+ },
+ "noData": {
+ "noData": "No Data"
+ },
+ "pullCommandButton": {
+ "copiedPullCommand": "Copied Pull Command",
+ "pull": "Pull"
+ },
+ "referrerCard": {
+ "type": "Type:",
+ "mediaType": "Media type:",
+ "size": "Size:",
+ "annotations": "ANNOTATIONS"
+ },
+ "repoCard": {
+ "downloads": "Downloads"
+ },
+ "signatureTooltip": {
+ "notSigned": "Not signed",
+ "tool": "Tool",
+ "signedBy": "Signed-by"
+ },
+ "tagCard": {
+ "tag": "Tag",
+ "by": "by",
+ "showMore": "Show more",
+ "showLess": "Show less",
+ "compressedSize": "COMPRESSED SIZE"
+ },
+ "vulnerabilityCard": {
+ "notFixed": "Not fixed",
+ "loadMore": "Load more",
+ "externalReference": "External reference",
+ "packages": "Packages",
+ "fixedIn": "Fixed in"
+ },
+ "vulnerabilityCountCard": {
+ "total": "Total",
+ "critical": "Critical",
+ "criticalShort": "C",
+ "high": "High",
+ "highShort": "H",
+ "medium": "Medium",
+ "mediumShort": "M",
+ "low": "Low",
+ "lowShort": "L",
+ "unknown": "Unknown",
+ "unknownShort": "U"
+ },
+ "vulnerabilityPackageSection": {
+ "packagePath": "Package Path",
+ "installedVersion": "Installed Version",
+ "fixedVersion": "Fixed Version"
+ },
+ "dependsOn": {},
+ "historyLayers": {
+ "noLayers": "No Layer data available"
+ },
+ "IsDependentOn": {},
+ "ReferredBy": {},
+ "VulnerabilitiesDetails": {
+ "noVulnerabilities": "No Vulnerabilities",
+ "gettingYourDataReady": "Getting your data ready for export",
+ "collapseListView": "Collapse list view",
+ "expandListView": "Expand list view",
+ "search": "Search",
+ "exclude": "Exclude"
+ },
+ "TagDetails": {
+ "digest": "Digest"
+ },
+ "tagDetailsMetadata": {
+ "lastPublished": "Last Published"
+ },
+ "apiKeyCard": {
+ "key": "KEY"
+ },
+ "apiKeyConfirmDialog": {
+ "apiKey": "Api Key",
+ "plsCopy": "Please copy the api key, you will not be able to see it once the page is refreshed"
+ },
+ "apiKeyDialog": {
+ "createApiKey": "Create Api Key",
+ "expDate": "Expiration date",
+ "expTime": "Expiration time",
+ "custom": "custom",
+ "create": "Create"
+ },
+ "apiKeyRevokeDialog": {
+ "key": "key",
+ "areYouSure": "Are you sure you want to revoke this api key?"
+ },
+ "apiKeys": {
+ "manageYourKeys": "Manage your API Keys",
+ "createNewKey": "Create new API key"
+ },
+ "sortCriteria": {
+ "relevance": "Relevance",
+ "recent": "Recent",
+ "alphabetical": "Alphabetical",
+ "alphabeticalDesc": "Alphabetical desc",
+ "mostStarred": "Most starred",
+ "mostDownloaded": "Most downloaded",
+ "newest": "Newest",
+ "oldest": "Oldest",
+ "AZ": "A - Z",
+ "ZA": "Z - A"
+ },
+ "filterConstants": {
+ "signedImages": "Signed Images",
+ "bookmarks": "Bookmarks",
+ "starredRepositories": "Starred Repositories"
+ },
+ "vulnerabilityAndSignatureComponents": {
+ "failed2scan": "Failed to scan"
+ }
+}
diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json
new file mode 100644
index 00000000..a0e4c235
--- /dev/null
+++ b/public/locales/ru/translation.json
@@ -0,0 +1,227 @@
+{
+ "main": {
+ "cancel": "Закрыть",
+ "close": "Закрыть",
+ "created": "Создано",
+ "days": "дней",
+ "description": "Описание",
+ "descriptionNA": "Описание недоступно",
+ "digest": "ДАЙДЖЕСТ",
+ "hours": "часов",
+ "layers": "Слои",
+ "license": "Лицензия",
+ "licenseNA": "Информация о лицензии недоступна",
+ "loading": "Загрузка...",
+ "minutes": "минут",
+ "NA": "недоступно",
+ "nothingFound": "Ничего не найдено",
+ "osOrArch": "ОС/Архитектура",
+ "published": "опубликовано",
+ "referredBy": "Упомянутый в",
+ "revoke": "Отозвать",
+ "signIn": "Войти",
+ "sort": "Сортировать по",
+ "stars": "Звезд",
+ "timestampNA": "Время НД",
+ "totalSize": "Общий размер",
+ "unknown": "Неизвестен",
+ "usedBy": "Используется",
+ "uses": "Использует",
+ "vendorNA": "Автор неизвестен",
+ "vulnerabilities": "Уязвимости",
+ "weeks": "недель"
+ },
+ "explore": {
+ "OS": "Операционная система",
+ "architectures": "Архитектуры",
+ "additionalFilters": "Дополнительные фильтры",
+ "showing": "Показано",
+ "resultsOutOf": "результатов из",
+ "filterResults": "Отфильтровать результаты",
+ "noResults": "Похоже, у нас нет ничего подходящего для этого поиска. Попробуйте поискать что-нибудь еще."
+ },
+ "filterDialog": {
+ "filter": "Фильтр",
+ "sortResults": "Сортировать результаты",
+ "confirm": "Подтвердить"
+ },
+ "header": {
+ "product": "Проект",
+ "docs": "Документация"
+ },
+ "exploreHeader": {
+ "home": "Домой"
+ },
+ "searchSuggestion": {
+ "search": "Поиск по содержимому сайта...",
+ "advancedSearch": "Нажмите Enter для продвинутого поиска",
+ "useAposChar": "Используйте символ ':' для поиска по тегам"
+ },
+ "userAccountMenu": {
+ "APIKeys": "API Ключи",
+ "logOut": "Выйти"
+ },
+ "home": {
+ "noImages": "Нет образов",
+ "viewAll": "Посмотреть все",
+ "mostPopularImages": "Наиболее популярные образы",
+ "recentlyUpdatedImages": "Недавно обновленные образы",
+ "bookmarks": "Закладки"
+ },
+ "signIn": {
+ "welcomeBack": "Добро пожаловать! Пожалуйста, войдите в систему.",
+ "or": "или",
+ "username": "Имя пользователя",
+ "enterPassword": "Введите пароль",
+ "authFailed": "Не удалось выполнить аутентификацию. Пожалуйста, попробуйте снова.",
+ "continue": "Продолжить",
+ "continueAsGuest": "Продолжить как гость"
+ },
+ "signInPresentation": {
+ "description": "OCI-native реестр образов контейнеров, упрощенный"
+ },
+ "thirdPartyLoginComponents": {
+ "continueWith": "Продолжить с",
+ "signInWith": "Войти при помощи"
+ },
+ "tags": {
+ "tagsHistory": "История тегов",
+ "searchTags": "Поиск тегов..."
+ },
+ "repoDetails": {
+ "titleNA": "Название недоступно"
+ },
+ "repoDetailsMetadata": {
+ "repository": "Репозиторий",
+ "totalDownloads": "Количество скачиваний",
+ "lastPublish": "Последняя публикация"
+ },
+ "deleteTag": {
+ "deleteImage": "Безвозвратно удалить изображение"
+ },
+ "deleteTagConfirmDialog": {
+ "delete": "Удалить"
+ },
+ "filterCard": {
+ "filterTitle": "Название фильтра"
+ },
+ "layerCard": {
+ "details": "ДЕТАЛИ",
+ "command": "Команда"
+ },
+ "noData": {
+ "noData": "Нет информации"
+ },
+ "pullCommandButton": {
+ "copiedPullCommand": "Команда Pull скопирована",
+ "pull": "Pull"
+ },
+ "referrerCard": {
+ "type": "Тип:",
+ "mediaType": "Тип медиа:",
+ "size": "Размер:",
+ "annotations": "АННОТАЦИИ"
+ },
+ "repoCard": {
+ "downloads": "Скачиваний"
+ },
+ "signatureTooltip": {
+ "notSigned": "Не подписан",
+ "tool": "Инструмент",
+ "signedBy": "Подписан"
+ },
+ "tagCard": {
+ "tag": "Тег",
+ "by": "",
+ "showMore": "Показать больше",
+ "showLess": "Скрыть",
+ "compressedSize": "РАЗМЕР СЖАТОГО"
+ },
+ "vulnerabilityCard": {
+ "notFixed": "Не исправлено",
+ "loadMore": "Показать больше",
+ "externalReference": "Внешняя ссылка",
+ "packages": "Пакеты",
+ "fixedIn": "Исправлено в"
+ },
+ "vulnerabilityCountCard": {
+ "total": "Всего",
+ "critical": "Критических",
+ "criticalShort": "К",
+ "high": "Серьезных",
+ "highShort": "С",
+ "medium": "Средних",
+ "mediumShort": "Ср",
+ "low": "Небольших",
+ "lowShort": "Н",
+ "unknown": "Неизвестных",
+ "unknownShort": "Не"
+ },
+ "vulnerabilityPackageSection": {
+ "packagePath": "Путь пакета",
+ "installedVersion": "Установленная версия",
+ "fixedVersion": "Исправленная версия"
+ },
+ "dependsOn": {},
+ "historyLayers": {
+ "noLayers": "Нет информаци о слое"
+ },
+ "IsDependentOn": {},
+ "ReferredBy": {},
+ "VulnerabilitiesDetails": {
+ "noVulnerabilities": "Никаких уязвимостей",
+ "gettingYourDataReady": "Подготовка ваших данных к экспорту",
+ "collapseListView": "Свернуть представление списка",
+ "expandListView": "Развернуть представление списка",
+ "search": "Поиск",
+ "exclude": "Исключить"
+ },
+ "TagDetails": {
+ "digest": "Дайджест"
+ },
+ "tagDetailsMetadata": {
+ "lastPublished": "Последняя публикация"
+ },
+ "apiKeyCard": {
+ "key": "КЛЮЧ"
+ },
+ "apiKeyConfirmDialog": {
+ "apiKey": "Api Ключ",
+ "plsCopy": "Пожалуйста, скопируйте данный API ключ, вы не сможете увидеть его после обновления страницы"
+ },
+ "apiKeyDialog": {
+ "createApiKey": "Создать API ключ",
+ "expDate": "Дата окончания",
+ "expTime": "Время окончания",
+ "custom": "пользовательский",
+ "create": "Создать"
+ },
+ "apiKeyRevokeDialog": {
+ "key": "ключ",
+ "areYouSure": "Вы уверены, что хотите отозвать этот API-ключ?"
+ },
+ "apiKeys": {
+ "manageYourKeys": "Управление вашими API ключами",
+ "createNewKey": "Создать новый API ключ"
+ },
+ "sortCriteria": {
+ "relevance": "По релевантности",
+ "recent": "Последние",
+ "alphabetical": "По алфавиту (А - Я)",
+ "alphabeticalDesc": "По алфавиту (Я - А)",
+ "mostStarred": "Наиболее любимые",
+ "mostDownloaded": "Наиболее скачиваемые",
+ "newest": "От новых к старым",
+ "oldest": "От старых к новым",
+ "AZ": "А- Я",
+ "ZA": "Я - А"
+ },
+ "filterConstants": {
+ "signedImages": "Подписанные образы",
+ "bookmarks": "Закладки",
+ "starredRepositories": "Понравившиеся"
+ },
+ "vulnerabilityAndSignatureComponents": {
+ "failed2scan": "Не удалось выполнить сканирование"
+ }
+}
diff --git a/src/App.js b/src/App.js
index 6fdd96cd..1a42d1b9 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { isAuthenticated, isApiKeyEnabled } from 'utilities/authUtilities';
@@ -18,23 +18,25 @@ function App() {
return (
-
-
- }>
- } />
- } />
- } />
- } />
- } />
- {isApiKeyEnabled() && } />}
- } />
-
- }>
- } />
- } />
-
-
-
+
+
+
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+ {isApiKeyEnabled() && } />}
+ } />
+
+ }>
+ } />
+ } />
+
+
+
+
);
}
diff --git a/src/components/Explore/Explore.jsx b/src/components/Explore/Explore.jsx
index ccaf3931..e3000076 100644
--- a/src/components/Explore/Explore.jsx
+++ b/src/components/Explore/Explore.jsx
@@ -1,5 +1,6 @@
// react global
import React, { useEffect, useMemo, useRef, useState } from 'react';
+import { useTranslation } from 'react-i18next';
// components
import RepoCard from '../Shared/RepoCard.jsx';
@@ -210,6 +211,8 @@ function Explore({ searchInputValue }) {
setFilterDialogOpen(true);
};
+ const { t } = useTranslation();
+
const renderRepoCards = () => {
return (
exploreData &&
@@ -243,21 +246,21 @@ function Explore({ searchInputValue }) {
return (
- Showing {exploreData?.length} results out of {totalItems}
+ {t('explore.showing')} {exploreData?.length} {t('explore.resultsOutOf')} {totalItems}
{!isLoading && (
)}
- Sort
+ {t('main.sort')}
@@ -324,7 +327,7 @@ function Explore({ searchInputValue }) {
- Looks like we don't have anything matching that search. Try searching something else.
+ {t('explore.noResults')}
diff --git a/src/components/Explore/FilterDialog.jsx b/src/components/Explore/FilterDialog.jsx
index 56249828..93fd31ca 100644
--- a/src/components/Explore/FilterDialog.jsx
+++ b/src/components/Explore/FilterDialog.jsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { useTranslation } from 'react-i18next';
import { makeStyles } from '@mui/styles';
import {
Dialog,
@@ -28,16 +29,23 @@ function FilterDialog(props) {
setOpen(false);
};
+ const { t } = useTranslation();
+
return (
);
diff --git a/src/components/Header/ExploreHeader.jsx b/src/components/Header/ExploreHeader.jsx
index e76c0008..e17f6a12 100644
--- a/src/components/Header/ExploreHeader.jsx
+++ b/src/components/Header/ExploreHeader.jsx
@@ -1,5 +1,7 @@
// react global
import { Link, useLocation, useNavigate } from 'react-router-dom';
+// localization
+import { useTranslation } from 'react-i18next';
// components
import { Typography, Breadcrumbs } from '@mui/material';
@@ -51,6 +53,7 @@ function ExploreHeader() {
const pathToBeDisplayed = pathWithoutImage.replace('/image/', '');
const pathHeader = pathToBeDisplayed.replace('/', ' / ').replace(/%2F/g, '/');
const pathWithTag = path.substring(0, path.lastIndexOf('/'));
+ const { t } = useTranslation();
return (
@@ -58,7 +61,7 @@ function ExploreHeader() {
- Home
+ {t('exploreHeader.home')}
diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx
index ad7e70fd..86fe8d9c 100644
--- a/src/components/Header/Header.jsx
+++ b/src/components/Header/Header.jsx
@@ -1,11 +1,12 @@
// react global
import React, { useState, useEffect } from 'react';
import { Link, useLocation } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
import { isAuthenticated, isAuthenticationEnabled, logoutUser } from '../../utilities/authUtilities';
// components
-import { AppBar, Toolbar, Grid, Button } from '@mui/material';
+import { AppBar, Toolbar, Grid, Button, FormControl, Select, MenuItem } from '@mui/material';
import SearchSuggestion from './SearchSuggestion';
import UserAccountMenu from './UserAccountMenu';
// styling
@@ -95,6 +96,10 @@ const useStyles = makeStyles((theme) => ({
fontSize: '1rem',
textTransform: 'none',
fontWeight: 600
+ },
+ selectLanguage: {
+ width: 'max-content',
+ backgroundColor: '#ffffff'
}
}));
@@ -131,6 +136,18 @@ function Header({ setSearchCurrentValue = () => {} }) {
const classes = useStyles();
const path = useLocation().pathname;
+ const locales = {
+ en: { title: 'English' },
+ ru: { title: 'Русский' }
+ };
+
+ const { t, i18n } = useTranslation();
+ const [selectedLanguage, setSelectedLanguage] = useState(i18n.language);
+ const handleLanguageChange = (event) => {
+ i18n.changeLanguage(event.target.value);
+ setSelectedLanguage(event.target.value);
+ };
+
const handleSignInClick = () => {
logoutUser();
};
@@ -150,7 +167,7 @@ function Header({ setSearchCurrentValue = () => {} }) {
- Product
+ {t('header.product')}
@@ -160,7 +177,7 @@ function Header({ setSearchCurrentValue = () => {} }) {
target="_blank"
rel="noreferrer"
>
- Docs
+ {t('header.docs')}
@@ -181,10 +198,26 @@ function Header({ setSearchCurrentValue = () => {} }) {
{!isAuthenticated() && isAuthenticationEnabled() && (
)}
+
+
+
+
+
diff --git a/src/components/Header/SearchSuggestion.jsx b/src/components/Header/SearchSuggestion.jsx
index ddb68913..accafaaa 100644
--- a/src/components/Header/SearchSuggestion.jsx
+++ b/src/components/Header/SearchSuggestion.jsx
@@ -3,6 +3,7 @@ import { makeStyles } from '@mui/styles';
import PhotoIcon from '@mui/icons-material/Photo';
import SearchIcon from '@mui/icons-material/Search';
import React, { useEffect, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
import { api, endpoints } from 'api';
import { host } from 'host';
import { mapToImage, mapToRepo } from 'utilities/objectModels';
@@ -274,6 +275,8 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) {
));
};
+ const { t } = useTranslation();
+
return (
{} }) {
{...getComboboxProps()}
>
{} }) {
spacing={2}
>
- Loading...
+ {t('main.loading')}
>
@@ -331,7 +334,7 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) {
onClick={() => {}}
>
- Press Enter for advanced search
+ {t('searchSuggestion.advancedSearch')}
{} }) {
onClick={() => {}}
>
- Use the ':' character to search for tags
+ {t('searchSuggestion.useAposChar')}
>
diff --git a/src/components/Header/UserAccountMenu.jsx b/src/components/Header/UserAccountMenu.jsx
index 5223dd65..a7fd3f3d 100644
--- a/src/components/Header/UserAccountMenu.jsx
+++ b/src/components/Header/UserAccountMenu.jsx
@@ -1,4 +1,5 @@
import React, { useState } from 'react';
+import { useTranslation } from 'react-i18next';
import { Menu, MenuItem, IconButton, Avatar, Divider } from '@mui/material';
@@ -22,6 +23,8 @@ function UserAccountMenu() {
setAnchorEl(null);
};
+ const { t } = useTranslation();
+
return (
<>
{isApiKeyEnabled() && (
)}
{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..474094d0 100644
--- a/src/components/Shared/FilterCard.jsx
+++ b/src/components/Shared/FilterCard.jsx
@@ -2,6 +2,7 @@ import { Card, CardContent, Checkbox, FormControlLabel, Stack, Tooltip, Typograp
import { makeStyles } from '@mui/styles';
import { isArray, isNil } from 'lodash';
import React from 'react';
+import { useTranslation } from 'react-i18next';
const useStyles = makeStyles((theme) => ({
card: {
@@ -70,6 +71,8 @@ function FilterCard(props) {
return filterValue[filter.value] || false;
};
+ const { t } = useTranslation();
+
const getFilterRows = () => {
const filterRows = filters;
return filterRows.map((filter, index) => {
@@ -79,7 +82,7 @@ function FilterCard(props) {
className={classes.formControl}
componentsProps={{ typography: { variant: 'body2', className: classes.cardContentText } }}
control={}
- label={filter.label}
+ label={t(filter.label)}
id={title}
checked={getCheckboxStatus(filter)}
onChange={() => handleFilterClicked(event, filter.value)}
@@ -93,7 +96,7 @@ function FilterCard(props) {
return (
- {title || 'Filter Title'}
+ {title || t('filterCard.filterTitle')}
{getFilterRows()}
diff --git a/src/components/Shared/LayerCard.jsx b/src/components/Shared/LayerCard.jsx
index 133cadc8..77e157b2 100644
--- a/src/components/Shared/LayerCard.jsx
+++ b/src/components/Shared/LayerCard.jsx
@@ -1,4 +1,5 @@
import React, { useState } from 'react';
+import { useTranslation } from 'react-i18next';
import transform from 'utilities/transform';
@@ -99,6 +100,8 @@ function LayerCard(props) {
else return layer.Size;
};
+ const { t } = useTranslation();
+
return (
@@ -123,17 +126,17 @@ function LayerCard(props) {
) : (
)}
- DETAILS
+ {t('layerCard.details')}
- Command
+ {t('layerCard.command')}
{historyDescription.CreatedBy}
{!historyDescription.EmptyLayer && (
<>
- DIGEST
+ {t('main.digest')}
{layer.Digest}
diff --git a/src/components/Shared/NoDataComponent.jsx b/src/components/Shared/NoDataComponent.jsx
index d6290816..9203928c 100644
--- a/src/components/Shared/NoDataComponent.jsx
+++ b/src/components/Shared/NoDataComponent.jsx
@@ -1,5 +1,6 @@
// react global
import React from 'react';
+import { useTranslation } from 'react-i18next';
// components
import { Stack, Typography } from '@mui/material';
@@ -28,11 +29,12 @@ const useStyles = makeStyles((theme) => ({
function NoDataComponent({ text }) {
const classes = useStyles();
+ const { t } = useTranslation();
return (
- {text ? text : 'No Data'}
+ {text ? text : t('noData.noData')}
);
}
diff --git a/src/components/Shared/PullCommandButton.jsx b/src/components/Shared/PullCommandButton.jsx
index aff8fb40..da964a97 100644
--- a/src/components/Shared/PullCommandButton.jsx
+++ b/src/components/Shared/PullCommandButton.jsx
@@ -1,4 +1,5 @@
import React, { useState, useEffect, useRef } from 'react';
+import { useTranslation } from 'react-i18next';
import makeStyles from '@mui/styles/makeStyles';
import { Grid, Button, FormControl, Menu, MenuItem, Box, Tab, InputBase, IconButton, ButtonBase } from '@mui/material';
@@ -162,9 +163,11 @@ function PullCommandButton(props) {
}
}, [isCopied]);
+ const { t } = useTranslation();
+
return isCopied ? (
) : (
@@ -174,7 +177,7 @@ function PullCommandButton(props) {
className={`${classes.copyStringSelect} ${open && classes.copyStringSelectOpened}`}
disableRipple
>
- Pull {imageName}
+ {t('pullCommandButton.pull')} {imageName}
{getButtonIcon()}
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 25d0501e..28152b80 100644
--- a/src/utilities/filterConstants.js
+++ b/src/utilities/filterConstants.js
@@ -15,17 +15,17 @@ const osFilters = [
const imageFilters = [
{
- label: 'Signed Images',
+ label: 'filterConstants.signedImages',
value: 'HasToBeSigned',
type: 'boolean'
},
{
- label: 'Bookmarks',
+ label: 'filterConstants.bookmarks',
value: 'IsBookmarked',
type: 'boolean'
},
{
- label: 'Starred Repositories',
+ label: 'filterConstants.starredRepositories',
value: 'IsStarred',
type: 'boolean'
}
diff --git a/src/utilities/i18n.js b/src/utilities/i18n.js
new file mode 100644
index 00000000..4467b194
--- /dev/null
+++ b/src/utilities/i18n.js
@@ -0,0 +1,23 @@
+import i18n from 'i18next';
+import { initReactI18next } from 'react-i18next';
+import LanguageDetector from 'i18next-browser-languagedetector';
+import Backend from 'i18next-http-backend';
+
+i18n
+ .use(Backend)
+ .use(LanguageDetector)
+ .use(initReactI18next)
+ .init({
+ fallbackLng: {
+ ru_RU: 'ru',
+ default: ['en']
+ },
+ backend: {
+ loadPath: '/locales/{{lng}}/{{ns}}.json'
+ },
+ interpolation: {
+ escapeValue: false
+ }
+ });
+
+export default i18n;
diff --git a/src/utilities/sortCriteria.js b/src/utilities/sortCriteria.js
index 25f660da..95cc3def 100644
--- a/src/utilities/sortCriteria.js
+++ b/src/utilities/sortCriteria.js
@@ -3,55 +3,55 @@ import { DateTime } from 'luxon';
export const sortByCriteria = {
relevance: {
value: 'RELEVANCE',
- label: 'Relevance'
+ label: 'sortCriteria.relevance'
},
updateTime: {
value: 'UPDATE_TIME',
- label: 'Recent'
+ label: 'sortCriteria.recent'
},
alphabetic: {
value: 'ALPHABETIC_ASC',
- label: 'Alphabetical'
+ label: 'sortCriteria.alphabetical'
},
alphabeticDesc: {
value: 'ALPHABETIC_DSC',
- label: 'Alphabetical desc'
+ label: 'sortCriteria.alphabeticalDesc'
},
// stars: {
// value: 'STARS',
- // label: 'Most starred'
+ // label: 'sortCriteria.mostStarred'
// },
downloads: {
value: 'DOWNLOADS',
- label: 'Most downloaded'
+ label: 'sortCriteria.mostDownloaded'
}
};
export const tagsSortByCriteria = {
updateTimeDesc: {
value: 'UPDATETIME_DESC',
- label: 'Newest',
+ label: 'sortCriteria.newest',
func: (a, b) => {
return DateTime.fromISO(b.lastUpdated).diff(DateTime.fromISO(a.lastUpdated));
}
},
updateTime: {
value: 'UPDATETIME',
- label: 'Oldest',
+ label: 'sortCriteria.oldest',
func: (a, b) => {
return DateTime.fromISO(a.lastUpdated).diff(DateTime.fromISO(b.lastUpdated));
}
},
alphabetic: {
value: 'ALPHABETIC',
- label: 'A - Z',
+ label: 'sortCriteria.AZ',
func: (a, b) => {
return a.tag?.localeCompare(b.tag);
}
},
alphabeticDesc: {
value: 'ALPHABETIC_DESC',
- label: 'Z - A',
+ label: 'sortCriteria.ZA',
func: (a, b) => {
return b.tag?.localeCompare(a.tag);
}
diff --git a/src/utilities/vulnerabilityAndSignatureComponents.jsx b/src/utilities/vulnerabilityAndSignatureComponents.jsx
index 1ec4e344..21c47173 100644
--- a/src/utilities/vulnerabilityAndSignatureComponents.jsx
+++ b/src/utilities/vulnerabilityAndSignatureComponents.jsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { useTranslation } from 'react-i18next';
import { Chip, Tooltip, Badge } from '@mui/material';
import SvgIcon from '@mui/material/SvgIcon';
import { ReactComponent as failedScanBug } from '../assets/failedScan.svg';
@@ -62,8 +63,10 @@ const UnknownVulnerabilityIcon = ({ vulnerabilityStringTitle }) => {
);
};
const FailedScanIcon = () => {
+ const { t } = useTranslation();
+
return (
-
+
{
);
};
const FailedScanChip = () => {
+ const { t } = useTranslation();
+
return (
}