Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .ai/skills/i18n/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<Trans i18nKey="key" count={n} …/>`.
- 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…"
<Trans i18nKey="workbench.runConfirm.body" count={commands.length} components={{ bold }} />
```

## 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.
Expand Down Expand Up @@ -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).
38 changes: 23 additions & 15 deletions redisinsight/ui/src/components/full-screen/FullScreen.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -15,20 +16,27 @@ const FullScreen = ({
onToggleFullScreen,
anchorClassName = '',
btnTestId = 'toggle-full-screen',
}: Props) => (
<RiTooltip
content={isFullScreen ? 'Exit Full Screen' : 'Full Screen'}
position="left"
anchorClassName={anchorClassName}
>
<IconButton
icon={isFullScreen ? ShrinkIcon : ExtendIcon}
color="primary"
aria-label="Open full screen"
onClick={onToggleFullScreen}
data-testid={btnTestId}
/>
</RiTooltip>
)
}: Props) => {
const { t } = useTranslation()
return (
<RiTooltip
content={
isFullScreen
? t('common.fullScreen.exit')
: t('common.fullScreen.enter')
}
position="left"
anchorClassName={anchorClassName}
>
<IconButton
icon={isFullScreen ? ShrinkIcon : ExtendIcon}
color="primary"
aria-label={t('common.fullScreen.openAria')}
onClick={onToggleFullScreen}
data-testid={btnTestId}
/>
</RiTooltip>
)
}

export { FullScreen }
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -24,6 +25,7 @@ export const RunButton = ({
isLoading?: boolean
onSubmit: () => void
}) => {
const { t } = useTranslation()
return (
<StyledEmptyButton
onClick={() => {
Expand All @@ -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')}
</StyledEmptyButton>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -24,6 +26,7 @@
}

const QueryActions = (props: Props) => {
const { t } = useTranslation()
const {
isLoading,
activeMode,
Expand All @@ -34,7 +37,14 @@
} = props
const KeyBoardTooltipContent = KEYBOARD_SHORTCUTS?.workbench?.runQuery && (
<>
<Text size="s">{KEYBOARD_SHORTCUTS.workbench.runQuery?.label}:</Text>
<Text size="s">
{t(
isMacOs()
? 'query.runShortcut.label'

Check warning on line 43 in redisinsight/ui/src/components/query/query-actions/QueryActions.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
: 'query.runShortcut.labelNonMac',
)}
:
</Text>
<Spacer size="s" />
<KeyboardShortcut
separator={KEYBOARD_SHORTCUTS?._separator}
Expand All @@ -48,7 +58,7 @@
{onChangeMode && (
<RiTooltip
position="left"
content="Enables the raw output mode"
content={t('query.actions.rawMode.tooltip')}
data-testid="change-mode-tooltip"
>
<ToggleButton
Expand All @@ -58,20 +68,18 @@
data-testid="btn-change-mode"
>
<RiIcon size="m" type="RawModeIcon" />
<Text size="s">Raw mode</Text>
<Text size="s">{t('query.actions.rawMode.label')}</Text>
</ToggleButton>
</RiTooltip>
)}
{onChangeGroupMode && (
<RiTooltip
position="left"
content={
<>
Groups the command results into a single window.
<br />
When grouped, the results can be visualized only in the text
format.
</>
<Trans
i18nKey="query.actions.groupMode.tooltip"
components={{ lineBreak: <br /> }}
/>
}
data-testid="group-results-tooltip"
>
Expand All @@ -82,18 +90,14 @@
data-testid="btn-change-group-mode"
>
<RiIcon size="m" type="GroupModeIcon" />
<Text size="s">Group results</Text>
<Text size="s">{t('query.actions.groupMode.label')}</Text>
</ToggleButton>
</RiTooltip>
)}
<QADivider orientation="vertical" colorVariable="separatorColor" />
<RiTooltip
position="left"
content={
isLoading
? 'Please wait while the commands are being executed…'
: KeyBoardTooltipContent
}
content={isLoading ? t('query.executing') : KeyBoardTooltipContent}
data-testid="run-query-tooltip"
>
<RunButton isLoading={isLoading} onSubmit={onSubmit} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 })}`
Comment thread
valkirilov marked this conversation as resolved.
}
return summaryText
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -75,6 +76,7 @@ export const getResultText = (
}

const QueryCardCliResultWrapper = (props: Props) => {
const { t } = useTranslation()
const {
result = [],
query,
Expand All @@ -99,16 +101,15 @@ const QueryCardCliResultWrapper = (props: Props) => {
<>
<CopyResultButton
copy={copyText}
aria-label="Copy result"
aria-label={t('query.cliResult.copy')}
data-testid="copy-result"
tooltipConfig={{ content: 'Copy result' }}
tooltipConfig={{ content: t('query.cliResult.copy') }}
/>
<div data-testid="query-cli-result" className={cx(styles.content)}>
{isNotStored && (
<Text className={styles.alert} data-testid="query-cli-warning">
<RiIcon type="ToastDangerIcon" className={styles.alertIcon} />
The result is too big to be saved. It will be deleted after the
application is closed.
{t('query.cliResult.tooBig')}
</Text>
)}
{isGroupResults(resultsMode) && isArray(result[0]?.response) ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -99,6 +101,7 @@ const getTruncatedExecutionTimeString = (value: number): string => {
}

const QueryCardHeader = (props: Props) => {
const { t } = useTranslation()
const {
isOpen,
toggleOpen,
Expand Down Expand Up @@ -339,7 +342,7 @@ const QueryCardHeader = (props: Props) => {
<CopyButton
copy={query || ''}
onCopy={handleCopy}
aria-label="Copy query"
aria-label={t('query.card.copyQuery.aria')}
disabled={emptyCommand}
withTooltip={false}
className={cx('copy-btn', styles.copyBtn)}
Expand Down Expand Up @@ -372,7 +375,7 @@ const QueryCardHeader = (props: Props) => {
>
{isNumber(executionTime) && (
<RiTooltip
title="Processing Time"
title={t('query.card.processingTime')}
content={getExecutionTimeString(executionTime)}
position="left"
anchorClassName={styles.executionTime}
Expand Down Expand Up @@ -452,11 +455,14 @@ const QueryCardHeader = (props: Props) => {
)}
</FlexItem>
<FlexItem className={styles.buttonIcon}>
<RiTooltip content="Clear result" position="left">
<RiTooltip
content={t('query.card.clearResult.tooltip')}
position="left"
>
<IconButton
disabled={loading || clearing}
icon={DeleteIcon}
aria-label="Delete command"
aria-label={t('query.card.delete.aria')}
data-testid="delete-command"
onClick={handleQueryDelete}
/>
Expand All @@ -465,14 +471,14 @@ const QueryCardHeader = (props: Props) => {
{!isFullScreen && (
<FlexItem className={cx(styles.buttonIcon, styles.playIcon)}>
<RiTooltip
content="Run again"
content={t('query.card.rerun.tooltip')}
position="left"
anchorClassName={cx(styles.buttonIcon, styles.playIcon)}
>
<IconButton
disabled={emptyCommand}
icon={PlayIcon}
aria-label="Re-run command"
aria-label={t('query.card.rerun.aria')}
data-testid="re-run-command"
onClick={handleQueryReRun}
/>
Expand All @@ -484,7 +490,7 @@ const QueryCardHeader = (props: Props) => {
{!isSilentModeWithoutError(resultsMode, summary?.fail) && (
<IconButton
icon={isOpen ? ChevronUpIcon : ChevronDownIcon}
aria-label="toggle collapse"
aria-label={t('query.card.toggleCollapse.aria')}
data-testid="toggle-collapse"
/>
)}
Expand All @@ -500,19 +506,19 @@ const QueryCardHeader = (props: Props) => {
{isGroupMode(resultsMode) && (
<ModeLabel data-testid="group-mode-tooltip">
<RiIcon type="GroupModeIcon" />
Group mode
{t('query.card.mode.group')}
</ModeLabel>
)}
{isSilentMode(resultsMode) && (
<ModeLabel data-testid="silent-mode-tooltip">
<RiIcon type="SilentModeIcon" />
Silent mode
{t('query.card.mode.silent')}
</ModeLabel>
)}
{isRawMode(mode) && (
<ModeLabel data-testid="raw-mode-tooltip">
<RiIcon type="RawModeIcon" />
Raw mode
{t('query.card.mode.raw')}
</ModeLabel>
)}
</>
Expand All @@ -522,7 +528,7 @@ const QueryCardHeader = (props: Props) => {
>
<IconButton
icon="MoreactionsIcon"
aria-label="Query parameters"
aria-label={t('query.card.queryParameters.aria')}
data-testid="parameters-anchor"
/>
</RiTooltip>
Expand Down
Loading
Loading