chore: Localization of superset pt. 2 (#22772)

This commit is contained in:
Artem Shumeiko 2023-01-30 19:20:43 +03:00 committed by GitHub
parent b94052e438
commit c839d0daf5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 260 additions and 157 deletions

View File

@ -81,7 +81,7 @@ const columnWidth: ControlFormItemSpec<'InputNumber'> = {
"Default minimal column width in pixels, actual width may still be larger than this if other columns don't need much space", "Default minimal column width in pixels, actual width may still be larger than this if other columns don't need much space",
), ),
width: 120, width: 120,
placeholder: 'auto', placeholder: t('auto'),
debounceDelay: 400, debounceDelay: 400,
validators: [validateNumber], validators: [validateNumber],
}; };

View File

@ -19,7 +19,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { extent as d3Extent, range as d3Range } from 'd3-array'; import { extent as d3Extent, range as d3Range } from 'd3-array';
import { select as d3Select } from 'd3-selection'; import { select as d3Select } from 'd3-selection';
import { getSequentialSchemeRegistry } from '@superset-ui/core'; import { getSequentialSchemeRegistry, t } from '@superset-ui/core';
import CalHeatMap from './vendor/cal-heatmap'; import CalHeatMap from './vendor/cal-heatmap';
const propTypes = { const propTypes = {
@ -85,10 +85,12 @@ function Calendar(element, props) {
const metricsData = data.data; const metricsData = data.data;
const METRIC_TEXT = t('Metric');
Object.keys(metricsData).forEach(metric => { Object.keys(metricsData).forEach(metric => {
const calContainer = div.append('div'); const calContainer = div.append('div');
if (showMetricName) { if (showMetricName) {
calContainer.text(`Metric: ${verboseMap[metric] || metric}`); calContainer.text(`${METRIC_TEXT}: ${verboseMap[metric] || metric}`);
} }
const timestamps = metricsData[metric]; const timestamps = metricsData[metric];
const extents = d3Extent(Object.keys(timestamps), key => timestamps[key]); const extents = d3Extent(Object.keys(timestamps), key => timestamps[key]);

View File

@ -9,7 +9,7 @@
/* eslint-disable */ /* eslint-disable */
import d3tip from 'd3-tip'; import d3tip from 'd3-tip';
import { getContrastingColor } from '@superset-ui/core'; import { getContrastingColor, t } from '@superset-ui/core';
var d3 = typeof require === 'function' ? require('d3') : window.d3; var d3 = typeof require === 'function' ? require('d3') : window.d3;
@ -256,9 +256,9 @@ var CalHeatMap = function () {
// Formatting of the title displayed when hovering a legend cell // Formatting of the title displayed when hovering a legend cell
legendTitleFormat: { legendTitleFormat: {
lower: 'less than {min} {name}', lower: t('less than {min} {name}'),
inner: 'between {down} and {up} {name}', inner: t('between {down} and {up} {name}'),
upper: 'more than {max} {name}', upper: t('more than {max} {name}'),
}, },
// Animation duration, in ms // Animation duration, in ms

View File

@ -111,7 +111,7 @@ class CustomHistogram extends React.PureComponent {
renderTooltip={({ datum, color }) => ( renderTooltip={({ datum, color }) => (
<div> <div>
<strong style={{ color }}> <strong style={{ color }}>
{datum.bin0} to {datum.bin1} {datum.bin0} {t('to')} {datum.bin1}
</strong> </strong>
<div> <div>
<strong>{t('count')} </strong> <strong>{t('count')} </strong>

View File

@ -24,6 +24,7 @@ import {
NumberFormats, NumberFormats,
CategoricalColorNamespace, CategoricalColorNamespace,
getSequentialSchemeRegistry, getSequentialSchemeRegistry,
t,
} from '@superset-ui/core'; } from '@superset-ui/core';
import wrapSvgText from './utils/wrapSvgText'; import wrapSvgText from './utils/wrapSvgText';
@ -381,7 +382,10 @@ function Sunburst(element, props) {
.append('text') .append('text')
.attr('class', 'path-abs-percent') .attr('class', 'path-abs-percent')
.attr('y', yOffsets[offsetIndex]) .attr('y', yOffsets[offsetIndex])
.text(`${absolutePercString} of total`); // eslint-disable-next-line prefer-template
.text(absolutePercString + ' ' + t('of total'));
const OF_PARENT_TEXT = t('of parent');
if (conditionalPercString) { if (conditionalPercString) {
offsetIndex += 1; offsetIndex += 1;
@ -389,7 +393,7 @@ function Sunburst(element, props) {
.append('text') .append('text')
.attr('class', 'path-cond-percent') .attr('class', 'path-cond-percent')
.attr('y', yOffsets[offsetIndex]) .attr('y', yOffsets[offsetIndex])
.text(`${conditionalPercString} of parent`); .text(`${conditionalPercString} ${OF_PARENT_TEXT}`);
} }
offsetIndex += 1; offsetIndex += 1;

View File

@ -50,7 +50,7 @@ const radarMetricMaxValue: { name: string; config: ControlFormItemSpec } = {
'The maximum value of metrics. It is an optional configuration', 'The maximum value of metrics. It is an optional configuration',
), ),
width: 120, width: 120,
placeholder: 'auto', placeholder: t('auto'),
debounceDelay: 400, debounceDelay: 400,
validators: [validateNumber], validators: [validateNumber],
}, },

View File

@ -52,7 +52,7 @@ function createAxisTitleControl(axis: 'x' | 'y'): ControlSetRow[] {
const isXAxis = axis === 'x'; const isXAxis = axis === 'x';
const isVertical = (controls: ControlStateMapping) => const isVertical = (controls: ControlStateMapping) =>
Boolean(controls?.orientation.value === OrientationType.vertical); Boolean(controls?.orientation.value === OrientationType.vertical);
const isHorizental = (controls: ControlStateMapping) => const isHorizontal = (controls: ControlStateMapping) =>
Boolean(controls?.orientation.value === OrientationType.horizontal); Boolean(controls?.orientation.value === OrientationType.horizontal);
return [ return [
[ [
@ -65,7 +65,7 @@ function createAxisTitleControl(axis: 'x' | 'y'): ControlSetRow[] {
default: '', default: '',
description: t('Changing this control takes effect instantly'), description: t('Changing this control takes effect instantly'),
visibility: ({ controls }: ControlPanelsContainerProps) => visibility: ({ controls }: ControlPanelsContainerProps) =>
isXAxis ? isVertical(controls) : isHorizental(controls), isXAxis ? isVertical(controls) : isHorizontal(controls),
}, },
}, },
], ],
@ -82,7 +82,7 @@ function createAxisTitleControl(axis: 'x' | 'y'): ControlSetRow[] {
choices: formatSelectOptions(sections.TITLE_MARGIN_OPTIONS), choices: formatSelectOptions(sections.TITLE_MARGIN_OPTIONS),
description: t('Changing this control takes effect instantly'), description: t('Changing this control takes effect instantly'),
visibility: ({ controls }: ControlPanelsContainerProps) => visibility: ({ controls }: ControlPanelsContainerProps) =>
isXAxis ? isVertical(controls) : isHorizental(controls), isXAxis ? isVertical(controls) : isHorizontal(controls),
}, },
}, },
], ],
@ -96,7 +96,7 @@ function createAxisTitleControl(axis: 'x' | 'y'): ControlSetRow[] {
default: '', default: '',
description: t('Changing this control takes effect instantly'), description: t('Changing this control takes effect instantly'),
visibility: ({ controls }: ControlPanelsContainerProps) => visibility: ({ controls }: ControlPanelsContainerProps) =>
isXAxis ? isHorizental(controls) : isVertical(controls), isXAxis ? isHorizontal(controls) : isVertical(controls),
}, },
}, },
], ],
@ -113,7 +113,7 @@ function createAxisTitleControl(axis: 'x' | 'y'): ControlSetRow[] {
choices: formatSelectOptions(sections.TITLE_MARGIN_OPTIONS), choices: formatSelectOptions(sections.TITLE_MARGIN_OPTIONS),
description: t('Changing this control takes effect instantly'), description: t('Changing this control takes effect instantly'),
visibility: ({ controls }: ControlPanelsContainerProps) => visibility: ({ controls }: ControlPanelsContainerProps) =>
isXAxis ? isHorizental(controls) : isVertical(controls), isXAxis ? isHorizontal(controls) : isVertical(controls),
}, },
}, },
], ],
@ -130,7 +130,7 @@ function createAxisTitleControl(axis: 'x' | 'y'): ControlSetRow[] {
choices: sections.TITLE_POSITION_OPTIONS, choices: sections.TITLE_POSITION_OPTIONS,
description: t('Changing this control takes effect instantly'), description: t('Changing this control takes effect instantly'),
visibility: ({ controls }: ControlPanelsContainerProps) => visibility: ({ controls }: ControlPanelsContainerProps) =>
isXAxis ? isHorizental(controls) : isVertical(controls), isXAxis ? isHorizontal(controls) : isVertical(controls),
}, },
}, },
], ],
@ -141,7 +141,7 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
const isXAxis = axis === 'x'; const isXAxis = axis === 'x';
const isVertical = (controls: ControlStateMapping) => const isVertical = (controls: ControlStateMapping) =>
Boolean(controls?.orientation.value === OrientationType.vertical); Boolean(controls?.orientation.value === OrientationType.vertical);
const isHorizental = (controls: ControlStateMapping) => const isHorizontal = (controls: ControlStateMapping) =>
Boolean(controls?.orientation.value === OrientationType.horizontal); Boolean(controls?.orientation.value === OrientationType.horizontal);
return [ return [
[ [
@ -154,7 +154,7 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
'When using other than adaptive formatting, labels may overlap.', 'When using other than adaptive formatting, labels may overlap.',
)}`, )}`,
visibility: ({ controls }: ControlPanelsContainerProps) => visibility: ({ controls }: ControlPanelsContainerProps) =>
isXAxis ? isVertical(controls) : isHorizental(controls), isXAxis ? isVertical(controls) : isHorizontal(controls),
}, },
}, },
], ],
@ -176,7 +176,7 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
'Input field supports custom rotation. e.g. 30 for 30°', 'Input field supports custom rotation. e.g. 30 for 30°',
), ),
visibility: ({ controls }: ControlPanelsContainerProps) => visibility: ({ controls }: ControlPanelsContainerProps) =>
isXAxis ? isVertical(controls) : isHorizental(controls), isXAxis ? isVertical(controls) : isHorizontal(controls),
}, },
}, },
], ],
@ -187,7 +187,7 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
...sharedControls.y_axis_format, ...sharedControls.y_axis_format,
label: t('Axis Format'), label: t('Axis Format'),
visibility: ({ controls }: ControlPanelsContainerProps) => visibility: ({ controls }: ControlPanelsContainerProps) =>
isXAxis ? isHorizental(controls) : isVertical(controls), isXAxis ? isHorizontal(controls) : isVertical(controls),
}, },
}, },
], ],
@ -201,7 +201,7 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
default: logAxis, default: logAxis,
description: t('Logarithmic axis'), description: t('Logarithmic axis'),
visibility: ({ controls }: ControlPanelsContainerProps) => visibility: ({ controls }: ControlPanelsContainerProps) =>
isXAxis ? isHorizental(controls) : isVertical(controls), isXAxis ? isHorizontal(controls) : isVertical(controls),
}, },
}, },
], ],
@ -215,7 +215,7 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
default: minorSplitLine, default: minorSplitLine,
description: t('Draw split lines for minor axis ticks'), description: t('Draw split lines for minor axis ticks'),
visibility: ({ controls }: ControlPanelsContainerProps) => visibility: ({ controls }: ControlPanelsContainerProps) =>
isXAxis ? isHorizental(controls) : isVertical(controls), isXAxis ? isHorizontal(controls) : isVertical(controls),
}, },
}, },
], ],
@ -229,7 +229,7 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
renderTrigger: true, renderTrigger: true,
description: t('Its not recommended to truncate axis in Bar chart.'), description: t('Its not recommended to truncate axis in Bar chart.'),
visibility: ({ controls }: ControlPanelsContainerProps) => visibility: ({ controls }: ControlPanelsContainerProps) =>
isXAxis ? isHorizental(controls) : isVertical(controls), isXAxis ? isHorizontal(controls) : isVertical(controls),
}, },
}, },
], ],
@ -249,7 +249,7 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
), ),
visibility: ({ controls }: ControlPanelsContainerProps) => visibility: ({ controls }: ControlPanelsContainerProps) =>
Boolean(controls?.truncateYAxis?.value) && Boolean(controls?.truncateYAxis?.value) &&
(isXAxis ? isHorizental(controls) : isVertical(controls)), (isXAxis ? isHorizontal(controls) : isVertical(controls)),
}, },
}, },
], ],

View File

@ -29,6 +29,7 @@ import {
useTheme, useTheme,
isAdhocColumn, isAdhocColumn,
BinaryQueryObjectFilterClause, BinaryQueryObjectFilterClause,
t,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { PivotTable, sortAs, aggregatorTemplates } from './react-pivottable'; import { PivotTable, sortAs, aggregatorTemplates } from './react-pivottable';
import { import {
@ -55,7 +56,7 @@ const PivotTableWrapper = styled.div`
overflow: auto; overflow: auto;
`; `;
const METRIC_KEY = 'metric'; const METRIC_KEY = t('metric');
const vals = ['value']; const vals = ['value'];
const StyledPlusSquareOutlined = styled(PlusSquareOutlined)` const StyledPlusSquareOutlined = styled(PlusSquareOutlined)`

View File

@ -487,7 +487,9 @@ export class TableRenderer extends React.Component {
true, true,
)} )}
> >
{`Total (${this.props.aggregatorName})`} {t('Total (%(aggregatorName)s)', {
aggregatorName: t(this.props.aggregatorName),
})}
</th> </th>
) : null; ) : null;
@ -550,7 +552,9 @@ export class TableRenderer extends React.Component {
)} )}
> >
{colAttrs.length === 0 {colAttrs.length === 0
? `Total (${this.props.aggregatorName})` ? t('Total (%(aggregatorName)s)', {
aggregatorName: t(this.props.aggregatorName),
})
: null} : null}
</th> </th>
</tr> </tr>
@ -764,10 +768,9 @@ export class TableRenderer extends React.Component {
true, true,
)} )}
> >
{ {t('Total (%(aggregatorName)s)', {
// eslint-disable-next-line prefer-template aggregatorName: t(this.props.aggregatorName),
t('Total') + ` (${this.props.aggregatorName})` })}
}
</th> </th>
); );

View File

@ -17,6 +17,7 @@
* under the License. * under the License.
*/ */
import React from 'react'; import React from 'react';
import { t } from '@superset-ui/core';
import { formatSelectOptions } from '@superset-ui/chart-controls'; import { formatSelectOptions } from '@superset-ui/chart-controls';
export type SizeOption = [number, string]; export type SizeOption = [number, string];
@ -34,7 +35,7 @@ function DefaultSelectRenderer({
}: SelectPageSizeRendererProps) { }: SelectPageSizeRendererProps) {
return ( return (
<span className="dt-select-page-size form-inline"> <span className="dt-select-page-size form-inline">
Show{' '} {t('Show')}{' '}
<select <select
className="form-control input-sm" className="form-control input-sm"
value={current} value={current}
@ -54,7 +55,7 @@ function DefaultSelectRenderer({
); );
})} })}
</select>{' '} </select>{' '}
entries {t('entries')}
</span> </span>
); );
} }

View File

@ -198,7 +198,7 @@ function SelectPageSize({
} }
const getNoResultsMessage = (filter: string) => const getNoResultsMessage = (filter: string) =>
t(filter ? 'No matching records found' : 'No records found'); filter ? t('No matching records found') : t('No records found');
export default function TableChart<D extends DataRecord = DataRecord>( export default function TableChart<D extends DataRecord = DataRecord>(
props: TableChartTransformedProps<D> & { props: TableChartTransformedProps<D> & {

View File

@ -1412,7 +1412,7 @@ export function popQuery(queryId) {
dbId: queryData.database.id, dbId: queryData.database.id,
schema: queryData.schema, schema: queryData.schema,
sql: queryData.sql, sql: queryData.sql,
name: `Copy of ${queryData.tab_name}`, name: t('Copy of %s', queryData.tab_name),
autorun: false, autorun: false,
}; };
return dispatch(addQueryEditor(queryEditorProps)); return dispatch(addQueryEditor(queryEditorProps));
@ -1422,6 +1422,7 @@ export function popQuery(queryId) {
} }
export function popDatasourceQuery(datasourceKey, sql) { export function popDatasourceQuery(datasourceKey, sql) {
return function (dispatch) { return function (dispatch) {
const QUERY_TEXT = t('Query');
const datasetId = datasourceKey.split('__')[0]; const datasetId = datasourceKey.split('__')[0];
return SupersetClient.get({ return SupersetClient.get({
endpoint: `/api/v1/dataset/${datasetId}?q=(keys:!(none))`, endpoint: `/api/v1/dataset/${datasetId}?q=(keys:!(none))`,
@ -1429,7 +1430,7 @@ export function popDatasourceQuery(datasourceKey, sql) {
.then(({ json }) => .then(({ json }) =>
dispatch( dispatch(
addQueryEditor({ addQueryEditor({
name: `Query ${json.result.name}`, name: `${QUERY_TEXT} ${json.result.name}`,
dbId: json.result.database.id, dbId: json.result.database.id,
schema: json.result.schema, schema: json.result.schema,
autorun: sql !== undefined, autorun: sql !== undefined,

View File

@ -18,7 +18,7 @@
*/ */
import React from 'react'; import React from 'react';
import Label from 'src/components/Label'; import Label from 'src/components/Label';
import { STATE_TYPE_MAP } from 'src/SqlLab/constants'; import { STATE_TYPE_MAP, STATE_TYPE_MAP_LOCALIZED } from 'src/SqlLab/constants';
import { styled, Query } from '@superset-ui/core'; import { styled, Query } from '@superset-ui/core';
interface QueryStateLabelProps { interface QueryStateLabelProps {
@ -31,6 +31,8 @@ const StyledLabel = styled(Label)`
export default function QueryStateLabel({ query }: QueryStateLabelProps) { export default function QueryStateLabel({ query }: QueryStateLabelProps) {
return ( return (
<StyledLabel type={STATE_TYPE_MAP[query.state]}>{query.state}</StyledLabel> <StyledLabel type={STATE_TYPE_MAP[query.state]}>
{STATE_TYPE_MAP_LOCALIZED[query.state]}
</StyledLabel>
); );
} }

View File

@ -360,8 +360,8 @@ const ResultSet = ({
message={t('%(rows)d rows returned', { rows })} message={t('%(rows)d rows returned', { rows })}
onClose={() => setAlertIsOpen(false)} onClose={() => setAlertIsOpen(false)}
description={t( description={t(
'The number of rows displayed is limited to %s by the dropdown.', 'The number of rows displayed is limited to %(rows)d by the dropdown.',
rows, { rows },
)} )}
/> />
</div> </div>

View File

@ -35,6 +35,7 @@ import {
STATUS_OPTIONS, STATUS_OPTIONS,
STATE_TYPE_MAP, STATE_TYPE_MAP,
LOCALSTORAGE_MAX_QUERY_AGE_MS, LOCALSTORAGE_MAX_QUERY_AGE_MS,
STATUS_OPTIONS_LOCALIZED,
} from '../../constants'; } from '../../constants';
const TAB_HEIGHT = 140; const TAB_HEIGHT = 140;
@ -145,7 +146,7 @@ const SouthPane = ({
}; };
const renderOfflineStatus = () => ( const renderOfflineStatus = () => (
<Label className="m-r-3" type={STATE_TYPE_MAP[STATUS_OPTIONS.offline]}> <Label className="m-r-3" type={STATE_TYPE_MAP[STATUS_OPTIONS.offline]}>
{STATUS_OPTIONS.offline} {STATUS_OPTIONS_LOCALIZED.offline}
</Label> </Label>
); );

View File

@ -16,6 +16,8 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { t } from '@superset-ui/core';
export const STATE_TYPE_MAP = { export const STATE_TYPE_MAP = {
offline: 'danger', offline: 'danger',
failed: 'danger', failed: 'danger',
@ -26,6 +28,16 @@ export const STATE_TYPE_MAP = {
success: 'success', success: 'success',
}; };
export const STATE_TYPE_MAP_LOCALIZED = {
offline: t('offline'),
failed: t('failed'),
pending: t('pending'),
fetching: t('fetching'),
running: t('running'),
stopped: t('stopped'),
success: t('success'),
};
export const STATUS_OPTIONS = { export const STATUS_OPTIONS = {
success: 'success', success: 'success',
failed: 'failed', failed: 'failed',
@ -34,6 +46,14 @@ export const STATUS_OPTIONS = {
pending: 'pending', pending: 'pending',
}; };
export const STATUS_OPTIONS_LOCALIZED = {
success: t('success'),
failed: t('failed'),
running: t('running'),
offline: t('offline'),
pending: t('pending'),
};
export const TIME_OPTIONS = [ export const TIME_OPTIONS = [
'now', 'now',
'1 hour ago', '1 hour ago',

View File

@ -248,7 +248,7 @@ export default function DatabaseSelector({
setSchemaOptions(options); setSchemaOptions(options);
setLoadingSchemas(false); setLoadingSchemas(false);
if (options.length === 1) changeSchema(options[0]); if (options.length === 1) changeSchema(options[0]);
if (refresh > 0) addSuccessToast('List refreshed'); if (refresh > 0) addSuccessToast(t('List refreshed'));
}) })
.catch(err => { .catch(err => {
setLoadingSchemas(false); setLoadingSchemas(false);
@ -321,6 +321,7 @@ export default function DatabaseSelector({
labelInValue labelInValue
loading={loadingSchemas} loading={loadingSchemas}
name="select-schema" name="select-schema"
notFoundContent={t('No compatible schema found')}
placeholder={t('Select schema or type schema name')} placeholder={t('Select schema or type schema name')}
onChange={item => changeSchema(item as SchemaValue)} onChange={item => changeSchema(item as SchemaValue)}
options={schemaOptions} options={schemaOptions}

View File

@ -190,7 +190,7 @@ const ChangeDatasourceModal: FunctionComponent<ChangeDatasourceModalProps> = ({
); );
}); });
onHide(); onHide();
addSuccessToast('Successfully changed dataset!'); addSuccessToast(t('Successfully changed dataset!'));
}; };
const handlerCancelConfirm = () => { const handlerCancelConfirm = () => {

View File

@ -491,7 +491,7 @@ ColumnCollectionTable.defaultProps = {
allowAddItem: false, allowAddItem: false,
allowEditDataType: false, allowEditDataType: false,
itemGenerator: () => ({ itemGenerator: () => ({
column_name: '<new column>', column_name: t('<new column>'),
filterable: true, filterable: true,
groupby: true, groupby: true,
}), }),
@ -976,8 +976,8 @@ class DatasourceEditor extends React.PureComponent {
tableColumns={['name', 'config']} tableColumns={['name', 'config']}
onChange={this.onDatasourcePropChange.bind(this, 'spatials')} onChange={this.onDatasourcePropChange.bind(this, 'spatials')}
itemGenerator={() => ({ itemGenerator={() => ({
name: '<new spatial>', name: t('<new spatial>'),
type: '<no type>', type: t('<no type>'),
config: null, config: null,
})} })}
collection={spatials} collection={spatials}
@ -1256,7 +1256,7 @@ class DatasourceEditor extends React.PureComponent {
allowAddItem allowAddItem
onChange={this.onDatasourcePropChange.bind(this, 'metrics')} onChange={this.onDatasourcePropChange.bind(this, 'metrics')}
itemGenerator={() => ({ itemGenerator={() => ({
metric_name: '<new metric>', metric_name: t('<new metric>'),
verbose_name: '', verbose_name: '',
expression: '', expression: '',
})} })}
@ -1418,10 +1418,10 @@ class DatasourceEditor extends React.PureComponent {
allowAddItem allowAddItem
allowEditDataType allowEditDataType
itemGenerator={() => ({ itemGenerator={() => ({
column_name: '<new column>', column_name: t('<new column>'),
filterable: true, filterable: true,
groupby: true, groupby: true,
expression: '<enter SQL expression here>', expression: t('<enter SQL expression here>'),
__expanded: true, __expanded: true,
})} })}
/> />

View File

@ -177,6 +177,8 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
content: renderSaveDialog(), content: renderSaveDialog(),
onOk: onConfirmSave, onOk: onConfirmSave,
icon: null, icon: null,
okText: t('OK'),
cancelText: t('Cancel'),
}); });
}; };

View File

@ -23,7 +23,7 @@ import React, {
useImperativeHandle, useImperativeHandle,
} from 'react'; } from 'react';
import moment, { Moment } from 'moment'; import moment, { Moment } from 'moment';
import { styled } from '@superset-ui/core'; import { styled, t } from '@superset-ui/core';
import { RangePicker } from 'src/components/DatePicker'; import { RangePicker } from 'src/components/DatePicker';
import { FormLabel } from 'src/components/Form'; import { FormLabel } from 'src/components/Form';
import { BaseFilter, FilterHandler } from './Base'; import { BaseFilter, FilterHandler } from './Base';
@ -64,6 +64,7 @@ function DateRangeFilter(
<RangeFilterContainer> <RangeFilterContainer>
<FormLabel>{Header}</FormLabel> <FormLabel>{Header}</FormLabel>
<RangePicker <RangePicker
placeholder={[t('Start date'), t('End date')]}
showTime showTime
value={momentValue} value={momentValue}
onChange={momentRange => { onChange={momentRange => {

View File

@ -16,6 +16,8 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { t } from '@superset-ui/core';
import { BootstrapData, CommonBootstrapData } from './types/bootstrapTypes'; import { BootstrapData, CommonBootstrapData } from './types/bootstrapTypes';
export const DATETIME_WITH_TIME_ZONE = 'YYYY-MM-DD HH:mm:ssZ'; export const DATETIME_WITH_TIME_ZONE = 'YYYY-MM-DD HH:mm:ssZ';
@ -142,7 +144,7 @@ export const SLOW_DEBOUNCE = 500;
/** /**
* Display null as `N/A` * Display null as `N/A`
*/ */
export const NULL_DISPLAY = 'N/A'; export const NULL_DISPLAY = t('N/A');
export const DEFAULT_COMMON_BOOTSTRAP_DATA: CommonBootstrapData = { export const DEFAULT_COMMON_BOOTSTRAP_DATA: CommonBootstrapData = {
flash_messages: [], flash_messages: [],

View File

@ -652,7 +652,10 @@ export function maxUndoHistoryToast() {
return dispatch( return dispatch(
addWarningToast( addWarningToast(
`You have used all ${historyLength} undo slots and will not be able to fully undo subsequent actions. You may save your current state to reset the history.`, t(
'You have used all %(historyLength)s undo slots and will not be able to fully undo subsequent actions. You may save your current state to reset the history.',
{ historyLength },
),
), ),
); );
}; };

View File

@ -81,7 +81,7 @@ class SaveModal extends React.PureComponent<SaveModalProps, SaveModalState> {
super(props); super(props);
this.state = { this.state = {
saveType: props.saveType, saveType: props.saveType,
newDashName: `${props.dashboardTitle} [copy]`, newDashName: props.dashboardTitle + t('[copy]'),
duplicateSlices: false, duplicateSlices: false,
}; };

View File

@ -18,7 +18,7 @@
*/ */
import React from 'react'; import React from 'react';
import cx from 'classnames'; import cx from 'classnames';
import { css, styled } from '@superset-ui/core'; import { css, styled, t } from '@superset-ui/core';
import backgroundStyleOptions from 'src/dashboard/util/backgroundStyleOptions'; import backgroundStyleOptions from 'src/dashboard/util/backgroundStyleOptions';
import PopoverDropdown, { import PopoverDropdown, {
@ -73,11 +73,12 @@ const BackgroundStyleOption = styled.div`
`; `;
function renderButton(option: OptionProps) { function renderButton(option: OptionProps) {
const BACKGROUND_TEXT = t('background');
return ( return (
<BackgroundStyleOption <BackgroundStyleOption
className={cx('background-style-option', option.className)} className={cx('background-style-option', option.className)}
> >
{`${option.label} background`} {`${option.label} ${BACKGROUND_TEXT}`}
</BackgroundStyleOption> </BackgroundStyleOption>
); );
} }

View File

@ -90,6 +90,7 @@ const DatasetSelect = ({ onChange, value }: DatasetSelectProps) => {
value={value} value={value}
options={loadDatasetOptions} options={loadDatasetOptions}
onChange={onChange} onChange={onChange}
notFoundContent={t('No compatible datasets found')}
/> />
); );
}; };

View File

@ -944,7 +944,9 @@ const FiltersConfigForm = (
<StyledRowFormItem <StyledRowFormItem
name={['filters', filterId, 'time_range']} name={['filters', filterId, 'time_range']}
label={<StyledLabel>{t('Time range')}</StyledLabel>} label={<StyledLabel>{t('Time range')}</StyledLabel>}
initialValue={filterToEdit?.time_range || 'No filter'} initialValue={
filterToEdit?.time_range || t('No filter')
}
required={!hasAdhoc} required={!hasAdhoc}
rules={[ rules={[
{ {

View File

@ -46,7 +46,7 @@ const typeToDefaultMetaData = {
}, },
[DIVIDER_TYPE]: null, [DIVIDER_TYPE]: null,
[HEADER_TYPE]: { [HEADER_TYPE]: {
text: 'New header', text: t('New header'),
headerSize: MEDIUM_HEADER, headerSize: MEDIUM_HEADER,
background: BACKGROUND_TRANSPARENT, background: BACKGROUND_TRANSPARENT,
}, },

View File

@ -56,33 +56,39 @@ export interface OperatorType {
export const OPERATOR_ENUM_TO_OPERATOR_TYPE: { export const OPERATOR_ENUM_TO_OPERATOR_TYPE: {
[key in Operators]: OperatorType; [key in Operators]: OperatorType;
} = { } = {
[Operators.EQUALS]: { display: 'Equal to (=)', operation: '==' }, [Operators.EQUALS]: { display: t('Equal to (=)'), operation: '==' },
[Operators.NOT_EQUALS]: { display: 'Not equal to (≠)', operation: '!=' }, [Operators.NOT_EQUALS]: { display: t('Not equal to (≠)'), operation: '!=' },
[Operators.LESS_THAN]: { display: 'Less than (<)', operation: '<' }, [Operators.LESS_THAN]: { display: t('Less than (<)'), operation: '<' },
[Operators.LESS_THAN_OR_EQUAL]: { [Operators.LESS_THAN_OR_EQUAL]: {
display: 'Less or equal (<=)', display: t('Less or equal (<=)'),
operation: '<=', operation: '<=',
}, },
[Operators.GREATER_THAN]: { display: 'Greater than (>)', operation: '>' }, [Operators.GREATER_THAN]: { display: t('Greater than (>)'), operation: '>' },
[Operators.GREATER_THAN_OR_EQUAL]: { [Operators.GREATER_THAN_OR_EQUAL]: {
display: 'Greater or equal (>=)', display: t('Greater or equal (>=)'),
operation: '>=', operation: '>=',
}, },
[Operators.IN]: { display: 'In', operation: 'IN' }, [Operators.IN]: { display: t('In'), operation: 'IN' },
[Operators.NOT_IN]: { display: 'Not in', operation: 'NOT IN' }, [Operators.NOT_IN]: { display: t('Not in'), operation: 'NOT IN' },
[Operators.LIKE]: { display: 'Like', operation: 'LIKE' }, [Operators.LIKE]: { display: t('Like'), operation: 'LIKE' },
[Operators.ILIKE]: { display: 'Like (case insensitive)', operation: 'ILIKE' }, [Operators.ILIKE]: {
[Operators.REGEX]: { display: 'Regex', operation: 'REGEX' }, display: t('Like (case insensitive)'),
[Operators.IS_NOT_NULL]: { display: 'Is not null', operation: 'IS NOT NULL' }, operation: 'ILIKE',
[Operators.IS_NULL]: { display: 'Is null', operation: 'IS NULL' }, },
[Operators.REGEX]: { display: t('Regex'), operation: 'REGEX' },
[Operators.IS_NOT_NULL]: {
display: t('Is not null'),
operation: 'IS NOT NULL',
},
[Operators.IS_NULL]: { display: t('Is null'), operation: 'IS NULL' },
[Operators.LATEST_PARTITION]: { [Operators.LATEST_PARTITION]: {
display: 'use latest_partition template', display: t('use latest_partition template'),
operation: 'LATEST PARTITION', operation: 'LATEST PARTITION',
}, },
[Operators.IS_TRUE]: { display: 'Is true', operation: '==' }, [Operators.IS_TRUE]: { display: t('Is true'), operation: '==' },
[Operators.IS_FALSE]: { display: 'Is false', operation: '==' }, [Operators.IS_FALSE]: { display: t('Is false'), operation: '==' },
[Operators.TEMPORAL_RANGE]: { [Operators.TEMPORAL_RANGE]: {
display: 'TEMPORAL_RANGE', display: t('TEMPORAL_RANGE'),
operation: 'TEMPORAL_RANGE', operation: 'TEMPORAL_RANGE',
}, },
}; };

View File

@ -18,6 +18,7 @@
*/ */
import React from 'react'; import React from 'react';
import moment from 'moment'; import moment from 'moment';
import { t } from '@superset-ui/core';
import rison from 'rison'; import rison from 'rison';
import TableLoader from '../../components/TableLoader'; import TableLoader from '../../components/TableLoader';
@ -48,6 +49,7 @@ export default function RecentActivity({ user }: RecentActivityProps) {
mutator={mutator} mutator={mutator}
sortable sortable
dataEndpoint={`/api/v1/log/recent_activity/${user?.userId}/?q=${params}`} dataEndpoint={`/api/v1/log/recent_activity/${user?.userId}/?q=${params}`}
noDataText={t('No Data')}
/> />
</div> </div>
); );

View File

@ -19,8 +19,11 @@
import { t } from '@superset-ui/core'; import { t } from '@superset-ui/core';
const CREATE_CHART_TEXT = t('Create chart');
const UPDATE_CHART_TEXT = t('Update chart');
export const getChartRequiredFieldsMissingMessage = (isCreating: boolean) => export const getChartRequiredFieldsMissingMessage = (isCreating: boolean) =>
t( t(
'Select values in highlighted field(s) in the control panel. Then run the query by clicking on the %s button.', 'Select values in highlighted field(s) in the control panel. Then run the query by clicking on the %s button.',
isCreating ? '"Create chart"' : '"Update chart"', `"${isCreating ? CREATE_CHART_TEXT : UPDATE_CHART_TEXT}"`,
); );

View File

@ -455,7 +455,7 @@ function AlertList({
id: 'owners', id: 'owners',
input: 'select', input: 'select',
operator: FilterOperator.relationManyMany, operator: FilterOperator.relationManyMany,
unfilteredLabel: 'All', unfilteredLabel: t('All'),
fetchSelects: createFetchRelated( fetchSelects: createFetchRelated(
'report', 'report',
'owners', 'owners',

View File

@ -571,7 +571,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
const shouldEnableForceScreenshot = contentType === 'chart' && !isReport; const shouldEnableForceScreenshot = contentType === 'chart' && !isReport;
const data: any = { const data: any = {
...currentAlert, ...currentAlert,
type: isReport ? 'Report' : 'Alert', type: isReport ? t('Report') : t('Alert'),
force_screenshot: shouldEnableForceScreenshot || forceScreenshot, force_screenshot: shouldEnableForceScreenshot || forceScreenshot,
validator_type: conditionNotNull ? 'not null' : 'operator', validator_type: conditionNotNull ? 'not null' : 'operator',
validator_config_json: conditionNotNull validator_config_json: conditionNotNull

View File

@ -159,13 +159,23 @@ function ExecutionLog({ addDangerToast, isReportEnabled }: ExecutionLogProps) {
[isReportEnabled], [isReportEnabled],
); );
const path = `/${isReportEnabled ? 'report' : 'alert'}/list/`; const path = `/${isReportEnabled ? 'report' : 'alert'}/list/`;
const ALERT_TEXT = t('Alert');
const REPORT_TEXT = t('Report');
return ( return (
<> <>
<SubMenu <SubMenu
name={ name={
<StyledHeader> <StyledHeader>
<span> <span>
{alertResource?.type} {alertResource?.name} {alertResource
? alertResource.type === 'Alert'
? `${ALERT_TEXT}:`
: alertResource.type === 'Report'
? `${REPORT_TEXT}:`
: null
: null}{' '}
{alertResource?.name}
</span> </span>
<span> <span>
<Link to={path}>{t('Back to all')}</Link> <Link to={path}>{t('Back to all')}</Link>

View File

@ -304,6 +304,7 @@ const AnnotationModal: FunctionComponent<AnnotationModalProps> = ({
<span className="required">*</span> <span className="required">*</span>
</div> </div>
<RangePicker <RangePicker
placeholder={[t('Start date'), t('End date')]}
format="YYYY-MM-DD HH:mm" format="YYYY-MM-DD HH:mm"
onChange={onDateChange} onChange={onDateChange}
showTime={{ format: 'hh:mm a' }} showTime={{ format: 'hh:mm a' }}

View File

@ -285,7 +285,7 @@ function AnnotationLayersList({
id: 'created_by', id: 'created_by',
input: 'select', input: 'select',
operator: FilterOperator.relationOneMany, operator: FilterOperator.relationOneMany,
unfilteredLabel: 'All', unfilteredLabel: t('All'),
fetchSelects: createFetchRelated( fetchSelects: createFetchRelated(
'annotation_layer', 'annotation_layer',
'created_by', 'created_by',

View File

@ -275,7 +275,7 @@ function CssTemplatesList({
id: 'created_by', id: 'created_by',
input: 'select', input: 'select',
operator: FilterOperator.relationOneMany, operator: FilterOperator.relationOneMany,
unfilteredLabel: 'All', unfilteredLabel: t('All'),
fetchSelects: createFetchRelated( fetchSelects: createFetchRelated(
'css_template', 'css_template',
'created_by', 'created_by',

View File

@ -117,7 +117,9 @@ export const EncryptedField = ({
name={encryptedField} name={encryptedField}
value={encryptedValue} value={encryptedValue}
onChange={changeMethods.onParametersChange} onChange={changeMethods.onParametersChange}
placeholder="Paste content of service credentials JSON file here" placeholder={t(
'Paste content of service credentials JSON file here',
)}
/> />
<span className="label-paste"> <span className="label-paste">
{t('Copy and paste the entire service account .json file here')} {t('Copy and paste the entire service account .json file here')}

View File

@ -26,14 +26,14 @@ const FIELD_TEXT_MAP = {
helpText: t( helpText: t(
'Copy the account name of that database you are trying to connect to.', 'Copy the account name of that database you are trying to connect to.',
), ),
placeholder: 'e.g. world_population', placeholder: t('e.g. world_population'),
}, },
warehouse: { warehouse: {
placeholder: 'e.g. compute_wh', placeholder: t('e.g. compute_wh'),
className: 'form-group-w-50', className: 'form-group-w-50',
}, },
role: { role: {
placeholder: 'e.g. AccountAdmin', placeholder: t('e.g. AccountAdmin'),
className: 'form-group-w-50', className: 'form-group-w-50',
}, },
}; };

View File

@ -193,11 +193,11 @@ const SSHTunnelForm = ({
data-test="ssh-tunnel-password-input" data-test="ssh-tunnel-password-input"
iconRender={visible => iconRender={visible =>
visible ? ( visible ? (
<Tooltip title="Hide password."> <Tooltip title={t('Hide password.')}>
<EyeInvisibleOutlined /> <EyeInvisibleOutlined />
</Tooltip> </Tooltip>
) : ( ) : (
<Tooltip title="Show password."> <Tooltip title={t('Show password.')}>
<EyeOutlined /> <EyeOutlined />
</Tooltip> </Tooltip>
) )

View File

@ -1216,7 +1216,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
required required
validationMethods={{ onBlur: () => {} }} validationMethods={{ onBlur: () => {} }}
errorMessage={validationErrors?.confirm_overwrite} errorMessage={validationErrors?.confirm_overwrite}
label={t(`TYPE "OVERWRITE" TO CONFIRM`)} label={t('Type "%s" to confirm', t('OVERWRITE'))}
onChange={confirmOverwrite} onChange={confirmOverwrite}
css={formScrollableStyles} css={formScrollableStyles}
/> />
@ -1615,7 +1615,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
<Alert <Alert
closable={false} closable={false}
css={(theme: SupersetTheme) => antDAlertStyles(theme)} css={(theme: SupersetTheme) => antDAlertStyles(theme)}
message="Additional fields may be required" message={t('Additional fields may be required')}
showIcon showIcon
description={ description={
<> <>

View File

@ -351,7 +351,7 @@ function QueryList({ addDangerToast }: QueryListProps) {
id: 'database', id: 'database',
input: 'select', input: 'select',
operator: FilterOperator.relationOneMany, operator: FilterOperator.relationOneMany,
unfilteredLabel: 'All', unfilteredLabel: t('All'),
fetchSelects: createFetchRelated( fetchSelects: createFetchRelated(
'query', 'query',
'database', 'database',

View File

@ -427,7 +427,7 @@ function SavedQueryList({
id: 'database', id: 'database',
input: 'select', input: 'select',
operator: FilterOperator.relationOneMany, operator: FilterOperator.relationOneMany,
unfilteredLabel: 'All', unfilteredLabel: t('All'),
fetchSelects: createFetchRelated( fetchSelects: createFetchRelated(
'saved_query', 'saved_query',
'database', 'database',

View File

@ -420,6 +420,10 @@ export function useImportResource(
const formData = new FormData(); const formData = new FormData();
formData.append('formData', bundle); formData.append('formData', bundle);
const RE_EXPORT_TEXT = t(
'Please re-export your file and try importing again',
);
/* The import bundle never contains database passwords; if required /* The import bundle never contains database passwords; if required
* they should be provided by the user during import. * they should be provided by the user during import.
*/ */
@ -468,7 +472,7 @@ export function useImportResource(
resourceLabel, resourceLabel,
[ [
...error.errors.map(payload => payload.message), ...error.errors.map(payload => payload.message),
t('Please re-export your file and try importing again.'), RE_EXPORT_TEXT,
].join('.\n'), ].join('.\n'),
), ),
); );

View File

@ -30,6 +30,13 @@ const welcomeTableLabels: Record<WelcomeTable, string> = {
[WelcomeTable.SavedQueries]: t('saved queries'), [WelcomeTable.SavedQueries]: t('saved queries'),
}; };
const welcomeTableEmpty: Record<WelcomeTable, string> = {
[WelcomeTable.Charts]: t('No charts yet'),
[WelcomeTable.Dashboards]: t('No dashboards yet'),
[WelcomeTable.Recents]: t('No recents yet'),
[WelcomeTable.SavedQueries]: t('No saved queries yet'),
};
export interface EmptyStateProps { export interface EmptyStateProps {
tableName: WelcomeTable; tableName: WelcomeTable;
tab?: string; tab?: string;
@ -75,11 +82,7 @@ export default function EmptyState({
[WelcomeTable.Recents]: 'union.svg', [WelcomeTable.Recents]: 'union.svg',
[WelcomeTable.SavedQueries]: 'empty-queries.svg', [WelcomeTable.SavedQueries]: 'empty-queries.svg',
}; };
const mine = ( const mine = <span>{welcomeTableEmpty[tableName]}</span>;
<span>
{t('No %(tableName)s yet', { tableName: welcomeTableLabels[tableName] })}
</span>
);
const recent = ( const recent = (
<span className="no-recents"> <span className="no-recents">
{(() => { {(() => {

View File

@ -30,6 +30,7 @@ from io import StringIO
from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING
import pandas as pd import pandas as pd
from flask_babel import gettext as __
from superset.common.chart_data import ChartDataResultFormat from superset.common.chart_data import ChartDataResultFormat
from superset.utils.core import ( from superset.utils.core import (
@ -68,7 +69,7 @@ def pivot_df( # pylint: disable=too-many-locals, too-many-arguments, too-many-s
show_columns_total: bool = False, show_columns_total: bool = False,
apply_metrics_on_rows: bool = False, apply_metrics_on_rows: bool = False,
) -> pd.DataFrame: ) -> pd.DataFrame:
metric_name = f"Total ({aggfunc})" metric_name = __("Total (%(aggfunc)s)", aggfunc=aggfunc)
if transpose_pivot: if transpose_pivot:
rows, columns = columns, rows rows, columns = columns, rows
@ -156,7 +157,7 @@ def pivot_df( # pylint: disable=too-many-locals, too-many-arguments, too-many-s
slice_ = df.columns.get_loc(subgroup) slice_ = df.columns.get_loc(subgroup)
subtotal = pivot_v2_aggfunc_map[aggfunc](df.iloc[:, slice_], axis=1) subtotal = pivot_v2_aggfunc_map[aggfunc](df.iloc[:, slice_], axis=1)
depth = df.columns.nlevels - len(subgroup) - 1 depth = df.columns.nlevels - len(subgroup) - 1
total = metric_name if level == 0 else "Subtotal" total = metric_name if level == 0 else __("Subtotal")
subtotal_name = tuple([*subgroup, total, *([""] * depth)]) subtotal_name = tuple([*subgroup, total, *([""] * depth)])
# insert column after subgroup # insert column after subgroup
df.insert(int(slice_.stop), subtotal_name, subtotal) df.insert(int(slice_.stop), subtotal_name, subtotal)
@ -173,7 +174,7 @@ def pivot_df( # pylint: disable=too-many-locals, too-many-arguments, too-many-s
df.iloc[slice_, :].apply(pd.to_numeric), axis=0 df.iloc[slice_, :].apply(pd.to_numeric), axis=0
) )
depth = df.index.nlevels - len(subgroup) - 1 depth = df.index.nlevels - len(subgroup) - 1
total = metric_name if level == 0 else "Subtotal" total = metric_name if level == 0 else __("Subtotal")
subtotal.name = tuple([*subgroup, total, *([""] * depth)]) subtotal.name = tuple([*subgroup, total, *([""] * depth)])
# insert row after subgroup # insert row after subgroup
df = pd.concat( df = pd.concat(

View File

@ -22,6 +22,7 @@ from enum import Enum
from typing import Any, Dict, Hashable, List, Optional, Set, Type, TYPE_CHECKING, Union from typing import Any, Dict, Hashable, List, Optional, Set, Type, TYPE_CHECKING, Union
from flask_appbuilder.security.sqla.models import User from flask_appbuilder.security.sqla.models import User
from flask_babel import gettext as __
from sqlalchemy import and_, Boolean, Column, Integer, String, Text from sqlalchemy import and_, Boolean, Column, Integer, String, Text
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import foreign, Query, relationship, RelationshipProperty, Session from sqlalchemy.orm import foreign, Query, relationship, RelationshipProperty, Session
@ -248,10 +249,10 @@ class BaseDatasource(
for column_name in self.column_names: for column_name in self.column_names:
column_name = str(column_name or "") column_name = str(column_name or "")
order_by_choices.append( order_by_choices.append(
(json.dumps([column_name, True]), column_name + " [asc]") (json.dumps([column_name, True]), f"{column_name} " + __("[asc]"))
) )
order_by_choices.append( order_by_choices.append(
(json.dumps([column_name, False]), column_name + " [desc]") (json.dumps([column_name, False]), f"{column_name} " + __("[desc]"))
) )
verbose_map = {"__timestamp": "Time"} verbose_map = {"__timestamp": "Time"}

View File

@ -52,7 +52,7 @@ cache_timeout_description = (
) )
expose_in_sqllab_description = "Expose this database to SQLLab" expose_in_sqllab_description = "Expose this database to SQLLab"
allow_run_async_description = ( allow_run_async_description = (
"Operate the database in asynchronous mode, meaning " "Operate the database in asynchronous mode, meaning "
"that the queries are executed on remote workers as opposed " "that the queries are executed on remote workers as opposed "
"to on the web server itself. " "to on the web server itself. "
"This assumes that you have a Celery worker setup as well " "This assumes that you have a Celery worker setup as well "

View File

@ -341,7 +341,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
) )
appbuilder.add_link( appbuilder.add_link(
"SQL Editor", "SQL Editor",
label=_("SQL Lab"), label=__("SQL Lab"),
href="/superset/sqllab/", href="/superset/sqllab/",
category_icon="fa-flask", category_icon="fa-flask",
icon="fa-flask", icon="fa-flask",
@ -349,7 +349,8 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
category_label=__("SQL"), category_label=__("SQL"),
) )
appbuilder.add_link( appbuilder.add_link(
__("Saved Queries"), "Saved Queries",
label=__("Saved Queries"),
href="/savedqueryview/list/", href="/savedqueryview/list/",
icon="fa-save", icon="fa-save",
category="SQL Lab", category="SQL Lab",
@ -357,7 +358,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
) )
appbuilder.add_link( appbuilder.add_link(
"Query Search", "Query Search",
label=_("Query History"), label=__("Query History"),
href="/superset/sqllab/history/", href="/superset/sqllab/history/",
icon="fa-search", icon="fa-search",
category_icon="fa-flask", category_icon="fa-flask",
@ -396,7 +397,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
appbuilder.add_view( appbuilder.add_view(
AnnotationLayerView, AnnotationLayerView,
"Annotation Layers", "Annotation Layers",
label=_("Annotation Layers"), label=__("Annotation Layers"),
href="/annotationlayer/list/", href="/annotationlayer/list/",
icon="fa-comment", icon="fa-comment",
category_icon="", category_icon="",

View File

@ -26,6 +26,7 @@ import sqlalchemy as sqla
from flask import current_app, Markup from flask import current_app, Markup
from flask_appbuilder import Model from flask_appbuilder import Model
from flask_appbuilder.models.decorators import renders from flask_appbuilder.models.decorators import renders
from flask_babel import gettext as __
from humanize import naturaltime from humanize import naturaltime
from sqlalchemy import ( from sqlalchemy import (
Boolean, Boolean,
@ -224,10 +225,10 @@ class Query(
for col in self.columns: for col in self.columns:
column_name = str(col.get("column_name") or "") column_name = str(col.get("column_name") or "")
order_by_choices.append( order_by_choices.append(
(json.dumps([column_name, True]), column_name + " [asc]") (json.dumps([column_name, True]), f"{column_name} " + __("[asc]"))
) )
order_by_choices.append( order_by_choices.append(
(json.dumps([column_name, False]), column_name + " [desc]") (json.dumps([column_name, False]), f"{column_name} " + __("[desc]"))
) )
return { return {

View File

@ -485,7 +485,11 @@ def execute_sql_statements( # pylint: disable=too-many-arguments, too-many-loca
or (query.ctas_method == CtasMethod.TABLE and i == len(statements) - 1) or (query.ctas_method == CtasMethod.TABLE and i == len(statements) - 1)
) )
# Run statement # Run statement
msg = f"Running statement {i+1} out of {statement_count}" msg = __(
"Running statement %(statement_num)s out of %(statement_count)s",
statement_num=i + 1,
statement_count=statement_count,
)
logger.info("Query %s: %s", str(query_id), msg) logger.info("Query %s: %s", str(query_id), msg)
query.set_extra_json_key("progress", msg) query.set_extra_json_key("progress", msg)
session.commit() session.commit()
@ -504,7 +508,11 @@ def execute_sql_statements( # pylint: disable=too-many-arguments, too-many-loca
except Exception as ex: # pylint: disable=broad-except except Exception as ex: # pylint: disable=broad-except
msg = str(ex) msg = str(ex)
prefix_message = ( prefix_message = (
f"[Statement {i+1} out of {statement_count}]" __(
"Statement %(statement_num)s out of %(statement_count)s",
statement_num=i + 1,
statement_count=statement_count,
)
if statement_count > 1 if statement_count > 1
else "" else ""
) )

View File

@ -19,11 +19,11 @@ from __future__ import annotations
import os import os
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING
from flask_babel import lazy_gettext as _
from superset.errors import SupersetError, SupersetErrorType from superset.errors import SupersetError, SupersetErrorType
from superset.exceptions import SupersetException from superset.exceptions import SupersetException
MSG_FORMAT = "Failed to execute {}"
if TYPE_CHECKING: if TYPE_CHECKING:
from superset.sqllab.sqllab_execution_context import SqlJsonExecutionContext from superset.sqllab.sqllab_execution_context import SqlJsonExecutionContext
@ -61,7 +61,10 @@ class SqlLabException(SupersetException):
super().__init__(self._generate_message(), exception, error_type) super().__init__(self._generate_message(), exception, error_type)
def _generate_message(self) -> str: def _generate_message(self) -> str:
msg = MSG_FORMAT.format(self.sql_json_execution_context.get_query_details()) msg = _(
"Failed to execute %(query)s",
query=self.sql_json_execution_context.get_query_details(),
)
if self.failed_reason_msg: if self.failed_reason_msg:
msg = msg + self.failed_reason_msg msg = msg + self.failed_reason_msg
if self.suggestion_help_msg is not None: if self.suggestion_help_msg is not None:

View File

@ -178,7 +178,7 @@ def get_since_until( # pylint: disable=too-many-arguments,too-many-locals,too-m
_relative_start = relative_start if relative_start else "today" _relative_start = relative_start if relative_start else "today"
_relative_end = relative_end if relative_end else "today" _relative_end = relative_end if relative_end else "today"
if time_range == NO_TIME_RANGE: if time_range == NO_TIME_RANGE or time_range == _(NO_TIME_RANGE):
return None, None return None, None
if time_range and time_range.startswith("Last") and separator not in time_range: if time_range and time_range.startswith("Last") and separator not in time_range:

View File

@ -19,6 +19,7 @@ from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from flask import Flask from flask import Flask
from flask_babel import lazy_gettext as _
from sqlalchemy import text, TypeDecorator from sqlalchemy import text, TypeDecorator
from sqlalchemy.engine import Connection, Dialect, Row from sqlalchemy.engine import Connection, Dialect, Row
from sqlalchemy_utils import EncryptedType from sqlalchemy_utils import EncryptedType
@ -109,7 +110,13 @@ class SecretsMigrator:
return bytes(value.encode("utf8")) return bytes(value.encode("utf8"))
# Just bail if we haven't seen this type before... # Just bail if we haven't seen this type before...
raise ValueError(f"DB column {col_name} has unknown type: {type(value)}") raise ValueError(
_(
"DB column %(col_name)s has unknown type: %(value_type)s",
col_name=col_name,
value_type=type(value),
)
)
@staticmethod @staticmethod
def _select_columns_from_table( def _select_columns_from_table(

View File

@ -23,6 +23,7 @@ from flask import request
from flask_appbuilder import expose from flask_appbuilder import expose
from flask_appbuilder.api import rison from flask_appbuilder.api import rison
from flask_appbuilder.security.decorators import has_access_api from flask_appbuilder.security.decorators import has_access_api
from flask_babel import lazy_gettext as _
from superset import db, event_logger from superset import db, event_logger
from superset.charts.commands.exceptions import ( from superset.charts.commands.exceptions import (
@ -105,7 +106,7 @@ class Api(BaseSupersetView):
} }
return self.json_response({"result": result}) return self.json_response({"result": result})
except (ValueError, TimeRangeParseFailError, TimeRangeAmbiguousError) as error: except (ValueError, TimeRangeParseFailError, TimeRangeAmbiguousError) as error:
error_msg = {"message": f"Unexpected time range: {error}"} error_msg = {"message": _("Unexpected time range: %s" % error)}
return self.json_response(error_msg, 400) return self.json_response(error_msg, 400)
def get_query_context_factory(self) -> QueryContextFactory: def get_query_context_factory(self) -> QueryContextFactory:

View File

@ -66,7 +66,7 @@ class DashboardMixin: # pylint: disable=too-few-public-methods
"roles": _( "roles": _(
"Roles is a list which defines access to the dashboard. " "Roles is a list which defines access to the dashboard. "
"Granting a role access to a dashboard will bypass dataset level checks." "Granting a role access to a dashboard will bypass dataset level checks."
"If no roles defined then the dashboard is available to all roles." "If no roles are defined then the dashboard is available to all roles."
), ),
"published": _( "published": _(
"Determines whether or not this dashboard is " "Determines whether or not this dashboard is "

View File

@ -25,7 +25,7 @@ from flask import flash, g, redirect
from flask_appbuilder import expose, SimpleFormView from flask_appbuilder import expose, SimpleFormView
from flask_appbuilder.models.sqla.interface import SQLAInterface from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_appbuilder.security.decorators import has_access from flask_appbuilder.security.decorators import has_access
from flask_babel import gettext as __, lazy_gettext as _ from flask_babel import lazy_gettext as _
from werkzeug.wrappers import Response from werkzeug.wrappers import Response
from wtforms.fields import StringField from wtforms.fields import StringField
from wtforms.validators import ValidationError from wtforms.validators import ValidationError
@ -176,7 +176,7 @@ class CsvToDatabaseView(CustomFormView):
delimiter_input = form.delimiter.data delimiter_input = form.delimiter.data
if not schema_allows_file_upload(database, csv_table.schema): if not schema_allows_file_upload(database, csv_table.schema):
message = __( message = _(
'Database "%(database_name)s" schema "%(schema_name)s" ' 'Database "%(database_name)s" schema "%(schema_name)s" '
"is not allowed for csv uploads. Please contact your Superset Admin.", "is not allowed for csv uploads. Please contact your Superset Admin.",
database_name=database.database_name, database_name=database.database_name,
@ -265,7 +265,7 @@ class CsvToDatabaseView(CustomFormView):
db.session.commit() db.session.commit()
except Exception as ex: # pylint: disable=broad-except except Exception as ex: # pylint: disable=broad-except
db.session.rollback() db.session.rollback()
message = __( message = _(
'Unable to upload CSV file "%(filename)s" to table ' 'Unable to upload CSV file "%(filename)s" to table '
'"%(table_name)s" in database "%(db_name)s". ' '"%(table_name)s" in database "%(db_name)s". '
"Error message: %(error_msg)s", "Error message: %(error_msg)s",
@ -280,7 +280,7 @@ class CsvToDatabaseView(CustomFormView):
return redirect("/csvtodatabaseview/form") return redirect("/csvtodatabaseview/form")
# Go back to welcome page / splash screen # Go back to welcome page / splash screen
message = __( message = _(
'CSV file "%(csv_filename)s" uploaded to table "%(table_name)s" in ' 'CSV file "%(csv_filename)s" uploaded to table "%(table_name)s" in '
'database "%(db_name)s"', 'database "%(db_name)s"',
csv_filename=form.csv_file.data.filename, csv_filename=form.csv_file.data.filename,
@ -315,7 +315,7 @@ class ExcelToDatabaseView(SimpleFormView):
excel_table = Table(table=form.name.data, schema=form.schema.data) excel_table = Table(table=form.name.data, schema=form.schema.data)
if not schema_allows_file_upload(database, excel_table.schema): if not schema_allows_file_upload(database, excel_table.schema):
message = __( message = _(
'Database "%(database_name)s" schema "%(schema_name)s" ' 'Database "%(database_name)s" schema "%(schema_name)s" '
"is not allowed for excel uploads. Please contact your Superset Admin.", "is not allowed for excel uploads. Please contact your Superset Admin.",
database_name=database.database_name, database_name=database.database_name,
@ -402,7 +402,7 @@ class ExcelToDatabaseView(SimpleFormView):
db.session.commit() db.session.commit()
except Exception as ex: # pylint: disable=broad-except except Exception as ex: # pylint: disable=broad-except
db.session.rollback() db.session.rollback()
message = __( message = _(
'Unable to upload Excel file "%(filename)s" to table ' 'Unable to upload Excel file "%(filename)s" to table '
'"%(table_name)s" in database "%(db_name)s". ' '"%(table_name)s" in database "%(db_name)s". '
"Error message: %(error_msg)s", "Error message: %(error_msg)s",
@ -417,7 +417,7 @@ class ExcelToDatabaseView(SimpleFormView):
return redirect("/exceltodatabaseview/form") return redirect("/exceltodatabaseview/form")
# Go back to welcome page / splash screen # Go back to welcome page / splash screen
message = __( message = _(
'Excel file "%(excel_filename)s" uploaded to table "%(table_name)s" in ' 'Excel file "%(excel_filename)s" uploaded to table "%(table_name)s" in '
'database "%(db_name)s"', 'database "%(db_name)s"',
excel_filename=form.excel_file.data.filename, excel_filename=form.excel_file.data.filename,
@ -462,7 +462,7 @@ class ColumnarToDatabaseView(SimpleFormView):
] ]
if len(file_type) > 1: if len(file_type) > 1:
message = __( message = _(
"Multiple file extensions are not allowed for columnar uploads." "Multiple file extensions are not allowed for columnar uploads."
" Please make sure all files are of the same extension.", " Please make sure all files are of the same extension.",
) )
@ -475,7 +475,7 @@ class ColumnarToDatabaseView(SimpleFormView):
} }
if not schema_allows_file_upload(database, columnar_table.schema): if not schema_allows_file_upload(database, columnar_table.schema):
message = __( message = _(
'Database "%(database_name)s" schema "%(schema_name)s" ' 'Database "%(database_name)s" schema "%(schema_name)s" '
"is not allowed for columnar uploads. " "is not allowed for columnar uploads. "
"Please contact your Superset Admin.", "Please contact your Superset Admin.",
@ -543,7 +543,7 @@ class ColumnarToDatabaseView(SimpleFormView):
db.session.commit() db.session.commit()
except Exception as ex: # pylint: disable=broad-except except Exception as ex: # pylint: disable=broad-except
db.session.rollback() db.session.rollback()
message = __( message = _(
'Unable to upload Columnar file "%(filename)s" to table ' 'Unable to upload Columnar file "%(filename)s" to table '
'"%(table_name)s" in database "%(db_name)s". ' '"%(table_name)s" in database "%(db_name)s". '
"Error message: %(error_msg)s", "Error message: %(error_msg)s",
@ -558,7 +558,7 @@ class ColumnarToDatabaseView(SimpleFormView):
return redirect("/columnartodatabaseview/form") return redirect("/columnartodatabaseview/form")
# Go back to welcome page / splash screen # Go back to welcome page / splash screen
message = __( message = _(
'Columnar file "%(columnar_filename)s" uploaded to table "%(table_name)s" ' 'Columnar file "%(columnar_filename)s" uploaded to table "%(table_name)s" '
'in database "%(db_name)s"', 'in database "%(db_name)s"',
columnar_filename=[file.filename for file in form.columnar_file.data], columnar_filename=[file.filename for file in form.columnar_file.data],

View File

@ -145,7 +145,7 @@ class TabStateView(BaseSupersetView):
user_id=get_user_id(), user_id=get_user_id(),
# This is for backward compatibility # This is for backward compatibility
label=query_editor.get("name") label=query_editor.get("name")
or query_editor.get("title", "Untitled Query"), or query_editor.get("title", _("Untitled Query")),
active=True, active=True,
database_id=query_editor["dbId"], database_id=query_editor["dbId"],
schema=query_editor.get("schema"), schema=query_editor.get("schema"),

View File

@ -3224,7 +3224,7 @@ class PartitionViz(NVD3TimeSeriesViz):
groups = get_column_names(self.form_data.get("groupby")) groups = get_column_names(self.form_data.get("groupby"))
time_op = self.form_data.get("time_series_option", "not_time") time_op = self.form_data.get("time_series_option", "not_time")
if not groups: if not groups:
raise ValueError("Please choose at least one groupby") raise ValueError(_("Please choose at least one groupby"))
if time_op == "not_time": if time_op == "not_time":
levels = self.levels_for("agg_sum", groups, df) levels = self.levels_for("agg_sum", groups, df)
elif time_op in ["agg_sum", "agg_mean"]: elif time_op in ["agg_sum", "agg_mean"]:

View File

@ -18,6 +18,7 @@
import json import json
import pandas as pd import pandas as pd
from flask_babel import lazy_gettext as _
from numpy import True_ from numpy import True_
from pytest import raises from pytest import raises
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
@ -57,10 +58,10 @@ def test_pivot_df_no_cols_no_rows_single_metric():
) )
assert ( assert (
pivoted.to_markdown() pivoted.to_markdown()
== """ == f"""
| | ('SUM(num)',) | | | ('SUM(num)',) |
|:-----------------|----------------:| |:-----------------|----------------:|
| ('Total (Sum)',) | 8.06797e+07 | | ('{_("Total")} (Sum)',) | 8.06797e+07 |
""".strip() """.strip()
) )
@ -79,10 +80,10 @@ def test_pivot_df_no_cols_no_rows_single_metric():
) )
assert ( assert (
pivoted.to_markdown() pivoted.to_markdown()
== """ == f"""
| | ('SUM(num)',) | | | ('SUM(num)',) |
|:-----------------|----------------:| |:-----------------|----------------:|
| ('Total (Sum)',) | 8.06797e+07 | | ('{_("Total")} (Sum)',) | 8.06797e+07 |
""".strip() """.strip()
) )
@ -102,8 +103,8 @@ def test_pivot_df_no_cols_no_rows_single_metric():
) )
assert ( assert (
pivoted.to_markdown() pivoted.to_markdown()
== """ == f"""
| | ('Total (Sum)',) | | | ('{_("Total")} (Sum)',) |
|:--------------|-------------------:| |:--------------|-------------------:|
| ('SUM(num)',) | 8.06797e+07 | | ('SUM(num)',) | 8.06797e+07 |
""".strip() """.strip()
@ -124,10 +125,10 @@ def test_pivot_df_no_cols_no_rows_single_metric():
) )
assert ( assert (
pivoted.to_markdown() pivoted.to_markdown()
== """ == f"""
| | ('SUM(num)',) | ('Total (Sum)',) | | | ('SUM(num)',) | ('Total (Sum)',) |
|:-----------------|----------------:|-------------------:| |:-----------------|----------------:|-------------------:|
| ('Total (Sum)',) | 8.06797e+07 | 8.06797e+07 | | ('{_("Total")} (Sum)',) | 8.06797e+07 | 8.06797e+07 |
""".strip() """.strip()
) )
@ -162,10 +163,10 @@ def test_pivot_df_no_cols_no_rows_two_metrics():
) )
assert ( assert (
pivoted.to_markdown() pivoted.to_markdown()
== """ == f"""
| | ('SUM(num)',) | ('MAX(num)',) | | | ('SUM(num)',) | ('MAX(num)',) |
|:-----------------|----------------:|----------------:| |:-----------------|----------------:|----------------:|
| ('Total (Sum)',) | 8.06797e+07 | 37296 | | ('{_("Total")} (Sum)',) | 8.06797e+07 | 37296 |
""".strip() """.strip()
) )
@ -207,8 +208,8 @@ def test_pivot_df_no_cols_no_rows_two_metrics():
) )
assert ( assert (
pivoted.to_markdown() pivoted.to_markdown()
== """ == f"""
| | ('Total (Sum)',) | | | ('{_("Total")} (Sum)',) |
|:--------------|-------------------:| |:--------------|-------------------:|
| ('SUM(num)',) | 8.06797e+07 | | ('SUM(num)',) | 8.06797e+07 |
| ('MAX(num)',) | 37296 | | ('MAX(num)',) | 37296 |
@ -231,10 +232,10 @@ def test_pivot_df_no_cols_no_rows_two_metrics():
) )
assert ( assert (
pivoted.to_markdown() pivoted.to_markdown()
== """ == f"""
| | ('SUM(num)',) | ('MAX(num)',) | ('Total (Sum)',) | | | ('SUM(num)',) | ('MAX(num)',) | ('{_("Total")} (Sum)',) |
|:-----------------|----------------:|----------------:|-------------------:| |:-----------------|----------------:|----------------:|-------------------:|
| ('Total (Sum)',) | 8.06797e+07 | 37296 | 8.0717e+07 | | ('{_("Total")} (Sum)',) | 8.06797e+07 | 37296 | 8.0717e+07 |
""".strip() """.strip()
) )
@ -297,10 +298,10 @@ def test_pivot_df_single_row_two_metrics():
) )
assert ( assert (
pivoted.to_markdown() pivoted.to_markdown()
== """ == f"""
| | ('SUM(num)', 'boy') | ('SUM(num)', 'girl') | ('MAX(num)', 'boy') | ('MAX(num)', 'girl') | | | ('SUM(num)', 'boy') | ('SUM(num)', 'girl') | ('MAX(num)', 'boy') | ('MAX(num)', 'girl') |
|:-----------------|----------------------:|-----------------------:|----------------------:|-----------------------:| |:-----------------|----------------------:|-----------------------:|----------------------:|-----------------------:|
| ('Total (Sum)',) | 47123 | 118065 | 1280 | 2588 | | ('{_("Total")} (Sum)',) | 47123 | 118065 | 1280 | 2588 |
""".strip() """.strip()
) )
@ -342,12 +343,12 @@ def test_pivot_df_single_row_two_metrics():
) )
assert ( assert (
pivoted.to_markdown() pivoted.to_markdown()
== """ == f"""
| | ('SUM(num)',) | ('MAX(num)',) | ('Total (Sum)',) | | | ('SUM(num)',) | ('MAX(num)',) | ('{_("Total")} (Sum)',) |
|:-----------------|----------------:|----------------:|-------------------:| |:-----------------|----------------:|----------------:|-------------------:|
| ('boy',) | 47123 | 1280 | 48403 | | ('boy',) | 47123 | 1280 | 48403 |
| ('girl',) | 118065 | 2588 | 120653 | | ('girl',) | 118065 | 2588 | 120653 |
| ('Total (Sum)',) | 165188 | 3868 | 169056 | | ('{_("Total")} (Sum)',) | 165188 | 3868 | 169056 |
""".strip() """.strip()
) )
@ -366,8 +367,8 @@ def test_pivot_df_single_row_two_metrics():
) )
assert ( assert (
pivoted.to_markdown() pivoted.to_markdown()
== """ == f"""
| | ('Total (Sum)',) | | | ('{_("Total")} (Sum)',) |
|:-------------------------|-------------------:| |:-------------------------|-------------------:|
| ('SUM(num)', 'boy') | 47123 | | ('SUM(num)', 'boy') | 47123 |
| ('SUM(num)', 'girl') | 118065 | | ('SUM(num)', 'girl') | 118065 |
@ -375,7 +376,7 @@ def test_pivot_df_single_row_two_metrics():
| ('MAX(num)', 'boy') | 1280 | | ('MAX(num)', 'boy') | 1280 |
| ('MAX(num)', 'girl') | 2588 | | ('MAX(num)', 'girl') | 2588 |
| ('MAX(num)', 'Subtotal') | 3868 | | ('MAX(num)', 'Subtotal') | 3868 |
| ('Total (Sum)', '') | 169056 | | ('{_("Total")} (Sum)', '') | 169056 |
""".strip() """.strip()
) )
@ -394,8 +395,8 @@ def test_pivot_df_single_row_two_metrics():
) )
assert ( assert (
pivoted.to_markdown() pivoted.to_markdown()
== """ == f"""
| | ('Total (Sum)',) | | | ('{_("Total")} (Sum)',) |
|:---------------------|-------------------:| |:---------------------|-------------------:|
| ('boy', 'SUM(num)') | 47123 | | ('boy', 'SUM(num)') | 47123 |
| ('boy', 'MAX(num)') | 1280 | | ('boy', 'MAX(num)') | 1280 |
@ -403,7 +404,7 @@ def test_pivot_df_single_row_two_metrics():
| ('girl', 'SUM(num)') | 118065 | | ('girl', 'SUM(num)') | 118065 |
| ('girl', 'MAX(num)') | 2588 | | ('girl', 'MAX(num)') | 2588 |
| ('girl', 'Subtotal') | 120653 | | ('girl', 'Subtotal') | 120653 |
| ('Total (Sum)', '') | 169056 | | ('{_("Total")} (Sum)', '') | 169056 |
""".strip() """.strip()
) )