mirror of
https://github.com/apache/superset.git
synced 2026-05-01 22:14:23 +00:00
Compare commits
1 Commits
fix/check-
...
eslit-no-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16b4ec347d |
@@ -403,6 +403,7 @@ module.exports = {
|
||||
'theme-colors/no-literal-colors': 'error',
|
||||
'icons/no-fa-icons-usage': 'error',
|
||||
'i18n-strings/no-template-vars': ['error', true],
|
||||
'i18n-strings/no-title-case': 'error',
|
||||
camelcase: [
|
||||
'error',
|
||||
{
|
||||
|
||||
@@ -41,7 +41,7 @@ module.exports = {
|
||||
context.report({
|
||||
node,
|
||||
message:
|
||||
"Don't use variables in translation string templates. Flask-babel is a static translation service, so it can’t handle strings that include variables",
|
||||
"Don't use variables in translation string templates. Flask-babel is a static translation service, so it can't handle strings that include variables",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -52,5 +52,134 @@ module.exports = {
|
||||
};
|
||||
},
|
||||
},
|
||||
'no-title-case': {
|
||||
create(context) {
|
||||
function checkTitleCase(str) {
|
||||
// Skip strings with placeholders like %s, %d, %(name)s, etc.
|
||||
if (/%[sdf]|%\([^)]+\)[sdf]/.test(str)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip strings that are all uppercase (likely acronyms)
|
||||
if (str === str.toUpperCase()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip strings with periods (likely multiple sentences)
|
||||
if (str.includes('.')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip single words
|
||||
const words = str.trim().split(/\s+/);
|
||||
if (words.length <= 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whitelist of words that are commonly capitalized in product names
|
||||
// but should not trigger title case warnings
|
||||
const productWords = [
|
||||
'Lab',
|
||||
'Server',
|
||||
'Studio',
|
||||
'Pro',
|
||||
'Plus',
|
||||
'Max',
|
||||
'Mini',
|
||||
];
|
||||
|
||||
// Common prepositions and articles that should be lowercase (unless at start)
|
||||
const lowercaseWords = [
|
||||
'a',
|
||||
'an',
|
||||
'the',
|
||||
'and',
|
||||
'or',
|
||||
'but',
|
||||
'for',
|
||||
'with',
|
||||
'to',
|
||||
'from',
|
||||
'in',
|
||||
'on',
|
||||
'at',
|
||||
'by',
|
||||
'of',
|
||||
];
|
||||
|
||||
// Check if the string uses title case (multiple words with first letter capitalized)
|
||||
const hasTitleCase = words.some((word, index) => {
|
||||
// Skip first word
|
||||
if (index === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip acronyms (all uppercase)
|
||||
if (word === word.toUpperCase()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip whitelisted product words when preceded by an uppercase word
|
||||
if (
|
||||
productWords.includes(word) &&
|
||||
index > 0 &&
|
||||
words[index - 1] === words[index - 1].toUpperCase()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if it's a lowercase word that's incorrectly capitalized
|
||||
if (
|
||||
lowercaseWords.includes(word.toLowerCase()) &&
|
||||
/^[A-Z]/.test(word)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For other words, check if they start with capital letter
|
||||
return (
|
||||
word.length > 1 &&
|
||||
/^[A-Z]/.test(word) &&
|
||||
!productWords.includes(word)
|
||||
);
|
||||
});
|
||||
|
||||
return hasTitleCase;
|
||||
}
|
||||
|
||||
function handler(node) {
|
||||
if (node.arguments.length) {
|
||||
const firstArg = node.arguments[0];
|
||||
let stringValue = null;
|
||||
|
||||
// Extract string value based on node type
|
||||
if (
|
||||
firstArg.type === 'Literal' &&
|
||||
typeof firstArg.value === 'string'
|
||||
) {
|
||||
stringValue = firstArg.value;
|
||||
} else if (
|
||||
firstArg.type === 'TemplateLiteral' &&
|
||||
firstArg.quasis.length === 1
|
||||
) {
|
||||
// Handle template literals without expressions
|
||||
stringValue = firstArg.quasis[0].value.raw;
|
||||
}
|
||||
|
||||
if (stringValue && checkTitleCase(stringValue)) {
|
||||
context.report({
|
||||
node: firstArg,
|
||||
message: `Avoid title case in i18n strings: "${stringValue}". Use sentence case instead.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"CallExpression[callee.name='t']": handler,
|
||||
"CallExpression[callee.name='tn']": handler,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
const { RuleTester } = require('eslint');
|
||||
const plugin = require('./index');
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parserOptions: {
|
||||
ecmaVersion: 6,
|
||||
},
|
||||
});
|
||||
|
||||
const rule = plugin.rules['no-title-case'];
|
||||
|
||||
ruleTester.run('no-title-case', rule, {
|
||||
valid: [
|
||||
// Sentence case (correct)
|
||||
{
|
||||
code: "t('Add a divider')",
|
||||
},
|
||||
{
|
||||
code: "t('Create new dashboard')",
|
||||
},
|
||||
{
|
||||
code: "t('Save and continue')",
|
||||
},
|
||||
// Single words
|
||||
{
|
||||
code: "t('Save')",
|
||||
},
|
||||
{
|
||||
code: "t('Delete')",
|
||||
},
|
||||
// All uppercase (acronyms)
|
||||
{
|
||||
code: "t('SQL')",
|
||||
},
|
||||
{
|
||||
code: "t('API KEY')",
|
||||
},
|
||||
// With placeholders
|
||||
{
|
||||
code: "t('Deleted: %s', name)",
|
||||
},
|
||||
{
|
||||
code: "t('User %(username)s added', { username })",
|
||||
},
|
||||
// Template literals without expressions
|
||||
{
|
||||
code: 't(`Add a new filter`)',
|
||||
},
|
||||
// Mixed case but not title case
|
||||
{
|
||||
code: "t('Use SQL Lab')",
|
||||
},
|
||||
// tn function
|
||||
{
|
||||
code: "tn('Add a filter', 'Add filters', count)",
|
||||
},
|
||||
// Multiple sentences with period
|
||||
{
|
||||
code: "t('Welcome Back. Please Login.')",
|
||||
},
|
||||
{
|
||||
code: "t('Save Changes. This Will Update All Records.')",
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
// Title case (incorrect)
|
||||
{
|
||||
code: "t('Add Divider')",
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'Avoid title case in i18n strings: "Add Divider". Use sentence case instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "t('Create New Dashboard')",
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'Avoid title case in i18n strings: "Create New Dashboard". Use sentence case instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "t('Save And Continue')",
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'Avoid title case in i18n strings: "Save And Continue". Use sentence case instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "t('Add Filter')",
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'Avoid title case in i18n strings: "Add Filter". Use sentence case instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "t('Edit User')",
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'Avoid title case in i18n strings: "Edit User". Use sentence case instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
// Template literals
|
||||
{
|
||||
code: 't(`Add Layer`)',
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'Avoid title case in i18n strings: "Add Layer". Use sentence case instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
// tn function
|
||||
{
|
||||
code: "tn('Delete Item', 'Delete Items', count)",
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'Avoid title case in i18n strings: "Delete Item". Use sentence case instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -30,10 +30,10 @@ export const DEFAULT_MAX_ROW_TABLE_SERVER = 500000;
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const TIME_FILTER_LABELS = {
|
||||
time_range: t('Time Range'),
|
||||
granularity_sqla: t('Time Column'),
|
||||
time_grain_sqla: t('Time Grain'),
|
||||
granularity: t('Time Granularity'),
|
||||
time_range: t('Time range'),
|
||||
granularity_sqla: t('Time column'),
|
||||
time_grain_sqla: t('Time grain'),
|
||||
granularity: t('Time granularity'),
|
||||
};
|
||||
|
||||
export const COLUMN_NAME_ALIASES: Record<string, string> = {
|
||||
|
||||
@@ -46,7 +46,7 @@ const ExploreResultsButton = ({
|
||||
tooltip={t('Explore the result set in the data exploration view')}
|
||||
data-test="explore-results-button"
|
||||
>
|
||||
{t('Create Chart')}
|
||||
{t('Create chart')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -51,7 +51,7 @@ export const KEY_MAP: Record<KeyboardShortcut, string | undefined> = {
|
||||
[KeyboardShortcut.CtrlE]: userOS !== 'MacOS' ? t('Stop query') : undefined,
|
||||
[KeyboardShortcut.CtrlQ]: userOS === 'Windows' ? t('New tab') : undefined,
|
||||
[KeyboardShortcut.CtrlT]: userOS !== 'Windows' ? t('New tab') : undefined,
|
||||
[KeyboardShortcut.CtrlP]: t('Previous Line'),
|
||||
[KeyboardShortcut.CtrlP]: t('Previous line'),
|
||||
[KeyboardShortcut.CtrlShiftF]: t('Format SQL'),
|
||||
[KeyboardShortcut.CtrlLeft]: t('Switch to the previous tab'),
|
||||
[KeyboardShortcut.CtrlRight]: t('Switch to the next tab'),
|
||||
|
||||
@@ -158,7 +158,7 @@ const updateDataset = async (
|
||||
return data.json.result;
|
||||
};
|
||||
|
||||
const UNTITLED = t('Untitled Dataset');
|
||||
const UNTITLED = t('Untitled dataset');
|
||||
|
||||
export const SaveDatasetModal = ({
|
||||
visible,
|
||||
@@ -374,10 +374,10 @@ export const SaveDatasetModal = ({
|
||||
return (
|
||||
<Modal
|
||||
show={visible}
|
||||
name={t('Save or Overwrite Dataset')}
|
||||
name={t('Save or overwrite dataset')}
|
||||
title={
|
||||
<ModalTitleWithIcon
|
||||
title={t('Save or Overwrite Dataset')}
|
||||
title={t('Save or overwrite dataset')}
|
||||
icon={<Icons.SaveOutlined />}
|
||||
data-test="save-or-overwrite-dataset-title"
|
||||
/>
|
||||
@@ -394,7 +394,7 @@ export const SaveDatasetModal = ({
|
||||
}
|
||||
/>
|
||||
<span style={{ marginLeft: '5px' }}>
|
||||
{t('Include Template Parameters')}
|
||||
{t('Include template parameters')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -637,7 +637,7 @@ function ExploreViewContainer(props) {
|
||||
}
|
||||
>
|
||||
<div className="title-container">
|
||||
<span className="horizontal-text">{t('Chart Source')}</span>
|
||||
<span className="horizontal-text">{t('Chart source')}</span>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -672,7 +672,7 @@ function ExploreViewContainer(props) {
|
||||
tabIndex={0}
|
||||
>
|
||||
<span role="button" tabIndex={0} className="action-button">
|
||||
<Tooltip title={t('Open Datasource tab')}>
|
||||
<Tooltip title={t('Open datasource tab')}>
|
||||
<Icons.VerticalAlignTopOutlined
|
||||
iconSize="xl"
|
||||
css={css`
|
||||
|
||||
@@ -370,7 +370,7 @@ class SaveModal extends Component<SaveModalProps, SaveModalState> {
|
||||
/>
|
||||
</FormItem>
|
||||
{this.props.datasource?.type === 'query' && (
|
||||
<FormItem label={t('Dataset Name')} required>
|
||||
<FormItem label={t('Dataset name')} required>
|
||||
<InfoTooltip
|
||||
tooltip={t('A reusable dataset will be saved with your chart.')}
|
||||
placement="right"
|
||||
|
||||
@@ -575,8 +575,8 @@ function ChartList(props: ChartListProps) {
|
||||
operator: FilterOperator.ChartIsFav,
|
||||
unfilteredLabel: t('Any'),
|
||||
selects: [
|
||||
{ label: t('Yes'), value: true },
|
||||
{ label: t('No'), value: false },
|
||||
{ label: t('yes'), value: true },
|
||||
{ label: t('no'), value: false },
|
||||
],
|
||||
}),
|
||||
[],
|
||||
|
||||
@@ -530,8 +530,8 @@ function DashboardList(props: DashboardListProps) {
|
||||
operator: FilterOperator.DashboardIsFav,
|
||||
unfilteredLabel: t('Any'),
|
||||
selects: [
|
||||
{ label: t('Yes'), value: true },
|
||||
{ label: t('No'), value: false },
|
||||
{ label: t('yes'), value: true },
|
||||
{ label: t('no'), value: false },
|
||||
],
|
||||
}),
|
||||
[],
|
||||
@@ -604,8 +604,8 @@ function DashboardList(props: DashboardListProps) {
|
||||
operator: FilterOperator.DashboardIsCertified,
|
||||
unfilteredLabel: t('Any'),
|
||||
selects: [
|
||||
{ label: t('Yes'), value: true },
|
||||
{ label: t('No'), value: false },
|
||||
{ label: t('yes'), value: true },
|
||||
{ label: t('no'), value: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -530,8 +530,8 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
||||
operator: FilterOperator.DatasetIsNullOrEmpty,
|
||||
unfilteredLabel: 'All',
|
||||
selects: [
|
||||
{ label: t('Virtual'), value: false },
|
||||
{ label: t('Physical'), value: true },
|
||||
{ label: t('virtual'), value: false },
|
||||
{ label: t('physical'), value: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -598,8 +598,8 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
||||
operator: FilterOperator.DatasetIsCertified,
|
||||
unfilteredLabel: t('Any'),
|
||||
selects: [
|
||||
{ label: t('Yes'), value: true },
|
||||
{ label: t('No'), value: false },
|
||||
{ label: t('yes'), value: true },
|
||||
{ label: t('no'), value: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -764,7 +764,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
||||
</p>
|
||||
{datasetCurrentlyDeleting.dashboards.count >= 1 && (
|
||||
<>
|
||||
<h4>{t('Affected Dashboards')}</h4>
|
||||
<h4>{t('Affected dashboards')}</h4>
|
||||
<List
|
||||
split={false}
|
||||
size="small"
|
||||
@@ -807,7 +807,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
||||
)}
|
||||
{datasetCurrentlyDeleting.charts.count >= 1 && (
|
||||
<>
|
||||
<h4>{t('Affected Charts')}</h4>
|
||||
<h4>{t('Affected charts')}</h4>
|
||||
<List
|
||||
split={false}
|
||||
size="small"
|
||||
@@ -860,7 +860,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
||||
}}
|
||||
onHide={closeDatasetDeleteModal}
|
||||
open
|
||||
title={t('Delete Dataset?')}
|
||||
title={t('Delete dataset?')}
|
||||
/>
|
||||
)}
|
||||
{datasetCurrentlyEditing && (
|
||||
@@ -931,7 +931,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
||||
);
|
||||
|
||||
if (!selected.length) {
|
||||
return t('0 Selected');
|
||||
return t('0 selected');
|
||||
}
|
||||
if (virtualCount && !physicalCount) {
|
||||
return t(
|
||||
|
||||
@@ -65,7 +65,7 @@ function RowLevelSecurityList(props: RLSProps) {
|
||||
toggleBulkSelect,
|
||||
} = useListViewResource<RLSObject>(
|
||||
'rowlevelsecurity',
|
||||
t('Row Level Security'),
|
||||
t('Row level security'),
|
||||
addDangerToast,
|
||||
true,
|
||||
undefined,
|
||||
@@ -130,13 +130,13 @@ function RowLevelSecurityList(props: RLSProps) {
|
||||
},
|
||||
{
|
||||
accessor: 'filter_type',
|
||||
Header: t('Filter Type'),
|
||||
Header: t('Filter type'),
|
||||
size: 'xl',
|
||||
id: 'filter_type',
|
||||
},
|
||||
{
|
||||
accessor: 'group_key',
|
||||
Header: t('Group Key'),
|
||||
Header: t('Group key'),
|
||||
size: 'xl',
|
||||
id: 'group_key',
|
||||
},
|
||||
@@ -246,7 +246,7 @@ function RowLevelSecurityList(props: RLSProps) {
|
||||
);
|
||||
|
||||
const emptyState = {
|
||||
title: t('No Rules yet'),
|
||||
title: t('No rules yet'),
|
||||
image: 'filter-results.svg',
|
||||
buttonAction: () => handleRuleEdit(null),
|
||||
buttonIcon: canEdit ? (
|
||||
@@ -265,7 +265,7 @@ function RowLevelSecurityList(props: RLSProps) {
|
||||
operator: FilterOperator.StartsWith,
|
||||
},
|
||||
{
|
||||
Header: t('Filter Type'),
|
||||
Header: t('Filter type'),
|
||||
key: 'filter_type',
|
||||
id: 'filter_type',
|
||||
input: 'select',
|
||||
@@ -277,7 +277,7 @@ function RowLevelSecurityList(props: RLSProps) {
|
||||
],
|
||||
},
|
||||
{
|
||||
Header: t('Group Key'),
|
||||
Header: t('Group key'),
|
||||
key: 'search',
|
||||
id: 'group_key',
|
||||
input: 'search',
|
||||
@@ -329,7 +329,7 @@ function RowLevelSecurityList(props: RLSProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SubMenu name={t('Row Level Security')} buttons={subMenuButtons} />
|
||||
<SubMenu name={t('Row level security')} buttons={subMenuButtons} />
|
||||
<ConfirmStatusChange
|
||||
title={t('Please confirm')}
|
||||
description={t('Are you sure you want to delete the selected rules?')}
|
||||
|
||||
@@ -519,7 +519,7 @@ function UsersList({ user }: UsersListProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SubMenu name={t('List Users')} buttons={subMenuButtons} />
|
||||
<SubMenu name={t('List users')} buttons={subMenuButtons} />
|
||||
<UserListAddModal
|
||||
onHide={() => closeModal(ModalType.ADD)}
|
||||
show={modalState.add}
|
||||
@@ -554,7 +554,7 @@ function UsersList({ user }: UsersListProps) {
|
||||
}}
|
||||
onHide={() => setUserCurrentlyDeleting(null)}
|
||||
open
|
||||
title={t('Delete User?')}
|
||||
title={t('Delete user?')}
|
||||
/>
|
||||
)}
|
||||
<ConfirmStatusChange
|
||||
|
||||
Reference in New Issue
Block a user