diff --git a/.ai/skills/i18n/SKILL.md b/.ai/skills/i18n/SKILL.md index ecee011a03..76f956b075 100644 --- a/.ai/skills/i18n/SKILL.md +++ b/.ai/skills/i18n/SKILL.md @@ -81,6 +81,29 @@ import { Trans } from 'uiSrc/i18n'; // getTranslatedApiError() fills {{databaseId}} from response.data.resource — no extra code. ``` +## Plurals + +Use i18next's native **`count`-based** plurals — never a hand-rolled `isPlural` branch with +`.single`/`.plural` keys. + +- Add one key per plural form with the i18next suffix: `key_one`, `key_other` (a language may + need more forms — `_few`, `_many` — but `en`/`bg` only use `_one`/`_other`). +- Reference the **base** key (no suffix) and pass `count`; i18next selects the form: + `t('key', { count })` or ``. +- The base key type-checks even though only the suffixed forms are in `en.json` — i18next's + types resolve it from the `_one`/`_other` entries. +- **Write the whole sentence in each form.** Don't interpolate the one differing word as a + fragment — word order, agreement, and the number of plural forms vary by language. +- Renaming a key (e.g. `.single` → `_one`) leaves the old key behind in `bg.json` because + `i18n:extract` doesn't prune — delete the orphan so en/bg parity holds. + +```tsx +// en.json: +// "workbench.runConfirm.body_one": "…This command is part of…" +// "workbench.runConfirm.body_other": "…These commands are part of…" + +``` + ## Keys - **Flat, dotted keys** — `keySeparator` and `nsSeparator` are `false`, so a dot is a literal character, not nesting. `"api.error.code.11000.title"` is a single key. @@ -146,3 +169,4 @@ The backend ships a stable `errorCode` on every user-facing error (see - ✅ Keep en/bg key parity; empty bg is an acceptable "later" placeholder. - ❌ Don't hardcode user-facing strings — add a key. - ❌ Don't hand-edit the locale-file key order — let `i18n:extract` sort. +- ❌ Don't hand-roll plurals with a JS branch — use `count` + `key_one`/`key_other` (see Plurals). diff --git a/redisinsight/ui/src/components/full-screen/FullScreen.tsx b/redisinsight/ui/src/components/full-screen/FullScreen.tsx index 1b2f23934a..01c7d16f6a 100644 --- a/redisinsight/ui/src/components/full-screen/FullScreen.tsx +++ b/redisinsight/ui/src/components/full-screen/FullScreen.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { useTranslation } from 'uiSrc/i18n' import { ExtendIcon, ShrinkIcon } from 'uiSrc/components/base/icons' import { IconButton } from 'uiSrc/components/base/forms/buttons' import { RiTooltip } from 'uiSrc/components' @@ -15,20 +16,27 @@ const FullScreen = ({ onToggleFullScreen, anchorClassName = '', btnTestId = 'toggle-full-screen', -}: Props) => ( - - - -) +}: Props) => { + const { t } = useTranslation() + return ( + + + + ) +} export { FullScreen } diff --git a/redisinsight/ui/src/components/query/components/RunButton.tsx b/redisinsight/ui/src/components/query/components/RunButton.tsx index 882f5d0dcc..ca710b33d5 100644 --- a/redisinsight/ui/src/components/query/components/RunButton.tsx +++ b/redisinsight/ui/src/components/query/components/RunButton.tsx @@ -1,5 +1,6 @@ import React from 'react' import styled from 'styled-components' +import { useTranslation } from 'uiSrc/i18n' import { PlayFilledIcon } from 'uiSrc/components/base/icons' import { EmptyButton } from 'uiSrc/components/base/forms/buttons' @@ -24,6 +25,7 @@ export const RunButton = ({ isLoading?: boolean onSubmit: () => void }) => { + const { t } = useTranslation() return ( { @@ -32,10 +34,10 @@ export const RunButton = ({ loading={isLoading} disabled={isLoading} icon={PlayFilledIcon} - aria-label="submit" + aria-label={t('query.runButton.aria')} data-testid="btn-submit" > - Run + {t('query.runButton.label')} ) } diff --git a/redisinsight/ui/src/components/query/query-actions/QueryActions.tsx b/redisinsight/ui/src/components/query/query-actions/QueryActions.tsx index fb188aeef9..598efc7559 100644 --- a/redisinsight/ui/src/components/query/query-actions/QueryActions.tsx +++ b/redisinsight/ui/src/components/query/query-actions/QueryActions.tsx @@ -1,9 +1,11 @@ import React from 'react' +import { Trans, useTranslation } from 'uiSrc/i18n' import { ResultsMode, RunQueryMode } from 'uiSrc/slices/interfaces' import { KEYBOARD_SHORTCUTS } from 'uiSrc/constants' import { KeyboardShortcut, RiTooltip } from 'uiSrc/components' import { isGroupMode } from 'uiSrc/utils' +import { isMacOs } from 'uiSrc/utils/dom' import { RiIcon } from 'uiSrc/components/base/icons' @@ -24,6 +26,7 @@ export interface Props { } const QueryActions = (props: Props) => { + const { t } = useTranslation() const { isLoading, activeMode, @@ -34,7 +37,14 @@ const QueryActions = (props: Props) => { } = props const KeyBoardTooltipContent = KEYBOARD_SHORTCUTS?.workbench?.runQuery && ( <> - {KEYBOARD_SHORTCUTS.workbench.runQuery?.label}: + + {t( + isMacOs() + ? 'query.runShortcut.label' + : 'query.runShortcut.labelNonMac', + )} + : + { {onChangeMode && ( { data-testid="btn-change-mode" > - Raw mode + {t('query.actions.rawMode.label')} )} @@ -66,12 +76,10 @@ const QueryActions = (props: Props) => { - Groups the command results into a single window. -
- When grouped, the results can be visualized only in the text - format. - + }} + /> } data-testid="group-results-tooltip" > @@ -82,18 +90,14 @@ const QueryActions = (props: Props) => { data-testid="btn-change-group-mode" > - Group results + {t('query.actions.groupMode.label')}
)} diff --git a/redisinsight/ui/src/components/query/query-card/QueryCard.spec.tsx b/redisinsight/ui/src/components/query/query-card/QueryCard.spec.tsx index ed4ee17fa0..fd3dfeac2c 100644 --- a/redisinsight/ui/src/components/query/query-card/QueryCard.spec.tsx +++ b/redisinsight/ui/src/components/query/query-card/QueryCard.spec.tsx @@ -138,7 +138,7 @@ describe('QueryCard', () => { it('Should return correct summary string', () => { const summary = { total: 2, success: 1, fail: 1 } - const summaryText = '2 Command(s) - 1 success, 1 error(s)' + const summaryText = '2 Commands - 1 success, 1 error' const summaryString = getSummaryText(summary) diff --git a/redisinsight/ui/src/components/query/query-card/QueryCard.tsx b/redisinsight/ui/src/components/query/query-card/QueryCard.tsx index 73d18b8b52..0461319709 100644 --- a/redisinsight/ui/src/components/query/query-card/QueryCard.tsx +++ b/redisinsight/ui/src/components/query/query-card/QueryCard.tsx @@ -3,6 +3,7 @@ import { useAppSelector } from 'uiSrc/slices/hooks' import cx from 'classnames' import { useParams } from 'react-router-dom' import { isNull } from 'lodash' +import i18n from 'uiSrc/i18n' import { KeyboardKeys as keys } from 'uiSrc/constants/keys' import { LoadingContent } from 'uiSrc/components/base/layout' @@ -112,9 +113,12 @@ export const getSummaryText = ( ) => { if (summary) { const { total, success, fail } = summary - const summaryText = `${total} Command(s) - ${success} success` + const summaryText = i18n.t('query.card.summary.commands', { + count: total, + success, + }) if (!isSilentModeWithoutError(mode, summary?.fail)) { - return `${summaryText}, ${fail} error(s)` + return `${summaryText}${i18n.t('query.card.summary.errors', { count: fail })}` } return summaryText } diff --git a/redisinsight/ui/src/components/query/query-card/QueryCardCliResultWrapper/QueryCardCliResultWrapper.tsx b/redisinsight/ui/src/components/query/query-card/QueryCardCliResultWrapper/QueryCardCliResultWrapper.tsx index 7c4639f26f..b32177281c 100644 --- a/redisinsight/ui/src/components/query/query-card/QueryCardCliResultWrapper/QueryCardCliResultWrapper.tsx +++ b/redisinsight/ui/src/components/query/query-card/QueryCardCliResultWrapper/QueryCardCliResultWrapper.tsx @@ -2,6 +2,7 @@ import React, { useMemo } from 'react' import cx from 'classnames' import { isArray } from 'lodash' +import { useTranslation } from 'uiSrc/i18n' import { LoadingContent } from 'uiSrc/components/base/layout' import { CommandExecutionResult } from 'uiSrc/slices/interfaces' import { ResultsMode } from 'uiSrc/slices/interfaces/workbench' @@ -75,6 +76,7 @@ export const getResultText = ( } const QueryCardCliResultWrapper = (props: Props) => { + const { t } = useTranslation() const { result = [], query, @@ -99,16 +101,15 @@ const QueryCardCliResultWrapper = (props: Props) => { <>
{isNotStored && ( - The result is too big to be saved. It will be deleted after the - application is closed. + {t('query.cliResult.tooBig')} )} {isGroupResults(resultsMode) && isArray(result[0]?.response) ? ( diff --git a/redisinsight/ui/src/components/query/query-card/QueryCardHeader/QueryCardHeader.tsx b/redisinsight/ui/src/components/query/query-card/QueryCardHeader/QueryCardHeader.tsx index 5d902cf8c5..b7d0eb0916 100644 --- a/redisinsight/ui/src/components/query/query-card/QueryCardHeader/QueryCardHeader.tsx +++ b/redisinsight/ui/src/components/query/query-card/QueryCardHeader/QueryCardHeader.tsx @@ -1,4 +1,6 @@ import React, { useContext } from 'react' + +import { useTranslation } from 'uiSrc/i18n' import cx from 'classnames' import { useAppSelector } from 'uiSrc/slices/hooks' import { useParams } from 'react-router-dom' @@ -99,6 +101,7 @@ const getTruncatedExecutionTimeString = (value: number): string => { } const QueryCardHeader = (props: Props) => { + const { t } = useTranslation() const { isOpen, toggleOpen, @@ -339,7 +342,7 @@ const QueryCardHeader = (props: Props) => { { > {isNumber(executionTime) && ( { )} - + @@ -465,14 +471,14 @@ const QueryCardHeader = (props: Props) => { {!isFullScreen && ( @@ -484,7 +490,7 @@ const QueryCardHeader = (props: Props) => { {!isSilentModeWithoutError(resultsMode, summary?.fail) && ( )} @@ -500,19 +506,19 @@ const QueryCardHeader = (props: Props) => { {isGroupMode(resultsMode) && ( - Group mode + {t('query.card.mode.group')} )} {isSilentMode(resultsMode) && ( - Silent mode + {t('query.card.mode.silent')} )} {isRawMode(mode) && ( - Raw mode + {t('query.card.mode.raw')} )} @@ -522,7 +528,7 @@ const QueryCardHeader = (props: Props) => { > diff --git a/redisinsight/ui/src/components/query/query-lite-actions/QueryLiteActions.tsx b/redisinsight/ui/src/components/query/query-lite-actions/QueryLiteActions.tsx index 2741f434c3..17e212cb24 100644 --- a/redisinsight/ui/src/components/query/query-lite-actions/QueryLiteActions.tsx +++ b/redisinsight/ui/src/components/query/query-lite-actions/QueryLiteActions.tsx @@ -1,7 +1,9 @@ import React from 'react' +import { useTranslation } from 'uiSrc/i18n' import { KEYBOARD_SHORTCUTS } from 'uiSrc/constants' import { KeyboardShortcut, RiTooltip } from 'uiSrc/components' +import { isMacOs } from 'uiSrc/utils/dom' import { Spacer } from 'uiSrc/components/base/layout/spacer' import { EmptyButton } from 'uiSrc/components/base/forms/buttons' @@ -15,10 +17,18 @@ export interface Props { } const QueryLiteActions = (props: Props) => { + const { t } = useTranslation() const { isLoading, onSubmit, onClear } = props const KeyBoardTooltipContent = KEYBOARD_SHORTCUTS?.workbench?.runQuery && ( <> - {KEYBOARD_SHORTCUTS.workbench.runQuery?.label}: + + {t( + isMacOs() + ? 'query.runShortcut.label' + : 'query.runShortcut.labelNonMac', + )} + : + { position="right" content={ isLoading - ? 'Please wait while the commands are being executed…' - : 'Clear query' + ? t('query.executing') + : t('query.liteActions.clear.tooltip') } data-testid="clear-query-tooltip" > @@ -42,20 +52,16 @@ const QueryLiteActions = (props: Props) => { onClick={onClear} loading={isLoading} disabled={isLoading} - aria-label="clear" + aria-label={t('query.liteActions.clear.aria')} data-testid="btn-clear" > - Clear + {t('query.liteActions.clear.label')} diff --git a/redisinsight/ui/src/components/query/query-results/QueryResults.tsx b/redisinsight/ui/src/components/query/query-results/QueryResults.tsx index 20b8b4c616..194960b3dc 100644 --- a/redisinsight/ui/src/components/query/query-results/QueryResults.tsx +++ b/redisinsight/ui/src/components/query/query-results/QueryResults.tsx @@ -1,5 +1,6 @@ import React from 'react' +import { useTranslation } from 'uiSrc/i18n' import { CodeButtonParams } from 'uiSrc/constants' import { ProfileQueryType } from 'uiSrc/pages/workbench/constants' import { generateProfileQueryForCommand } from 'uiSrc/pages/workbench/utils/profile' @@ -39,6 +40,7 @@ export interface QueryResultsProps { } const QueryResults = (props: QueryResultsProps) => { + const { t } = useTranslation() const { isResultsLoaded, items = [], @@ -91,7 +93,7 @@ const QueryResults = (props: QueryResultsProps) => { disabled={clearing || processing} data-testid="clear-history-btn" > - Clear Results + {t('query.results.clear')} )} diff --git a/redisinsight/ui/src/components/query/query-tutorials/QueryTutorials.tsx b/redisinsight/ui/src/components/query/query-tutorials/QueryTutorials.tsx index 3ee1fbeceb..f229d9bcd9 100644 --- a/redisinsight/ui/src/components/query/query-tutorials/QueryTutorials.tsx +++ b/redisinsight/ui/src/components/query/query-tutorials/QueryTutorials.tsx @@ -1,5 +1,6 @@ import React from 'react' +import { useTranslation } from 'uiSrc/i18n' import { useAppDispatch } from 'uiSrc/slices/hooks' import { useHistory, useParams } from 'react-router-dom' import styled from 'styled-components' @@ -47,6 +48,7 @@ const QueryTutorialsButton = styled(EmptyButton)` ` const QueryTutorials = ({ tutorials, source }: Props) => { + const { t } = useTranslation() const dispatch = useAppDispatch() const history = useHistory() const { instanceId } = useParams<{ instanceId: string }>() @@ -67,7 +69,7 @@ const QueryTutorials = ({ tutorials, source }: Props) => { return (
- Tutorials: + {t('query.tutorials.title')} {tutorials.map(({ id, title }) => ( Когато са групирани, резултатите могат да се визуализират само в текстов формат.", + "query.actions.rawMode.label": "Необработен режим", + "query.actions.rawMode.tooltip": "Активира режима на необработен изход", + "query.card.clearResult.tooltip": "Изчистване на резултата", + "query.card.copyQuery.aria": "Копиране на заявката", + "query.card.delete.aria": "Изтриване на командата", + "query.card.mode.group": "Групов режим", + "query.card.mode.raw": "Необработен режим", + "query.card.mode.silent": "Тих режим", + "query.card.processingTime": "Време за обработка", + "query.card.queryParameters.aria": "Параметри на заявката", + "query.card.rerun.aria": "Повторно изпълнение на командата", + "query.card.rerun.tooltip": "Изпълни отново", + "query.card.summary.commands_one": "{{count}} команда - {{success}} успешни", + "query.card.summary.commands_other": "{{count}} команди - {{success}} успешни", + "query.card.summary.errors_one": ", {{count}} грешка", + "query.card.summary.errors_other": ", {{count}} грешки", + "query.card.toggleCollapse.aria": "Превключи резултата", + "query.cliResult.copy": "Копиране на резултата", + "query.cliResult.tooBig": "Резултатът е твърде голям, за да бъде запазен. Ще бъде изтрит след затваряне на приложението.", + "query.executing": "Моля, изчакайте, докато командите се изпълняват…", + "query.liteActions.clear.aria": "Изчисти заявката", + "query.liteActions.clear.label": "Изчисти", + "query.liteActions.clear.tooltip": "Изчистване на заявката", + "query.results.clear": "Изчистване на резултатите", + "query.runButton.aria": "Изпълни заявката", + "query.runButton.label": "Изпълни", + "query.runShortcut.label": "Изпълнение на командите", + "query.runShortcut.labelNonMac": "Изпълнение", + "query.tutorials.title": "Ръководства:", "settings.advanced.keysToScan.label": "Ключове за сканиране:", "settings.advanced.keysToScan.summary": "Задава броя ключове, сканирани на една итерация. Филтрирането по шаблон при голям брой ключове може да намали производителността.", "settings.advanced.keysToScan.title": "Ключове за сканиране в изглед Списък", @@ -384,5 +418,27 @@ "whatsNew.releaseNotes.link": "Вижте пълните бележки по изданието за {{version}}", "whatsNew.title": "Какво ново", "whatsNew.version.option": "v{{version}}", - "whatsNew.version.optionLatest": "v{{version}} (най-нова)" + "whatsNew.version.optionLatest": "v{{version}} (най-нова)", + "workbench.noResults.button.explore": "Разгледайте", + "workbench.noResults.cliSubtitle": "за Redis команди.", + "workbench.noResults.cliTitle": "Това е нашият усъвършенстван CLI", + "workbench.noResults.hint": "Или щракнете върху иконата в горния десен ъгъл.", + "workbench.noResults.imageAlt": "няма резултати", + "workbench.noResults.summary": "Изпробвайте Работна среда с нашите интерактивни ръководства, за да научите как Redis може да реши вашите случаи на употреба.", + "workbench.noResults.title": "Все още няма резултати за показване", + "workbench.pageTitle": "{{name}} {{db}} - Работна среда", + "workbench.results.clear": "Изчистване на резултатите", + "workbench.runConfirm.body_one": "На път сте да изпълните {{commands}} на {{db}}. Тази команда е част от списъка с опасни команди. Тази операция може да повлияе на стабилността на сървъра.", + "workbench.runConfirm.body_other": "На път сте да изпълните {{commands}} на {{db}}. Тези команди са част от списъка с опасни команди. Тази операция може да повлияе на стабилността на сървъра.", + "workbench.runConfirm.button.run": "Изпълнение на командата", + "workbench.runConfirm.title": "Продължете внимателно в production среда", + "workbench.suggestions.noIndexes.detail": "Създайте индекс", + "workbench.suggestions.noIndexes.documentation": "Вижте [документацията]({{link}}) за подробни инструкции как да създадете индекс.", + "workbench.suggestions.noIndexes.label": "Няма индекси за показване", + "workbench.tutorials.basicUseCases": "Основни случаи на употреба", + "workbench.tutorials.introToSearch": "Въведение в търсенето", + "workbench.tutorials.introToVectorSearch": "Въведение във векторното търсене", + "workbench.viewType.explain": "Обяснение на командата", + "workbench.viewType.profile": "Профилиране на командата", + "workbench.viewType.text": "Текст" } diff --git a/redisinsight/ui/src/i18n/locales/en.json b/redisinsight/ui/src/i18n/locales/en.json index 2f23969001..7386ace7d4 100644 --- a/redisinsight/ui/src/i18n/locales/en.json +++ b/redisinsight/ui/src/i18n/locales/en.json @@ -201,6 +201,9 @@ "browser.array.delete.range.trigger": "Delete range", "browser.array.delete.row.message": "This element will be permanently removed from the array.", "browser.array.delete.row.title": "Delete element", + "common.fullScreen.enter": "Full Screen", + "common.fullScreen.exit": "Exit Full Screen", + "common.fullScreen.openAria": "Open full screen", "common.privacyPolicy": "Privacy Policy", "notification.error.arrayBulkDeleteLimit.message": "You can delete up to {{max}} elements at once. Clear some of the selection and try again.", "notification.error.arrayBulkDeleteLimit.title": "Too many elements selected", @@ -316,6 +319,37 @@ "notification.success.uploadDataBulk.success": "Success", "notification.success.uploadDataBulk.timeTaken": "Time Taken", "notification.success.uploadDataBulk.title": "Action completed", + "query.actions.groupMode.label": "Group results", + "query.actions.groupMode.tooltip": "Groups the command results into a single window.When grouped, the results can be visualized only in the text format.", + "query.actions.rawMode.label": "Raw mode", + "query.actions.rawMode.tooltip": "Enables the raw output mode", + "query.card.clearResult.tooltip": "Clear result", + "query.card.copyQuery.aria": "Copy query", + "query.card.delete.aria": "Delete command", + "query.card.mode.group": "Group mode", + "query.card.mode.raw": "Raw mode", + "query.card.mode.silent": "Silent mode", + "query.card.processingTime": "Processing Time", + "query.card.queryParameters.aria": "Query parameters", + "query.card.rerun.aria": "Re-run command", + "query.card.rerun.tooltip": "Run again", + "query.card.summary.commands_one": "{{count}} Command - {{success}} success", + "query.card.summary.commands_other": "{{count}} Commands - {{success}} success", + "query.card.summary.errors_one": ", {{count}} error", + "query.card.summary.errors_other": ", {{count}} errors", + "query.card.toggleCollapse.aria": "Toggle result", + "query.cliResult.copy": "Copy result", + "query.cliResult.tooBig": "The result is too big to be saved. It will be deleted after the application is closed.", + "query.executing": "Please wait while the commands are being executed…", + "query.liteActions.clear.aria": "Clear query", + "query.liteActions.clear.label": "Clear", + "query.liteActions.clear.tooltip": "Clear query", + "query.results.clear": "Clear Results", + "query.runButton.aria": "Run query", + "query.runButton.label": "Run", + "query.runShortcut.label": "Run commands", + "query.runShortcut.labelNonMac": "Run", + "query.tutorials.title": "Tutorials:", "settings.advanced.keysToScan.label": "Keys to Scan:", "settings.advanced.keysToScan.summary": "Sets the amount of keys to scan per one iteration. Filtering by pattern per a large number of keys may decrease performance.", "settings.advanced.keysToScan.title": "Keys to Scan in List view", @@ -384,5 +418,27 @@ "whatsNew.releaseNotes.link": "See full release notes for {{version}}", "whatsNew.title": "What's New", "whatsNew.version.option": "v{{version}}", - "whatsNew.version.optionLatest": "v{{version}} (Latest)" + "whatsNew.version.optionLatest": "v{{version}} (Latest)", + "workbench.noResults.button.explore": "Explore", + "workbench.noResults.cliSubtitle": "for Redis commands.", + "workbench.noResults.cliTitle": "This is our advanced CLI", + "workbench.noResults.hint": "Or click the icon in the top right corner.", + "workbench.noResults.imageAlt": "no results", + "workbench.noResults.summary": "Try Workbench with our interactive Tutorials to learn how Redis can solve your use cases.", + "workbench.noResults.title": "No results to display yet", + "workbench.pageTitle": "{{name}} {{db}} - Workbench", + "workbench.results.clear": "Clear Results", + "workbench.runConfirm.body_one": "You're about to run {{commands}} on {{db}}. This command is part of the list of dangerous commands. This operation may affect server stability.", + "workbench.runConfirm.body_other": "You're about to run {{commands}} on {{db}}. These commands are part of the list of dangerous commands. This operation may affect server stability.", + "workbench.runConfirm.button.run": "Run command", + "workbench.runConfirm.title": "Proceed with caution in production", + "workbench.suggestions.noIndexes.detail": "Create an index", + "workbench.suggestions.noIndexes.documentation": "See the [documentation]({{link}}) for detailed instructions on how to create an index.", + "workbench.suggestions.noIndexes.label": "No indexes to display", + "workbench.tutorials.basicUseCases": "Basic use cases", + "workbench.tutorials.introToSearch": "Intro to search", + "workbench.tutorials.introToVectorSearch": "Intro to vector search", + "workbench.viewType.explain": "Explain the command", + "workbench.viewType.profile": "Profile the command", + "workbench.viewType.text": "Text" } diff --git a/redisinsight/ui/src/pages/workbench/WorkbenchPage.tsx b/redisinsight/ui/src/pages/workbench/WorkbenchPage.tsx index 0007a80324..5670ce25ca 100644 --- a/redisinsight/ui/src/pages/workbench/WorkbenchPage.tsx +++ b/redisinsight/ui/src/pages/workbench/WorkbenchPage.tsx @@ -2,12 +2,14 @@ import React, { useEffect, useState } from 'react' import { useAppSelector } from 'uiSrc/slices/hooks' import { useParams } from 'react-router-dom' +import { useTranslation } from 'uiSrc/i18n' import { formatLongName, getDbIndex, setTitle } from 'uiSrc/utils' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry' import WBViewWrapper from './components/wb-view' const WorkbenchPage = () => { + const { t } = useTranslation() const [isPageViewSent, setIsPageViewSent] = useState(false) const { name: connectedInstanceName, db } = useAppSelector( @@ -17,7 +19,10 @@ const WorkbenchPage = () => { const { instanceId } = useParams<{ instanceId: string }>() setTitle( - `${formatLongName(connectedInstanceName, 33, 0, '...')} ${getDbIndex(db)} - Workbench`, + t('workbench.pageTitle', { + name: formatLongName(connectedInstanceName, 33, 0, '...'), + db: getDbIndex(db), + }), ) useEffect(() => { diff --git a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx index bd4dc96e6d..a0d37f28bb 100644 --- a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx +++ b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx @@ -2,6 +2,7 @@ import React, { useRef } from 'react' import { useAppDispatch, useAppSelector } from 'uiSrc/slices/hooks' import { monaco as monacoEditor } from 'react-monaco-editor' +import { useTranslation } from 'uiSrc/i18n' import { MonacoLanguage } from 'uiSrc/constants' import { CodeEditor } from 'uiSrc/components/base/code-editor' import { @@ -23,6 +24,7 @@ import { Props } from './Query.types' import * as S from './Query.styles' const Query = (props: Props) => { + const { t } = useTranslation() const { activeMode, resultsMode, @@ -138,7 +140,10 @@ const Query = (props: Props) => { ) : ( <> ({ + id, + title: t(titleKey), + }))} source="advanced_workbench_editor" /> { + const { t } = useTranslation() const { provider } = useAppSelector(connectedInstanceSelector) const { instanceId } = useParams<{ instanceId: string }>() @@ -50,13 +53,13 @@ const WbNoResultsMessage = () => { className={styles.noResultsTitle} data-testid="wb_no-results__title" > - No results to display yet + {t('workbench.noResults.title')} - This is our advanced CLI + {t('workbench.noResults.cliTitle')} - for Redis commands. + {t('workbench.noResults.cliSubtitle')} @@ -67,7 +70,7 @@ const WbNoResultsMessage = () => { no results @@ -76,8 +79,7 @@ const WbNoResultsMessage = () => { className={styles.noResultsText} data-testid="wb_no-results__summary" > - Try Workbench with our interactive Tutorials to learn how Redis - can solve your use cases. + {t('workbench.noResults.summary')}
@@ -87,12 +89,12 @@ const WbNoResultsMessage = () => { className={styles.exploreBtn} data-testid="no-results-explore-btn" > - Explore + {t('workbench.noResults.button.explore')}
- Or click the icon in the top right corner. + {t('workbench.noResults.hint')} diff --git a/redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/WBResults.tsx b/redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/WBResults.tsx index 00b7c8e143..6addbed710 100644 --- a/redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/WBResults.tsx +++ b/redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/WBResults.tsx @@ -1,6 +1,7 @@ import React from 'react' import cx from 'classnames' +import { useTranslation } from 'uiSrc/i18n' import { CodeButtonParams } from 'uiSrc/constants' import { ProfileQueryType } from 'uiSrc/pages/workbench/constants' import { generateProfileQueryForCommand } from 'uiSrc/pages/workbench/utils/profile' @@ -42,6 +43,7 @@ export interface Props { /** @deprecated Use QueryResults from 'uiSrc/components/query/query-results' instead. */ const WBResults = (props: Props) => { + const { t } = useTranslation() const { isResultsLoaded, items = [], @@ -91,7 +93,7 @@ const WBResults = (props: Props) => { disabled={clearing || processing} data-testid="clear-history-btn" > - Clear Results + {t('workbench.results.clear')}
)} diff --git a/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx b/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx index 95482c9f60..468458add1 100644 --- a/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx +++ b/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx @@ -3,6 +3,7 @@ import { useAppDispatch, useAppSelector } from 'uiSrc/slices/hooks' import { useParams } from 'react-router-dom' import { monaco as monacoEditor } from 'react-monaco-editor' +import { Trans, escapeTrans, useTranslation } from 'uiSrc/i18n' import { getMonacoLines, getParsedParamsInQuery, @@ -79,6 +80,7 @@ let state: IState = { /** @deprecated Use useQuery hook from 'pages/vector-search/pages/VectorSearchQueryPage/hooks/useQuery' instead. */ const WBViewWrapper = () => { + const { t } = useTranslation() const { instanceId } = useParams<{ instanceId: string }>() const { @@ -283,20 +285,20 @@ const WBViewWrapper = () => { ), ), ) - const isPlural = dangerousCommands.length > 1 requestConfirmation({ - title: 'Proceed with caution in production', + title: t('workbench.runConfirm.title'), actionDescription: ( - <> - You're about to run{' '} - {dangerousCommands.join(', ')} on{' '} - {confirmationText}.{' '} - {isPlural ? 'These commands are' : 'This command is'} part of the - list of dangerous commands. This operation may affect server - stability. - + }} + /> ), - confirmButtonText: 'Run command', + confirmButtonText: t('workbench.runConfirm.button.run'), commandId: dangerousVerbs, tip: , onConfirm: () => { diff --git a/redisinsight/ui/src/pages/workbench/constants.ts b/redisinsight/ui/src/pages/workbench/constants.ts index 883f2503b4..2de6cda0dd 100644 --- a/redisinsight/ui/src/pages/workbench/constants.ts +++ b/redisinsight/ui/src/pages/workbench/constants.ts @@ -1,4 +1,5 @@ import { AllIconsType } from 'uiSrc/components/base/icons/RiIcon' +import i18n from 'uiSrc/i18n' export const WORKBENCH_HISTORY_WRAPPER_NAME = 'WORKBENCH' export const WORKBENCH_HISTORY_MAX_LENGTH = 30 @@ -10,7 +11,6 @@ export enum WBQueryType { export const DEFAULT_TEXT_VIEW_TYPE = { id: 'default__Text', - text: 'Text', name: 'default__Text', value: WBQueryType.Text, iconDark: 'TextViewIconDarkIcon' as AllIconsType, @@ -18,9 +18,9 @@ export const DEFAULT_TEXT_VIEW_TYPE = { internal: true, } -export const VIEW_TYPE_OPTIONS = [DEFAULT_TEXT_VIEW_TYPE] - -export const getViewTypeOptions = () => [...VIEW_TYPE_OPTIONS] +export const getViewTypeOptions = () => [ + { ...DEFAULT_TEXT_VIEW_TYPE, text: i18n.t('workbench.viewType.text') }, +] export const SEARCH_COMMANDS = ['ft.search', 'ft.aggregate'] export const GRAPH_COMMANDS = ['graph.query'] @@ -35,23 +35,21 @@ export enum ProfileQueryType { Explain = 'Explain', } -const PROFILE_VIEW_TYPE_OPTIONS = [ +export const getProfileViewTypeOptions = () => [ { id: ProfileQueryType.Profile, - text: 'Profile the command', + text: i18n.t('workbench.viewType.profile'), name: 'Profile', value: WBQueryType.Text, }, { id: ProfileQueryType.Explain, - text: 'Explain the command', + text: i18n.t('workbench.viewType.explain'), name: 'Explain', value: WBQueryType.Text, }, ] -export const getProfileViewTypeOptions = () => [...PROFILE_VIEW_TYPE_OPTIONS] - export enum ModuleCommandPrefix { RediSearch = 'FT.', JSON = 'JSON.', diff --git a/redisinsight/ui/src/pages/workbench/utils/suggestions.ts b/redisinsight/ui/src/pages/workbench/utils/suggestions.ts index 80254cb366..896600ae2f 100644 --- a/redisinsight/ui/src/pages/workbench/utils/suggestions.ts +++ b/redisinsight/ui/src/pages/workbench/utils/suggestions.ts @@ -19,6 +19,7 @@ import { } from 'uiSrc/pages/workbench/constants' import { getUtmExternalLink } from 'uiSrc/utils/links' import { IRedisCommand } from 'uiSrc/constants' +import i18n from 'uiSrc/i18n' import { generateDetail } from './query' import { buildSuggestion } from './monaco' @@ -39,15 +40,17 @@ const NO_INDEXES_DOC_LINK = getUtmExternalLink( export const getNoIndexesSuggestion = (range: monaco.IRange) => [ { id: EmptySuggestionsIds.NoIndexes, - label: 'No indexes to display', + label: i18n.t('workbench.suggestions.noIndexes.label'), kind: monacoEditor.languages.CompletionItemKind.Issue, insertText: '', insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, range, - detail: 'Create an index', + detail: i18n.t('workbench.suggestions.noIndexes.detail'), documentation: { - value: `See the [documentation](${NO_INDEXES_DOC_LINK}) for detailed instructions on how to create an index.`, + value: i18n.t('workbench.suggestions.noIndexes.documentation', { + link: NO_INDEXES_DOC_LINK, + }), }, }, ]