Internationalisation: Mark up @grafana/sql package (#105842)

* scaffolding for package

* crowdin scaffolding

* markup

* add translations

* fix locale location

* fix tsconfig?

* undo bundler change

* object tranlsations, expose loadResources and call in mssql

* prettier

* remove useTranslate

* extract translations

* last couple of fixes

* remove deleted files
This commit is contained in:
Ashley Harrison
2025-06-12 10:52:04 +01:00
committed by GitHub
parent c42c89d5e4
commit 84eafb9a56
31 changed files with 405 additions and 89 deletions

View File

@ -140,6 +140,8 @@ endif
i18n-extract: i18n-extract-enterprise
@echo "Extracting i18n strings for OSS"
yarn run i18next --config public/locales/i18next-parser.config.cjs
@echo "Extracting i18n strings for packages"
yarn run packages:i18n-extract
@echo "Extracting i18n strings for plugins"
yarn run plugin:i18n-extract

View File

@ -21,4 +21,10 @@ files: [
"type": "i18next_json",
"dest": "plugins/mssql/en-US/%original_file_name%"
},
{
"source": "packages/grafana-sql/src/locales/en-US/grafana-sql.json",
"translation": "packages/grafana-sql/src/locales/%locale%/%original_file_name%",
"type": "i18next_json",
"dest": "packages/grafana-sql/en-US/%original_file_name%"
},
]

View File

@ -304,6 +304,7 @@ module.exports = [
files: [
'public/app/!(plugins)/**/*.{ts,tsx,js,jsx}',
'packages/grafana-ui/**/*.{ts,tsx,js,jsx}',
'packages/grafana-sql/**/*.{ts,tsx,js,jsx}',
...pluginsToTranslate.map((plugin) => `${plugin}/**/*.{ts,tsx,js,jsx}`),
],
ignores: [

View File

@ -38,6 +38,7 @@
"lint:fix": "yarn lint:ts --fix",
"packages:build": "nx run-many -t build --projects='tag:scope:package'",
"packages:clean": "rimraf ./npm-artifacts && nx run-many -t clean --projects='tag:scope:package' --maxParallel=100",
"packages:i18n-extract": "nx run-many -t i18n-extract --projects='tag:scope:package'",
"packages:prepare": "lerna version --no-push --no-git-tag-version --force-publish --exact",
"packages:pack": "mkdir -p ./npm-artifacts && lerna exec --no-private -- yarn pack --out \"../../npm-artifacts/%s-%v.tgz\"",
"packages:typecheck": "nx run-many -t typecheck --projects='tag:scope:package'",

View File

@ -11,12 +11,14 @@
},
"main": "src/index.ts",
"scripts": {
"typecheck": "tsc --emitDeclarationOnly false --noEmit"
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
"i18n-extract": "i18next --config src/locales/i18next-parser.config.cjs"
},
"dependencies": {
"@emotion/css": "11.13.5",
"@grafana/data": "12.1.0-pre",
"@grafana/e2e-selectors": "12.1.0-pre",
"@grafana/i18n": "12.1.0-pre",
"@grafana/plugin-ui": "0.10.6",
"@grafana/runtime": "12.1.0-pre",
"@grafana/ui": "12.1.0-pre",
@ -47,6 +49,7 @@
"@types/react-virtualized-auto-sizer": "1.0.4",
"@types/systemjs": "6.15.1",
"@types/uuid": "10.0.0",
"i18next-parser": "9.3.0",
"jest": "^29.6.4",
"ts-jest": "29.2.5",
"ts-node": "10.9.2",

View File

@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { useRef, useEffect } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { Button, Icon, Modal, useStyles2 } from '@grafana/ui';
type ConfirmModalProps = {
@ -27,26 +28,32 @@ export function ConfirmModal({ isOpen, onCancel, onDiscard, onCopy }: ConfirmMod
title={
<div className={styles.modalHeaderTitle}>
<Icon name="exclamation-triangle" size="lg" />
<span className={styles.titleText}>Warning</span>
<span className={styles.titleText}>
<Trans i18nKey="components.confirm-modal.warning">Warning</Trans>
</span>
</div>
}
onDismiss={onCancel}
isOpen={isOpen}
>
<p>
Builder mode does not display changes made in code. The query builder will display the last changes you made in
builder mode.
<Trans i18nKey="components.confirm-modal.builder-mode">
Builder mode does not display changes made in code. The query builder will display the last changes you made
in builder mode.
</Trans>
</p>
<p>
<Trans i18nKey="components.confirm-modal.clipboard">Do you want to copy your code to the clipboard?</Trans>
</p>
<p>Do you want to copy your code to the clipboard?</p>
<Modal.ButtonRow>
<Button type="button" variant="secondary" onClick={onCancel} fill="outline">
Cancel
<Trans i18nKey="components.confirm-modal.cancel">Cancel</Trans>
</Button>
<Button variant="destructive" type="button" onClick={onDiscard} ref={buttonRef}>
Discard code and switch
<Trans i18nKey="components.confirm-modal.discard-code-and-switch">Discard code and switch</Trans>
</Button>
<Button variant="primary" onClick={onCopy}>
Copy code and switch
<Trans i18nKey="components.confirm-modal.copy-code-and-switch">Copy code and switch</Trans>
</Button>
</Modal.ButtonRow>
</Modal>

View File

@ -2,6 +2,7 @@ import { useEffect } from 'react';
import { useAsync } from 'react-use';
import { SelectableValue } from '@grafana/data';
import { t } from '@grafana/i18n';
import { Select } from '@grafana/ui';
import { DB, ResourceSelectorProps, SQLDialect, toOption } from '../types';
@ -75,7 +76,7 @@ export const DatasetSelector = ({
return (
<Select
aria-label="Dataset selector"
aria-label={t('components.dataset-selector.aria-label-dataset-selector', 'Dataset selector')}
inputId={inputId}
value={dataset}
options={state.value}

View File

@ -1,5 +1,7 @@
import * as React from 'react';
import { Trans } from '@grafana/i18n';
type Props = {
fallBackComponent?: React.ReactNode;
};
@ -16,7 +18,11 @@ export class ErrorBoundary extends React.Component<React.PropsWithChildren<Props
render() {
if (this.state.hasError) {
const FallBack = this.props.fallBackComponent || <div>Error</div>;
const FallBack = this.props.fallBackComponent || (
<div>
<Trans i18nKey="components.error-boundary.fall-back.error">Error</Trans>
</div>
);
return FallBack;
}

View File

@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { lazy, Suspense } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n';
import { LoadingPlaceholder, useStyles2 } from '@grafana/ui';
import type { SqlQueryEditorProps } from './QueryEditor';
@ -11,7 +12,14 @@ export function SqlQueryEditorLazy(props: SqlQueryEditorProps) {
const styles = useStyles2(getStyles);
return (
<Suspense fallback={<LoadingPlaceholder text={'Loading editor'} className={styles.container} />}>
<Suspense
fallback={
<LoadingPlaceholder
text={t('components.sql-query-editor-lazy.text-loading-editor', 'Loading editor')}
className={styles.container}
/>
}
>
<QueryEditor {...props} />
</Suspense>
);

View File

@ -3,6 +3,7 @@ import { useCopyToClipboard } from 'react-use';
import { SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { t, Trans } from '@grafana/i18n';
import { EditorField, EditorHeader, EditorMode, EditorRow, FlexItem, InlineSelect } from '@grafana/plugin-ui';
import { reportInteraction } from '@grafana/runtime';
import { Button, InlineSwitch, RadioButtonGroup, Tooltip, Space } from '@grafana/ui';
@ -126,9 +127,9 @@ export function QueryHeader({
<>
<EditorHeader>
<InlineSelect
label="Format"
label={t('components.query-header.label-format', 'Format')}
value={query.format}
placeholder="Select format"
placeholder={t('components.query-header.placeholder-select-format', 'Select format')}
menuShouldPortal
onChange={onFormatChange}
options={QUERY_FORMAT_OPTIONS}
@ -138,7 +139,7 @@ export function QueryHeader({
<>
<InlineSwitch
id={`sql-filter-${htmlId}`}
label="Filter"
label={t('components.query-header.label-filter', 'Filter')}
data-testid={selectors.components.SQLQueryEditor.headerFilterSwitch}
transparent={true}
showLabel={true}
@ -159,7 +160,7 @@ export function QueryHeader({
<InlineSwitch
id={`sql-group-${htmlId}`}
label="Group"
label={t('components.query-header.label-group', 'Group')}
data-testid={selectors.components.SQLQueryEditor.headerGroupSwitch}
transparent={true}
showLabel={true}
@ -180,7 +181,7 @@ export function QueryHeader({
<InlineSwitch
id={`sql-order-${htmlId}`}
label="Order"
label={t('components.query-header.label-order', 'Order')}
data-testid={selectors.components.SQLQueryEditor.headerOrderSwitch}
transparent={true}
showLabel={true}
@ -201,7 +202,7 @@ export function QueryHeader({
<InlineSwitch
id={`sql-preview-${htmlId}`}
label="Preview"
label={t('components.query-header.label-preview', 'Preview')}
data-testid={selectors.components.SQLQueryEditor.headerPreviewSwitch}
transparent={true}
showLabel={true}
@ -226,21 +227,21 @@ export function QueryHeader({
{isQueryRunnable ? (
<Button icon="play" variant="primary" size="sm" onClick={() => onRunQuery()}>
Run query
<Trans i18nKey="components.query-header.run-query">Run query</Trans>
</Button>
) : (
<Tooltip
theme="error"
content={
<>
<Trans i18nKey="components.query-header.content-invalid-query">
Your query is invalid. Check below for details. <br />
However, you can still run this query.
</>
</Trans>
}
placement="top"
>
<Button icon="exclamation-triangle" variant="secondary" size="sm" onClick={() => onRunQuery()}>
Run query
<Trans i18nKey="components.query-header.run-query">Run query</Trans>
</Button>
</Tooltip>
)}
@ -295,7 +296,7 @@ export function QueryHeader({
<Space v={0.5} />
<EditorRow>
{datasetDropdownIsAvailable() && (
<EditorField label="Dataset" width={25}>
<EditorField label={t('components.query-header.label-dataset', 'Dataset')} width={25}>
<DatasetSelector
db={db}
inputId={`sql-dataset-${htmlId}`}
@ -306,7 +307,7 @@ export function QueryHeader({
/>
</EditorField>
)}
<EditorField label="Table" width={25}>
<EditorField label={t('components.query-header.label-table', 'Table')} width={25}>
<TableSelector
db={db}
inputId={`sql-tableselect-${htmlId}`}

View File

@ -2,6 +2,7 @@ import { useAsync } from 'react-use';
import { SelectableValue, toOption } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { t } from '@grafana/i18n';
import { Select } from '@grafana/ui';
import { DB, ResourceSelectorProps } from '../types';
@ -29,7 +30,7 @@ export const TableSelector = ({ db, dataset, table, className, onChange, inputId
<Select
className={className}
disabled={state.loading}
aria-label="Table selector"
aria-label={t('components.table-selector.aria-label-table-selector', 'Table selector')}
inputId={inputId}
data-testid={selectors.components.SQLQueryEditor.headerTableSelector}
value={table}
@ -37,7 +38,11 @@ export const TableSelector = ({ db, dataset, table, className, onChange, inputId
onChange={onChange}
isLoading={state.loading}
menuShouldPortal={true}
placeholder={state.loading ? 'Loading tables' : 'Select table'}
placeholder={
state.loading
? t('components.table-selector.placeholder-loading', 'Loading tables')
: t('components.table-selector.placeholder-select-table', 'Select table')
}
allowCustomValue={true}
/>
);

View File

@ -1,4 +1,5 @@
import { DataSourceSettings } from '@grafana/data';
import { t, Trans } from '@grafana/i18n';
import { ConfigSubSection } from '@grafana/plugin-ui';
import { config } from '@grafana/runtime';
import { Field, Icon, InlineLabel, Label, Stack, Switch, Tooltip } from '@grafana/ui';
@ -83,19 +84,23 @@ export const ConnectionLimits = <T extends SQLConnectionLimits>(props: Props<T>)
const labelWidth = 40;
return (
<ConfigSubSection title="Connection limits">
<ConfigSubSection title={t('components.connection-limits.title-connection-limits', 'Connection limits')}>
<Field
label={
<Label>
<Stack gap={0.5}>
<span>Max open</span>
<span>
<Trans i18nKey="components.connection-limits.max-open">Max open</Trans>
</span>
<Tooltip
content={
<span>
<Trans i18nKey="components.connection-limits.content-max-open">
The maximum number of open connections to the database. If <i>Max idle connections</i> is greater
than 0 and the <i>Max open connections</i> is less than <i>Max idle connections</i>, then
<i>Max idle connections</i> will be reduced to match the <i>Max open connections</i> limit. If set
to 0, there is no limit on the number of open connections.
</Trans>
</span>
}
>
@ -119,13 +124,20 @@ export const ConnectionLimits = <T extends SQLConnectionLimits>(props: Props<T>)
label={
<Label>
<Stack gap={0.5}>
<span>Auto max idle</span>
<span>
<Trans i18nKey="components.connection-limits.auto-max-idle">Auto max idle</Trans>
</span>
<Tooltip
content={
<span>
<Trans
i18nKey="components.connection-limits.content-auto-max-idle"
values={{ defaultMaxIdle: config.sqlConnectionLimits.maxIdleConns }}
>
If enabled, automatically set the number of <i>Maximum idle connections</i> to the same value as
<i> Max open connections</i>. If the number of maximum open connections is not set it will be set to
the default ({config.sqlConnectionLimits.maxIdleConns}).
<i> Max open connections</i>. If the number of maximum open connections is not set it will be set
to the default ({'{{defaultMaxIdle}}'}).
</Trans>
</span>
}
>
@ -142,14 +154,18 @@ export const ConnectionLimits = <T extends SQLConnectionLimits>(props: Props<T>)
label={
<Label>
<Stack gap={0.5}>
<span>Max idle</span>
<span>
<Trans i18nKey="components.connection-limits.max-idle">Max idle</Trans>
</span>
<Tooltip
content={
<span>
<Trans i18nKey="components.connection-limits.content-max-idle">
The maximum number of connections in the idle connection pool.If <i>Max open connections</i> is
greater than 0 but less than the <i>Max idle connections</i>, then the <i>Max idle connections</i>{' '}
will be reduced to match the <i>Max open connections</i> limit. If set to 0, no idle connections are
retained.
will be reduced to match the <i>Max open connections</i> limit. If set to 0, no idle connections
are retained.
</Trans>
</span>
}
>
@ -177,12 +193,16 @@ export const ConnectionLimits = <T extends SQLConnectionLimits>(props: Props<T>)
label={
<Label>
<Stack gap={0.5}>
<span>Max lifetime</span>
<span>
<Trans i18nKey="components.connection-limits.max-lifetime">Max lifetime</Trans>
</span>
<Tooltip
content={
<span>
<Trans i18nKey="components.connection-limits.content-max-lifetime">
The maximum amount of time in seconds a connection may be reused. If set to 0, connections are
reused forever.
</Trans>
</span>
}
>

View File

@ -5,6 +5,7 @@ import {
onUpdateDatasourceSecureJsonDataOption,
updateDatasourcePluginResetOption,
} from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { Field, Icon, Label, SecretTextArea, Tooltip, Stack } from '@grafana/ui';
export interface Props<T extends DataSourceJsonData, S> {
@ -25,11 +26,17 @@ export const TLSSecretsConfig = <T extends DataSourceJsonData, S extends {} = {}
label={
<Label>
<Stack gap={0.5}>
<span>TLS/SSL Client Certificate</span>
<span>
<Trans i18nKey="components.tlssecrets-config.tlsssl-client-certificate">
TLS/SSL Client Certificate
</Trans>
</span>
<Tooltip
content={
<span>
<Trans i18nKey="components.tlssecrets-config.content-tlsssl-client-certificate">
To authenticate with an TLS/SSL client certificate, provide the client certificate here.
</Trans>
</span>
}
>
@ -40,6 +47,7 @@ export const TLSSecretsConfig = <T extends DataSourceJsonData, S extends {} = {}
}
>
<SecretTextArea
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
placeholder="-----BEGIN CERTIFICATE-----"
cols={45}
rows={7}
@ -56,10 +64,16 @@ export const TLSSecretsConfig = <T extends DataSourceJsonData, S extends {} = {}
label={
<Label>
<Stack gap={0.5}>
<span>TLS/SSL Root Certificate</span>
<span>
<Trans i18nKey="components.tlssecrets-config.tlsssl-root-certificate">TLS/SSL Root Certificate</Trans>
</span>
<Tooltip
content={
<span>If the selected TLS/SSL mode requires a server root certificate, provide it here.</span>
<span>
<Trans i18nKey="components.tlssecrets-config.content-tlsssl-root-certificate">
If the selected TLS/SSL mode requires a server root certificate, provide it here
</Trans>
</span>
}
>
<Icon name="info-circle" size="sm" />
@ -69,6 +83,7 @@ export const TLSSecretsConfig = <T extends DataSourceJsonData, S extends {} = {}
}
>
<SecretTextArea
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
placeholder="-----BEGIN CERTIFICATE-----"
cols={45}
rows={7}
@ -85,9 +100,17 @@ export const TLSSecretsConfig = <T extends DataSourceJsonData, S extends {} = {}
label={
<Label>
<Stack gap={0.5}>
<span>TLS/SSL Client Key</span>
<span>
<Trans i18nKey="components.tlssecrets-config.tlsssl-client-key">TLS/SSL Client Key</Trans>
</span>
<Tooltip
content={<span>To authenticate with a client TLS/SSL certificate, provide the key here.</span>}
content={
<span>
<Trans i18nKey="components.tlssecrets-config.content-tlsssl-client-key">
To authenticate with a client TLS/SSL certificate, provide the key here.
</Trans>
</span>
}
>
<Icon name="info-circle" size="sm" />
</Tooltip>
@ -96,6 +119,7 @@ export const TLSSecretsConfig = <T extends DataSourceJsonData, S extends {} = {}
}
>
<SecretTextArea
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
placeholder="-----BEGIN RSA PRIVATE KEY-----"
cols={45}
rows={7}

View File

@ -1,6 +1,7 @@
import { css } from '@emotion/css';
import { useMemo, useState } from 'react';
import { t } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import { HorizontalGroup, Icon, IconButton, Tooltip, useTheme2 } from '@grafana/ui';
@ -80,7 +81,7 @@ export function QueryToolbox({ showTools, onFormatCode, onExpand, isExpanded, ..
}}
name="brackets-curly"
size="xs"
tooltip="Format query"
tooltip={t('components.query-toolbox.tooltip-format-query', 'Format query')}
/>
)}
{onExpand && (
@ -95,10 +96,19 @@ export function QueryToolbox({ showTools, onFormatCode, onExpand, isExpanded, ..
}}
name={isExpanded ? 'angle-up' : 'angle-down'}
size="xs"
tooltip={isExpanded ? 'Collapse editor' : 'Expand editor'}
tooltip={
isExpanded
? t('components.query-toolbox.tooltip-collapse', 'Collapse editor')
: t('components.query-toolbox.tooltip-expand', 'Expand editor')
}
/>
)}
<Tooltip content="Hit CTRL/CMD+Return to run query">
<Tooltip
content={t(
'components.query-toolbox.content-hit-ctrlcmdreturn-to-run-query',
'Hit CTRL/CMD+Return to run query'
)}
>
<Icon className={styles.hint} name="keyboard" />
</Tooltip>
</HorizontalGroup>

View File

@ -3,6 +3,7 @@ import { useState, useMemo, useEffect } from 'react';
import { useAsyncFn, useDebounce } from 'react-use';
import { formattedValueToString, getValueFormat, TimeRange } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { Icon, Spinner, useTheme2 } from '@grafana/ui';
import { DB, SQLQuery, ValidationResults } from '../../types';
@ -78,7 +79,8 @@ export function QueryValidator({ db, query, onValidate, range }: QueryValidatorP
<>
{state.loading && (
<div className={styles.info}>
<Spinner inline={true} size="xs" /> Validating query...
<Spinner inline={true} size="xs" />{' '}
<Trans i18nKey="components.query-validator.validating-query">Validating query...</Trans>
</div>
)}
{!state.loading && state.value && (
@ -86,9 +88,12 @@ export function QueryValidator({ db, query, onValidate, range }: QueryValidatorP
<>
{state.value.isValid && state.value.statistics && (
<div className={styles.valid}>
<Icon name="check" /> This query will process{' '}
<strong>{formattedValueToString(valueFormatter(state.value.statistics.TotalBytesProcessed))}</strong>{' '}
when run.
<Trans
i18nKey="components.query-validator.query-will-process"
values={{ bytes: formattedValueToString(valueFormatter(state.value.statistics.TotalBytesProcessed)) }}
>
<Icon name="check" /> This query will process <strong>{'{{bytes}}'}</strong> when run.
</Trans>
</div>
)}
</>

View File

@ -4,6 +4,7 @@ import { useMeasure } from 'react-use';
import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import { Modal, useStyles2, useTheme2 } from '@grafana/ui';
@ -81,7 +82,9 @@ export function RawEditor({ db, query, onChange, onRunQuery, onValidate, queryTo
justifyContent: 'center',
}}
>
<Trans i18nKey="components.raw-editor.render-placeholder.editing-in-expanded-code-editor">
Editing in expanded code editor
</Trans>
</div>
);
};
@ -91,7 +94,7 @@ export function RawEditor({ db, query, onChange, onRunQuery, onValidate, queryTo
{isExpanded ? renderPlaceholder() : renderEditor()}
{isExpanded && (
<Modal
title={`Query ${query.refId}`}
title={t('components.raw-editor.title-query-num', 'Query {{queryNum}}', { queryNum: query.refId })}
closeOnBackdropClick={false}
closeOnEscape={false}
className={styles.modal}

View File

@ -17,6 +17,7 @@ import { isString } from 'lodash';
import { dateTime, toOption } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { t } from '@grafana/i18n';
import { Button, DateTimePicker, Input, Select } from '@grafana/ui';
const buttonLabels = {
@ -68,7 +69,7 @@ export const widgets: Widgets = {
return (
<Select
id={props.id}
aria-label="Macros value selector"
aria-label={t('components.widgets.aria-label-macros-value-selector', 'Macros value selector')}
menuShouldPortal
options={macros.map(toOption)}
value={props?.value}
@ -124,7 +125,7 @@ export const settings: Settings = {
return (
<Select
id={conjProps?.id}
aria-label="Conjunction"
aria-label={t('components.settings.aria-label-conjunction', 'Conjunction')}
data-testid={selectors.components.SQLQueryEditor.filterConjunction}
menuShouldPortal
options={conjProps?.conjunctionOptions ? Object.keys(conjProps?.conjunctionOptions).map(toOption) : undefined}
@ -140,7 +141,7 @@ export const settings: Settings = {
<Select
id={fieldProps?.id}
width={25}
aria-label="Field"
aria-label={t('components.settings.aria-label-field', 'Field')}
data-testid={selectors.components.SQLQueryEditor.filterField}
menuShouldPortal
options={fieldProps?.items.map((f) => {
@ -164,7 +165,9 @@ export const settings: Settings = {
return (
<Button
type="button"
title={`${buttonProps?.label} filter`}
title={t('components.settings.title-button-filter', '{{ buttonLabel }} filter', {
buttonLabel: buttonProps?.label,
})}
onClick={buttonProps?.onClick}
variant="secondary"
size="md"
@ -177,7 +180,7 @@ export const settings: Settings = {
return (
<Select
options={operatorProps?.items.map((op) => ({ label: op.label, value: op.key }))}
aria-label="Operator"
aria-label={t('components.settings.aria-label-operator', 'Operator')}
data-testid={selectors.components.SQLQueryEditor.filterOperator}
menuShouldPortal
value={operatorProps?.selectedKey}
@ -301,7 +304,7 @@ function getCustomOperators(config: BasicConfig) {
sqlFormatOp: customSqlNotInFormatter,
},
[Op.MACROS]: {
label: 'Macros',
label: t('components.get-custom-operators.custom-operators.label.macros', 'Macros'),
sqlFormatOp: (field: string, _operator: string, value: string | string[] | ImmutableList<string>) => {
if (value === TIME_FILTER) {
return `$__timeFilter(${field})`;

View File

@ -1,6 +1,7 @@
import { useCallback } from 'react';
import { SelectableValue, toOption } from '@grafana/data';
import { t } from '@grafana/i18n';
import { AccessoryButton, EditorList, InputGroup } from '@grafana/plugin-ui';
import { Select } from '@grafana/ui';
@ -46,12 +47,20 @@ function makeRenderColumn({ options }: { options?: Array<SelectableValue<string>
<InputGroup>
<Select
value={item.property?.name ? toOption(item.property.name) : null}
aria-label="Group by"
aria-label={t('components.make-render-column.render-column.aria-label-group-by', 'Group by')}
options={options}
menuShouldPortal
onChange={({ value }) => value && onChangeItem(setGroupByField(value))}
/>
<AccessoryButton title="Remove group by column" icon="times" variant="secondary" onClick={onDeleteItem} />
<AccessoryButton
title={t(
'components.make-render-column.render-column.title-remove-group-by-column',
'Remove group by column'
)}
icon="times"
variant="secondary"
onClick={onDeleteItem}
/>
</InputGroup>
);
};

View File

@ -3,6 +3,7 @@ import { useCallback } from 'react';
import * as React from 'react';
import { SelectableValue, toOption } from '@grafana/data';
import { t } from '@grafana/i18n';
import { EditorField, InputGroup } from '@grafana/plugin-ui';
import { Input, RadioButtonGroup, Select, Space } from '@grafana/ui';
@ -59,10 +60,10 @@ export function OrderByRow({ sql, onSqlChange, columns, showOffset }: OrderByRow
return (
<>
<EditorField label="Order by" width={25}>
<EditorField label={t('components.order-by-row.label-order-by', 'Order by')} width={25}>
<InputGroup>
<Select
aria-label="Order by"
aria-label={t('components.order-by-row.aria-label-order-by', 'Order by')}
options={columns}
value={sql.orderBy?.property.name ? toOption(sql.orderBy.property.name) : null}
isClearable
@ -80,11 +81,11 @@ export function OrderByRow({ sql, onSqlChange, columns, showOffset }: OrderByRow
/>
</InputGroup>
</EditorField>
<EditorField label="Limit" optional width={25}>
<EditorField label={t('components.order-by-row.label-limit', 'Limit')} optional width={25}>
<Input type="number" min={0} id={uniqueId('limit-')} value={sql.limit || ''} onChange={onLimitChange} />
</EditorField>
{showOffset && (
<EditorField label="Offset" optional width={25}>
<EditorField label={t('components.order-by-row.label-offset', 'Offset')} optional width={25}>
<Input type="number" id={uniqueId('offset-')} value={sql.offset || ''} onChange={onOffsetChange} />
</EditorField>
)}

View File

@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { useCopyToClipboard } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import { CodeEditor, Field, IconButton, useStyles2 } from '@grafana/ui';
@ -26,8 +27,14 @@ export function Preview({ rawSql, datasourceType }: PreviewProps) {
const labelElement = (
<div className={styles.labelWrapper}>
<span className={styles.label}>Preview</span>
<IconButton tooltip="Copy to clipboard" onClick={() => copyPreview(rawSql)} name="copy" />
<span className={styles.label}>
<Trans i18nKey="components.preview.label-element.preview">Preview</Trans>
</span>
<IconButton
tooltip={t('components.preview.label-element.tooltip-copy-to-clipboard', 'Copy to clipboard')}
onClick={() => copyPreview(rawSql)}
name="copy"
/>
</div>
);

View File

@ -2,6 +2,7 @@ import { useId } from 'react';
import { SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { t } from '@grafana/i18n';
import { EditorField } from '@grafana/plugin-ui';
import { Select } from '@grafana/ui';
@ -15,7 +16,7 @@ export function SelectColumn({ columns, onParameterChange, value }: Props) {
const selectInputId = useId();
return (
<EditorField label="Column" width={25}>
<EditorField label={t('components.select-column.label-column', 'Column')} width={25}>
<Select
value={value}
data-testid={selectors.components.SQLQueryEditor.selectColumn}

View File

@ -3,6 +3,7 @@ import { useCallback } from 'react';
import { SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { t } from '@grafana/i18n';
import { Button, InlineLabel, Input, Stack, useStyles2 } from '@grafana/ui';
import { QueryEditorExpressionType } from '../../expressions';
@ -85,11 +86,18 @@ export function SelectCustomFunctionParameters({
<Input
onChange={(e) => onParameterChange(index)(e.currentTarget.value)}
value={param.name}
aria-label={`Parameter ${index} for column ${columnIndex}`}
aria-label={t(
'components.select-custom-function-parameters.aria-label-parameter',
'Parameter {{index}} for column {{columnIndex}}',
{ index, columnIndex }
)}
data-testid={selectors.components.SQLQueryEditor.selectInputParameter}
addonAfter={
<Button
title="Remove parameter"
title={t(
'components.select-custom-function-parameters.render-parameters.params.title-remove-parameter',
'Remove parameter'
)}
type="button"
icon="times"
variant="secondary"
@ -119,7 +127,7 @@ export function SelectCustomFunctionParameters({
variant="secondary"
size="md"
icon="plus"
title="Add parameter"
title={t('components.select-custom-function-parameters.title-add-parameter', 'Add parameter')}
/>
<InlineLabel className={styles.label}>)</InlineLabel>
</>

View File

@ -4,6 +4,7 @@ import { useCallback } from 'react';
import { SelectableValue, toOption } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { t } from '@grafana/i18n';
import { EditorField } from '@grafana/plugin-ui';
import { Button, Select, Stack, useStyles2 } from '@grafana/ui';
@ -29,8 +30,8 @@ export function SelectRow({ query, onQueryChange, db, columns }: SelectRowProps)
// Add necessary alias options for time series format
// when that format has been selected
if (query.format === QueryFormat.Timeseries) {
timeSeriesAliasOpts.push({ label: 'time', value: 'time' });
timeSeriesAliasOpts.push({ label: 'value', value: 'value' });
timeSeriesAliasOpts.push({ label: t('components.select-row.label.time', 'time'), value: 'time' });
timeSeriesAliasOpts.push({ label: t('components.select-row.label.value', 'value'), value: 'value' });
}
const onAggregationChange = useCallback(
@ -92,8 +93,8 @@ export function SelectRow({ query, onQueryChange, db, columns }: SelectRowProps)
const aggregateOptions = () => {
const options: Array<SelectableValue<string>> = [
{ label: 'Aggregations', options: [] },
{ label: 'Macros', options: [] },
{ label: t('components.select-row.aggregate-options.options.label.aggregations', 'Aggregations'), options: [] },
{ label: t('components.select-row.aggregate-options.options.label.macros', 'Macros'), options: [] },
];
for (const func of db.functions()) {
// Create groups for macros
@ -111,7 +112,11 @@ export function SelectRow({ query, onQueryChange, db, columns }: SelectRowProps)
{query.sql?.columns?.map((item, index) => (
<div key={index}>
<Stack gap={2} alignItems="end">
<EditorField label="Data operations" optional width={25}>
<EditorField
label={t('components.select-row.label-data-operations', 'Data operations')}
optional
width={25}
>
<Select
value={item.name ? toOption(item.name) : null}
inputId={`select-aggregation-${index}-${uniqueId()}`}
@ -132,7 +137,7 @@ export function SelectRow({ query, onQueryChange, db, columns }: SelectRowProps)
db={db}
/>
<EditorField label="Alias" optional width={15}>
<EditorField label={t('components.select-row.label-alias', 'Alias')} optional width={15}>
<Select
value={item.alias ? toOption(item.alias) : null}
inputId={`select-alias-${index}-${uniqueId()}`}
@ -145,7 +150,7 @@ export function SelectRow({ query, onQueryChange, db, columns }: SelectRowProps)
/>
</EditorField>
<Button
title="Remove column"
title={t('components.select-row.title-remove-column', 'Remove column')}
type="button"
icon="trash-alt"
variant="secondary"
@ -159,7 +164,7 @@ export function SelectRow({ query, onQueryChange, db, columns }: SelectRowProps)
type="button"
onClick={addColumn}
variant="secondary"
title="Add column"
title={t('components.select-row.title-add-column', 'Add column')}
size="md"
icon="plus"
className={styles.addButton}

View File

@ -1,5 +1,6 @@
import { useAsync } from 'react-use';
import { t } from '@grafana/i18n';
import { EditorRows, EditorRow, EditorField } from '@grafana/plugin-ui';
import { DB, QueryEditorProps, QueryRowFilter } from '../../types';
@ -31,14 +32,17 @@ export const VisualEditor = ({ query, db, queryRowFilter, onChange, onValidate,
</EditorRow>
{queryRowFilter.filter && (
<EditorRow>
<EditorField label="Filter by column value" optional>
<EditorField
label={t('components.visual-editor.label-filter-by-column-value', 'Filter by column value')}
optional
>
<SQLWhereRow fields={state.value || []} query={query} onQueryChange={onChange} db={db} />
</EditorField>
</EditorRow>
)}
{queryRowFilter.group && (
<EditorRow>
<EditorField label="Group by column">
<EditorField label={t('components.visual-editor.label-group-by-column', 'Group by column')}>
<SQLGroupByRow fields={state.value || []} query={query} onQueryChange={onChange} db={db} />
</EditorField>
</EditorRow>

View File

@ -23,3 +23,4 @@ export { createSelectClause, haveColumns } from './utils/sql.utils';
export { applyQueryDefaults } from './defaults';
export { makeVariable } from './utils/testHelpers';
export { QueryEditorExpressionType } from './expressions';
export { loadResources } from './loadResources';

View File

@ -0,0 +1,11 @@
import { LANGUAGES, ResourceLoader, Resources } from '@grafana/i18n';
const resources = LANGUAGES.reduce<Record<string, () => Promise<{ default: Resources }>>>((acc, lang) => {
acc[lang.code] = async () => await import(`./locales/${lang.code}/grafana-sql.json`);
return acc;
}, {});
export const loadResources: ResourceLoader = async (resolvedLanguage: string) => {
const translation = await resources[resolvedLanguage]();
return translation.default;
};

View File

@ -0,0 +1,148 @@
{
"components": {
"confirm-modal": {
"builder-mode": "Builder mode does not display changes made in code. The query builder will display the last changes you made in builder mode.",
"cancel": "Cancel",
"clipboard": "Do you want to copy your code to the clipboard?",
"copy-code-and-switch": "Copy code and switch",
"discard-code-and-switch": "Discard code and switch",
"warning": "Warning"
},
"connection-limits": {
"auto-max-idle": "Auto max idle",
"content-auto-max-idle": "If enabled, automatically set the number of <1>Maximum idle connections</1> to the same value as<3> Max open connections</3>. If the number of maximum open connections is not set it will be set to the default ({{defaultMaxIdle}}).",
"content-max-idle": "The maximum number of connections in the idle connection pool.If <1>Max open connections</1> is greater than 0 but less than the <3>Max idle connections</3>, then the <5>Max idle connections</5> will be reduced to match the <8>Max open connections</8> limit. If set to 0, no idle connections are retained.",
"content-max-lifetime": "The maximum amount of time in seconds a connection may be reused. If set to 0, connections are reused forever.",
"content-max-open": "The maximum number of open connections to the database. If <1>Max idle connections</1> is greater than 0 and the <3>Max open connections</3> is less than <5>Max idle connections</5>, then<7>Max idle connections</7> will be reduced to match the <9>Max open connections</9> limit. If set to 0, there is no limit on the number of open connections.",
"max-idle": "Max idle",
"max-lifetime": "Max lifetime",
"max-open": "Max open",
"title-connection-limits": "Connection limits"
},
"dataset-selector": {
"aria-label-dataset-selector": "Dataset selector"
},
"error-boundary": {
"fall-back": {
"error": "Error"
}
},
"get-custom-operators": {
"custom-operators": {
"label": {
"macros": "Macros"
}
}
},
"make-render-column": {
"render-column": {
"aria-label-group-by": "Group by",
"title-remove-group-by-column": "Remove group by column"
}
},
"order-by-row": {
"aria-label-order-by": "Order by",
"label-limit": "Limit",
"label-offset": "Offset",
"label-order-by": "Order by"
},
"preview": {
"label-element": {
"preview": "Preview",
"tooltip-copy-to-clipboard": "Copy to clipboard"
}
},
"query-header": {
"content-invalid-query": "Your query is invalid. Check below for details. <1></1>However, you can still run this query.",
"label-dataset": "Dataset",
"label-filter": "Filter",
"label-format": "Format",
"label-group": "Group",
"label-order": "Order",
"label-preview": "Preview",
"label-table": "Table",
"placeholder-select-format": "Select format",
"run-query": "Run query"
},
"query-toolbox": {
"content-hit-ctrlcmdreturn-to-run-query": "Hit CTRL/CMD+Return to run query",
"tooltip-collapse": "Collapse editor",
"tooltip-expand": "Expand editor",
"tooltip-format-query": "Format query"
},
"query-validator": {
"query-will-process": "<0></0> This query will process <2>{{bytes}}</2> when run.",
"validating-query": "Validating query..."
},
"raw-editor": {
"render-placeholder": {
"editing-in-expanded-code-editor": "Editing in expanded code editor"
},
"title-query-num": "Query {{queryNum}}"
},
"select-column": {
"label-column": "Column"
},
"select-custom-function-parameters": {
"aria-label-parameter": "Parameter {{index}} for column {{columnIndex}}",
"render-parameters": {
"params": {
"title-remove-parameter": "Remove parameter"
}
},
"title-add-parameter": "Add parameter"
},
"select-row": {
"aggregate-options": {
"options": {
"label": {
"aggregations": "Aggregations",
"macros": "Macros"
}
}
},
"label": {
"time": "time",
"value": "value"
},
"label-alias": "Alias",
"label-data-operations": "Data operations",
"title-add-column": "Add column",
"title-remove-column": "Remove column"
},
"settings": {
"aria-label-conjunction": "Conjunction",
"aria-label-field": "Field",
"aria-label-operator": "Operator",
"title-button-filter": "{{ buttonLabel }} filter"
},
"sql-query-editor-lazy": {
"text-loading-editor": "Loading editor"
},
"table-selector": {
"aria-label-table-selector": "Table selector",
"placeholder-loading": "Loading tables",
"placeholder-select-table": "Select table"
},
"tlssecrets-config": {
"content-tlsssl-client-certificate": "To authenticate with an TLS/SSL client certificate, provide the client certificate here.",
"content-tlsssl-client-key": "To authenticate with a client TLS/SSL certificate, provide the key here.",
"content-tlsssl-root-certificate": "If the selected TLS/SSL mode requires a server root certificate, provide it here",
"tlsssl-client-certificate": "TLS/SSL Client Certificate",
"tlsssl-client-key": "TLS/SSL Client Key",
"tlsssl-root-certificate": "TLS/SSL Root Certificate"
},
"visual-editor": {
"label-filter-by-column-value": "Filter by column value",
"label-group-by-column": "Group by column"
},
"widgets": {
"aria-label-macros-value-selector": "Macros value selector"
}
},
"utils": {
"get-columns-width-indices": {
"label-selected-columns": "Selected columns"
}
}
}

View File

@ -0,0 +1,12 @@
module.exports = {
locales: ['en-US'], // Only en-US is updated - Crowdin will PR with other languages
sort: true,
createOldCatalogs: false,
failOnWarnings: true,
verbose: false,
resetDefaultValueLocale: 'en-US', // Updates extracted values when they change in code
defaultNamespace: 'grafana-sql',
input: ['../**/*.{tsx,ts}'],
output: './src/locales/$LOCALE/$NAMESPACE.json',
};

View File

@ -1,4 +1,5 @@
import { SelectableValue } from '@grafana/data';
import { t } from '@grafana/i18n';
import { SQLQuery } from '../types';
@ -20,7 +21,7 @@ export function getColumnsWithIndices(query: SQLQuery, fields: SelectableValue[]
return [
{
value: '',
label: 'Selected columns',
label: t('utils.get-columns-width-indices.label-selected-columns', 'Selected columns'),
options,
expanded: true,
},

View File

@ -1,6 +1,6 @@
import { DataSourcePlugin } from '@grafana/data';
import { initPluginTranslations } from '@grafana/i18n';
import { SQLQuery, SqlQueryEditorLazy } from '@grafana/sql';
import { SQLQuery, SqlQueryEditorLazy, loadResources as loadSQLResources } from '@grafana/sql';
import { CheatSheet } from './CheatSheet';
import { ConfigurationEditor } from './configuration/ConfigurationEditor';
@ -8,7 +8,7 @@ import { MssqlDatasource } from './datasource';
import pluginJson from './plugin.json';
import { MssqlOptions } from './types';
initPluginTranslations(pluginJson.id);
initPluginTranslations(pluginJson.id, [loadSQLResources]);
export const plugin = new DataSourcePlugin<MssqlDatasource, SQLQuery, MssqlOptions>(MssqlDatasource)
.setQueryEditor(SqlQueryEditorLazy)

View File

@ -3572,6 +3572,7 @@ __metadata:
"@emotion/css": "npm:11.13.5"
"@grafana/data": "npm:12.1.0-pre"
"@grafana/e2e-selectors": "npm:12.1.0-pre"
"@grafana/i18n": "npm:12.1.0-pre"
"@grafana/plugin-ui": "npm:0.10.6"
"@grafana/runtime": "npm:12.1.0-pre"
"@grafana/tsconfig": "npm:^2.0.0"
@ -3589,6 +3590,7 @@ __metadata:
"@types/react-virtualized-auto-sizer": "npm:1.0.4"
"@types/systemjs": "npm:6.15.1"
"@types/uuid": "npm:10.0.0"
i18next-parser: "npm:9.3.0"
immutable: "npm:5.0.3"
jest: "npm:^29.6.4"
lodash: "npm:4.17.21"