-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add XML validation and quickfix for hardcoded UI text
This is a validator that warns the user of hardcoded UI texts that could be externalized to a resource bundle. Includes a quick fix for the warning, where externalized strings from the project's i18n.properties file (if existent) may be suggested as replacements.
- Loading branch information
Showing
23 changed files
with
1,653 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
122 changes: 122 additions & 0 deletions
122
packages/language-server/src/resource-bundle-handling.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import { dirname } from "path"; | ||
import { maxBy, map, filter } from "lodash"; | ||
import { readFile } from "fs-extra"; | ||
import { URI } from "vscode-uri"; | ||
import globby from "globby"; | ||
import { FileChangeType } from "vscode-languageserver"; | ||
import { getLogger } from "./logger"; | ||
import * as propertiesParser from "properties-file/content"; | ||
import { Property } from "properties-file"; | ||
|
||
type AbsolutePath = string; | ||
type ResourceBundleData = Record<AbsolutePath, Property[]>; | ||
const resourceBundleData: ResourceBundleData = Object.create(null); | ||
|
||
export function isResourceBundleDoc(uri: string): boolean { | ||
return uri.endsWith("i18n.properties"); | ||
} | ||
|
||
export async function initializeResourceBundleData( | ||
workspaceFolderPath: string | ||
): Promise<void[]> { | ||
const resourceBundleDocuments = await findAllResourceBundleDocumentsInWorkspace( | ||
workspaceFolderPath | ||
); | ||
|
||
const readResourceBundlePromises = map( | ||
resourceBundleDocuments, | ||
async (resourceBundleDoc) => { | ||
const response = await readResourceBundleFile(resourceBundleDoc); | ||
|
||
// Parsing of i18n.properties failed because the file is invalid | ||
if (response !== "INVALID") { | ||
resourceBundleData[resourceBundleDoc] = response; | ||
} | ||
} | ||
); | ||
|
||
getLogger().info("resourceBundle data initialized", { | ||
resourceBundleDocuments, | ||
}); | ||
return Promise.all(readResourceBundlePromises); | ||
} | ||
|
||
export function getResourceBundleData(documentPath: string): Property[] { | ||
const resourceBundleFilesForCurrentFolder = filter( | ||
Object.keys(resourceBundleData), | ||
(resourceBundlePath) => | ||
documentPath.startsWith(dirname(resourceBundlePath.replace("/i18n", ""))) | ||
); | ||
|
||
const closestResourceBundlePath = maxBy( | ||
resourceBundleFilesForCurrentFolder, | ||
(resourceBundlePath) => resourceBundlePath.length | ||
); | ||
|
||
if (closestResourceBundlePath === undefined) { | ||
return []; | ||
} | ||
|
||
return resourceBundleData[closestResourceBundlePath]; | ||
} | ||
|
||
export async function updateResourceBundleData( | ||
resourceBundleUri: string, | ||
changeType: FileChangeType | ||
): Promise<void> { | ||
getLogger().debug("`updateResourceBundleData` function called", { | ||
resourceBundleUri, | ||
changeType, | ||
}); | ||
const resourceBundlePath = URI.parse(resourceBundleUri).fsPath; | ||
switch (changeType) { | ||
case 1: //created | ||
case 2: { | ||
//changed | ||
const response = await readResourceBundleFile(resourceBundleUri); | ||
// Parsing of i18n.properties failed because the file is invalid | ||
// We want to keep last successfully read state - i18n.properties file may be actively edited | ||
if (response !== "INVALID") { | ||
resourceBundleData[resourceBundlePath] = response; | ||
} | ||
return; | ||
} | ||
case 3: //deleted | ||
delete resourceBundleData[resourceBundlePath]; | ||
return; | ||
} | ||
} | ||
|
||
async function findAllResourceBundleDocumentsInWorkspace( | ||
workspaceFolderPath: string | ||
): Promise<string[]> { | ||
return globby(`${workspaceFolderPath}/**/i18n.properties`).catch((reason) => { | ||
getLogger().error( | ||
`Failed to find all i18n.properties files in current workspace!`, | ||
{ | ||
workspaceFolderPath, | ||
reason, | ||
} | ||
); | ||
return []; | ||
}); | ||
} | ||
|
||
async function readResourceBundleFile( | ||
resourceBundleUri: string | ||
): Promise<Property[] | "INVALID"> { | ||
const resourceBundleContent = await readFile( | ||
URI.parse(resourceBundleUri).fsPath, | ||
"utf-8" | ||
); | ||
|
||
let resourceBundleObject: Property[]; | ||
try { | ||
resourceBundleObject = propertiesParser.getProperties(resourceBundleContent) | ||
.collection; | ||
} catch (err) { | ||
return "INVALID"; | ||
} | ||
|
||
return resourceBundleObject; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 6 additions & 0 deletions
6
...anguage-server/test/snapshots/xml-view-diagnostics/use-of-hardcoded-string-i18n/input.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
<mvc:View xmlns:m="sap.m" | ||
xmlns:mvc="sap.ui.core.mvc"> | ||
<m:Page> | ||
<m:Button text=🢂"i18n_dummy_text"🢀/> | ||
</m:Page> | ||
</mvc:View> |
12 changes: 12 additions & 0 deletions
12
...test/snapshots/xml-view-diagnostics/use-of-hardcoded-string-i18n/output-lsp-response.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
[ | ||
{ | ||
"range": { | ||
"start": { "line": 3, "character": 19 }, | ||
"end": { "line": 3, "character": 36 } | ||
}, | ||
"severity": 2, | ||
"source": "UI5 Language Assistant", | ||
"message": "Consider externalizing UI texts to a resource bundle or other model: \"i18n_dummy_text\".", | ||
"code": 1013 | ||
} | ||
] |
Oops, something went wrong.