mirror of https://github.com/apache/superset.git
js translation -- performance improvment (#3390)
* Chinese page * Using react-intl-universal to improve multi language in react page * Using react-intl-universal to improve multi language in react page * react_intl_universal * change * change * change * change * change * change * change * merge * multiple page in js * merge * merge * merge * merge * Js Translations * JS Translation * JS Translations * Js translation * JS translations * JS translations * Js translaion * JS en Translation * JS Translation * upgrade document Fixing the damn build (#3179) * Fixing the build * Going deeper [bugfix] only filterable columns should show up in FilterBox list (#3105) * [bugfix] only filterable columns should show up in FilterBox list * Touchups Datasource cannot be empty (#3035) add title description to model view (#3045) * add title description to model view * add missing import Add 'show/hide totals' option to pivot table vis (#3101) [bugfix] numeric value for date fields in table viz (#3036) Bug was present only when using the NOT GROUPED BY option fixes https://github.com/ApacheInfra/superset/issues/3027 fix hive.fetch_logs (#2968) add Zalando to the list of organizations (#3171) docs: fixup installation examples code indentation (#3169) [bugfix] fix bar order (#3180) [bugfix] visualize flow error: 'Metric x is not valid' (#3181) The metric name in the frontend doesn't match the one generated on the backend. It turns out the explore view will default to the first metric so specifying one isn't needed. Fix the segment interval for pulling metadata (#3174) The end of the interval would be on the truncated today date, which means that you will exclude today. If your realtime ingestion job runs shorter than a day, the metadata cannot be pulled from the druid cluster. Bump cryptography to 1.9 (#3065) As 1.7.2 doesn't compile here with openssl 1.1.0f Escaping the user's SQL in the explore view (#3186) * Escaping the user's SQL in the explore view When executing SQL from SQL Lab, we use a lower level API to the database which doesn't require escaping the SQL. When going through the explore view, the stack chain leading to the same method may need escaping depending on how the DBAPI driver is written, and that is the case for Presto (and perhaps other drivers). * Using regex to avoid doubling doubles [sqllab] improve Hive support (#3187) * [sqllab] improve Hive support * Fix "Transport not open" bug * Getting progress bar to show * Bump pyhive to 0.4.0 * Getting [Track Job] button to show * Fix testzz Add BigQuery engine specifications (#3193) As contributed by @mxmzdlv on issue #945 [bugfix] fix merge conflict that broke Hive support (#3196) Adding 'apache' to docs (#3194) [druid] Allow custom druid postaggregators (#3146) * [druid] Allow custom druid postaggregators Also, fix the postaggregation for approxHistogram quantiles so it adds the dependent field and that can show up in the graphs/tables. In general, postAggregators add significant power, we should probably support including custom postAggregators. Plywood has standard postAggregators here, and a customAggregator escape hatch that allows you to define custom postAggregators. This commit adds a similar capability for Superset and a additional field/fields/fieldName breakdown of the typical naming for dependent aggregations, which should make it significantly easier to develop approxHistogram and custom postAggregation-required dashboards. * [druid] Minor style cleanup in tests file. * [druid] Apply code review suggestions * break out CustomPostAggregator into separate class. This just cleans up the creation of the postaggregator a little bit. * minor style issues. * move the function around so the git diff is more readable add combine config for metrics in pivot table (#3086) * add combine config for metrics in pivot table * change method to stack/unstack * update backendSync Autofocus search input in VizTypeControl modal onEnter (#2929) Speed up JS build time (#3203) Also bumping a few related libs JS Translation JS translations js translation fix issue 3204 (#3205) [bugfix] capture Hive job_id pre-url transformation (#3213) js translation fix issue 3204 (#3205) [bugfix] capture Hive job_id pre-url transformation (#3213) [docs] update url in CONTRIBUTING.md (#3212) [sqllab/cosmetics] add margin-top for labels in query history (#3222) [explore] nvd3 sort values in rich tooltip (#3197) [sqllab] fix UI shows 'The query returned no results' momentarily (#3214) this is visible when running async queries between the fetching and success state as the rows are getting cached in the component [explore] DatasourceControl to pick datasource in modal (#3210) * [explore] DatasourceControl to pick datasource in modal Makes it easier to change datasource, also makes it such that the list of all datasources doesn't need to be loaded upfront. * Adding more metadata * Js translation * js tran * js trans * js trans * js tran * js trans * js trans * js tran * js translation * js trans * js translation * try load language pack async * Backend translations things * create language pack inside common data * performance improvement for js i18n. - js bundle should not contain localized content - we populate translation content from server-side, in boostrap.common.language_pack - in client-side, use promise to wrap around translation content. text will be translated after translation content arrived/parsed. - fix linting * fix Timer unit test * 1. add global hook for all tests, to make translation pack avaialble before each test starts. 2. fix unit test for Timer component 3. remove noused method get_locale, and modules 4. fix page reload after user change page language * parse and build i18n dictionary as a module * fix sync-backend task, which should run without DOM
This commit is contained in:
parent
1cf634afa2
commit
9af34ba51c
|
@ -331,6 +331,8 @@ key is to instrument the strings that need translation using
|
|||
a module, all you have to do is to `_("Wrap your strings")` using the
|
||||
underscore `_` "function".
|
||||
|
||||
We use `import {t, tn, TCT} from locales;` in js, JSX file, locales is in `./superset/assets/javascripts/` directory.
|
||||
|
||||
To enable changing language in your environment, you can simply add the
|
||||
`LANGUAGES` parameter to your `superset_config.py`. Having more than one
|
||||
options here will add a language selection dropdown on the right side of the
|
||||
|
@ -342,6 +344,10 @@ navigation bar.
|
|||
'zh': {'flag': 'cn', 'name': 'Chinese'},
|
||||
}
|
||||
|
||||
We need to extract the string to be translated, run the following command:
|
||||
|
||||
pybabel extract -F ./babel/babel.cfg -k _ -k __ -k t -k tn -k tct -o ./babel/messages.pot .
|
||||
|
||||
As per the [Flask AppBuilder documentation] about translation, to create a
|
||||
new language dictionary, run the following command:
|
||||
|
||||
|
@ -358,6 +364,14 @@ to take effect, they need to be compiled using this command:
|
|||
|
||||
fabmanager babel-compile --target superset/translations/
|
||||
|
||||
In the case of JS translation, we need to convert the PO file into a JSON file, and we need the global download of the npm package po2json.
|
||||
We need to be compiled using this command:
|
||||
|
||||
npm install po2json -g
|
||||
|
||||
Execute this command to convert the en PO file into a json file:
|
||||
|
||||
po2json -d superset -f jed1.x superset/translations/en/LC_MESSAGES/messages.po superset/translations/en/LC_MESSAGES/messages.json
|
||||
|
||||
## Adding new datasources
|
||||
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
[ignore: superset/assets/node_modules/**]
|
||||
[python: superset/**.py]
|
||||
[jinja2: superset/**/templates/**.html]
|
||||
[javascript: superset/assets/javascripts/**.js]
|
||||
[javascript: superset/assets/javascripts/**.jsx]
|
||||
[javascript: superset/assets/visualizations/**.js]
|
||||
[javascript: superset/assets/visualizations/**.jsx]
|
||||
encoding = utf-8
|
||||
|
|
2710
babel/messages.pot
2710
babel/messages.pot
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,7 @@
|
|||
/* global notify */
|
||||
import shortid from 'shortid';
|
||||
import { now } from '../modules/dates';
|
||||
import { t } from '../locales';
|
||||
|
||||
const $ = require('jquery');
|
||||
|
||||
|
@ -53,8 +54,8 @@ export function saveQuery(query) {
|
|||
type: 'POST',
|
||||
url,
|
||||
data: query,
|
||||
success: () => notify.success('Your query was saved'),
|
||||
error: () => notify.error('Your query could not be saved'),
|
||||
success: () => notify.success(t('Your query was saved')),
|
||||
error: () => notify.error(t('Your query could not be saved')),
|
||||
dataType: 'json',
|
||||
});
|
||||
return { type: SAVE_QUERY };
|
||||
|
@ -107,7 +108,7 @@ export function fetchQueryResults(query) {
|
|||
dispatch(querySuccess(query, results));
|
||||
},
|
||||
error(err) {
|
||||
let msg = 'Failed at retrieving results from the results backend';
|
||||
let msg = t('Failed at retrieving results from the results backend');
|
||||
if (err.responseJSON && err.responseJSON.error) {
|
||||
msg = err.responseJSON.error;
|
||||
}
|
||||
|
@ -153,12 +154,12 @@ export function runQuery(query) {
|
|||
}
|
||||
}
|
||||
if (textStatus === 'error' && errorThrown === '') {
|
||||
msg = 'Could not connect to server';
|
||||
msg = t('Could not connect to server');
|
||||
} else if (msg === null) {
|
||||
msg = `[${textStatus}] ${errorThrown}`;
|
||||
}
|
||||
if (msg.indexOf('CSRF token') > 0) {
|
||||
msg = 'Your session timed out, please refresh your page and try again.';
|
||||
msg = t('Your session timed out, please refresh your page and try again.');
|
||||
}
|
||||
dispatch(queryFailed(query, msg));
|
||||
},
|
||||
|
@ -177,10 +178,10 @@ export function postStopQuery(query) {
|
|||
url: stopQueryUrl,
|
||||
data: stopQueryRequestData,
|
||||
success() {
|
||||
notify.success('Query was stopped.');
|
||||
notify.success(t('Query was stopped.'));
|
||||
},
|
||||
error() {
|
||||
notify.error('Failed at stopping query.');
|
||||
notify.error(t('Failed at stopping query.'));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -293,7 +294,7 @@ export function addTable(query, tableName, schemaName) {
|
|||
isMetadataLoading: false,
|
||||
});
|
||||
dispatch(mergeTable(newTable));
|
||||
notify.error('Error occurred while fetching table metadata');
|
||||
notify.error(t('Error occurred while fetching table metadata'));
|
||||
});
|
||||
|
||||
url = `/superset/extra_table_metadata/${query.dbId}/${tableName}/${schemaName}/`;
|
||||
|
@ -306,7 +307,7 @@ export function addTable(query, tableName, schemaName) {
|
|||
isExtraMetadataLoading: false,
|
||||
});
|
||||
dispatch(mergeTable(newTable));
|
||||
notify.error('Error occurred while fetching table metadata');
|
||||
notify.error(t('Error occurred while fetching table metadata'));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -360,7 +361,7 @@ export function popStoredQuery(urlId) {
|
|||
success: (data) => {
|
||||
const newQuery = JSON.parse(data);
|
||||
const queryEditorProps = {
|
||||
title: newQuery.title ? newQuery.title : 'shared query',
|
||||
title: newQuery.title ? newQuery.title : t('shared query'),
|
||||
dbId: newQuery.dbId ? parseInt(newQuery.dbId, 10) : null,
|
||||
schema: newQuery.schema ? newQuery.schema : null,
|
||||
autorun: newQuery.autorun ? newQuery.autorun : false,
|
||||
|
@ -368,7 +369,7 @@ export function popStoredQuery(urlId) {
|
|||
};
|
||||
dispatch(addQueryEditor(queryEditorProps));
|
||||
},
|
||||
error: () => notify.error("The query couldn't be loaded"),
|
||||
error: () => notify.error(t('The query couldn\'t be loaded')),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -388,7 +389,7 @@ export function popSavedQuery(saveQueryId) {
|
|||
};
|
||||
dispatch(addQueryEditor(queryEditorProps));
|
||||
},
|
||||
error: () => notify.error("The query couldn't be loaded"),
|
||||
error: () => notify.error(t('The query couldn\'t be loaded')),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -421,7 +422,7 @@ export function createDatasource(vizOptions, context) {
|
|||
dispatch(createDatasourceSuccess(resp));
|
||||
},
|
||||
error: () => {
|
||||
dispatch(createDatasourceFailed('An error occurred while creating the data source'));
|
||||
dispatch(createDatasourceFailed(t('An error occurred while creating the data source')));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import CopyToClipboard from '../../components/CopyToClipboard';
|
||||
import { storeQuery } from '../../../utils/common';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
queryEditor: PropTypes.object.isRequired,
|
||||
|
@ -26,10 +27,10 @@ export default class CopyQueryTabUrl extends React.PureComponent {
|
|||
inMenu
|
||||
copyNode={(
|
||||
<div>
|
||||
<i className="fa fa-clipboard" /> <span>share query</span>
|
||||
<i className="fa fa-clipboard" /> <span>{t('share query')}</span>
|
||||
</div>
|
||||
)}
|
||||
tooltipText="copy URL to clipboard"
|
||||
tooltipText={t('copy URL to clipboard')}
|
||||
shouldShowText={false}
|
||||
getText={this.getUrl.bind(this)}
|
||||
/>
|
||||
|
|
|
@ -6,6 +6,7 @@ import sql from 'react-syntax-highlighter/dist/languages/sql';
|
|||
import github from 'react-syntax-highlighter/dist/styles/github';
|
||||
|
||||
import ModalTrigger from '../../components/ModalTrigger';
|
||||
import { t } from '../../locales';
|
||||
|
||||
registerLanguage('sql', sql);
|
||||
|
||||
|
@ -57,7 +58,7 @@ class HighlightedSql extends React.Component {
|
|||
if (this.props.rawSql && this.props.rawSql !== this.props.sql) {
|
||||
rawSql = (
|
||||
<div>
|
||||
<h4>Raw SQL</h4>
|
||||
<h4>{t('Raw SQL')}</h4>
|
||||
<SyntaxHighlighter language="sql" style={github}>
|
||||
{this.props.rawSql}
|
||||
</SyntaxHighlighter>
|
||||
|
@ -67,7 +68,7 @@ class HighlightedSql extends React.Component {
|
|||
this.setState({
|
||||
modalBody: (
|
||||
<div>
|
||||
<h4>Source SQL</h4>
|
||||
<h4>{t('Source SQL')}</h4>
|
||||
<SyntaxHighlighter language="sql" style={github}>
|
||||
{this.props.sql}
|
||||
</SyntaxHighlighter>
|
||||
|
@ -79,7 +80,7 @@ class HighlightedSql extends React.Component {
|
|||
render() {
|
||||
return (
|
||||
<ModalTrigger
|
||||
modalTitle="SQL"
|
||||
modalTitle={t('SQL')}
|
||||
triggerNode={this.triggerNode()}
|
||||
modalBody={this.state.modalBody}
|
||||
beforeOpen={this.generateModal.bind(this)}
|
||||
|
|
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||
import { Alert } from 'react-bootstrap';
|
||||
|
||||
import QueryTable from './QueryTable';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
queries: PropTypes.array.isRequired,
|
||||
|
@ -24,7 +25,7 @@ const QueryHistory = (props) => {
|
|||
}
|
||||
return (
|
||||
<Alert bsStyle="info">
|
||||
No query history yet...
|
||||
{t('No query history yet...')}
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@ import { now, epochTimeXHoursAgo,
|
|||
epochTimeXDaysAgo, epochTimeXYearsAgo } from '../../modules/dates';
|
||||
import { STATUS_OPTIONS, TIME_OPTIONS } from '../constants';
|
||||
import AsyncSelect from '../../components/AsyncSelect';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const $ = window.$ = require('jquery');
|
||||
|
||||
|
@ -102,7 +103,7 @@ class QuerySearch extends React.PureComponent {
|
|||
if (data.result.length === 0) {
|
||||
this.props.actions.addAlert({
|
||||
bsStyle: 'danger',
|
||||
msg: "It seems you don't have access to any database",
|
||||
msg: t('It seems you don\'t have access to any database'),
|
||||
});
|
||||
}
|
||||
return options;
|
||||
|
@ -150,15 +151,15 @@ class QuerySearch extends React.PureComponent {
|
|||
type="text"
|
||||
onChange={this.changeSearch.bind(this)}
|
||||
className="form-control input-sm"
|
||||
placeholder="Search Results"
|
||||
placeholder={t('Search Results')}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-4 search-date-filter-container">
|
||||
<Select
|
||||
name="select-from"
|
||||
placeholder="[From]-"
|
||||
placeholder={t('[From]-')}
|
||||
options={TIME_OPTIONS
|
||||
.slice(1, TIME_OPTIONS.length).map(t => ({ value: t, label: t }))}
|
||||
.slice(1, TIME_OPTIONS.length).map(xt => ({ value: xt, label: xt }))}
|
||||
value={this.state.from}
|
||||
autosize={false}
|
||||
onChange={this.changeFrom.bind(this)}
|
||||
|
@ -166,8 +167,8 @@ class QuerySearch extends React.PureComponent {
|
|||
|
||||
<Select
|
||||
name="select-to"
|
||||
placeholder="[To]-"
|
||||
options={TIME_OPTIONS.map(t => ({ value: t, label: t }))}
|
||||
placeholder={t('[To]-')}
|
||||
options={TIME_OPTIONS.map(xt => ({ value: xt, label: xt }))}
|
||||
value={this.state.to}
|
||||
autosize={false}
|
||||
onChange={this.changeTo.bind(this)}
|
||||
|
@ -175,7 +176,7 @@ class QuerySearch extends React.PureComponent {
|
|||
|
||||
<Select
|
||||
name="select-status"
|
||||
placeholder="[Query Status]"
|
||||
placeholder={t('[Query Status]')}
|
||||
options={STATUS_OPTIONS.map(s => ({ value: s, label: s }))}
|
||||
value={this.state.status}
|
||||
isLoading={false}
|
||||
|
@ -184,7 +185,7 @@ class QuerySearch extends React.PureComponent {
|
|||
/>
|
||||
|
||||
<Button bsSize="small" bsStyle="success" onClick={this.refreshQueries.bind(this)}>
|
||||
Search
|
||||
{t('Search')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -12,6 +12,7 @@ import HighlightedSql from './HighlightedSql';
|
|||
import { fDuration } from '../../modules/dates';
|
||||
import { storeQuery } from '../../../utils/common';
|
||||
import QueryStateLabel from './QueryStateLabel';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
columns: PropTypes.array,
|
||||
|
@ -45,7 +46,7 @@ class QueryTable extends React.PureComponent {
|
|||
openQuery(dbId, schema, sql) {
|
||||
const newQuery = {
|
||||
dbId,
|
||||
title: 'Untitled Query',
|
||||
title: t('Untitled Query'),
|
||||
schema,
|
||||
sql,
|
||||
};
|
||||
|
@ -110,7 +111,7 @@ class QueryTable extends React.PureComponent {
|
|||
className="btn btn-link btn-xs"
|
||||
onClick={this.openQuery.bind(this, q.dbId, q.schema, q.sql)}
|
||||
>
|
||||
<i className="fa fa-external-link" />Open in SQL Editor
|
||||
<i className="fa fa-external-link" />{t('Open in SQL Editor')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
@ -129,10 +130,10 @@ class QueryTable extends React.PureComponent {
|
|||
bsStyle="info"
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
view results
|
||||
{t('view results')}
|
||||
</Label>
|
||||
)}
|
||||
modalTitle={'Data preview'}
|
||||
modalTitle={t('Data preview')}
|
||||
beforeOpen={this.openAsyncResults.bind(this, query)}
|
||||
onExit={this.clearQueryResults.bind(this, query)}
|
||||
modalBody={
|
||||
|
@ -172,24 +173,24 @@ class QueryTable extends React.PureComponent {
|
|||
<div style={{ width: '75px' }}>
|
||||
<Link
|
||||
className="fa fa-line-chart m-r-3"
|
||||
tooltip="Visualize the data out of this query"
|
||||
tooltip={t('Visualize the data out of this query')}
|
||||
onClick={this.showVisualizeModal.bind(this, query)}
|
||||
/>
|
||||
<Link
|
||||
className="fa fa-pencil m-r-3"
|
||||
onClick={this.restoreSql.bind(this, query)}
|
||||
tooltip="Overwrite text in editor with a query on this table"
|
||||
tooltip={t('Overwrite text in editor with a query on this table')}
|
||||
placement="top"
|
||||
/>
|
||||
<Link
|
||||
className="fa fa-plus-circle m-r-3"
|
||||
onClick={this.openQueryInNewTab.bind(this, query)}
|
||||
tooltip="Run query in a new tab"
|
||||
tooltip={t('Run query in a new tab')}
|
||||
placement="top"
|
||||
/>
|
||||
<Link
|
||||
className="fa fa-trash m-r-3"
|
||||
tooltip="Remove query from log"
|
||||
tooltip={t('Remove query from log')}
|
||||
onClick={this.removeQuery.bind(this, query)}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -7,6 +7,7 @@ import VisualizeModal from './VisualizeModal';
|
|||
import HighlightedSql from './HighlightedSql';
|
||||
import FilterableTable from '../../components/FilterableTable/FilterableTable';
|
||||
import QueryStateLabel from './QueryStateLabel';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
actions: PropTypes.object,
|
||||
|
@ -63,7 +64,7 @@ export default class ResultSet extends React.PureComponent {
|
|||
if (this.props.csv) {
|
||||
csvButton = (
|
||||
<Button bsSize="small" href={'/superset/csv/' + this.props.query.id}>
|
||||
<i className="fa fa-file-text-o" /> .CSV
|
||||
<i className="fa fa-file-text-o" /> {t('.CSV')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
@ -74,7 +75,7 @@ export default class ResultSet extends React.PureComponent {
|
|||
bsSize="small"
|
||||
onClick={this.showModal.bind(this)}
|
||||
>
|
||||
<i className="fa fa-line-chart m-l-1" /> Visualize
|
||||
<i className="fa fa-line-chart m-l-1" /> {t('Visualize')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
@ -85,7 +86,7 @@ export default class ResultSet extends React.PureComponent {
|
|||
type="text"
|
||||
onChange={this.changeSearch.bind(this)}
|
||||
className="form-control input-sm"
|
||||
placeholder="Search Results"
|
||||
placeholder={t('Search Results')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -158,14 +159,14 @@ export default class ResultSet extends React.PureComponent {
|
|||
return (
|
||||
<div>
|
||||
<Alert bsStyle="info">
|
||||
Table [<strong>{query.tempTable}</strong>] was
|
||||
created
|
||||
{t('Table')} [<strong>{query.tempTable}</strong>] {t('was ' +
|
||||
'created')}
|
||||
<Button
|
||||
bsSize="small"
|
||||
className="m-r-5"
|
||||
onClick={this.popSelectStar.bind(this)}
|
||||
>
|
||||
Query in a new tab
|
||||
{t('Query in a new tab')}
|
||||
</Button>
|
||||
</Alert>
|
||||
</div>);
|
||||
|
@ -206,7 +207,7 @@ export default class ResultSet extends React.PureComponent {
|
|||
bsStyle="primary"
|
||||
onClick={this.reFetchQueryResults.bind(this, query)}
|
||||
>
|
||||
Fetch data preview
|
||||
{t('Fetch data preview')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
@ -226,13 +227,13 @@ export default class ResultSet extends React.PureComponent {
|
|||
bsSize="small"
|
||||
onClick={() => { window.open(query.trackingUrl); }}
|
||||
>
|
||||
Track Job
|
||||
{t('Track Job')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<img className="loading" alt="Loading..." src="/static/assets/images/loading.gif" />
|
||||
<img className="loading" alt={t('Loading...')} src="/static/assets/images/loading.gif" />
|
||||
<QueryStateLabel query={query} />
|
||||
{progressBar}
|
||||
<div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from '../../components/Button';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
allowAsync: PropTypes.bool.isRequired,
|
||||
|
@ -15,10 +16,10 @@ const defaultProps = {
|
|||
};
|
||||
|
||||
export default function RunQueryActionButton(props) {
|
||||
const runBtnText = props.selectedText ? 'Run Selected Query' : 'Run Query';
|
||||
const runBtnText = props.selectedText ? t('Run Selected Query') : t('Run Query');
|
||||
const btnStyle = props.selectedText ? 'warning' : 'primary';
|
||||
const shouldShowStopBtn = ['running', 'pending'].indexOf(props.queryState) > -1;
|
||||
const tooltip = 'shortcut: [alt+enter]';
|
||||
const asyncToolTip = t('Run query asynchronously');
|
||||
|
||||
const commonBtnProps = {
|
||||
bsSize: 'small',
|
||||
|
@ -31,7 +32,7 @@ export default function RunQueryActionButton(props) {
|
|||
{...commonBtnProps}
|
||||
onClick={() => props.runQuery(false)}
|
||||
key="run-btn"
|
||||
tooltip={tooltip}
|
||||
tooltip={asyncToolTip}
|
||||
>
|
||||
<i className="fa fa-refresh" /> {runBtnText}
|
||||
</Button>
|
||||
|
@ -42,7 +43,7 @@ export default function RunQueryActionButton(props) {
|
|||
{...commonBtnProps}
|
||||
onClick={() => props.runQuery(true)}
|
||||
key="run-async-btn"
|
||||
tooltip={tooltip}
|
||||
tooltip={asyncToolTip}
|
||||
>
|
||||
<i className="fa fa-table" /> {runBtnText}
|
||||
</Button>
|
||||
|
@ -53,7 +54,7 @@ export default function RunQueryActionButton(props) {
|
|||
{...commonBtnProps}
|
||||
onClick={props.stopQuery}
|
||||
>
|
||||
<i className="fa fa-stop" /> Stop
|
||||
<i className="fa fa-stop" /> {t('Stop')}
|
||||
</Button>
|
||||
);
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { FormControl, FormGroup, Overlay, Popover, Row, Col } from 'react-bootstrap';
|
||||
import Button from '../../components/Button';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
defaultLabel: PropTypes.string,
|
||||
|
@ -12,7 +13,7 @@ const propTypes = {
|
|||
onSave: PropTypes.func,
|
||||
};
|
||||
const defaultProps = {
|
||||
defaultLabel: 'Undefined',
|
||||
defaultLabel: t('Undefined'),
|
||||
animation: true,
|
||||
onSave: () => {},
|
||||
};
|
||||
|
@ -62,12 +63,12 @@ class SaveQuery extends React.PureComponent {
|
|||
<Col md={12}>
|
||||
<small>
|
||||
<label className="control-label" htmlFor="embed-height">
|
||||
Label
|
||||
{t('Label')}
|
||||
</label>
|
||||
</small>
|
||||
<FormControl
|
||||
type="text"
|
||||
placeholder="Label for your query"
|
||||
placeholder={t('Label for your query')}
|
||||
value={this.state.label}
|
||||
onChange={this.onLabelChange}
|
||||
/>
|
||||
|
@ -77,11 +78,11 @@ class SaveQuery extends React.PureComponent {
|
|||
<Row>
|
||||
<Col md={12}>
|
||||
<small>
|
||||
<label className="control-label" htmlFor="embed-height">Description</label>
|
||||
<label className="control-label" htmlFor="embed-height">{t('Description')}</label>
|
||||
</small>
|
||||
<FormControl
|
||||
componentClass="textarea"
|
||||
placeholder="Write a description for your query"
|
||||
placeholder={t('Write a description for your query')}
|
||||
value={this.state.description}
|
||||
onChange={this.onDescriptionChange}
|
||||
/>
|
||||
|
@ -95,10 +96,10 @@ class SaveQuery extends React.PureComponent {
|
|||
onClick={this.onSave}
|
||||
className="m-r-3"
|
||||
>
|
||||
Save
|
||||
{t('Save')}
|
||||
</Button>
|
||||
<Button onClick={this.onCancel} className="cancelQuery">
|
||||
Cancel
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -119,7 +120,7 @@ class SaveQuery extends React.PureComponent {
|
|||
{this.renderPopover()}
|
||||
</Overlay>
|
||||
<Button bsSize="small" className="toggleSave" onClick={this.toggleSave}>
|
||||
<i className="fa fa-save" /> Save Query
|
||||
<i className="fa fa-save" /> {t('Save Query')}
|
||||
</Button>
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -8,6 +8,7 @@ import { bindActionCreators } from 'redux';
|
|||
import * as Actions from '../actions';
|
||||
import QueryHistory from './QueryHistory';
|
||||
import ResultSet from './ResultSet';
|
||||
import { t } from '../../locales';
|
||||
|
||||
/*
|
||||
editorQueries are queries executed by users passed from SqlEditor component
|
||||
|
@ -48,12 +49,12 @@ class SouthPane extends React.PureComponent {
|
|||
/>
|
||||
);
|
||||
} else {
|
||||
results = <Alert bsStyle="info">Run a query to display results here</Alert>;
|
||||
results = <Alert bsStyle="info">{t('Run a query to display results here')}</Alert>;
|
||||
}
|
||||
|
||||
const dataPreviewTabs = props.dataPreviewQueries.map(query => (
|
||||
<Tab
|
||||
title={`Preview for ${query.tableName}`}
|
||||
title={t('Preview for %s', query.tableName)}
|
||||
eventKey={query.id}
|
||||
key={query.id}
|
||||
>
|
||||
|
@ -77,13 +78,13 @@ class SouthPane extends React.PureComponent {
|
|||
onSelect={this.switchTab.bind(this)}
|
||||
>
|
||||
<Tab
|
||||
title="Results"
|
||||
title={t('Results')}
|
||||
eventKey="Results"
|
||||
>
|
||||
{results}
|
||||
</Tab>
|
||||
<Tab
|
||||
title="Query History"
|
||||
title={t('Query History')}
|
||||
eventKey="History"
|
||||
>
|
||||
<div style={{ height: `${innerTabHeight}px`, overflow: 'scroll' }}>
|
||||
|
|
|
@ -22,6 +22,7 @@ import SqlEditorLeftBar from './SqlEditorLeftBar';
|
|||
import AceEditorWrapper from './AceEditorWrapper';
|
||||
import { STATE_BSSTYLE_MAP } from '../constants';
|
||||
import RunQueryActionButton from './RunQueryActionButton';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
actions: PropTypes.object.isRequired,
|
||||
|
@ -119,7 +120,7 @@ class SqlEditor extends React.PureComponent {
|
|||
renderEditorBottomBar() {
|
||||
let ctasControls;
|
||||
if (this.props.database && this.props.database.allow_ctas) {
|
||||
const ctasToolTip = 'Create table as with query results';
|
||||
const ctasToolTip = t('Create table as with query results');
|
||||
ctasControls = (
|
||||
<FormGroup>
|
||||
<InputGroup>
|
||||
|
@ -127,7 +128,7 @@ class SqlEditor extends React.PureComponent {
|
|||
type="text"
|
||||
bsSize="small"
|
||||
className="input-sm"
|
||||
placeholder="new table name"
|
||||
placeholder={t('new table name')}
|
||||
onChange={this.ctasChanged.bind(this)}
|
||||
/>
|
||||
<InputGroup.Button>
|
||||
|
|
|
@ -7,6 +7,7 @@ import createFilterOptions from 'react-select-fast-filter-options';
|
|||
|
||||
import TableElement from './TableElement';
|
||||
import AsyncSelect from '../../components/AsyncSelect';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const $ = window.$ = require('jquery');
|
||||
|
||||
|
@ -62,7 +63,7 @@ class SqlEditorLeftBar extends React.PureComponent {
|
|||
if (data.result.length === 0) {
|
||||
this.props.actions.addAlert({
|
||||
bsStyle: 'danger',
|
||||
msg: "It seems you don't have access to any database",
|
||||
msg: t('It seems you don\'t have access to any database'),
|
||||
});
|
||||
}
|
||||
return options;
|
||||
|
@ -86,7 +87,7 @@ class SqlEditorLeftBar extends React.PureComponent {
|
|||
})
|
||||
.fail(() => {
|
||||
this.setState({ tableLoading: false, tableOptions: [], tableLength: 0 });
|
||||
notify.error('Error while fetching table list');
|
||||
notify.error(t('Error while fetching table list'));
|
||||
});
|
||||
} else {
|
||||
this.setState({ tableLoading: false, tableOptions: [], filterOptions: null });
|
||||
|
@ -127,7 +128,7 @@ class SqlEditorLeftBar extends React.PureComponent {
|
|||
})
|
||||
.fail(() => {
|
||||
this.setState({ schemaLoading: false, schemaOptions: [] });
|
||||
notify.error('Error while fetching schema list');
|
||||
notify.error(t('Error while fetching schema list'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -149,29 +150,29 @@ class SqlEditorLeftBar extends React.PureComponent {
|
|||
'_od_DatabaseAsync=asc'
|
||||
}
|
||||
onChange={this.onDatabaseChange.bind(this)}
|
||||
onAsyncError={() => notify.error('Error while fetching database list')}
|
||||
onAsyncError={() => notify.error(t('Error while fetching database list'))}
|
||||
value={this.props.queryEditor.dbId}
|
||||
databaseId={this.props.queryEditor.dbId}
|
||||
actions={this.props.actions}
|
||||
valueRenderer={o => (
|
||||
<div>
|
||||
<span className="text-muted">Database:</span> {o.label}
|
||||
<span className="text-muted">{t('Database:')}</span> {o.label}
|
||||
</div>
|
||||
)}
|
||||
mutator={this.dbMutator.bind(this)}
|
||||
placeholder="Select a database"
|
||||
placeholder={t('Select a database')}
|
||||
autoSelect
|
||||
/>
|
||||
</div>
|
||||
<div className="m-t-5">
|
||||
<Select
|
||||
name="select-schema"
|
||||
placeholder={`Select a schema (${this.state.schemaOptions.length})`}
|
||||
placeholder={t('Select a schema (%s)', this.state.schemaOptions.length)}
|
||||
options={this.state.schemaOptions}
|
||||
value={this.props.queryEditor.schema}
|
||||
valueRenderer={o => (
|
||||
<div>
|
||||
<span className="text-muted">Schema:</span> {o.label}
|
||||
<span className="text-muted">{t('Schema:')}</span> {o.label}
|
||||
</div>
|
||||
)}
|
||||
isLoading={this.state.schemaLoading}
|
||||
|
@ -186,7 +187,7 @@ class SqlEditorLeftBar extends React.PureComponent {
|
|||
ref="selectTable"
|
||||
isLoading={this.state.tableLoading}
|
||||
value={this.state.tableName}
|
||||
placeholder={`Add a table (${this.state.tableOptions.length})`}
|
||||
placeholder={t('Add a table (%s)', this.state.tableOptions.length)}
|
||||
autosize={false}
|
||||
onChange={this.changeTable.bind(this)}
|
||||
filterOptions={this.state.filterOptions}
|
||||
|
@ -199,7 +200,7 @@ class SqlEditorLeftBar extends React.PureComponent {
|
|||
name="async-select-table"
|
||||
ref="selectTable"
|
||||
value={this.state.tableName}
|
||||
placeholder={'Type to search ...'}
|
||||
placeholder={t('Type to search ...')}
|
||||
autosize={false}
|
||||
onChange={this.changeTable.bind(this)}
|
||||
loadOptions={this.getTableNamesBySubStr.bind(this)}
|
||||
|
@ -222,7 +223,7 @@ class SqlEditorLeftBar extends React.PureComponent {
|
|||
</div>
|
||||
{shouldShowReset &&
|
||||
<Button bsSize="small" bsStyle="danger" onClick={this.resetState.bind(this)}>
|
||||
<i className="fa fa-bomb" /> Reset State
|
||||
<i className="fa fa-bomb" /> {t('Reset State')}
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -9,6 +9,7 @@ import * as Actions from '../actions';
|
|||
import SqlEditor from './SqlEditor';
|
||||
import CopyQueryTabUrl from './CopyQueryTabUrl';
|
||||
import { areArraysShallowEqual } from '../../reduxUtils';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
actions: PropTypes.object.isRequired,
|
||||
|
@ -101,7 +102,7 @@ class TabbedSqlEditors extends React.PureComponent {
|
|||
}
|
||||
renameTab(qe) {
|
||||
/* eslint no-alert: 0 */
|
||||
const newTitle = prompt('Enter a new title for the tab');
|
||||
const newTitle = prompt(t('Enter a new title for the tab'));
|
||||
if (newTitle) {
|
||||
this.props.actions.queryEditorSetTitle(qe, newTitle);
|
||||
}
|
||||
|
@ -120,7 +121,7 @@ class TabbedSqlEditors extends React.PureComponent {
|
|||
queryCount++;
|
||||
const activeQueryEditor = this.activeQueryEditor();
|
||||
const qe = {
|
||||
title: `Untitled Query ${queryCount}`,
|
||||
title: t('Untitled Query %s', queryCount),
|
||||
dbId: (activeQueryEditor && activeQueryEditor.dbId) ?
|
||||
activeQueryEditor.dbId :
|
||||
this.props.defaultDbId,
|
||||
|
@ -166,10 +167,10 @@ class TabbedSqlEditors extends React.PureComponent {
|
|||
title=""
|
||||
>
|
||||
<MenuItem eventKey="1" onClick={this.removeQueryEditor.bind(this, qe)}>
|
||||
<i className="fa fa-close" /> close tab
|
||||
<i className="fa fa-close" /> {t('close tab')}
|
||||
</MenuItem>
|
||||
<MenuItem eventKey="2" onClick={this.renameTab.bind(this, qe)}>
|
||||
<i className="fa fa-i-cursor" /> rename tab
|
||||
<i className="fa fa-i-cursor" /> {t('rename tab')}
|
||||
</MenuItem>
|
||||
{qe &&
|
||||
<CopyQueryTabUrl queryEditor={qe} />
|
||||
|
@ -177,7 +178,7 @@ class TabbedSqlEditors extends React.PureComponent {
|
|||
<MenuItem eventKey="4" onClick={this.toggleLeftBar.bind(this)}>
|
||||
<i className="fa fa-cogs" />
|
||||
|
||||
{this.state.hideLeftBar ? 'expand tool bar' : 'hide tool bar'}
|
||||
{this.state.hideLeftBar ? t('expand tool bar') : t('hide tool bar')}
|
||||
</MenuItem>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
|
@ -193,7 +194,7 @@ class TabbedSqlEditors extends React.PureComponent {
|
|||
{isSelected &&
|
||||
<SqlEditor
|
||||
height={this.props.editorHeight}
|
||||
tables={this.props.tables.filter(t => (t.queryEditorId === qe.id))}
|
||||
tables={this.props.tables.filter(xt => (xt.queryEditorId === qe.id))}
|
||||
queryEditor={qe}
|
||||
editorQueries={this.state.queriesArray}
|
||||
dataPreviewQueries={this.state.dataPreviewQueries}
|
||||
|
|
|
@ -9,6 +9,7 @@ import Link from './Link';
|
|||
import ColumnElement from './ColumnElement';
|
||||
import ModalTrigger from '../../components/ModalTrigger';
|
||||
import Loading from '../../components/Loading';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
table: PropTypes.object,
|
||||
|
@ -71,7 +72,7 @@ class TableElement extends React.PureComponent {
|
|||
let partitionClipBoard;
|
||||
if (table.partitions.partitionQuery) {
|
||||
partitionQuery = table.partitions.partitionQuery;
|
||||
const tt = 'Copy partition query to clipboard';
|
||||
const tt = t('Copy partition query to clipboard');
|
||||
partitionClipBoard = (
|
||||
<CopyToClipboard
|
||||
text={partitionQuery}
|
||||
|
@ -90,7 +91,7 @@ class TableElement extends React.PureComponent {
|
|||
<Well bsSize="small">
|
||||
<div>
|
||||
<small>
|
||||
latest partition: {latest}
|
||||
{t('latest partition:')} {latest}
|
||||
</small> {partitionClipBoard}
|
||||
</div>
|
||||
</Well>
|
||||
|
@ -106,7 +107,7 @@ class TableElement extends React.PureComponent {
|
|||
<ModalTrigger
|
||||
modalTitle={
|
||||
<div>
|
||||
Keys for table <strong>{table.name}</strong>
|
||||
{t('Keys for table')} <strong>{table.name}</strong>
|
||||
</div>
|
||||
}
|
||||
modalBody={table.indexes.map((ix, i) => (
|
||||
|
@ -115,7 +116,7 @@ class TableElement extends React.PureComponent {
|
|||
triggerNode={
|
||||
<Link
|
||||
className="fa fa-key pull-left m-l-2"
|
||||
tooltip={`View keys & indexes (${table.indexes.length})`}
|
||||
tooltip={t('View keys & indexes (%s)', table.indexes.length)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
@ -131,8 +132,8 @@ class TableElement extends React.PureComponent {
|
|||
onClick={this.toggleSortColumns.bind(this)}
|
||||
tooltip={
|
||||
!this.state.sortColumns ?
|
||||
'Sort columns alphabetically' :
|
||||
'Original table column order'}
|
||||
t('Sort columns alphabetically') :
|
||||
t('Original table column order')}
|
||||
href="#"
|
||||
/>
|
||||
{table.selectStar &&
|
||||
|
@ -142,13 +143,13 @@ class TableElement extends React.PureComponent {
|
|||
}
|
||||
text={table.selectStar}
|
||||
shouldShowText={false}
|
||||
tooltipText="Copy SELECT statement to clipboard"
|
||||
tooltipText={t('Copy SELECT statement to clipboard')}
|
||||
/>
|
||||
}
|
||||
<Link
|
||||
className="fa fa-times table-remove pull-left m-l-2"
|
||||
onClick={this.removeTable.bind(this)}
|
||||
tooltip="Remove table preview"
|
||||
tooltip={t('Remove table preview')}
|
||||
href="#"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
|
|
|
@ -13,6 +13,7 @@ import { getExploreUrl } from '../../explore/exploreUtils';
|
|||
import * as actions from '../actions';
|
||||
import { VISUALIZE_VALIDATION_ERRORS } from '../constants';
|
||||
import visTypes from '../../explore/stores/visTypes';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const CHART_TYPES = Object.keys(visTypes)
|
||||
.filter(typeName => !!visTypes[typeName].showOnExplore)
|
||||
|
@ -86,9 +87,9 @@ class VisualizeModal extends React.PureComponent {
|
|||
if (!re.test(colName)) {
|
||||
hints.push(
|
||||
<div>
|
||||
"{colName}" is not right as a column name, please alias it
|
||||
(as in SELECT count(*) <strong>AS my_alias</strong>) using only
|
||||
alphanumeric characters and underscores
|
||||
{t('%s is not right as a column name, please alias it ' +
|
||||
'(as in SELECT count(*) ', colName)} <strong>{t('AS my_alias')}</strong>) {t('using only ' +
|
||||
'alphanumeric characters and underscores')}
|
||||
</div>);
|
||||
}
|
||||
});
|
||||
|
@ -162,7 +163,7 @@ class VisualizeModal extends React.PureComponent {
|
|||
if (mainGroupBy) {
|
||||
formData.groupby = [mainGroupBy.name];
|
||||
}
|
||||
notify.info('Creating a data source and popping a new tab');
|
||||
notify.info(t('Creating a data source and popping a new tab'));
|
||||
|
||||
window.open(getExploreUrl(formData));
|
||||
})
|
||||
|
@ -192,7 +193,7 @@ class VisualizeModal extends React.PureComponent {
|
|||
<div className="VisualizeModal">
|
||||
<Modal show={this.props.show} onHide={this.props.onHide}>
|
||||
<Modal.Body>
|
||||
No results available for this query
|
||||
{t('No results available for this query')}
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</div>
|
||||
|
@ -237,17 +238,17 @@ class VisualizeModal extends React.PureComponent {
|
|||
<div className="VisualizeModal">
|
||||
<Modal show={this.props.show} onHide={this.props.onHide}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Visualize</Modal.Title>
|
||||
<Modal.Title>{t('Visualize')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{alerts}
|
||||
{this.buildVisualizeAdvise()}
|
||||
<div className="row">
|
||||
<Col md={6}>
|
||||
Chart Type
|
||||
{t('Chart Type')}
|
||||
<Select
|
||||
name="select-chart-type"
|
||||
placeholder="[Chart Type]"
|
||||
placeholder={t('[Chart Type]')}
|
||||
options={CHART_TYPES}
|
||||
value={(this.state.chartType) ? this.state.chartType.value : null}
|
||||
autosize={false}
|
||||
|
@ -255,11 +256,11 @@ class VisualizeModal extends React.PureComponent {
|
|||
/>
|
||||
</Col>
|
||||
<Col md={6}>
|
||||
Datasource Name
|
||||
{t('Datasource Name')}
|
||||
<input
|
||||
type="text"
|
||||
className="form-control input-sm"
|
||||
placeholder="datasource name"
|
||||
placeholder={t('datasource name')}
|
||||
onChange={this.changeDatasourceName.bind(this)}
|
||||
value={this.state.datasourceName}
|
||||
/>
|
||||
|
@ -276,7 +277,7 @@ class VisualizeModal extends React.PureComponent {
|
|||
bsStyle="primary"
|
||||
disabled={(this.state.hints.length > 0)}
|
||||
>
|
||||
Visualize
|
||||
{t('Visualize')}
|
||||
</Button>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { t } from '../locales';
|
||||
|
||||
export const STATE_BSSTYLE_MAP = {
|
||||
failed: 'danger',
|
||||
pending: 'info',
|
||||
|
@ -25,8 +27,8 @@ export const TIME_OPTIONS = [
|
|||
];
|
||||
|
||||
export const VISUALIZE_VALIDATION_ERRORS = {
|
||||
REQUIRE_CHART_TYPE: 'Pick a chart type!',
|
||||
REQUIRE_TIME: 'To use this chart type you need at least one column flagged as a date',
|
||||
REQUIRE_DIMENSION: 'To use this chart type you need at least one dimension',
|
||||
REQUIRE_AGGREGATION_FUNCTION: 'To use this chart type you need at least one aggregation function',
|
||||
REQUIRE_CHART_TYPE: t('Pick a chart type!'),
|
||||
REQUIRE_TIME: t('To use this chart type you need at least one column flagged as a date'),
|
||||
REQUIRE_DIMENSION: t('To use this chart type you need at least one dimension'),
|
||||
REQUIRE_AGGREGATION_FUNCTION: t('To use this chart type you need at least one aggregation function'),
|
||||
};
|
||||
|
|
|
@ -3,11 +3,12 @@ import * as actions from './actions';
|
|||
import { now } from '../modules/dates';
|
||||
import { addToObject, alterInObject, alterInArr, removeFromArr, getFromArr, addToArr }
|
||||
from '../reduxUtils';
|
||||
import { t } from '../locales';
|
||||
|
||||
export function getInitialState(defaultDbId) {
|
||||
const defaultQueryEditor = {
|
||||
id: shortid.generate(),
|
||||
title: 'Untitled Query',
|
||||
title: t('Untitled Query'),
|
||||
sql: 'SELECT *\nFROM\nWHERE',
|
||||
selectedText: null,
|
||||
latestQueryId: null,
|
||||
|
@ -40,7 +41,7 @@ export const sqlLabReducer = function (state, action) {
|
|||
qe.id === state.tabHistory[state.tabHistory.length - 1]);
|
||||
const qe = {
|
||||
id: shortid.generate(),
|
||||
title: `Copy of ${progenitor.title}`,
|
||||
title: t('Copy of %s', progenitor.title),
|
||||
dbId: (action.query.dbId) ? action.query.dbId : null,
|
||||
schema: (action.query.schema) ? action.query.schema : null,
|
||||
autorun: true,
|
||||
|
@ -76,13 +77,13 @@ export const sqlLabReducer = function (state, action) {
|
|||
[actions.MERGE_TABLE]() {
|
||||
const at = Object.assign({}, action.table);
|
||||
let existingTable;
|
||||
state.tables.forEach((t) => {
|
||||
state.tables.forEach((xt) => {
|
||||
if (
|
||||
t.dbId === at.dbId &&
|
||||
t.queryEditorId === at.queryEditorId &&
|
||||
t.schema === at.schema &&
|
||||
t.name === at.name) {
|
||||
existingTable = t;
|
||||
xt.dbId === at.dbId &&
|
||||
xt.queryEditorId === at.queryEditorId &&
|
||||
xt.schema === at.schema &&
|
||||
xt.name === at.name) {
|
||||
existingTable = xt;
|
||||
}
|
||||
});
|
||||
if (existingTable) {
|
||||
|
@ -115,11 +116,11 @@ export const sqlLabReducer = function (state, action) {
|
|||
delete queries[action.oldQueryId];
|
||||
|
||||
const newTables = [];
|
||||
state.tables.forEach((t) => {
|
||||
if (t.dataPreviewQueryId === action.oldQueryId) {
|
||||
newTables.push(Object.assign({}, t, { dataPreviewQueryId: action.newQuery.id }));
|
||||
state.tables.forEach((xt) => {
|
||||
if (xt.dataPreviewQueryId === action.oldQueryId) {
|
||||
newTables.push(Object.assign({}, xt, { dataPreviewQueryId: action.newQuery.id }));
|
||||
} else {
|
||||
newTables.push(t);
|
||||
newTables.push(xt);
|
||||
}
|
||||
});
|
||||
return Object.assign(
|
||||
|
|
|
@ -50,30 +50,30 @@ export default class AddSliceContainer extends React.PureComponent {
|
|||
render() {
|
||||
return (
|
||||
<div className="container">
|
||||
<Panel header={<h3>Create a new slice</h3>}>
|
||||
<Panel header={<h3>{('Create a new slice')}</h3>}>
|
||||
<Grid>
|
||||
<Row>
|
||||
<Col xs={12} sm={6}>
|
||||
<div>
|
||||
<p>Choose a datasource</p>
|
||||
<p>{('Choose a datasource')}</p>
|
||||
<Select
|
||||
clearable={false}
|
||||
name="select-datasource"
|
||||
onChange={this.changeDatasource.bind(this)}
|
||||
options={this.props.datasources}
|
||||
placeholder="Choose a datasource"
|
||||
placeholder={('Choose a datasource')}
|
||||
value={this.state.datasourceValue}
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<p>Choose a visualization type</p>
|
||||
<p>{('Choose a visualization type')}</p>
|
||||
<Select
|
||||
clearable={false}
|
||||
name="select-vis-type"
|
||||
onChange={this.changeVisType.bind(this)}
|
||||
options={this.vizTypeOptions}
|
||||
placeholder="Choose a visualization type"
|
||||
placeholder={('Choose a visualization type')}
|
||||
value={this.state.visType}
|
||||
/>
|
||||
</div>
|
||||
|
@ -83,7 +83,7 @@ export default class AddSliceContainer extends React.PureComponent {
|
|||
disabled={this.isBtnDisabled()}
|
||||
onClick={this.gotoSlice.bind(this)}
|
||||
>
|
||||
Create new slice
|
||||
{('Create new slice')}
|
||||
</Button>
|
||||
<br /><br />
|
||||
</Col>
|
||||
|
|
|
@ -10,6 +10,17 @@ $(document).ready(function () {
|
|||
const id = $this.attr('id');
|
||||
utils.toggleCheckbox(prefix, '#' + id);
|
||||
});
|
||||
|
||||
// for language picker dropdown
|
||||
$('#language-picker a').click(function (ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
const targetUrl = ev.currentTarget.href;
|
||||
$.ajax(targetUrl)
|
||||
.then(() => {
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export function appSetup() {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Select from 'react-select';
|
||||
import { t } from '../locales';
|
||||
|
||||
const $ = window.$ = require('jquery');
|
||||
|
||||
|
@ -16,7 +17,7 @@ const propTypes = {
|
|||
};
|
||||
|
||||
const defaultProps = {
|
||||
placeholder: 'Select ...',
|
||||
placeholder: t('Select ...'),
|
||||
valueRenderer: o => (<div>{o.label}</div>),
|
||||
onAsyncError: () => {},
|
||||
};
|
||||
|
|
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||
import { Label } from 'react-bootstrap';
|
||||
import moment from 'moment';
|
||||
import TooltipWrapper from './TooltipWrapper';
|
||||
import { t } from '../locales';
|
||||
|
||||
const propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
|
@ -22,14 +23,14 @@ class CacheLabel extends React.PureComponent {
|
|||
updateTooltipContent() {
|
||||
const cachedText = this.props.cachedTimestamp ? (
|
||||
<span>
|
||||
Loaded data cached <b>{moment.utc(this.props.cachedTimestamp).fromNow()}</b>
|
||||
t('Loaded data cached') <b>{moment.utc(this.props.cachedTimestamp).fromNow()}</b>
|
||||
</span>) :
|
||||
'Loaded from cache';
|
||||
t('Loaded from cache');
|
||||
|
||||
const tooltipContent = (
|
||||
<span>
|
||||
{cachedText}.
|
||||
Click to force-refresh
|
||||
{t('Click to force-refresh')}
|
||||
</span>
|
||||
);
|
||||
this.setState({ tooltipContent });
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Tooltip, OverlayTrigger, MenuItem } from 'react-bootstrap';
|
||||
import { t } from '../locales';
|
||||
|
||||
const propTypes = {
|
||||
copyNode: PropTypes.node,
|
||||
|
@ -17,7 +18,7 @@ const defaultProps = {
|
|||
onCopyEnd: () => {},
|
||||
shouldShowText: true,
|
||||
inMenu: false,
|
||||
tooltipText: 'Copy to clipboard',
|
||||
tooltipText: t('Copy to clipboard'),
|
||||
};
|
||||
|
||||
export default class CopyToClipboard extends React.Component {
|
||||
|
@ -61,10 +62,10 @@ export default class CopyToClipboard extends React.Component {
|
|||
textArea.select();
|
||||
try {
|
||||
if (!document.execCommand('copy')) {
|
||||
throw new Error('Not successful');
|
||||
throw new Error(t('Not successful'));
|
||||
}
|
||||
} catch (err) {
|
||||
window.alert('Sorry, your browser does not support copying. Use Ctrl / Cmd + C!'); // eslint-disable-line
|
||||
window.alert(t('Sorry, your browser does not support copying. Use Ctrl / Cmd + C!')); // eslint-disable-line
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
|
@ -75,7 +76,7 @@ export default class CopyToClipboard extends React.Component {
|
|||
|
||||
tooltipText() {
|
||||
if (this.state.hasCopied) {
|
||||
return 'Copied!';
|
||||
return t('Copied!');
|
||||
}
|
||||
return this.props.tooltipText;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TooltipWrapper from './TooltipWrapper';
|
||||
import { t } from '../locales';
|
||||
|
||||
const propTypes = {
|
||||
title: PropTypes.string,
|
||||
|
@ -8,7 +9,7 @@ const propTypes = {
|
|||
onSaveTitle: PropTypes.func.isRequired,
|
||||
};
|
||||
const defaultProps = {
|
||||
title: 'Title',
|
||||
title: t('Title'),
|
||||
canEdit: false,
|
||||
};
|
||||
|
||||
|
@ -71,7 +72,7 @@ class EditableTitle extends React.PureComponent {
|
|||
<span className="editable-title">
|
||||
<TooltipWrapper
|
||||
label="title"
|
||||
tooltip={this.props.canEdit ? 'click to edit title' : 'You don\'t have the rights to alter this title.'}
|
||||
tooltip={this.props.canEdit ? t('click to edit title') : t('You don\'t have the rights to alter this title.')}
|
||||
>
|
||||
<input
|
||||
required
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import TooltipWrapper from './TooltipWrapper';
|
||||
import { t } from '../locales';
|
||||
|
||||
const propTypes = {
|
||||
sliceId: PropTypes.number.isRequired,
|
||||
|
@ -28,7 +29,7 @@ export default class FaveStar extends React.Component {
|
|||
return (
|
||||
<TooltipWrapper
|
||||
label="fave-unfave"
|
||||
tooltip="Click to favorite/unfavorite"
|
||||
tooltip={t('Click to favorite/unfavorite')}
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
|
|
|
@ -8,7 +8,7 @@ import GridLayout from './components/GridLayout';
|
|||
import Header from './components/Header';
|
||||
import { appSetup } from '../common';
|
||||
import AlertsWrapper from '../components/AlertsWrapper';
|
||||
|
||||
import { t } from '../locales';
|
||||
import '../../stylesheets/dashboard.css';
|
||||
|
||||
const superset = require('../modules/superset');
|
||||
|
@ -39,7 +39,7 @@ export function getInitialState(boostrapData) {
|
|||
}
|
||||
|
||||
function unload() {
|
||||
const message = 'You have unsaved changes.';
|
||||
const message = t('You have unsaved changes.');
|
||||
window.event.returnValue = message; // Gecko + IE
|
||||
return message; // Gecko + Webkit, Safari, Chrome etc.
|
||||
}
|
||||
|
@ -56,9 +56,9 @@ function renderAlert() {
|
|||
render(
|
||||
<div className="container-fluid">
|
||||
<Alert bsStyle="warning">
|
||||
<strong>You have unsaved changes.</strong> Click the
|
||||
<strong>{t('You have unsaved changes.')}</strong> {t('Click the')}
|
||||
<i className="fa fa-save" />
|
||||
button on the top right to save your changes.
|
||||
{t('button on the top right to save your changes.')}
|
||||
</Alert>
|
||||
</div>,
|
||||
document.getElementById('alert-container'),
|
||||
|
@ -161,13 +161,12 @@ export function dashboardContainer(dashboard, datasources, userid) {
|
|||
.addClass('danger')
|
||||
.attr(
|
||||
'title',
|
||||
`Served from data cached ${cachedWhen}. ` +
|
||||
'Click to force refresh')
|
||||
t('Served from data cached %s . Click to force refresh.', cachedWhen))
|
||||
.tooltip('fixTitle');
|
||||
} else {
|
||||
refresh
|
||||
.removeClass('danger')
|
||||
.attr('title', 'Click to force refresh')
|
||||
.attr('title', t('Click to force refresh'))
|
||||
.tooltip('fixTitle');
|
||||
}
|
||||
},
|
||||
|
@ -351,8 +350,8 @@ export function dashboardContainer(dashboard, datasources, userid) {
|
|||
error(error) {
|
||||
const errorMsg = getAjaxErrorMsg(error);
|
||||
utils.showModal({
|
||||
title: 'Error',
|
||||
body: 'Sorry, there was an error adding slices to this dashboard: ' + errorMsg,
|
||||
title: t('Error'),
|
||||
body: t('Sorry, there was an error adding slices to this dashboard: %s', errorMsg),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import ModalTrigger from '../../components/ModalTrigger';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
triggerNode: PropTypes.node.isRequired,
|
||||
|
@ -31,7 +32,7 @@ export default class CodeModal extends React.PureComponent {
|
|||
triggerNode={this.props.triggerNode}
|
||||
isButton
|
||||
beforeOpen={this.beforeOpen.bind(this)}
|
||||
modalTitle="Active Dashboard Filters"
|
||||
modalTitle={t('Active Dashboard Filters')}
|
||||
modalBody={
|
||||
<div className="CodeModal">
|
||||
<pre>
|
||||
|
|
|
@ -8,6 +8,7 @@ import RefreshIntervalModal from './RefreshIntervalModal';
|
|||
import SaveModal from './SaveModal';
|
||||
import CodeModal from './CodeModal';
|
||||
import SliceAdder from './SliceAdder';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const $ = window.$ = require('jquery');
|
||||
|
||||
|
@ -44,13 +45,13 @@ class Controls extends React.PureComponent {
|
|||
}
|
||||
render() {
|
||||
const dashboard = this.props.dashboard;
|
||||
const emailBody = `Checkout this dashboard: ${window.location.href}`;
|
||||
const emailBody = t('Checkout this dashboard: %s', window.location.href);
|
||||
const emailLink = 'mailto:?Subject=Superset%20Dashboard%20'
|
||||
+ `${dashboard.dashboard_title}&Body=${emailBody}`;
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
tooltip="Force refresh the whole dashboard"
|
||||
tooltip={t('Force refresh the whole dashboard')}
|
||||
onClick={this.refresh.bind(this)}
|
||||
>
|
||||
<i className="fa fa-refresh" />
|
||||
|
@ -90,7 +91,7 @@ class Controls extends React.PureComponent {
|
|||
onClick={() => {
|
||||
window.location = `/dashboardmodelview/edit/${dashboard.id}`;
|
||||
}}
|
||||
tooltip="Edit this dashboard's properties"
|
||||
tooltip={t('Edit this dashboard\'s properties')}
|
||||
>
|
||||
<i className="fa fa-edit" />
|
||||
</Button>
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'brace/mode/css';
|
|||
import 'brace/theme/github';
|
||||
|
||||
import ModalTrigger from '../../components/ModalTrigger';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
initialCss: PropTypes.string,
|
||||
|
@ -61,10 +62,10 @@ class CssEditor extends React.PureComponent {
|
|||
if (this.props.templates) {
|
||||
return (
|
||||
<div style={{ zIndex: 10 }}>
|
||||
<h5>Load a template</h5>
|
||||
<h5>{t('Load a template')}</h5>
|
||||
<Select
|
||||
options={this.props.templates}
|
||||
placeholder="Load a CSS template"
|
||||
placeholder={t('Load a CSS template')}
|
||||
onChange={this.changeCssTemplate.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
|
@ -76,13 +77,13 @@ class CssEditor extends React.PureComponent {
|
|||
return (
|
||||
<ModalTrigger
|
||||
triggerNode={this.props.triggerNode}
|
||||
modalTitle="CSS"
|
||||
modalTitle={t('CSS')}
|
||||
isButton
|
||||
modalBody={
|
||||
<div>
|
||||
{this.renderTemplateSelector()}
|
||||
<div style={{ zIndex: 1 }}>
|
||||
<h5>Live CSS Editor</h5>
|
||||
<h5>{t('Live CSS Editor')}</h5>
|
||||
<div style={{ border: 'solid 1px grey' }}>
|
||||
<AceEditor
|
||||
mode="css"
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import Select from 'react-select';
|
||||
import ModalTrigger from '../../components/ModalTrigger';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
triggerNode: PropTypes.node.isRequired,
|
||||
|
@ -15,11 +16,11 @@ const defaultProps = {
|
|||
};
|
||||
|
||||
const options = [
|
||||
[0, "Don't refresh"],
|
||||
[10, '10 seconds'],
|
||||
[30, '30 seconds'],
|
||||
[60, '1 minute'],
|
||||
[300, '5 minutes'],
|
||||
[0, t('Don\'t refresh')],
|
||||
[10, t('10 seconds')],
|
||||
[30, t('30 seconds')],
|
||||
[60, t('1 minute')],
|
||||
[300, t('5 minutes')],
|
||||
].map(o => ({ value: o[0], label: o[1] }));
|
||||
|
||||
class RefreshIntervalModal extends React.PureComponent {
|
||||
|
@ -34,10 +35,10 @@ class RefreshIntervalModal extends React.PureComponent {
|
|||
<ModalTrigger
|
||||
triggerNode={this.props.triggerNode}
|
||||
isButton
|
||||
modalTitle="Refresh Interval"
|
||||
modalTitle={t('Refresh Interval')}
|
||||
modalBody={
|
||||
<div>
|
||||
Choose the refresh frequency for this dashboard
|
||||
{t('Choose the refresh frequency for this dashboard')}
|
||||
<Select
|
||||
options={options}
|
||||
value={this.state.refreshFrequency}
|
||||
|
|
|
@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
|
|||
import { Button, FormControl, FormGroup, Radio } from 'react-bootstrap';
|
||||
import { getAjaxErrorMsg } from '../../modules/utils';
|
||||
import ModalTrigger from '../../components/ModalTrigger';
|
||||
import { t } from '../../locales';
|
||||
import Checkbox from '../../components/Checkbox';
|
||||
|
||||
const $ = window.$ = require('jquery');
|
||||
|
@ -59,13 +60,13 @@ class SaveModal extends React.PureComponent {
|
|||
if (saveType === 'newDashboard') {
|
||||
window.location = `/superset/dashboard/${resp.id}/`;
|
||||
} else {
|
||||
notify.success('This dashboard was saved successfully.');
|
||||
notify.success(t('This dashboard was saved successfully.'));
|
||||
}
|
||||
},
|
||||
error(error) {
|
||||
saveModal.close();
|
||||
const errorMsg = getAjaxErrorMsg(error);
|
||||
notify.error('Sorry, there was an error saving this dashboard: </ br>' + errorMsg);
|
||||
notify.error(t('Sorry, there was an error saving this dashboard: ') + '</ br>' + errorMsg);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -96,8 +97,8 @@ class SaveModal extends React.PureComponent {
|
|||
if (!newDashboardTitle) {
|
||||
this.modal.close();
|
||||
showModal({
|
||||
title: 'Error',
|
||||
body: 'You must pick a name for the new dashboard',
|
||||
title: t('Error'),
|
||||
body: t('You must pick a name for the new dashboard'),
|
||||
});
|
||||
} else {
|
||||
data.dashboard_title = newDashboardTitle;
|
||||
|
@ -111,7 +112,7 @@ class SaveModal extends React.PureComponent {
|
|||
<ModalTrigger
|
||||
ref={(modal) => { this.modal = modal; }}
|
||||
triggerNode={this.props.triggerNode}
|
||||
modalTitle="Save Dashboard"
|
||||
modalTitle={t('Save Dashboard')}
|
||||
modalBody={
|
||||
<FormGroup>
|
||||
<Radio
|
||||
|
@ -119,7 +120,7 @@ class SaveModal extends React.PureComponent {
|
|||
onChange={this.handleSaveTypeChange}
|
||||
checked={this.state.saveType === 'overwrite'}
|
||||
>
|
||||
Overwrite Dashboard [{this.props.dashboard.dashboard_title}]
|
||||
{t('Overwrite Dashboard [%s]', this.props.dashboard.dashboard_title)}
|
||||
</Radio>
|
||||
<hr />
|
||||
<Radio
|
||||
|
@ -127,11 +128,11 @@ class SaveModal extends React.PureComponent {
|
|||
onChange={this.handleSaveTypeChange}
|
||||
checked={this.state.saveType === 'newDashboard'}
|
||||
>
|
||||
Save as:
|
||||
{t('Save as:')}
|
||||
</Radio>
|
||||
<FormControl
|
||||
type="text"
|
||||
placeholder="[dashboard name]"
|
||||
placeholder={t('[dashboard name]')}
|
||||
value={this.state.newDashName}
|
||||
onFocus={this.handleNameChange}
|
||||
onChange={this.handleNameChange}
|
||||
|
@ -151,7 +152,7 @@ class SaveModal extends React.PureComponent {
|
|||
bsStyle="primary"
|
||||
onClick={() => { this.saveDashboard(this.state.saveType, this.state.newDashName); }}
|
||||
>
|
||||
Save
|
||||
{t('Save')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
|
|||
import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table';
|
||||
|
||||
import ModalTrigger from '../../components/ModalTrigger';
|
||||
import { t } from '../../locales';
|
||||
|
||||
require('react-bootstrap-table/css/react-bootstrap-table.css');
|
||||
|
||||
|
@ -138,13 +139,13 @@ class SliceAdder extends React.Component {
|
|||
dataField="sliceName"
|
||||
dataSort
|
||||
>
|
||||
Name
|
||||
{t('Name')}
|
||||
</TableHeaderColumn>
|
||||
<TableHeaderColumn
|
||||
dataField="vizType"
|
||||
dataSort
|
||||
>
|
||||
Viz
|
||||
{t('Viz')}
|
||||
</TableHeaderColumn>
|
||||
<TableHeaderColumn
|
||||
dataField="modified"
|
||||
|
@ -153,7 +154,7 @@ class SliceAdder extends React.Component {
|
|||
// Will cause react-bootstrap-table to interpret the HTML returned
|
||||
dataFormat={modified => modified}
|
||||
>
|
||||
Modified
|
||||
{t('Modified')}
|
||||
</TableHeaderColumn>
|
||||
</BootstrapTable>
|
||||
<button
|
||||
|
@ -163,7 +164,7 @@ class SliceAdder extends React.Component {
|
|||
onClick={this.addSlices}
|
||||
disabled={!enableAddSlice}
|
||||
>
|
||||
Add Slices
|
||||
{t('Add Slices')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -172,12 +173,12 @@ class SliceAdder extends React.Component {
|
|||
return (
|
||||
<ModalTrigger
|
||||
triggerNode={this.props.triggerNode}
|
||||
tooltip="Add a new slice to the dashboard"
|
||||
tooltip={t('Add a new slice to the dashboard')}
|
||||
beforeOpen={this.onEnterModal.bind(this)}
|
||||
isButton
|
||||
modalBody={modalContent}
|
||||
bsSize="large"
|
||||
modalTitle="Add Slices to Dashboard"
|
||||
modalTitle={t('Add Slices to Dashboard')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { t } from '../../locales';
|
||||
import { getExploreUrl } from '../../explore/exploreUtils';
|
||||
|
||||
const propTypes = {
|
||||
|
@ -20,14 +21,14 @@ function SliceCell({ expandedSlices, removeSlice, slice }) {
|
|||
</div>
|
||||
<div className="col-md-12 chart-controls">
|
||||
<div id={'controls_' + slice.slice_id} className="pull-right">
|
||||
<a title="Move chart" data-toggle="tooltip">
|
||||
<a title={t('Move chart')} data-toggle="tooltip">
|
||||
<i className="fa fa-arrows drag" />
|
||||
</a>
|
||||
<a className="refresh" title="Force refresh data" data-toggle="tooltip">
|
||||
<a className="refresh" title={t('Force refresh data')} data-toggle="tooltip">
|
||||
<i className="fa fa-repeat" />
|
||||
</a>
|
||||
{slice.description &&
|
||||
<a title="Toggle chart description">
|
||||
<a title={t('Toggle chart description')}>
|
||||
<i
|
||||
className="fa fa-info-circle slice_info"
|
||||
title={slice.description}
|
||||
|
@ -37,7 +38,7 @@ function SliceCell({ expandedSlices, removeSlice, slice }) {
|
|||
}
|
||||
<a
|
||||
href={slice.edit_url}
|
||||
title="Edit chart"
|
||||
title={t('Edit chart')}
|
||||
data-toggle="tooltip"
|
||||
>
|
||||
<i className="fa fa-pencil" />
|
||||
|
@ -45,7 +46,7 @@ function SliceCell({ expandedSlices, removeSlice, slice }) {
|
|||
<a
|
||||
className="exportCSV"
|
||||
href={getExploreUrl(slice.form_data, 'csv')}
|
||||
title="Export CSV"
|
||||
title={t('Export CSV')}
|
||||
data-toggle="tooltip"
|
||||
>
|
||||
<i className="fa fa-table" />
|
||||
|
@ -53,14 +54,14 @@ function SliceCell({ expandedSlices, removeSlice, slice }) {
|
|||
<a
|
||||
className="exploreChart"
|
||||
href={getExploreUrl(slice.form_data)}
|
||||
title="Explore chart"
|
||||
title={t('Explore chart')}
|
||||
data-toggle="tooltip"
|
||||
>
|
||||
<i className="fa fa-share" />
|
||||
</a>
|
||||
<a
|
||||
className="remove-chart"
|
||||
title="Remove chart from dashboard"
|
||||
title={t('Remove chart from dashboard')}
|
||||
data-toggle="tooltip"
|
||||
>
|
||||
<i
|
||||
|
|
|
@ -14,6 +14,7 @@ import Timer from '../../components/Timer';
|
|||
import { getExploreUrl } from '../exploreUtils';
|
||||
import { getFormDataFromControls } from '../stores/store';
|
||||
import CachedLabel from '../../components/CachedLabel';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const CHART_STATUS_MAP = {
|
||||
failed: 'danger',
|
||||
|
@ -169,7 +170,7 @@ class ChartContainer extends React.PureComponent {
|
|||
if (this.props.slice) {
|
||||
title = this.props.slice.slice_name;
|
||||
} else {
|
||||
title = `[${this.props.table_name}] - untitled`;
|
||||
title = t('%s - untitled', this.props.table_name);
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
@ -276,7 +277,7 @@ class ChartContainer extends React.PureComponent {
|
|||
|
||||
<TooltipWrapper
|
||||
label="edit-desc"
|
||||
tooltip="Edit slice properties"
|
||||
tooltip={t('Edit slice properties')}
|
||||
>
|
||||
<a
|
||||
className="edit-desc-icon"
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { ControlLabel, OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
|
@ -28,7 +29,7 @@ export default class ControlHeader extends React.Component {
|
|||
{this.props.description &&
|
||||
<span>
|
||||
<InfoTooltipWithTrigger
|
||||
label="descr"
|
||||
label={t('description')}
|
||||
tooltip={this.props.description}
|
||||
placement="top"
|
||||
/>
|
||||
|
@ -38,7 +39,7 @@ export default class ControlHeader extends React.Component {
|
|||
{this.props.renderTrigger &&
|
||||
<span>
|
||||
<InfoTooltipWithTrigger
|
||||
label="bolt"
|
||||
label={t('bolt')}
|
||||
tooltip={this.props.description}
|
||||
placement="top"
|
||||
icon="bolt"
|
||||
|
|
|
@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
|||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { Alert } from 'react-bootstrap';
|
||||
import { sectionsToRender, visTypes } from '../stores/visTypes';
|
||||
import visTypes, { sectionsToRender } from '../stores/visTypes';
|
||||
import ControlPanelSection from './ControlPanelSection';
|
||||
import ControlRow from './ControlRow';
|
||||
import Control from './Control';
|
||||
|
|
|
@ -6,6 +6,7 @@ import markdown from 'react-syntax-highlighter/dist/languages/markdown';
|
|||
import github from 'react-syntax-highlighter/dist/styles/github';
|
||||
|
||||
import ModalTrigger from './../../components/ModalTrigger';
|
||||
import { t } from '../../locales';
|
||||
|
||||
registerLanguage('markdown', markdown);
|
||||
registerLanguage('html', html);
|
||||
|
@ -57,7 +58,7 @@ export default class DisplayQueryButton extends React.PureComponent {
|
|||
},
|
||||
error: (data) => {
|
||||
this.setState({
|
||||
error: data.responseJSON ? data.responseJSON.error : 'Error...',
|
||||
error: data.responseJSON ? data.responseJSON.error : t('Error...'),
|
||||
isLoading: false,
|
||||
});
|
||||
},
|
||||
|
@ -93,7 +94,7 @@ export default class DisplayQueryButton extends React.PureComponent {
|
|||
animation={this.props.animation}
|
||||
isButton
|
||||
triggerNode={<span>Query</span>}
|
||||
modalTitle="Query"
|
||||
modalTitle={t('Query')}
|
||||
bsSize="large"
|
||||
beforeOpen={this.beforeOpen}
|
||||
modalBody={this.renderModalBody()}
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { Popover, OverlayTrigger } from 'react-bootstrap';
|
||||
import CopyToClipboard from './../../components/CopyToClipboard';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
slice: PropTypes.object.isRequired,
|
||||
|
@ -63,7 +64,7 @@ export default class EmbedCodeButton extends React.Component {
|
|||
<CopyToClipboard
|
||||
shouldShowText={false}
|
||||
text={html}
|
||||
copyNode={<i className="fa fa-clipboard" title="Copy to clipboard" />}
|
||||
copyNode={<i className="fa fa-clipboard" title={t('Copy to clipboard')} />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -72,7 +73,7 @@ export default class EmbedCodeButton extends React.Component {
|
|||
<div className="col-md-6 col-sm-12">
|
||||
<div className="form-group">
|
||||
<small>
|
||||
<label className="control-label" htmlFor="embed-height">Height</label>
|
||||
<label className="control-label" htmlFor="embed-height">t('Height')</label>
|
||||
</small>
|
||||
<input
|
||||
className="form-control input-sm"
|
||||
|
@ -86,7 +87,7 @@ export default class EmbedCodeButton extends React.Component {
|
|||
<div className="col-md-6 col-sm-12">
|
||||
<div className="form-group">
|
||||
<small>
|
||||
<label className="control-label" htmlFor="embed-width">Width</label>
|
||||
<label className="control-label" htmlFor="embed-width">t('Width')</label>
|
||||
</small>
|
||||
<input
|
||||
className="form-control input-sm"
|
||||
|
|
|
@ -4,6 +4,7 @@ import cx from 'classnames';
|
|||
import URLShortLinkButton from './URLShortLinkButton';
|
||||
import EmbedCodeButton from './EmbedCodeButton';
|
||||
import DisplayQueryButton from './DisplayQueryButton';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
canDownload: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).isRequired,
|
||||
|
@ -28,7 +29,7 @@ export default function ExploreActionButtons({
|
|||
<a
|
||||
href={slice.data.json_endpoint}
|
||||
className="btn btn-default btn-sm"
|
||||
title="Export to .json"
|
||||
title={t('Export to .json')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
@ -38,7 +39,7 @@ export default function ExploreActionButtons({
|
|||
<a
|
||||
href={slice.data.csv_endpoint}
|
||||
className={exportToCSVClasses}
|
||||
title="Export to .csv format"
|
||||
title={t('Export to .csv format')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
|
|
@ -6,6 +6,7 @@ import { connect } from 'react-redux';
|
|||
import { Modal, Alert, Button, Radio } from 'react-bootstrap';
|
||||
import Select from 'react-select';
|
||||
import { getExploreUrl } from '../exploreUtils';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
can_overwrite: PropTypes.bool,
|
||||
|
@ -70,7 +71,7 @@ class SaveModal extends React.Component {
|
|||
if (sliceParams.action === 'saveas') {
|
||||
sliceName = this.state.newSliceName;
|
||||
if (sliceName === '') {
|
||||
this.setState({ alert: 'Please enter a slice name' });
|
||||
this.setState({ alert: t('Please enter a slice name') });
|
||||
return;
|
||||
}
|
||||
sliceParams.slice_name = sliceName;
|
||||
|
@ -85,7 +86,7 @@ class SaveModal extends React.Component {
|
|||
case ('existing'):
|
||||
dashboard = this.state.saveToDashboardId;
|
||||
if (!dashboard) {
|
||||
this.setState({ alert: 'Please select a dashboard' });
|
||||
this.setState({ alert: t('Please select a dashboard') });
|
||||
return;
|
||||
}
|
||||
sliceParams.save_to_dashboard_id = dashboard;
|
||||
|
@ -93,7 +94,7 @@ class SaveModal extends React.Component {
|
|||
case ('new'):
|
||||
dashboard = this.state.newDashboardName;
|
||||
if (dashboard === '') {
|
||||
this.setState({ alert: 'Please enter a dashboard name' });
|
||||
this.setState({ alert: t('Please enter a dashboard name') });
|
||||
return;
|
||||
}
|
||||
sliceParams.new_dashboard_name = dashboard;
|
||||
|
@ -126,7 +127,7 @@ class SaveModal extends React.Component {
|
|||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
Save A Slice
|
||||
{t('Save A Slice')}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
|
@ -147,7 +148,7 @@ class SaveModal extends React.Component {
|
|||
checked={this.state.action === 'overwrite'}
|
||||
onChange={this.changeAction.bind(this, 'overwrite')}
|
||||
>
|
||||
{`Overwrite slice ${this.props.slice.slice_name}`}
|
||||
{t('Overwrite slice %s', this.props.slice.slice_name)}
|
||||
</Radio>
|
||||
}
|
||||
|
||||
|
@ -156,11 +157,11 @@ class SaveModal extends React.Component {
|
|||
inline
|
||||
checked={this.state.action === 'saveas'}
|
||||
onChange={this.changeAction.bind(this, 'saveas')}
|
||||
> Save as
|
||||
> {t('Save as')}
|
||||
</Radio>
|
||||
<input
|
||||
name="new_slice_name"
|
||||
placeholder="[slice name]"
|
||||
placeholder={t('[slice name]')}
|
||||
onChange={this.onChange.bind(this, 'newSliceName')}
|
||||
onFocus={this.changeAction.bind(this, 'saveas')}
|
||||
/>
|
||||
|
@ -173,7 +174,7 @@ class SaveModal extends React.Component {
|
|||
checked={this.state.addToDash === 'noSave'}
|
||||
onChange={this.changeDash.bind(this, 'noSave')}
|
||||
>
|
||||
Do not add to a dashboard
|
||||
{t('Do not add to a dashboard')}
|
||||
</Radio>
|
||||
|
||||
<Radio
|
||||
|
@ -181,7 +182,7 @@ class SaveModal extends React.Component {
|
|||
checked={this.state.addToDash === 'existing'}
|
||||
onChange={this.changeDash.bind(this, 'existing')}
|
||||
>
|
||||
Add slice to existing dashboard
|
||||
{t('Add slice to existing dashboard')}
|
||||
</Radio>
|
||||
<Select
|
||||
options={this.props.dashboards}
|
||||
|
@ -196,13 +197,14 @@ class SaveModal extends React.Component {
|
|||
checked={this.state.addToDash === 'new'}
|
||||
onChange={this.changeDash.bind(this, 'new')}
|
||||
>
|
||||
Add to new dashboard
|
||||
{t('Add to new dashboard')}
|
||||
</Radio>
|
||||
<input
|
||||
onChange={this.onChange.bind(this, 'newDashboardName')}
|
||||
onFocus={this.changeDash.bind(this, 'new')}
|
||||
placeholder="[dashboard name]"
|
||||
placeholder={t('[dashboard name]')}
|
||||
/>
|
||||
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Footer>
|
||||
|
@ -212,7 +214,7 @@ class SaveModal extends React.Component {
|
|||
className="btn pull-left"
|
||||
onClick={this.saveOrOverwrite.bind(this, false)}
|
||||
>
|
||||
Save
|
||||
{t('Save')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
|
@ -221,7 +223,7 @@ class SaveModal extends React.Component {
|
|||
disabled={this.state.addToDash === 'noSave'}
|
||||
onClick={this.saveOrOverwrite.bind(this, true)}
|
||||
>
|
||||
Save & go to dashboard
|
||||
{t('Save & go to dashboard')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
|
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||
import { Popover, OverlayTrigger } from 'react-bootstrap';
|
||||
import CopyToClipboard from './../../components/CopyToClipboard';
|
||||
import { getShortUrl } from '../../../utils/common';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
slice: PropTypes.object.isRequired,
|
||||
|
@ -28,12 +29,12 @@ export default class URLShortLinkButton extends React.Component {
|
|||
}
|
||||
|
||||
renderPopover() {
|
||||
const emailBody = `Check out this slice: ${this.state.shortUrl}`;
|
||||
const emailBody = t('Check out this slice: %s', this.state.shortUrl);
|
||||
return (
|
||||
<Popover id="shorturl-popover">
|
||||
<CopyToClipboard
|
||||
text={this.state.shortUrl}
|
||||
copyNode={<i className="fa fa-clipboard" title="Copy to clipboard" />}
|
||||
copyNode={<i className="fa fa-clipboard" title={t('Copy to clipboard')} />}
|
||||
/>
|
||||
|
||||
<a href={`mailto:?Subject=Superset%20Slice%20&Body=${emailBody}`}>
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { Col, Row, FormGroup, FormControl } from 'react-bootstrap';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import { t } from '../../../locales';
|
||||
|
||||
const propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
|
@ -51,10 +52,10 @@ export default class BoundsControl extends React.Component {
|
|||
const mm = this.state.minMax;
|
||||
const errors = [];
|
||||
if (mm[0] && isNaN(mm[0])) {
|
||||
errors.push('`Min` value should be numeric or empty');
|
||||
errors.push(t('`Min` value should be numeric or empty'));
|
||||
}
|
||||
if (mm[1] && isNaN(mm[1])) {
|
||||
errors.push('`Max` value should be numeric or empty');
|
||||
errors.push(t('`Max` value should be numeric or empty'));
|
||||
}
|
||||
if (errors.length === 0) {
|
||||
this.props.onChange([parseFloat(mm[0]), parseFloat(mm[1])], errors);
|
||||
|
@ -71,7 +72,7 @@ export default class BoundsControl extends React.Component {
|
|||
<Col xs={6}>
|
||||
<FormControl
|
||||
type="text"
|
||||
placeholder="Min"
|
||||
placeholder={t('Min')}
|
||||
onChange={this.onMinChange}
|
||||
value={this.state.minMax[0]}
|
||||
/>
|
||||
|
@ -79,7 +80,7 @@ export default class BoundsControl extends React.Component {
|
|||
<Col xs={6}>
|
||||
<FormControl
|
||||
type="text"
|
||||
placeholder="Max"
|
||||
placeholder={t('Max')}
|
||||
onChange={this.onMaxChange}
|
||||
value={this.state.minMax[1]}
|
||||
/>
|
||||
|
|
|
@ -5,6 +5,7 @@ import { Table } from 'reactable';
|
|||
import { Label, FormControl, Modal, OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import { t } from '../../../locales';
|
||||
|
||||
const propTypes = {
|
||||
description: PropTypes.string,
|
||||
|
@ -66,7 +67,7 @@ export default class DatasourceControl extends React.PureComponent {
|
|||
},
|
||||
error() {
|
||||
that.setState({ loading: false });
|
||||
notify.error('Something went wrong while fetching the datasource list');
|
||||
notify.error(t('Something went wrong while fetching the datasource list'));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -91,7 +92,7 @@ export default class DatasourceControl extends React.PureComponent {
|
|||
<OverlayTrigger
|
||||
placement="right"
|
||||
overlay={
|
||||
<Tooltip id={'error-tooltip'}>Click to point to another datasource</Tooltip>
|
||||
<Tooltip id={'error-tooltip'}>{t('Click to point to another datasource')}</Tooltip>
|
||||
}
|
||||
>
|
||||
<Label onClick={this.toggleModal} style={{ cursor: 'pointer' }} className="m-r-5">
|
||||
|
@ -102,7 +103,7 @@ export default class DatasourceControl extends React.PureComponent {
|
|||
placement="right"
|
||||
overlay={
|
||||
<Tooltip id={'edit-datasource-tooltip'}>
|
||||
Edit the datasource's configuration
|
||||
{t('Edit the datasource\'s configuration')}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
|
@ -118,7 +119,7 @@ export default class DatasourceControl extends React.PureComponent {
|
|||
bsSize="lg"
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Select a datasource</Modal.Title>
|
||||
<Modal.Title>{t('Select a datasource')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div>
|
||||
|
@ -128,7 +129,7 @@ export default class DatasourceControl extends React.PureComponent {
|
|||
type="text"
|
||||
bsSize="sm"
|
||||
value={this.state.filter}
|
||||
placeholder="Search / Filter"
|
||||
placeholder={t('Search / Filter')}
|
||||
onChange={this.changeSearch}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||
import Select from 'react-select';
|
||||
import { Button, Row, Col } from 'react-bootstrap';
|
||||
import SelectControl from './SelectControl';
|
||||
import { t } from '../../../locales';
|
||||
|
||||
const operatorsArr = [
|
||||
{ val: 'in', type: 'array', useSelect: true, multi: true },
|
||||
|
@ -110,7 +111,7 @@ export default class Filter extends React.Component {
|
|||
onChange={this.changeText.bind(this)}
|
||||
value={filter.val}
|
||||
className="form-control input-sm"
|
||||
placeholder="Filter value"
|
||||
placeholder={t('Filter value')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -139,7 +140,7 @@ export default class Filter extends React.Component {
|
|||
<Col md={12}>
|
||||
<Select
|
||||
id="select-col"
|
||||
placeholder={this.props.having ? 'Select metric' : 'Select column'}
|
||||
placeholder={this.props.having ? t('Select metric') : t('Select column')}
|
||||
clearable={false}
|
||||
options={colChoices}
|
||||
value={filter.col}
|
||||
|
@ -151,7 +152,7 @@ export default class Filter extends React.Component {
|
|||
<Col md={3}>
|
||||
<Select
|
||||
id="select-op"
|
||||
placeholder="Select operator"
|
||||
placeholder={t('Select operator')}
|
||||
options={opsChoices}
|
||||
clearable={false}
|
||||
value={filter.op}
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { Button, Row, Col } from 'react-bootstrap';
|
||||
import Filter from './Filter';
|
||||
import { t } from '../../../locales';
|
||||
|
||||
const $ = window.$ = require('jquery');
|
||||
|
||||
|
@ -134,7 +135,7 @@ export default class FilterControl extends React.Component {
|
|||
bsSize="sm"
|
||||
onClick={this.addFilter.bind(this)}
|
||||
>
|
||||
<i className="fa fa-plus" /> Add Filter
|
||||
<i className="fa fa-plus" /> {t('Add Filter')}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import Select, { Creatable } from 'react-select';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import { t } from '../../../locales';
|
||||
|
||||
const propTypes = {
|
||||
choices: PropTypes.array,
|
||||
|
@ -102,7 +103,7 @@ export default class SelectControl extends React.PureComponent {
|
|||
const selectProps = {
|
||||
multi: this.props.multi,
|
||||
name: `select-${this.props.name}`,
|
||||
placeholder: `Select (${this.state.options.length})`,
|
||||
placeholder: t('Select %s', this.state.options.length),
|
||||
options: this.state.options,
|
||||
value: this.props.value,
|
||||
valueKey: this.props.valueKey,
|
||||
|
|
|
@ -12,6 +12,7 @@ import 'brace/theme/textmate';
|
|||
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import ModalTrigger from '../../../components/ModalTrigger';
|
||||
import { t } from '../../../locales';
|
||||
|
||||
const propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
|
@ -59,7 +60,7 @@ export default class TextAreaControl extends React.Component {
|
|||
<FormGroup controlId="formControlsTextarea">
|
||||
<FormControl
|
||||
componentClass="textarea"
|
||||
placeholder="textarea"
|
||||
placeholder={t('textarea')}
|
||||
onChange={this.onControlChange.bind(this)}
|
||||
value={this.props.value}
|
||||
style={{ height: this.props.height }}
|
||||
|
@ -77,7 +78,7 @@ export default class TextAreaControl extends React.Component {
|
|||
modalTitle={controlHeader}
|
||||
triggerNode={
|
||||
<Button bsSize="small" className="m-t-5">
|
||||
Edit <strong>{this.props.language}</strong> in modal
|
||||
{t('Edit')} <strong>{this.props.language}</strong> {t('in modal')}
|
||||
</Button>
|
||||
}
|
||||
modalBody={this.renderEditor(true)}
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
Tooltip } from 'react-bootstrap';
|
||||
import visTypes from '../../stores/visTypes';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import { t } from '../../../locales';
|
||||
|
||||
const propTypes = {
|
||||
description: PropTypes.string,
|
||||
|
@ -106,7 +107,7 @@ export default class VizTypeControl extends React.PureComponent {
|
|||
bsSize="lg"
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Select a visualization type</Modal.Title>
|
||||
<Modal.Title>{t('Select a visualization type')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div>
|
||||
|
@ -116,7 +117,7 @@ export default class VizTypeControl extends React.PureComponent {
|
|||
type="text"
|
||||
bsSize="sm"
|
||||
value={this.state.filter}
|
||||
placeholder="Search / Filter"
|
||||
placeholder={t('Search / Filter')}
|
||||
onChange={this.changeSearch}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -24,7 +24,8 @@ const exploreViewContainer = document.getElementById('app');
|
|||
const bootstrapData = JSON.parse(exploreViewContainer.getAttribute('data-bootstrap'));
|
||||
const controls = getControlsState(bootstrapData, bootstrapData.form_data);
|
||||
delete bootstrapData.form_data;
|
||||
|
||||
delete bootstrapData.common.locale;
|
||||
delete bootstrapData.common.language_pack;
|
||||
|
||||
// Initial state
|
||||
const bootstrappedState = Object.assign(
|
||||
|
@ -56,6 +57,7 @@ const initState = {
|
|||
const store = createStore(rootReducer, initState,
|
||||
compose(applyMiddleware(thunk), initEnhancer(false)),
|
||||
);
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* eslint camelcase: 0 */
|
||||
import { now } from '../../modules/dates';
|
||||
import * as actions from '../actions/chartActions';
|
||||
import { t } from '../../locales';
|
||||
|
||||
export default function chartReducer(state = {}, action) {
|
||||
const actionHandlers = {
|
||||
|
@ -28,13 +29,13 @@ export default function chartReducer(state = {}, action) {
|
|||
return Object.assign({}, state,
|
||||
{
|
||||
chartStatus: 'stopped',
|
||||
chartAlert: 'Updating chart was stopped',
|
||||
chartAlert: t('Updating chart was stopped'),
|
||||
});
|
||||
},
|
||||
[actions.CHART_RENDERING_FAILED]() {
|
||||
return Object.assign({}, state, {
|
||||
chartStatus: 'failed',
|
||||
chartAlert: 'An error occurred while rendering the visualization: ' + action.error,
|
||||
chartAlert: t('An error occurred while rendering the visualization: %s', action.error),
|
||||
});
|
||||
},
|
||||
[actions.CHART_UPDATE_TIMEOUT]() {
|
||||
|
@ -43,16 +44,16 @@ export default function chartReducer(state = {}, action) {
|
|||
chartAlert: (
|
||||
'<strong>Query timeout</strong> - visualization query are set to timeout at ' +
|
||||
`${action.timeout} seconds. ` +
|
||||
'Perhaps your data has grown, your database is under unusual load, ' +
|
||||
t('Perhaps your data has grown, your database is under unusual load, ' +
|
||||
'or you are simply querying a data source that is to large ' +
|
||||
'to be processed within the timeout range. ' +
|
||||
'If that is the case, we recommend that you summarize your data further.'),
|
||||
'If that is the case, we recommend that you summarize your data further.')),
|
||||
});
|
||||
},
|
||||
[actions.CHART_UPDATE_FAILED]() {
|
||||
return Object.assign({}, state, {
|
||||
chartStatus: 'failed',
|
||||
chartAlert: action.queryResponse ? action.queryResponse.error : 'Network error.',
|
||||
chartAlert: action.queryResponse ? action.queryResponse.error : t('Network error.'),
|
||||
chartUpdateEndTime: now(),
|
||||
queryResponse: action.queryResponse,
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,18 +1,19 @@
|
|||
import { D3_TIME_FORMAT_OPTIONS } from './controls';
|
||||
import * as v from '../validators';
|
||||
import { t } from '../../locales';
|
||||
|
||||
export const sections = {
|
||||
druidTimeSeries: {
|
||||
label: 'Time',
|
||||
label: t('Time'),
|
||||
expanded: true,
|
||||
description: 'Time related form attributes',
|
||||
description: t('Time related form attributes'),
|
||||
controlSetRows: [
|
||||
['granularity', 'druid_time_origin'],
|
||||
['since', 'until'],
|
||||
],
|
||||
},
|
||||
datasourceAndVizType: {
|
||||
label: 'Datasource & Chart Type',
|
||||
label: t('Datasource & Chart Type'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['datasource'],
|
||||
|
@ -21,14 +22,14 @@ export const sections = {
|
|||
],
|
||||
},
|
||||
colorScheme: {
|
||||
label: 'Color Scheme',
|
||||
label: t('Color Scheme'),
|
||||
controlSetRows: [
|
||||
['color_scheme'],
|
||||
],
|
||||
},
|
||||
sqlaTimeSeries: {
|
||||
label: 'Time',
|
||||
description: 'Time related form attributes',
|
||||
label: t('Time'),
|
||||
description: t('Time related form attributes'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['granularity_sqla', 'time_grain_sqla'],
|
||||
|
@ -36,16 +37,16 @@ export const sections = {
|
|||
],
|
||||
},
|
||||
sqlClause: {
|
||||
label: 'SQL',
|
||||
label: t('SQL'),
|
||||
controlSetRows: [
|
||||
['where'],
|
||||
['having'],
|
||||
],
|
||||
description: 'This section exposes ways to include snippets of SQL in your query',
|
||||
description: t('This section exposes ways to include snippets of SQL in your query'),
|
||||
},
|
||||
NVD3TimeSeries: [
|
||||
{
|
||||
label: 'Query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['metrics'],
|
||||
|
@ -55,10 +56,10 @@ export const sections = {
|
|||
],
|
||||
},
|
||||
{
|
||||
label: 'Advanced Analytics',
|
||||
description: 'This section contains options ' +
|
||||
label: t('Advanced Analytics'),
|
||||
description: t('This section contains options ' +
|
||||
'that allow for advanced analytical post processing ' +
|
||||
'of query results',
|
||||
'of query results'),
|
||||
controlSetRows: [
|
||||
['rolling_type', 'rolling_periods', 'min_periods'],
|
||||
['time_compare', null],
|
||||
|
@ -69,15 +70,15 @@ export const sections = {
|
|||
],
|
||||
filters: [
|
||||
{
|
||||
label: 'Filters',
|
||||
label: t('Filters'),
|
||||
expanded: true,
|
||||
controlSetRows: [['filters']],
|
||||
},
|
||||
{
|
||||
label: 'Result Filters',
|
||||
label: t('Result Filters'),
|
||||
expanded: true,
|
||||
description: 'The filters to apply after post-aggregation.' +
|
||||
'Leave the value control empty to filter empty strings or nulls',
|
||||
description: t('The filters to apply after post-aggregation.' +
|
||||
'Leave the value control empty to filter empty strings or nulls'),
|
||||
controlSetRows: [['having_filters']],
|
||||
},
|
||||
],
|
||||
|
@ -85,11 +86,11 @@ export const sections = {
|
|||
|
||||
export const visTypes = {
|
||||
dist_bar: {
|
||||
label: 'Distribution - Bar Chart',
|
||||
label: t('Distribution - Bar Chart'),
|
||||
showOnExplore: true,
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Query',
|
||||
label: t('Chart Options'),
|
||||
controlSetRows: [
|
||||
['metrics'],
|
||||
['groupby'],
|
||||
|
@ -98,7 +99,7 @@ export const visTypes = {
|
|||
],
|
||||
},
|
||||
{
|
||||
label: 'Chart Options',
|
||||
label: t('Chart Options'),
|
||||
controlSetRows: [
|
||||
['color_scheme'],
|
||||
['show_legend', 'show_bar_value'],
|
||||
|
@ -112,21 +113,21 @@ export const visTypes = {
|
|||
],
|
||||
controlOverrides: {
|
||||
groupby: {
|
||||
label: 'Series',
|
||||
label: t('Series'),
|
||||
},
|
||||
columns: {
|
||||
label: 'Breakdowns',
|
||||
description: 'Defines how each series is broken down',
|
||||
label: t('Breakdowns'),
|
||||
description: t('Defines how each series is broken down'),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
pie: {
|
||||
label: 'Pie Chart',
|
||||
label: t('Pie Chart'),
|
||||
showOnExplore: true,
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['metrics', 'groupby'],
|
||||
|
@ -134,7 +135,7 @@ export const visTypes = {
|
|||
],
|
||||
},
|
||||
{
|
||||
label: 'Chart Options',
|
||||
label: t('Chart Options'),
|
||||
controlSetRows: [
|
||||
['pie_label_type'],
|
||||
['donut', 'show_legend'],
|
||||
|
@ -146,13 +147,13 @@ export const visTypes = {
|
|||
},
|
||||
|
||||
line: {
|
||||
label: 'Time Series - Line Chart',
|
||||
label: t('Time Series - Line Chart'),
|
||||
showOnExplore: true,
|
||||
requiresTime: true,
|
||||
controlPanelSections: [
|
||||
sections.NVD3TimeSeries[0],
|
||||
{
|
||||
label: 'Chart Options',
|
||||
label: t('Chart Options'),
|
||||
controlSetRows: [
|
||||
['color_scheme'],
|
||||
['show_brush', 'show_legend'],
|
||||
|
@ -161,14 +162,14 @@ export const visTypes = {
|
|||
],
|
||||
},
|
||||
{
|
||||
label: 'X Axis',
|
||||
label: t('X Axis'),
|
||||
controlSetRows: [
|
||||
['x_axis_label', 'bottom_margin'],
|
||||
['x_axis_showminmax', 'x_axis_format'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Y Axis',
|
||||
label: t('Y Axis'),
|
||||
controlSetRows: [
|
||||
['y_axis_label', 'left_margin'],
|
||||
['y_axis_showminmax', 'y_log_scale'],
|
||||
|
@ -186,24 +187,24 @@ export const visTypes = {
|
|||
},
|
||||
|
||||
dual_line: {
|
||||
label: 'Dual Axis Line Chart',
|
||||
label: t('Dual Axis Line Chart'),
|
||||
requiresTime: true,
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Chart Options',
|
||||
label: t('Chart Options'),
|
||||
controlSetRows: [
|
||||
['color_scheme'],
|
||||
['x_axis_format'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Y Axis 1',
|
||||
label: t('Y Axis 1'),
|
||||
controlSetRows: [
|
||||
['metric', 'y_axis_format'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Y Axis 2',
|
||||
label: t('Y Axis 2'),
|
||||
controlSetRows: [
|
||||
['metric_2', 'y_axis_2_format'],
|
||||
],
|
||||
|
@ -211,11 +212,11 @@ export const visTypes = {
|
|||
],
|
||||
controlOverrides: {
|
||||
metric: {
|
||||
label: 'Left Axis Metric',
|
||||
description: 'Choose a metric for left axis',
|
||||
label: t('Left Axis Metric'),
|
||||
description: t('Choose a metric for left axis'),
|
||||
},
|
||||
y_axis_format: {
|
||||
label: 'Left Axis Format',
|
||||
label: t('Left Axis Format'),
|
||||
},
|
||||
x_axis_format: {
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
|
@ -225,13 +226,13 @@ export const visTypes = {
|
|||
},
|
||||
|
||||
bar: {
|
||||
label: 'Time Series - Bar Chart',
|
||||
label: t('Time Series - Bar Chart'),
|
||||
showOnExplore: true,
|
||||
requiresTime: true,
|
||||
controlPanelSections: [
|
||||
sections.NVD3TimeSeries[0],
|
||||
{
|
||||
label: 'Chart Options',
|
||||
label: t('Chart Options'),
|
||||
controlSetRows: [
|
||||
['color_scheme'],
|
||||
['show_brush', 'show_legend', 'show_bar_value'],
|
||||
|
@ -241,7 +242,7 @@ export const visTypes = {
|
|||
],
|
||||
},
|
||||
{
|
||||
label: 'Axes',
|
||||
label: t('Axes'),
|
||||
controlSetRows: [
|
||||
['x_axis_format', 'y_axis_format'],
|
||||
['x_axis_showminmax', 'reduce_x_ticks'],
|
||||
|
@ -260,12 +261,12 @@ export const visTypes = {
|
|||
},
|
||||
|
||||
compare: {
|
||||
label: 'Time Series - Percent Change',
|
||||
label: t('Time Series - Percent Change'),
|
||||
requiresTime: true,
|
||||
controlPanelSections: [
|
||||
sections.NVD3TimeSeries[0],
|
||||
{
|
||||
label: 'Chart Options',
|
||||
label: t('Chart Options'),
|
||||
controlSetRows: [
|
||||
['color_scheme'],
|
||||
['x_axis_format', 'y_axis_format'],
|
||||
|
@ -282,12 +283,12 @@ export const visTypes = {
|
|||
},
|
||||
|
||||
area: {
|
||||
label: 'Time Series - Stacked',
|
||||
label: t('Time Series - Stacked'),
|
||||
requiresTime: true,
|
||||
controlPanelSections: [
|
||||
sections.NVD3TimeSeries[0],
|
||||
{
|
||||
label: 'Chart Options',
|
||||
label: t('Chart Options'),
|
||||
controlSetRows: [
|
||||
['show_brush', 'show_legend'],
|
||||
['line_interpolation', 'stacked_style'],
|
||||
|
@ -297,7 +298,7 @@ export const visTypes = {
|
|||
],
|
||||
},
|
||||
{
|
||||
label: 'Axes',
|
||||
label: t('Axes'),
|
||||
controlSetRows: [
|
||||
['x_axis_format', 'x_axis_showminmax'],
|
||||
['y_axis_format', 'y_axis_bounds'],
|
||||
|
@ -318,11 +319,11 @@ export const visTypes = {
|
|||
},
|
||||
|
||||
table: {
|
||||
label: 'Table View',
|
||||
label: t('Table View'),
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'GROUP BY',
|
||||
description: 'Use this section if you want a query that aggregates',
|
||||
label: t('GROUP BY'),
|
||||
description: t('Use this section if you want a query that aggregates'),
|
||||
controlSetRows: [
|
||||
['groupby', 'metrics'],
|
||||
['include_time', null],
|
||||
|
@ -330,15 +331,15 @@ export const visTypes = {
|
|||
],
|
||||
},
|
||||
{
|
||||
label: 'NOT GROUPED BY',
|
||||
description: 'Use this section if you want to query atomic rows',
|
||||
label: t('NOT GROUPED BY'),
|
||||
description: t('Use this section if you want to query atomic rows'),
|
||||
controlSetRows: [
|
||||
['all_columns'],
|
||||
['order_by_cols'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Options',
|
||||
label: t('Options'),
|
||||
controlSetRows: [
|
||||
['table_timestamp_format'],
|
||||
['row_limit', 'page_length'],
|
||||
|
@ -357,10 +358,10 @@ export const visTypes = {
|
|||
},
|
||||
|
||||
markup: {
|
||||
label: 'Markup',
|
||||
label: t('Markup'),
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Code',
|
||||
label: t('Code'),
|
||||
controlSetRows: [
|
||||
['markup_type'],
|
||||
['code'],
|
||||
|
@ -370,10 +371,10 @@ export const visTypes = {
|
|||
},
|
||||
|
||||
pivot_table: {
|
||||
label: 'Pivot Table',
|
||||
label: t('Pivot Table'),
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['groupby', 'columns'],
|
||||
|
@ -395,10 +396,10 @@ export const visTypes = {
|
|||
},
|
||||
|
||||
separator: {
|
||||
label: 'Separator',
|
||||
label: t('Separator'),
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Code',
|
||||
label: t('Code'),
|
||||
controlSetRows: [
|
||||
['markup_type'],
|
||||
['code'],
|
||||
|
@ -417,10 +418,10 @@ export const visTypes = {
|
|||
},
|
||||
|
||||
word_cloud: {
|
||||
label: 'Word Cloud',
|
||||
label: t('Word Cloud'),
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['series', 'metric', 'limit'],
|
||||
|
@ -438,10 +439,10 @@ export const visTypes = {
|
|||
},
|
||||
|
||||
treemap: {
|
||||
label: 'Treemap',
|
||||
label: t('Treemap'),
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['metrics'],
|
||||
|
@ -449,7 +450,7 @@ export const visTypes = {
|
|||
],
|
||||
},
|
||||
{
|
||||
label: 'Chart Options',
|
||||
label: t('Chart Options'),
|
||||
controlSetRows: [
|
||||
['color_scheme'],
|
||||
['treemap_ratio'],
|
||||
|
@ -465,11 +466,11 @@ export const visTypes = {
|
|||
},
|
||||
|
||||
cal_heatmap: {
|
||||
label: 'Calendar Heatmap',
|
||||
label: t('Calendar Heatmap'),
|
||||
requiresTime: true,
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['metric'],
|
||||
|
@ -486,10 +487,10 @@ export const visTypes = {
|
|||
},
|
||||
|
||||
box_plot: {
|
||||
label: 'Box Plot',
|
||||
label: t('Box Plot'),
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['metrics'],
|
||||
|
@ -497,7 +498,7 @@ export const visTypes = {
|
|||
],
|
||||
},
|
||||
{
|
||||
label: 'Chart Options',
|
||||
label: t('Chart Options'),
|
||||
controlSetRows: [
|
||||
['color_scheme'],
|
||||
['whisker_options'],
|
||||
|
@ -507,10 +508,10 @@ export const visTypes = {
|
|||
},
|
||||
|
||||
bubble: {
|
||||
label: 'Bubble Chart',
|
||||
label: t('Bubble Chart'),
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['series', 'entity'],
|
||||
|
@ -518,20 +519,20 @@ export const visTypes = {
|
|||
],
|
||||
},
|
||||
{
|
||||
label: 'Chart Options',
|
||||
label: t('Chart Options'),
|
||||
controlSetRows: [
|
||||
['color_scheme'],
|
||||
['show_legend', null],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Bubbles',
|
||||
label: t('Bubbles'),
|
||||
controlSetRows: [
|
||||
['size', 'max_bubble_size'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'X Axis',
|
||||
label: t('X Axis'),
|
||||
controlSetRows: [
|
||||
['x_axis_label', 'left_margin'],
|
||||
['x', 'x_axis_format'],
|
||||
|
@ -539,7 +540,7 @@ export const visTypes = {
|
|||
],
|
||||
},
|
||||
{
|
||||
label: 'Y Axis',
|
||||
label: t('Y Axis'),
|
||||
controlSetRows: [
|
||||
['y_axis_label', 'bottom_margin'],
|
||||
['y', 'y_axis_format'],
|
||||
|
@ -558,11 +559,11 @@ export const visTypes = {
|
|||
},
|
||||
|
||||
bullet: {
|
||||
label: 'Bullet Chart',
|
||||
label: t('Bullet Chart'),
|
||||
requiresTime: false,
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['metric'],
|
||||
|
@ -581,10 +582,10 @@ export const visTypes = {
|
|||
},
|
||||
|
||||
big_number: {
|
||||
label: 'Big Number with Trendline',
|
||||
label: t('Big Number with Trendline'),
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['metric'],
|
||||
|
@ -600,16 +601,16 @@ export const visTypes = {
|
|||
],
|
||||
controlOverrides: {
|
||||
y_axis_format: {
|
||||
label: 'Number format',
|
||||
label: t('Number format'),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
big_number_total: {
|
||||
label: 'Big Number',
|
||||
label: t('Big Number'),
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['metric'],
|
||||
|
@ -625,16 +626,16 @@ export const visTypes = {
|
|||
],
|
||||
controlOverrides: {
|
||||
y_axis_format: {
|
||||
label: 'Number format',
|
||||
label: t('Number format'),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
histogram: {
|
||||
label: 'Histogram',
|
||||
label: t('Histogram'),
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['all_columns_x'],
|
||||
|
@ -642,7 +643,7 @@ export const visTypes = {
|
|||
],
|
||||
},
|
||||
{
|
||||
label: 'Chart Options',
|
||||
label: t('Chart Options'),
|
||||
controlSetRows: [
|
||||
['color_scheme'],
|
||||
['link_length'],
|
||||
|
@ -651,22 +652,22 @@ export const visTypes = {
|
|||
],
|
||||
controlOverrides: {
|
||||
all_columns_x: {
|
||||
label: 'Numeric Column',
|
||||
description: 'Select the numeric column to draw the histogram',
|
||||
label: t('Numeric Column'),
|
||||
description: t('Select the numeric column to draw the histogram'),
|
||||
},
|
||||
link_length: {
|
||||
label: 'No of Bins',
|
||||
description: 'Select number of bins for the histogram',
|
||||
label: t('No of Bins'),
|
||||
description: t('Select number of bins for the histogram'),
|
||||
default: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
sunburst: {
|
||||
label: 'Sunburst',
|
||||
label: t('Sunburst'),
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['groupby'],
|
||||
|
@ -675,7 +676,7 @@ export const visTypes = {
|
|||
],
|
||||
},
|
||||
{
|
||||
label: 'Chart Options',
|
||||
label: t('Chart Options'),
|
||||
controlSetRows: [
|
||||
['color_scheme'],
|
||||
],
|
||||
|
@ -683,27 +684,27 @@ export const visTypes = {
|
|||
],
|
||||
controlOverrides: {
|
||||
metric: {
|
||||
label: 'Primary Metric',
|
||||
description: 'The primary metric is used to define the arc segment sizes',
|
||||
label: t('Primary Metric'),
|
||||
description: t('The primary metric is used to define the arc segment sizes'),
|
||||
},
|
||||
secondary_metric: {
|
||||
label: 'Secondary Metric',
|
||||
description: 'This secondary metric is used to ' +
|
||||
label: t('Secondary Metric'),
|
||||
description: t('This secondary metric is used to ' +
|
||||
'define the color as a ratio against the primary metric. ' +
|
||||
'If the two metrics match, color is mapped level groups',
|
||||
'If the two metrics match, color is mapped level groups'),
|
||||
},
|
||||
groupby: {
|
||||
label: 'Hierarchy',
|
||||
description: 'This defines the level of the hierarchy',
|
||||
label: t('Hierarchy'),
|
||||
description: t('This defines the level of the hierarchy'),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
sankey: {
|
||||
label: 'Sankey',
|
||||
label: t('Sankey'),
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['groupby'],
|
||||
|
@ -712,7 +713,7 @@ export const visTypes = {
|
|||
],
|
||||
},
|
||||
{
|
||||
label: 'Chart Options',
|
||||
label: t('Chart Options'),
|
||||
controlSetRows: [
|
||||
['color_scheme'],
|
||||
],
|
||||
|
@ -720,17 +721,17 @@ export const visTypes = {
|
|||
],
|
||||
controlOverrides: {
|
||||
groupby: {
|
||||
label: 'Source / Target',
|
||||
description: 'Choose a source and a target',
|
||||
label: t('Source / Target'),
|
||||
description: t('Choose a source and a target'),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
directed_force: {
|
||||
label: 'Directed Force Layout',
|
||||
label: t('Directed Force Layout'),
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['groupby'],
|
||||
|
@ -739,7 +740,7 @@ export const visTypes = {
|
|||
],
|
||||
},
|
||||
{
|
||||
label: 'Options',
|
||||
label: t('Options'),
|
||||
controlSetRows: [
|
||||
['link_length'],
|
||||
['charge'],
|
||||
|
@ -748,16 +749,16 @@ export const visTypes = {
|
|||
],
|
||||
controlOverrides: {
|
||||
groupby: {
|
||||
label: 'Source / Target',
|
||||
description: 'Choose a source and a target',
|
||||
label: t('Source / Target'),
|
||||
description: t('Choose a source and a target'),
|
||||
},
|
||||
},
|
||||
},
|
||||
chord: {
|
||||
label: 'Chord Diagram',
|
||||
label: t('Chord Diagram'),
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['groupby', 'columns'],
|
||||
|
@ -765,7 +766,7 @@ export const visTypes = {
|
|||
],
|
||||
},
|
||||
{
|
||||
label: 'Chart Options',
|
||||
label: t('Chart Options'),
|
||||
controlSetRows: [
|
||||
['y_axis_format', null],
|
||||
['color_scheme'],
|
||||
|
@ -774,28 +775,28 @@ export const visTypes = {
|
|||
],
|
||||
controlOverrides: {
|
||||
y_axis_format: {
|
||||
label: 'Number format',
|
||||
description: 'Choose a number format',
|
||||
label: t('Number format'),
|
||||
description: t('Choose a number format'),
|
||||
},
|
||||
groupby: {
|
||||
label: 'Source',
|
||||
label: t('Source'),
|
||||
multi: false,
|
||||
validators: [v.nonEmpty],
|
||||
description: 'Choose a source',
|
||||
description: t('Choose a source'),
|
||||
},
|
||||
columns: {
|
||||
label: 'Target',
|
||||
label: t('Target'),
|
||||
multi: false,
|
||||
validators: [v.nonEmpty],
|
||||
description: 'Choose a target',
|
||||
description: t('Choose a target'),
|
||||
},
|
||||
},
|
||||
},
|
||||
country_map: {
|
||||
label: 'Country Map',
|
||||
label: t('Country Map'),
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['entity'],
|
||||
|
@ -803,7 +804,7 @@ export const visTypes = {
|
|||
],
|
||||
},
|
||||
{
|
||||
label: 'Options',
|
||||
label: t('Options'),
|
||||
controlSetRows: [
|
||||
['select_country'],
|
||||
['linear_color_scheme'],
|
||||
|
@ -812,11 +813,11 @@ export const visTypes = {
|
|||
],
|
||||
controlOverrides: {
|
||||
entity: {
|
||||
label: 'ISO 3166-1 codes of region/province/department',
|
||||
description: "It's ISO 3166-1 of your region/province/department in your table. (see documentation for list of ISO 3166-1)",
|
||||
label: t('ISO 3166-1 codes of region/province/department'),
|
||||
description: t('It\'s ISO 3166-1 of your region/province/department in your table. (see documentation for list of ISO 3166-1)'),
|
||||
},
|
||||
metric: {
|
||||
label: 'Metric',
|
||||
label: t('Metric'),
|
||||
description: 'Metric to display bottom title',
|
||||
},
|
||||
linear_color_scheme: {
|
||||
|
@ -825,10 +826,10 @@ export const visTypes = {
|
|||
},
|
||||
},
|
||||
world_map: {
|
||||
label: 'World Map',
|
||||
label: t('World Map'),
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['entity'],
|
||||
|
@ -837,7 +838,7 @@ export const visTypes = {
|
|||
],
|
||||
},
|
||||
{
|
||||
label: 'Bubbles',
|
||||
label: t('Bubbles'),
|
||||
controlSetRows: [
|
||||
['show_bubbles'],
|
||||
['secondary_metric'],
|
||||
|
@ -847,25 +848,25 @@ export const visTypes = {
|
|||
],
|
||||
controlOverrides: {
|
||||
entity: {
|
||||
label: 'Country Control',
|
||||
description: '3 letter code of the country',
|
||||
label: t('Country Control'),
|
||||
description: t('3 letter code of the country'),
|
||||
},
|
||||
metric: {
|
||||
label: 'Metric for color',
|
||||
description: 'Metric that defines the color of the country',
|
||||
label: t('Metric for color'),
|
||||
description: t('Metric that defines the color of the country'),
|
||||
},
|
||||
secondary_metric: {
|
||||
label: 'Bubble size',
|
||||
description: 'Metric that defines the size of the bubble',
|
||||
label: t('Bubble size'),
|
||||
description: t('Metric that defines the size of the bubble'),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
filter_box: {
|
||||
label: 'Filter Box',
|
||||
label: t('Filter Box'),
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['groupby'],
|
||||
|
@ -881,11 +882,10 @@ export const visTypes = {
|
|||
],
|
||||
controlOverrides: {
|
||||
groupby: {
|
||||
label: 'Filter controls',
|
||||
description: (
|
||||
label: t('Filter controls'),
|
||||
description: t(
|
||||
'The controls you want to filter on. Note that only columns ' +
|
||||
'checked as "filterable" will show up on this list.'
|
||||
),
|
||||
'checked as "filterable" will show up on this list.'),
|
||||
mapStateToProps: state => ({
|
||||
options: (state.datasource) ? state.datasource.columns.filter(c => c.filterable) : [],
|
||||
}),
|
||||
|
@ -894,10 +894,10 @@ export const visTypes = {
|
|||
},
|
||||
|
||||
iframe: {
|
||||
label: 'iFrame',
|
||||
label: t('iFrame'),
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Options',
|
||||
label: t('Options'),
|
||||
controlSetRows: [
|
||||
['url'],
|
||||
],
|
||||
|
@ -906,10 +906,10 @@ export const visTypes = {
|
|||
},
|
||||
|
||||
para: {
|
||||
label: 'Parallel Coordinates',
|
||||
label: t('Parallel Coordinates'),
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['series'],
|
||||
|
@ -928,10 +928,10 @@ export const visTypes = {
|
|||
},
|
||||
|
||||
heatmap: {
|
||||
label: 'Heatmap',
|
||||
label: t('Heatmap'),
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['all_columns_x', 'all_columns_y'],
|
||||
|
@ -939,7 +939,7 @@ export const visTypes = {
|
|||
],
|
||||
},
|
||||
{
|
||||
label: 'Heatmap Options',
|
||||
label: t('Heatmap Options'),
|
||||
controlSetRows: [
|
||||
['linear_color_scheme'],
|
||||
['xscale_interval', 'yscale_interval'],
|
||||
|
@ -974,11 +974,11 @@ export const visTypes = {
|
|||
},
|
||||
|
||||
horizon: {
|
||||
label: 'Horizon',
|
||||
label: t('Horizon'),
|
||||
controlPanelSections: [
|
||||
sections.NVD3TimeSeries[0],
|
||||
{
|
||||
label: 'Chart Options',
|
||||
label: t('Chart Options'),
|
||||
controlSetRows: [
|
||||
['series_height', 'horizon_color_scale'],
|
||||
],
|
||||
|
@ -987,10 +987,10 @@ export const visTypes = {
|
|||
},
|
||||
|
||||
mapbox: {
|
||||
label: 'Mapbox',
|
||||
label: t('Mapbox'),
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['all_columns_x', 'all_columns_y'],
|
||||
|
@ -1000,21 +1000,21 @@ export const visTypes = {
|
|||
],
|
||||
},
|
||||
{
|
||||
label: 'Points',
|
||||
label: t('Points'),
|
||||
controlSetRows: [
|
||||
['point_radius'],
|
||||
['point_radius_unit'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Labelling',
|
||||
label: t('Labelling'),
|
||||
controlSetRows: [
|
||||
['mapbox_label'],
|
||||
['pandas_aggfunc'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Visual Tweaks',
|
||||
label: t('Visual Tweaks'),
|
||||
controlSetRows: [
|
||||
['render_while_dragging'],
|
||||
['mapbox_style'],
|
||||
|
@ -1023,7 +1023,7 @@ export const visTypes = {
|
|||
],
|
||||
},
|
||||
{
|
||||
label: 'Viewport',
|
||||
label: t('Viewport'),
|
||||
controlSetRows: [
|
||||
['viewport_longitude'],
|
||||
['viewport_latitude'],
|
||||
|
@ -1033,36 +1033,36 @@ export const visTypes = {
|
|||
],
|
||||
controlOverrides: {
|
||||
all_columns_x: {
|
||||
label: 'Longitude',
|
||||
description: 'Column containing longitude data',
|
||||
label: t('Longitude'),
|
||||
description: t('Column containing longitude data'),
|
||||
},
|
||||
all_columns_y: {
|
||||
label: 'Latitude',
|
||||
description: 'Column containing latitude data',
|
||||
label: t('Latitude'),
|
||||
description: t('Column containing latitude data'),
|
||||
},
|
||||
pandas_aggfunc: {
|
||||
label: 'Cluster label aggregator',
|
||||
description: 'Aggregate function applied to the list of points ' +
|
||||
'in each cluster to produce the cluster label.',
|
||||
label: t('Cluster label aggregator'),
|
||||
description: t('Aggregate function applied to the list of points ' +
|
||||
'in each cluster to produce the cluster label.'),
|
||||
},
|
||||
rich_tooltip: {
|
||||
label: 'Tooltip',
|
||||
description: 'Show a tooltip when hovering over points and clusters ' +
|
||||
'describing the label',
|
||||
label: t('Tooltip'),
|
||||
description: t('Show a tooltip when hovering over points and clusters ' +
|
||||
'describing the label'),
|
||||
},
|
||||
groupby: {
|
||||
description: 'One or many controls to group by. If grouping, latitude ' +
|
||||
'and longitude columns must be present.',
|
||||
description: t('One or many controls to group by. If grouping, latitude ' +
|
||||
'and longitude columns must be present.'),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
event_flow: {
|
||||
label: 'Event flow',
|
||||
label: t('Event flow'),
|
||||
requiresTime: true,
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Event definition',
|
||||
label: t('Event definition'),
|
||||
controlSetRows: [
|
||||
['entity'],
|
||||
['all_columns_x'],
|
||||
|
@ -1072,7 +1072,7 @@ export const visTypes = {
|
|||
],
|
||||
},
|
||||
{
|
||||
label: 'Additional meta data',
|
||||
label: t('Additional meta data'),
|
||||
controlSetRows: [
|
||||
['all_columns'],
|
||||
],
|
||||
|
@ -1080,11 +1080,11 @@ export const visTypes = {
|
|||
],
|
||||
controlOverrides: {
|
||||
entity: {
|
||||
label: 'Column containing entity ids',
|
||||
description: 'e.g., a "user id" column',
|
||||
label: t('Column containing entity ids'),
|
||||
description: t('e.g., a "user id" column'),
|
||||
},
|
||||
all_columns_x: {
|
||||
label: 'Column containing event names',
|
||||
label: t('Column containing event names'),
|
||||
validators: [v.nonEmpty],
|
||||
default: control => (
|
||||
control.choices && control.choices.length > 0 ?
|
||||
|
@ -1092,12 +1092,12 @@ export const visTypes = {
|
|||
),
|
||||
},
|
||||
row_limit: {
|
||||
label: 'Event count limit',
|
||||
description: 'The maximum number of events to return, equivalent to number of rows',
|
||||
label: t('Event count limit'),
|
||||
description: t('The maximum number of events to return, equivalent to number of rows'),
|
||||
},
|
||||
all_columns: {
|
||||
label: 'Meta data',
|
||||
description: 'Select any columns for meta data inspection',
|
||||
label: t('Meta data'),
|
||||
description: t('Select any columns for meta data inspection'),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import Jed from 'jed';
|
||||
|
||||
const DEFAULT_LANGUAGE_PACK = {
|
||||
domain: 'superset',
|
||||
locale_data: {
|
||||
superset: {
|
||||
'': {
|
||||
domain: 'superset',
|
||||
lang: 'en',
|
||||
plural_forms: 'nplurals=1; plural=0',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const i18n = (function () {
|
||||
let languagePack = DEFAULT_LANGUAGE_PACK;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const root = document.getElementById('app');
|
||||
const bootstrapData = root ? JSON.parse(root.getAttribute('data-bootstrap')) : {};
|
||||
if (bootstrapData.common && bootstrapData.common.language_pack) {
|
||||
languagePack = bootstrapData.common.language_pack;
|
||||
delete bootstrapData.common.locale;
|
||||
delete bootstrapData.common.language_pack;
|
||||
}
|
||||
}
|
||||
|
||||
return new Jed(languagePack);
|
||||
}());
|
||||
|
||||
export default i18n;
|
|
@ -0,0 +1,148 @@
|
|||
/* eslint-disable global-require, import/no-dynamic-require */
|
||||
import React from 'react';
|
||||
import { sprintf } from 'sprintf-js';
|
||||
import i18n from './i18n';
|
||||
|
||||
function formatForReact(formatString, args) {
|
||||
const rv = [];
|
||||
let cursor = 0;
|
||||
sprintf.parse(formatString).forEach((match, idx) => {
|
||||
const cpoyMatch = match;
|
||||
let copyIdx = idx;
|
||||
if (typeof match === 'string') {
|
||||
rv.push(match);
|
||||
} else {
|
||||
let arg = null;
|
||||
if (match[2]) {
|
||||
arg = args[0][match[2][0]];
|
||||
} else if (match[1]) {
|
||||
arg = args[parseInt(match[1], 10) - 1];
|
||||
} else {
|
||||
arg = args[cursor++];
|
||||
}
|
||||
if (React.isValidElement(arg)) {
|
||||
rv.push(React.cloneElement(arg, { key: idx }));
|
||||
} else {
|
||||
cpoyMatch[2] = null;
|
||||
cpoyMatch[1] = 1;
|
||||
rv.push(<span key={copyIdx++}>
|
||||
{sprintf.format([cpoyMatch], [null, arg])}
|
||||
</span>);
|
||||
}
|
||||
}
|
||||
});
|
||||
return rv;
|
||||
}
|
||||
|
||||
function argsInvolveReact(args) {
|
||||
if (args.some(React.isValidElement)) {
|
||||
return true;
|
||||
}
|
||||
if (args.length === 1 && typeof args[0] === 'object') {
|
||||
return Object.keys(args[0]).some(function (key) {
|
||||
return React.isValidElement(args[0][key]);
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function parseComponentTemplate(string) {
|
||||
const rv = {};
|
||||
function process(startPos, group, inGroup) {
|
||||
const regex = /\[(.*?)(:|\])|\]/g;
|
||||
let match;
|
||||
const buf = [];
|
||||
let satisfied = false;
|
||||
let pos = regex.lastIndex = startPos;
|
||||
match = regex.exec(string);
|
||||
while (match !== null) {
|
||||
const substr = string.substr(pos, match.index - pos);
|
||||
if (substr !== '') {
|
||||
buf.push(substr);
|
||||
}
|
||||
if (match[0] === ']') {
|
||||
if (inGroup) {
|
||||
satisfied = true;
|
||||
break;
|
||||
} else {
|
||||
pos = regex.lastIndex;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (match[2] === ']') {
|
||||
pos = regex.lastIndex;
|
||||
} else {
|
||||
pos = regex.lastIndex = process(regex.lastIndex, match[1], true);
|
||||
}
|
||||
buf.push({ group: match[1] });
|
||||
match = regex.exec(string);
|
||||
}
|
||||
let endPos = regex.lastIndex;
|
||||
if (!satisfied) {
|
||||
const rest = string.substr(pos);
|
||||
if (rest) {
|
||||
buf.push(rest);
|
||||
}
|
||||
endPos = string.length;
|
||||
}
|
||||
rv[group] = buf;
|
||||
return endPos;
|
||||
}
|
||||
process(0, 'root', false);
|
||||
return rv;
|
||||
}
|
||||
|
||||
export function renderComponentTemplate(template, components) {
|
||||
let idx = 0;
|
||||
function renderGroup(group) {
|
||||
const children = [];
|
||||
(template[group] || []).forEach((item) => {
|
||||
if (typeof item === 'string') {
|
||||
children.push(<span key={idx++}>{item}</span>);
|
||||
} else {
|
||||
children.push(renderGroup(item.group));
|
||||
}
|
||||
});
|
||||
let reference = components[group] || <span key={idx++} />;
|
||||
if (!React.isValidElement(reference)) {
|
||||
reference = <span key={idx++}>{reference}</span>;
|
||||
}
|
||||
if (children.length > 0) {
|
||||
return React.cloneElement(reference, { key: idx++ }, children);
|
||||
}
|
||||
return React.cloneElement(reference, { key: idx++ });
|
||||
}
|
||||
return renderGroup('root');
|
||||
}
|
||||
|
||||
export function format(formatString, args) {
|
||||
if (argsInvolveReact(args)) {
|
||||
return formatForReact(formatString, args);
|
||||
}
|
||||
return sprintf(formatString, ...args);
|
||||
}
|
||||
|
||||
export function gettext(string, ...args) {
|
||||
if (!string || !i18n) {
|
||||
return string;
|
||||
}
|
||||
|
||||
let rv = i18n.gettext(string);
|
||||
if (args.length > 0) {
|
||||
rv = format(rv, args);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
export function ngettext(singular, plural, ...args) {
|
||||
return format(i18n.ngettext(singular, plural, args[0] || 0), args);
|
||||
}
|
||||
|
||||
export function gettextComponentTemplate(template, components) {
|
||||
const tmpl = parseComponentTemplate(i18n.gettext(template));
|
||||
return renderComponentTemplate(tmpl, components);
|
||||
}
|
||||
|
||||
export const t = gettext;
|
||||
export const tn = ngettext;
|
||||
export const tct = gettextComponentTemplate;
|
|
@ -4,6 +4,7 @@ import Mustache from 'mustache';
|
|||
import vizMap from '../../visualizations/main';
|
||||
import { getExploreUrl } from '../explore/exploreUtils';
|
||||
import { applyDefaultFormData } from '../explore/stores/store';
|
||||
import { t } from '../locales';
|
||||
|
||||
const utils = require('./utils');
|
||||
|
||||
|
@ -29,7 +30,7 @@ const px = function (state) {
|
|||
}
|
||||
}
|
||||
$('.favstar')
|
||||
.attr('title', 'Click to favorite/unfavorite')
|
||||
.attr('title', t('Click to favorite/unfavorite'))
|
||||
.css('cursor', 'pointer')
|
||||
.each(show)
|
||||
.each(function () {
|
||||
|
@ -126,10 +127,10 @@ const px = function (state) {
|
|||
if (status === 0) {
|
||||
// This may happen when the worker in gunicorn times out
|
||||
msg += (
|
||||
'The server could not be reached. You may want to ' +
|
||||
'verify your connection and try again.');
|
||||
t('The server could not be reached. You may want to ' +
|
||||
'verify your connection and try again.'));
|
||||
} else {
|
||||
msg += 'An unknown error occurred. (Status: ' + status + ')';
|
||||
msg += (t('An unknown error occurred. (Status: %s )', status));
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
|
@ -219,7 +220,7 @@ const px = function (state) {
|
|||
vizMap[formData.viz_type](this, queryResponse);
|
||||
this.done(queryResponse);
|
||||
} catch (e) {
|
||||
this.error('An error occurred while rendering the visualization: ' + e);
|
||||
this.error(t('An error occurred while rendering the visualization: %s', e));
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
|
|
|
@ -6,6 +6,7 @@ import UserInfo from './UserInfo';
|
|||
import Security from './Security';
|
||||
import RecentActivity from './RecentActivity';
|
||||
import CreatedContent from './CreatedContent';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
user: PropTypes.object.isRequired,
|
||||
|
@ -20,25 +21,25 @@ export default function App(props) {
|
|||
</Col>
|
||||
<Col md={9}>
|
||||
<Tabs id="options">
|
||||
<Tab eventKey={1} title={<div><i className="fa fa-star" /> Favorites</div>}>
|
||||
<Tab eventKey={1} title={<div><i className="fa fa-star" /> {t('Favorites')}</div>}>
|
||||
<Panel><Favorites user={props.user} /></Panel>
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey={2}
|
||||
title={
|
||||
<div><i className="fa fa-paint-brush" /> Created Content</div>
|
||||
<div><i className="fa fa-paint-brush" /> {t('Created Content')}</div>
|
||||
}
|
||||
>
|
||||
<Panel>
|
||||
<CreatedContent user={props.user} />
|
||||
</Panel>
|
||||
</Tab>
|
||||
<Tab eventKey={3} title={<div><i className="fa fa-list" /> Recent Activity</div>}>
|
||||
<Tab eventKey={3} title={<div><i className="fa fa-list" /> {t('Recent Activity')}</div>}>
|
||||
<Panel>
|
||||
<RecentActivity user={props.user} />
|
||||
</Panel>
|
||||
</Tab>
|
||||
<Tab eventKey={4} title={<div><i className="fa fa-lock" /> Security & Access</div>}>
|
||||
<Tab eventKey={4} title={<div><i className="fa fa-lock" /> {t('Security & Access')}</div>}>
|
||||
<Panel>
|
||||
<Security user={props.user} />
|
||||
</Panel>
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
import TableLoader from './TableLoader';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
user: PropTypes.object.isRequired,
|
||||
|
@ -29,7 +30,7 @@ class CreatedContent extends React.PureComponent {
|
|||
className="table table-condensed"
|
||||
columns={['slice', 'favorited']}
|
||||
mutator={mutator}
|
||||
noDataText="No slices"
|
||||
noDataText={t('No slices')}
|
||||
sortable
|
||||
/>
|
||||
);
|
||||
|
@ -45,7 +46,7 @@ class CreatedContent extends React.PureComponent {
|
|||
className="table table-condensed"
|
||||
mutator={mutator}
|
||||
dataEndpoint={`/superset/created_dashboards/${this.props.user.userId}/`}
|
||||
noDataText="No dashboards"
|
||||
noDataText={t('No dashboards')}
|
||||
columns={['dashboard', 'favorited']}
|
||||
sortable
|
||||
/>
|
||||
|
@ -54,10 +55,10 @@ class CreatedContent extends React.PureComponent {
|
|||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h3>Dashboards</h3>
|
||||
<h3>{t('Dashboards')}</h3>
|
||||
{this.renderDashboardTable()}
|
||||
<hr />
|
||||
<h3>Slices</h3>
|
||||
<h3>{t('Slices')}</h3>
|
||||
{this.renderSliceTable()}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
import TableLoader from './TableLoader';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
user: PropTypes.object.isRequired,
|
||||
|
@ -30,7 +31,7 @@ export default class Favorites extends React.PureComponent {
|
|||
className="table table-condensed"
|
||||
columns={['slice', 'creator', 'favorited']}
|
||||
mutator={mutator}
|
||||
noDataText="No favorite slices yet, go click on stars!"
|
||||
noDataText={t('No favorite slices yet, go click on stars!')}
|
||||
sortable
|
||||
/>
|
||||
);
|
||||
|
@ -46,7 +47,7 @@ export default class Favorites extends React.PureComponent {
|
|||
className="table table-condensed"
|
||||
mutator={mutator}
|
||||
dataEndpoint={`/superset/fave_dashboards/${this.props.user.userId}/`}
|
||||
noDataText="No favorite dashboards yet, go click on stars!"
|
||||
noDataText={t('No favorite dashboards yet, go click on stars!')}
|
||||
columns={['dashboard', 'creator', 'favorited']}
|
||||
sortable
|
||||
/>
|
||||
|
@ -55,10 +56,10 @@ export default class Favorites extends React.PureComponent {
|
|||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h3>Dashboards</h3>
|
||||
<h3>{t('Dashboards')}</h3>
|
||||
{this.renderDashboardTable()}
|
||||
<hr />
|
||||
<h3>Slices</h3>
|
||||
<h3>{t('Slices')}</h3>
|
||||
{this.renderSliceTable()}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Badge, Label } from 'react-bootstrap';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
user: PropTypes.object.isRequired,
|
||||
|
@ -10,7 +11,7 @@ export default function Security({ user }) {
|
|||
<div>
|
||||
<div className="roles">
|
||||
<h4>
|
||||
Roles <Badge>{Object.keys(user.roles).length}</Badge>
|
||||
{t('Roles')} <Badge>{Object.keys(user.roles).length}</Badge>
|
||||
</h4>
|
||||
{Object.keys(user.roles).map(role => <Label key={role}>{role}</Label>)}
|
||||
<hr />
|
||||
|
@ -19,7 +20,7 @@ export default function Security({ user }) {
|
|||
{user.permissions.database_access &&
|
||||
<div>
|
||||
<h4>
|
||||
Databases <Badge>{user.permissions.database_access.length}</Badge>
|
||||
{t('Databases')} <Badge>{user.permissions.database_access.length}</Badge>
|
||||
</h4>
|
||||
{user.permissions.database_access.map(role => <Label key={role}>{role}</Label>)}
|
||||
<hr />
|
||||
|
@ -30,7 +31,7 @@ export default function Security({ user }) {
|
|||
{user.permissions.datasource_access &&
|
||||
<div>
|
||||
<h4>
|
||||
Datasources <Badge>{user.permissions.datasource_access.length}</Badge>
|
||||
{t('Datasources')} <Badge>{user.permissions.datasource_access.length}</Badge>
|
||||
</h4>
|
||||
{user.permissions.datasource_access.map(role => <Label key={role}>{role}</Label>)}
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||
import Gravatar from 'react-gravatar';
|
||||
import moment from 'moment';
|
||||
import { Panel } from 'react-bootstrap';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
user: PropTypes.object.isRequired,
|
||||
|
@ -14,7 +15,7 @@ const UserInfo = ({ user }) => (
|
|||
email={user.email}
|
||||
width="100%"
|
||||
height=""
|
||||
alt="Profile picture provided by Gravatar"
|
||||
alt={t('Profile picture provided by Gravatar')}
|
||||
className="img-rounded"
|
||||
style={{ borderRadius: 15 }}
|
||||
/>
|
||||
|
@ -29,7 +30,7 @@ const UserInfo = ({ user }) => (
|
|||
</h4>
|
||||
<hr />
|
||||
<p>
|
||||
<i className="fa fa-clock-o" /> joined {moment(user.createdOn, 'YYYYMMDD').fromNow()}
|
||||
<i className="fa fa-clock-o" /> {t('joined')} {moment(user.createdOn, 'YYYYMMDD').fromNow()}
|
||||
</p>
|
||||
<p className="email">
|
||||
<i className="fa fa-envelope-o" /> {user.email}
|
||||
|
@ -39,7 +40,7 @@ const UserInfo = ({ user }) => (
|
|||
</p>
|
||||
<p>
|
||||
<i className="fa fa-key" />
|
||||
<span className="text-muted">id:</span>
|
||||
<span className="text-muted">{t('id:')}</span>
|
||||
<span className="user-id">{user.userId}</span>
|
||||
</p>
|
||||
</Panel>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint no-unused-vars: 0 */
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Badge, Col, Label, Row, Tabs, Tab, Panel } from 'react-bootstrap';
|
||||
|
||||
import App from './components/App';
|
||||
import { appSetup } from '../common';
|
||||
|
||||
|
|
|
@ -54,6 +54,8 @@
|
|||
"datamaps": "^0.5.8",
|
||||
"datatables.net-bs": "^1.10.15",
|
||||
"immutable": "^3.8.1",
|
||||
"jed": "^1.1.1",
|
||||
"po2json": "^0.4.5",
|
||||
"jquery": "3.1.1",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"moment": "^2.14.1",
|
||||
|
@ -85,6 +87,7 @@
|
|||
"redux-localstorage": "^0.4.1",
|
||||
"redux-thunk": "^2.1.0",
|
||||
"shortid": "^2.2.6",
|
||||
"sprintf-js": "^1.1.1",
|
||||
"supercluster": "https://github.com/georgeke/supercluster/tarball/ac3492737e7ce98e07af679623aad452373bbc40",
|
||||
"urijs": "^1.18.10",
|
||||
"viewport-mercator-project": "^2.1.0"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import { shallow } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
|
@ -11,10 +11,12 @@ describe('AsyncSelect', () => {
|
|||
const mockedProps = {
|
||||
dataEndpoint: '/slicemodelview/api/read',
|
||||
onChange: sinon.spy(),
|
||||
placeholder: 'Select...',
|
||||
mutator: () => [
|
||||
{ value: 1, label: 'main' },
|
||||
{ value: 2, label: 'another' },
|
||||
],
|
||||
valueRenderer: opt => opt.label,
|
||||
};
|
||||
it('is valid element', () => {
|
||||
expect(
|
||||
|
@ -49,34 +51,34 @@ describe('AsyncSelect', () => {
|
|||
server.restore();
|
||||
});
|
||||
it('should be off by default', () => {
|
||||
const wrapper = mount(
|
||||
const wrapper = shallow(
|
||||
<AsyncSelect {...mockedProps} />,
|
||||
);
|
||||
wrapper.instance().fetchOptions();
|
||||
const spy = sinon.spy(wrapper.instance(), 'onChange');
|
||||
expect(spy.callCount).to.equal(0);
|
||||
});
|
||||
it('should auto select first option', () => {
|
||||
const wrapper = mount(
|
||||
const wrapper = shallow(
|
||||
<AsyncSelect {...mockedProps} autoSelect />,
|
||||
);
|
||||
const spy = sinon.spy(wrapper.instance(), 'onChange');
|
||||
|
||||
wrapper.instance().fetchOptions();
|
||||
server.respond();
|
||||
|
||||
expect(spy.callCount).to.equal(1);
|
||||
expect(spy.calledWith(wrapper.instance().state.options[0])).to.equal(true);
|
||||
});
|
||||
it('should not auto select when value prop is set', () => {
|
||||
const wrapper = mount(
|
||||
const wrapper = shallow(
|
||||
<AsyncSelect {...mockedProps} value={2} autoSelect />,
|
||||
);
|
||||
const spy = sinon.spy(wrapper.instance(), 'onChange');
|
||||
|
||||
wrapper.instance().fetchOptions();
|
||||
server.respond();
|
||||
|
||||
expect(spy.callCount).to.equal(0);
|
||||
expect(wrapper.find(Select)).to.have.length(1);
|
||||
expect(wrapper.find('.Select-value-label').children().first().text()).to.equal('another');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,7 +19,7 @@ describe('EditableTitle', () => {
|
|||
},
|
||||
};
|
||||
const editableWrapper = shallow(<EditableTable {...mockProps} />);
|
||||
const notEditableWrapper = shallow(<EditableTable title="my title" />);
|
||||
const notEditableWrapper = shallow(<EditableTable title="my title" onSaveTitle={callback} />);
|
||||
it('is valid', () => {
|
||||
expect(
|
||||
React.isValidElement(<EditableTable {...mockProps} />),
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import React from 'react';
|
||||
import { Overlay, Popover, FormControl } from 'react-bootstrap';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import { shallow } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import SaveQuery from '../../../javascripts/SqlLab/components/SaveQuery';
|
||||
|
||||
describe('SavedQuery', () => {
|
||||
|
@ -30,11 +29,11 @@ describe('SavedQuery', () => {
|
|||
expect(wrapper.find(Popover)).to.have.length(1);
|
||||
});
|
||||
it('pops and hides', () => {
|
||||
const wrapper = mount(<SaveQuery {...mockedProps} />);
|
||||
const wrapper = shallow(<SaveQuery {...mockedProps} />);
|
||||
expect(wrapper.state().showSave).to.equal(false);
|
||||
wrapper.find('.toggleSave').simulate('click');
|
||||
wrapper.find('.toggleSave').simulate('click', { target: { value: 'test' } });
|
||||
expect(wrapper.state().showSave).to.equal(true);
|
||||
wrapper.find('.toggleSave').simulate('click');
|
||||
wrapper.find('.toggleSave').simulate('click', { target: { value: 'test' } });
|
||||
expect(wrapper.state().showSave).to.equal(false);
|
||||
});
|
||||
it('has a cancel button', () => {
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import { mount } from 'enzyme';
|
||||
import { describe, it, beforeEach } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import Timer from '../../../javascripts/components/Timer';
|
||||
import { now } from '../../../javascripts/modules/dates';
|
||||
|
@ -9,16 +10,21 @@ import { now } from '../../../javascripts/modules/dates';
|
|||
|
||||
describe('Timer', () => {
|
||||
let wrapper;
|
||||
let clock;
|
||||
const mockedProps = {
|
||||
startTime: now(),
|
||||
endTime: null,
|
||||
isRunning: true,
|
||||
status: 'warning',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers();
|
||||
mockedProps.startTime = now() + 1;
|
||||
wrapper = mount(<Timer {...mockedProps} />);
|
||||
});
|
||||
afterEach(() => {
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
it('is a valid element', () => {
|
||||
expect(React.isValidElement(<Timer {...mockedProps} />)).to.equal(true);
|
||||
|
@ -26,9 +32,8 @@ describe('Timer', () => {
|
|||
|
||||
it('componentWillMount starts timer after 30ms and sets state.clockStr', () => {
|
||||
expect(wrapper.state().clockStr).to.equal('');
|
||||
setTimeout(() => {
|
||||
expect(wrapper.state().clockStr).not.equal('');
|
||||
}, 31);
|
||||
clock.tick(31);
|
||||
expect(wrapper.state().clockStr).not.equal('');
|
||||
});
|
||||
|
||||
it('calls startTimer on mount', () => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { beforeEach, describe, it } from 'mocha';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import * as r from '../../../javascripts/SqlLab/reducers';
|
||||
|
@ -9,7 +9,9 @@ describe('sqlLabReducer', () => {
|
|||
describe('CLONE_QUERY_TO_NEW_TAB', () => {
|
||||
const testQuery = { sql: 'SELECT * FROM...', dbId: 1, id: 'flasj233' };
|
||||
let newState = Object.assign({}, initialState, { queries: { [testQuery.id]: testQuery } });
|
||||
newState = r.sqlLabReducer(newState, actions.cloneQueryToNewTab(testQuery));
|
||||
beforeEach(() => {
|
||||
newState = r.sqlLabReducer(newState, actions.cloneQueryToNewTab(testQuery));
|
||||
});
|
||||
|
||||
it('should have at most one more tab', () => {
|
||||
expect(newState.queryEditors).have.length(2);
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
EVENT_NAME,
|
||||
ENTITY_ID,
|
||||
} from '@data-ui/event-flow';
|
||||
import { t } from '../javascripts/locales';
|
||||
|
||||
/*
|
||||
* This function takes the slice object and json payload as input and renders a
|
||||
|
@ -52,7 +53,7 @@ function renderEventFlow(slice, json) {
|
|||
|
||||
Component = <ResponsiveVis data={cleanData} initialMinEventCount={minEventCount} />;
|
||||
} else {
|
||||
Component = <div>Sorry, there appears to be no data</div>;
|
||||
Component = <div>{t('Sorry, there appears to be no data')}</div>;
|
||||
}
|
||||
|
||||
ReactDOM.render(Component, container);
|
||||
|
|
|
@ -8,6 +8,7 @@ import { Button } from 'react-bootstrap';
|
|||
|
||||
import { TIME_CHOICES } from './constants';
|
||||
import './filter_box.css';
|
||||
import { t } from '../javascripts/locales';
|
||||
|
||||
const propTypes = {
|
||||
origSelectedValues: PropTypes.object,
|
||||
|
@ -102,7 +103,7 @@ class FilterBox extends React.Component {
|
|||
<div key={filter} className="m-b-5">
|
||||
{this.props.datasource.verbose_map[filter] || filter}
|
||||
<Select.Creatable
|
||||
placeholder={`Select [${filter}]`}
|
||||
placeholder={t('Select [%s]', filter)}
|
||||
key={filter}
|
||||
multi
|
||||
value={this.state.selectedValues[filter]}
|
||||
|
|
|
@ -159,8 +159,8 @@ BABEL_DEFAULT_FOLDER = 'babel/translations'
|
|||
LANGUAGES = {
|
||||
'en': {'flag': 'us', 'name': 'English'},
|
||||
'it': {'flag': 'it', 'name': 'Italian'},
|
||||
# 'fr': {'flag': 'fr', 'name': 'French'},
|
||||
# 'zh': {'flag': 'cn', 'name': 'Chinese'},
|
||||
'fr': {'flag': 'fr', 'name': 'French'},
|
||||
'zh': {'flag': 'cn', 'name': 'Chinese'},
|
||||
}
|
||||
# ---------------------------------------------------
|
||||
# Image and file configuration
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<div class="f16"><i class="flag {{languages[locale].get('flag')}}"></i><b class="caret"></b>
|
||||
</div>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<ul class="dropdown-menu" id="language-picker">
|
||||
<li class="dropdown">
|
||||
{% for lang in languages %}
|
||||
{% if lang != locale %}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
{% block body %}
|
||||
<div
|
||||
id="app"
|
||||
class="dashboard container-fluid"
|
||||
data-bootstrap="{{ bootstrap_data }}"
|
||||
>
|
||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,33 @@
|
|||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
# Global caching for JSON language packs
|
||||
ALL_LANGUAGE_PACKS = {'en': {}}
|
||||
|
||||
DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def get_language_pack(locale):
|
||||
"""Get/cache a language pack
|
||||
|
||||
Returns the langugage pack from cache if it exists, caches otherwise
|
||||
|
||||
>>> get_language_pack('fr')['Dashboards']
|
||||
"Tableaux de bords"
|
||||
"""
|
||||
pack = ALL_LANGUAGE_PACKS.get(locale)
|
||||
if not pack:
|
||||
filename = DIR + '/{}/LC_MESSAGES/messages.json'.format(locale)
|
||||
with open(filename) as f:
|
||||
try:
|
||||
pack = json.load(f)
|
||||
ALL_LANGUAGE_PACKS[locale] = pack
|
||||
except Exception:
|
||||
# Assuming english, client side falls back on english
|
||||
pass
|
||||
return pack
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
@ -6,6 +6,7 @@ import traceback
|
|||
from flask import g, redirect, Response, flash, abort, get_flashed_messages
|
||||
from flask_babel import gettext as __
|
||||
from flask_babel import lazy_gettext as _
|
||||
from flask_babel import get_locale
|
||||
|
||||
from flask_appbuilder import BaseView
|
||||
from flask_appbuilder import ModelView
|
||||
|
@ -16,6 +17,7 @@ from flask_appbuilder.models.sqla.filters import BaseFilter
|
|||
from superset import appbuilder, conf, db, utils, sm, sql_parse
|
||||
from superset.connectors.connector_registry import ConnectorRegistry
|
||||
from superset.connectors.sqla.models import SqlaTable
|
||||
from superset.translations.utils import get_language_pack
|
||||
|
||||
FRONTEND_CONF_KEYS = ('SUPERSET_WEBSERVER_TIMEOUT',)
|
||||
|
||||
|
@ -191,9 +193,12 @@ class BaseSupersetView(BaseView):
|
|||
def common_bootsrap_payload(self):
|
||||
"""Common data always sent to the client"""
|
||||
messages = get_flashed_messages(with_categories=True)
|
||||
locale = str(get_locale())
|
||||
return {
|
||||
'flash_messages': messages,
|
||||
'conf': {k: conf.get(k) for k in FRONTEND_CONF_KEYS},
|
||||
'locale': locale,
|
||||
'language_pack': get_language_pack(locale),
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue