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 Nov 22, 2021
1 parent 39fe372 commit 7cdf340
Show file tree
Hide file tree
Showing 27 changed files with 192 additions and 38 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/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/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/eslint-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"version": "1.19.0",
"description": "Custom ESLint rules for developing Theia extensions and applications",
"dependencies": {
"@theia/core": "1.19.0"
"@theia/core": "1.19.0",
"js-levenshtein": "^1.1.6"
}
}
109 changes: 109 additions & 0 deletions dev-packages/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) {
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 secondArgument = node.arguments[1];
if (secondArgument && secondArgument.type === 'Literal' && typeof secondArgument.value === 'string') {
return {
value: secondArgument.value,
byDefault: false
};
}
} else if (callee.property.name === 'localizeByDefault') {
const firstArgument = node.arguments[0];
if (firstArgument && firstArgument.type === 'Literal' && typeof firstArgument.value === 'string') {
return {
node: firstArgument,
value: firstArgument.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
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 @@ -228,7 +228,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
12 changes: 6 additions & 6 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 @@ -1397,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/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
1 change: 1 addition & 0 deletions packages/git/src/common/git-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export namespace GitFileStatus {
case GitFileStatus.New: return !!staged ? nls.localize('theia/git/added', 'Added') : nls.localize('theia/git/unstaged', 'Unstaged');
case GitFileStatus.Renamed: return nls.localize('theia/git/renamed', 'Renamed');
case GitFileStatus.Copied: return nls.localize('theia/git/copied', 'Copied');
// eslint-disable-next-line @theia/localization-check
case GitFileStatus.Modified: return nls.localize('vscode.git/repository/modified', 'Modified');
case GitFileStatus.Deleted: return nls.localize('vscode.git/repository/deleted', 'Deleted');
case GitFileStatus.Conflicted: return nls.localize('theia/git/conflicted', 'Conflicted');
Expand Down
2 changes: 1 addition & 1 deletion packages/outline-view/src/browser/outline-view-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ export class OutlineViewWidget extends TreeWidget {

protected renderTree(model: TreeModel): React.ReactNode {
if (CompositeTreeNode.is(this.model.root) && !this.model.root.children.length) {
return <div className='theia-widget-noInfo no-outline'>{nls.localizeByDefault('No outline information available.')}</div>;
return <div className='theia-widget-noInfo no-outline'>{nls.localizeByDefault('The active editor cannot provide outline information.')}</div>;
}
return super.renderTree(model);
}
Expand Down
2 changes: 2 additions & 0 deletions packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts
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 { inject, injectable } from '@theia/core/shared/inversify';
import {
AutoClosingPair,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,12 @@ export class ResourcePropertyViewTreeWidget extends TreeWidget implements Proper
this.propertiesTree.set('info', infoNode);

infoNode.children.push(this.createResultLineNode('isDirectory', nls.localize('theia/property-view/directory', 'Directory'), fileStatObject.isDirectory, infoNode));
infoNode.children.push(this.createResultLineNode('isFile', nls.localize('theia/property-view/file', 'File'), fileStatObject.isFile, infoNode));
infoNode.children.push(this.createResultLineNode('isFile', nls.localizeByDefault('File'), fileStatObject.isFile, infoNode));
infoNode.children.push(this.createResultLineNode('isSymbolicLink', nls.localize('theia/property-view/symbolicLink', 'Symbolic link'),
fileStatObject.isSymbolicLink, infoNode));
infoNode.children.push(this.createResultLineNode('location', nls.localize('theia/property-view/location', 'Location'),
this.getLocationString(fileStatObject), infoNode));
infoNode.children.push(this.createResultLineNode('name', nls.localize('theia/property-view/name', 'Name'), this.getFileName(fileStatObject), infoNode));
infoNode.children.push(this.createResultLineNode('name', nls.localizeByDefault('Name'), this.getFileName(fileStatObject), infoNode));
infoNode.children.push(this.createResultLineNode('path', nls.localize('theia/property-view/path', 'Path'), this.getFilePath(fileStatObject), infoNode));
infoNode.children.push(this.createResultLineNode('lastModification', nls.localize('theia/property-view/lastModified', 'Last modified'),
this.getLastModificationString(fileStatObject), infoNode));
Expand Down Expand Up @@ -134,7 +134,7 @@ export class ResourcePropertyViewTreeWidget extends TreeWidget implements Proper
}

protected getSizeString(fileStat: FileStat): string {
return fileStat.size ? nls.localizeByDefault('{0} B', fileStat.size.toString()) : '';
return fileStat.size ? nls.localizeByDefault('{0}B', fileStat.size.toString()) : '';
}

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ export class ScmHistoryWidget extends ScmNavigableListWidget<ScmHistoryListNode>
} else {
this.status = {
state: 'error',
errorMessage: <React.Fragment>{nls.localizeByDefault('There is no repository selected in this workspace.')}</React.Fragment>
errorMessage: <React.Fragment>{nls.localizeByDefault('No source control providers registered.')}</React.Fragment>
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
protected readonly toDisposeOnActiveEditorChanged = new DisposableCollection();

// The default root name to add external search results in the case that a workspace is opened.
protected readonly defaultRootName = nls.localize('theia/searchResultsView/searchFolderMatch.other.label', 'Other files');
protected readonly defaultRootName = nls.localizeByDefault('Other files');
protected forceVisibleRootNode = false;

protected appliedDecorations = new Map<string, string[]>();
Expand Down
Loading

0 comments on commit 7cdf340

Please sign in to comment.