Skip to content

Commit

Permalink
Add eslint rule for localization issues
Browse files Browse the repository at this point in the history
  • Loading branch information
msujew committed Feb 17, 2022
1 parent 40227dc commit 518fed8
Show file tree
Hide file tree
Showing 36 changed files with 272 additions and 102 deletions.
1 change: 1 addition & 0 deletions configs/errors.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
}
}
],
"@theia/localization-check": "error",
"@theia/no-src-import": "error",
"@theia/runtime-import-check": "error",
"@theia/shared-dependencies": "error",
Expand Down
6 changes: 6 additions & 0 deletions dev-packages/private-eslint-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ The plugin helps identify problems during development through static analysis in

## Rules

### `localization-check`:

The rule prevents the following localization related issues:
- incorrect usage of the `nls.localizeByDefault` function by using an incorrect default value.
- unnecessary call to `nls.localize` which could be replaced by `nls.localizeByDefault`.

### `no-src-import`:

The rule prevents imports using `/src/` rather than `/lib/` as it causes build failures.
Expand Down
1 change: 1 addition & 0 deletions dev-packages/private-eslint-plugin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

/** @type {{[ruleId: string]: import('eslint').Rule.RuleModule}} */
exports.rules = {
"localization-check": require('./rules/localization-check'),
"no-src-import": require('./rules/no-src-import'),
"runtime-import-check": require('./rules/runtime-import-check'),
"shared-dependencies": require('./rules/shared-dependencies')
Expand Down
3 changes: 2 additions & 1 deletion dev-packages/private-eslint-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"dependencies": {
"@theia/core": "1.22.1",
"@theia/ext-scripts": "1.22.1",
"@theia/re-exports": "1.22.1"
"@theia/re-exports": "1.22.1",
"js-levenshtein": "^1.1.6"
}
}
109 changes: 109 additions & 0 deletions dev-packages/private-eslint-plugin/rules/localization-check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// @ts-check
/********************************************************************************
* Copyright (C) 2021 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

const levenshtein = require('js-levenshtein');

const metadata = require('@theia/core/src/common/i18n/nls.metadata.json');
const messages = new Set(Object.values(metadata.messages)
.reduceRight((prev, curr) => prev.concat(curr), [])
.map(e => e.replace(/&&/g, '')));

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
fixable: 'code',
docs: {
description: 'prevent incorrect use of \'nls.localize\'.',
},
},
create(context) {
return {
CallExpression(node) {
const callee = node.callee;
if (callee.type === 'Super') {
return;
}
const { value, byDefault, node: localizeNode } = evaluateLocalize(node);
if (value !== undefined) {
if (byDefault && !messages.has(value)) {
let lowestDistance = Number.MAX_VALUE;
let lowestMessage = '';
for (const message of messages) {
const distance = levenshtein(value, message);
if (distance < lowestDistance) {
lowestDistance = distance;
lowestMessage = message;
}
}
if (lowestMessage) {
context.report({
node: localizeNode,
message: `'${value}' is not a valid default value. Did you mean '${lowestMessage}'?`,
fix: function (fixer) {
const updatedCall = `'${lowestMessage.replace(/'/g, "\\'")}'`;
return fixer.replaceText(localizeNode, updatedCall);
}
});
} else {
context.report({
node: localizeNode,
message: `'${value}' is not a valid default value.`
});
}
} else if (!byDefault && messages.has(value)) {
context.report({
node,
message: `'${value}' can be translated using the 'nls.localizeByDefault' function.`,
fix: function (fixer) {
const code = context.getSourceCode();
const args = node.arguments.slice(1);
const argsCode = args.map(e => code.getText(e)).join(', ');
const updatedCall = `nls.localizeByDefault(${argsCode})`;
return fixer.replaceText(node, updatedCall);
}
});
}
}
}
};
function evaluateLocalize(/** @type {import('estree').CallExpression} */ node) {
const callee = node.callee;
if ('object' in callee && 'name' in callee.object && 'property' in callee && 'name' in callee.property && callee.object.name === 'nls') {
if (callee.property.name === 'localize') {
const defaultTextNode = node.arguments[1]; // The default text node is the second argument for `nls.localize`
if (defaultTextNode && defaultTextNode.type === 'Literal' && typeof defaultTextNode.value === 'string') {
return {
value: defaultTextNode.value,
byDefault: false
};
}
} else if (callee.property.name === 'localizeByDefault') {
const defaultTextNode = node.arguments[0]; // The default text node is the first argument for ``nls.localizeByDefault`
if (defaultTextNode && defaultTextNode.type === 'Literal' && typeof defaultTextNode.value === 'string') {
return {
node: defaultTextNode,
value: defaultTextNode.value,
byDefault: true
};
}
}
}
return {};
}
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export class BulkEditTreeWidget extends TreeWidget {
if (CompositeTreeNode.is(model.root) && model.root.children.length > 0) {
return super.renderTree(model);
}
return <div className='theia-widget-noInfo noEdits'>{nls.localizeByDefault('No edits have been detected in the workspace so far.')}</div>;
return <div className='theia-widget-noInfo noEdits'>{nls.localizeByDefault('Made no edits')}</div>;
}

protected renderCaption(node: TreeNode, props: NodeProps): React.ReactNode {
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/browser/core-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,19 @@ export const corePreferenceSchema: PreferenceSchema = {
default: 300,
minimum: 0,
maximum: 2000,
description: nls.localizeByDefault('Controls the hover feedback delay in milliseconds of the dragging area in between views/editors.')
// nls-todo: Will be available with VSCode API 1.55
description: nls.localize('theia/core/sashDelay', 'Controls the hover feedback delay in milliseconds of the dragging area in between views/editors.')
},
'workbench.sash.size': {
type: 'number',
default: 4,
minimum: 1,
maximum: 20,
description: nls.localizeByDefault('Controls the feedback area size in pixels of the dragging area in between views/editors. Set it to a larger value if needed.')
// nls-todo: Will be available with VSCode API 1.55
description: nls.localize(
'theia/core/sashSize',
'Controls the feedback area size in pixels of the dragging area in between views/editors. Set it to a larger value if needed.'
)
},
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export class BrowserKeyboardFrontendContribution implements CommandContribution
protected async chooseLayout(): Promise<KeyboardLayoutData | undefined> {
const current = this.layoutProvider.currentLayoutData;
const autodetect: QuickPickValue<'autodetect'> = {
label: nls.localizeByDefault('Auto-detect'),
label: nls.localizeByDefault('Auto Detect'),
description: this.layoutProvider.currentLayoutSource !== 'user-choice' ? nls.localize('theia/core/keyboard/current', '(current: {0})', current.name) : undefined,
detail: nls.localize('theia/core/keyboard/tryDetect', 'Try to detect the keyboard layout from browser information and pressed keys.'),
value: 'autodetect'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme
protected async handleRequiredRestart(): Promise<void> {
const msgNode = document.createElement('div');
const message = document.createElement('p');
message.textContent = nls.localizeByDefault('A setting has changed that requires a restart to take effect');
message.textContent = nls.localizeByDefault('A setting has changed that requires a restart to take effect.');
const detail = document.createElement('p');
detail.textContent = nls.localizeByDefault(
'Press the restart button to restart {0} and enable the setting.', FrontendApplicationConfigProvider.get().applicationName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const electronWindowPreferencesSchema: PreferenceSchema = {
'maximum': ZoomLevel.MAX,
'scope': 'application',
// eslint-disable-next-line max-len
'description': nls.localizeByDefault('Adjust the zoom level of the window. The original size is 0 and each increment above (e.g. 1.0) or below (e.g. -1.0) represents zooming 20% larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity.')
'description': nls.localizeByDefault('Adjust the zoom level of the window. The original size is 0 and each increment above (e.g. 1) or below (e.g. -1) represents zooming 20% larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity.')
},
'window.titleBarStyle': {
type: 'string',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

/* eslint-disable @theia/localization-check */

import * as fs from '@theia/core/shared/fs-extra';
import * as path from 'path';
import { DebugAdapterExecutable, DebugAdapterContribution } from '../debug-model';
Expand Down
2 changes: 1 addition & 1 deletion packages/editor/src/browser/editor-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ export class EditorCommandContribution implements CommandContribution {
return;
}
if (editor.document.dirty && isReopenWithEncoding) {
this.messageService.info(nls.localize('theia/editor/reopenDirty', 'The file is dirty. Please save it first before reopening it with another encoding.'));
this.messageService.info(nls.localizeByDefault('The file is dirty. Please save it first before reopening it with another encoding.'));
return;
} else if (selectedFileEncoding.value) {
editor.setEncoding(selectedFileEncoding.value.id, isReopenWithEncoding ? EncodingMode.Decode : EncodingMode.Encode);
Expand Down
18 changes: 8 additions & 10 deletions packages/editor/src/browser/editor-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,7 @@ const codeEditorPreferenceProperties = {
'default': false
},
'editor.highlightActiveIndentGuide': {
'description': nls.localize('theia/editor/highlightActiveIndentGuide', 'Controls whether the editor should highlight the active indent guide.'),
'description': nls.localizeByDefault('Controls whether the editor should highlight the active indent guide.'),
'type': 'boolean',
'default': true
},
Expand Down Expand Up @@ -743,7 +743,7 @@ const codeEditorPreferenceProperties = {
'description': nls.localizeByDefault('Controls the display of line numbers.')
},
'editor.lineNumbersMinChars': {
'description': nls.localize('theia/editor/lineNumbersMinChars', 'Controls the line height. Use 0 to compute the line height from the font size.'),
'description': nls.localizeByDefault('Controls the line height. Use 0 to compute the line height from the font size.'),
'type': 'integer',
'default': 5,
'minimum': 1,
Expand Down Expand Up @@ -900,15 +900,13 @@ const codeEditorPreferenceProperties = {
'editor.peekWidgetDefaultFocus': {
'enumDescriptions': [
nls.localizeByDefault('Focus the tree when opening peek'),
nls.localizeByDefault('Focus the editor when opening peek'),
nls.localizeByDefault('Focus the webview when opening peek')
nls.localizeByDefault('Focus the editor when opening peek')
],
'description': nls.localizeByDefault('Controls whether to focus the inline editor or the tree in the peek widget.'),
'type': 'string',
'enum': [
'tree',
'editor',
'webview'
'editor'
],
'default': 'tree'
},
Expand Down Expand Up @@ -1399,17 +1397,17 @@ const codeEditorPreferenceProperties = {
'default': 'off'
},
'editor.tabIndex': {
'markdownDescription': nls.localize('theia/editor/tabIndex', 'Controls the wrapping column of the editor when `#editor.wordWrap#` is `wordWrapColumn` or `bounded`.'),
'markdownDescription': nls.localizeByDefault('Controls the wrapping column of the editor when `#editor.wordWrap#` is `wordWrapColumn` or `bounded`.'),
'type': 'integer',
'default': 0,
'minimum': -1,
'maximum': 1073741824
},
'editor.unusualLineTerminators': {
'markdownEnumDescriptions': [
nls.localize('unusualLineTerminators.auto', 'Unusual line terminators are automatically removed.'),
nls.localize('unusualLineTerminators.off', 'Unusual line terminators are ignored.'),
nls.localize('unusualLineTerminators.prompt', 'Unusual line terminators prompt to be removed.')
nls.localizeByDefault('Unusual line terminators are automatically removed.'),
nls.localizeByDefault('Unusual line terminators are ignored.'),
nls.localizeByDefault('Unusual line terminators prompt to be removed.')
],
'description': nls.localizeByDefault('Remove unusual line terminators that might cause problems.'),
'type': 'string',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export async function getExternalTerminalSchema(externalTerminalService: Externa
},
'terminal.external.osxExec': {
type: 'string',
description: nls.localizeByDefault('Customizes which terminal to run on macOS.'),
description: nls.localizeByDefault('Customizes which terminal application to run on macOS.'),
default: `${isOSX ? hostExec : 'Terminal.app'}`
},
'terminal.external.linuxExec': {
Expand Down
6 changes: 3 additions & 3 deletions packages/file-search/src/browser/quick-file-open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { OpenerService, KeybindingRegistry, QuickAccessRegistry, QuickAccessProv
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import URI from '@theia/core/lib/common/uri';
import { FileSearchService, WHITESPACE_QUERY_SEPARATOR } from '../common/file-search-service';
import { CancellationToken, Command, MAX_SAFE_INTEGER } from '@theia/core/lib/common';
import { CancellationToken, Command, nls, MAX_SAFE_INTEGER } from '@theia/core/lib/common';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { NavigationLocationService } from '@theia/editor/lib/browser/navigation/navigation-location-service';
import * as fuzzy from '@theia/core/shared/fuzzy';
Expand Down Expand Up @@ -328,10 +328,10 @@ export class QuickFileOpenService implements QuickAccessProvider {
}

private getPlaceHolder(): string {
let placeholder = 'File name to search (append : to go to line).';
let placeholder = nls.localizeByDefault('Search files by name (append {0} to go to line or {1} to go to symbol)', ':', '@');
const keybinding = this.getKeyCommand();
if (keybinding) {
placeholder += ` (Press ${keybinding} to show/hide ignored files)`;
placeholder += nls.localize('theia/file-search/toggleIgnoredFiles', ' (Press {0} to show/hide ignored files)', keybinding);
}
return placeholder;
}
Expand Down
6 changes: 3 additions & 3 deletions packages/filesystem/src/browser/file-upload-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,9 @@ export class FileUploadService {

protected async confirmOverwrite(fileUri: URI): Promise<boolean> {
const dialog = new ConfirmDialog({
title: nls.localizeByDefault('Replace file'),
msg: nls.localizeByDefault('File "{0}" already exists in the destination folder. Do you want to replace it?', fileUri.path.base),
ok: nls.localizeByDefault('Replace file'),
title: nls.localizeByDefault('Replace'),
msg: nls.localizeByDefault("A file or folder with the name '{0}' already exists in the destination folder. Do you want to replace it?", fileUri.path.base),
ok: nls.localizeByDefault('Replace'),
cancel: Dialog.CANCEL
});
return !!await dialog.open();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ export class GettingStartedWidget extends ReactWidget {
<h3 className='gs-section-header'>
<i className={codicon('history')}></i>{nls.localizeByDefault('Recent')}
</h3>
{items.length > 0 ? content : <p className='gs-no-recent'>{nls.localizeByDefault('No Recent Workspaces')}</p>}
{items.length > 0 ? content : <p className='gs-no-recent'>{nls.localizeByDefault('No recent folders')}</p>}
{more}
</div>;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/git/src/browser/git-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,7 @@ export class GitContribution implements CommandContribution, MenuContribution, T
} catch (e) {
scmRepository.input.issue = {
type: ScmInputIssueType.Warning,
message: nls.localizeByDefault('Make sure you configure your \'user.name\' and \'user.email\' in git.')
message: nls.localize('theia/git/missingUserInfo', 'Make sure you configure your \'user.name\' and \'user.email\' in git.')
};
}

Expand Down
Loading

0 comments on commit 518fed8

Please sign in to comment.