From 5c5968d2d770cf618a1ce177cfe45f593fb18eb9 Mon Sep 17 00:00:00 2001 From: fausto-m <55789814+fausto-m@users.noreply.github.com> Date: Tue, 6 Sep 2022 11:30:01 +0400 Subject: [PATCH] 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. --- packages/language-server/src/commands.ts | 16 + packages/language-server/src/quick-fix.ts | 85 +- .../src/resource-bundle-handling.ts | 122 +++ packages/language-server/src/server.ts | 24 +- packages/language-server/src/swa.ts | 1 + .../src/xml-view-diagnostics.ts | 5 + .../use-of-hardcoded-string-i18n/input.xml | 6 + .../output-lsp-response.json | 12 + packages/user-facing-text/api.d.ts | 2 + packages/user-facing-text/src/commands.ts | 4 + packages/user-facing-text/src/validations.ts | 4 + .../vscode-ui5-language-assistant/README.md | 2 + .../src/extension.ts | 1 + packages/xml-views-quick-fix/api.d.ts | 8 + packages/xml-views-quick-fix/src/api.ts | 1 + .../src/quick-fix-hardcoded-i18n-string.ts | 90 ++ .../quick-fix-hardcoded-i18n-string-spec.ts | 118 +++ packages/xml-views-validation/api.d.ts | 8 +- packages/xml-views-validation/src/api.ts | 2 + .../src/utils/ui5-user-facing-attributes.ts | 976 ++++++++++++++++++ .../use-of-hardcoded-i18n-string.ts | 66 ++ .../src/validators/index.ts | 3 + .../use-of-hardcoded-string-i18n-spec.ts | 101 ++ 23 files changed, 1653 insertions(+), 4 deletions(-) create mode 100644 packages/language-server/src/resource-bundle-handling.ts create mode 100644 packages/language-server/test/snapshots/xml-view-diagnostics/use-of-hardcoded-string-i18n/input.xml create mode 100644 packages/language-server/test/snapshots/xml-view-diagnostics/use-of-hardcoded-string-i18n/output-lsp-response.json create mode 100644 packages/xml-views-quick-fix/src/quick-fix-hardcoded-i18n-string.ts create mode 100644 packages/xml-views-quick-fix/test/quick-fix-hardcoded-i18n-string-spec.ts create mode 100644 packages/xml-views-validation/src/utils/ui5-user-facing-attributes.ts create mode 100644 packages/xml-views-validation/src/validators/attributes/use-of-hardcoded-i18n-string.ts create mode 100644 packages/xml-views-validation/test/validators/attributes/use-of-hardcoded-string-i18n-spec.ts diff --git a/packages/language-server/src/commands.ts b/packages/language-server/src/commands.ts index 666bf7520..1c9c24ae1 100644 --- a/packages/language-server/src/commands.ts +++ b/packages/language-server/src/commands.ts @@ -3,6 +3,7 @@ import { commands } from "@ui5-language-assistant/user-facing-text"; import { executeQuickFixStableIdCommand, executeQuickFixFileStableIdCommand, + executeQuickFixHardcodedI18nStringCommand, } from "./quick-fix"; import { track } from "./swa"; @@ -44,6 +45,21 @@ export function executeCommand( track("MANIFEST_STABLE_ID", "multiple"); return; } + case commands.QUICK_FIX_HARDCODED_I18N_STRING_ERROR.name: { + const change = executeQuickFixHardcodedI18nStringCommand({ + // Assumption that this command has the following arguments. + // We passed them when the command was created. + documentUri: params.arguments[0], + documentVersion: params.arguments[1], + quickFixReplaceRange: params.arguments[2], + quickFixNewText: params.arguments[3], + }); + connection.workspace.applyEdit({ + documentChanges: change, + }); + track("MANIFEST_HARDCODED_I18N_STRING", "single"); + return; + } default: return undefined; } diff --git a/packages/language-server/src/quick-fix.ts b/packages/language-server/src/quick-fix.ts index 9944293f0..b5730a2c2 100644 --- a/packages/language-server/src/quick-fix.ts +++ b/packages/language-server/src/quick-fix.ts @@ -18,11 +18,13 @@ import { } from "@ui5-language-assistant/xml-views-validation"; import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; import { computeQuickFixStableIdInfo } from "@ui5-language-assistant/xml-views-quick-fix"; +import { computeQuickFixHardcodedI18nStringInfo } from "@ui5-language-assistant/xml-views-quick-fix"; import { validations, commands, } from "@ui5-language-assistant/user-facing-text"; import { LSPRangeToOffsetRange, offsetRangeToLSPRange } from "./range-utils"; +import { Property } from "properties-file"; type QuickFixStableIdLSPInfo = { newText: string; @@ -32,7 +34,8 @@ type QuickFixStableIdLSPInfo = { export function diagnosticToCodeActionFix( document: TextDocument, diagnostics: Diagnostic[], - ui5Model: UI5SemanticModel + ui5Model: UI5SemanticModel, + resourceBundle: Property[] ): CodeAction[] { const documentText = document.getText(); // We prefer to parse the document again to avoid cache state handling @@ -49,6 +52,16 @@ export function diagnosticToCodeActionFix( ui5Model, }); } + case validations.HARDCODED_I18N_STRING.code: { + // hardcoded i18n string + return computeCodeActionsForQuickFixHardcodedI18nString({ + document, + xmlDocument: xmlDocAst, + hardcodedI18nStringDiagnostic: diagnostic, + ui5Model, + resourceBundle, + }); + } default: return []; } @@ -191,3 +204,73 @@ export function executeQuickFixFileStableIdCommand(opts: { return documentEdit; } + +function computeCodeActionsForQuickFixHardcodedI18nString(opts: { + document: TextDocument; + xmlDocument: XMLDocument; + hardcodedI18nStringDiagnostic: Diagnostic; + ui5Model: UI5SemanticModel; + resourceBundle: Property[]; +}): CodeAction[] { + const codeActions: CodeAction[] = []; + + const errorOffset = LSPRangeToOffsetRange( + opts.hardcodedI18nStringDiagnostic.range, + opts.document + ); + + const quickFixHardcodedI18nStringInfo = computeQuickFixHardcodedI18nStringInfo( + opts.xmlDocument, + [errorOffset], + opts.resourceBundle + ); + + const replaceRange = offsetRangeToLSPRange( + quickFixHardcodedI18nStringInfo[0].replaceRange, + opts.document + ); + + quickFixHardcodedI18nStringInfo[0].newTextSuggestions.forEach( + (suggestion) => { + const codeActionTitle = + commands.QUICK_FIX_HARDCODED_I18N_STRING_ERROR.title + + ": " + + suggestion.suggestionValue + + " (" + + suggestion.suggestionKey + + ")"; + codeActions.push( + CodeAction.create( + codeActionTitle, + Command.create( + commands.QUICK_FIX_HARDCODED_I18N_STRING_ERROR.title, + commands.QUICK_FIX_HARDCODED_I18N_STRING_ERROR.name, + opts.document.uri, + opts.document.version, + replaceRange, + suggestion.newText + ), + CodeActionKind.QuickFix + ) + ); + } + ); + + return codeActions; +} + +export function executeQuickFixHardcodedI18nStringCommand(opts: { + documentUri: string; + documentVersion: number; + quickFixReplaceRange: LSPRange; + quickFixNewText: string; +}): TextDocumentEdit[] { + const documentEdit = [ + TextDocumentEdit.create( + { uri: opts.documentUri, version: opts.documentVersion }, + [TextEdit.replace(opts.quickFixReplaceRange, `${opts.quickFixNewText}`)] + ), + ]; + + return documentEdit; +} diff --git a/packages/language-server/src/resource-bundle-handling.ts b/packages/language-server/src/resource-bundle-handling.ts new file mode 100644 index 000000000..31a10d67e --- /dev/null +++ b/packages/language-server/src/resource-bundle-handling.ts @@ -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; +const resourceBundleData: ResourceBundleData = Object.create(null); + +export function isResourceBundleDoc(uri: string): boolean { + return uri.endsWith("i18n.properties"); +} + +export async function initializeResourceBundleData( + workspaceFolderPath: string +): Promise { + 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 { + 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 { + 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 { + 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; +} diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 76a0deb72..39ed2ba59 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -34,6 +34,12 @@ import { initializeManifestData, updateManifestData, } from "./manifest-handling"; +import { + getResourceBundleData, + initializeResourceBundleData, + isResourceBundleDoc, + updateResourceBundleData, +} from "./resource-bundle-handling"; import { getUI5FrameworkForXMLFile, isUI5YamlDoc, @@ -50,6 +56,7 @@ const connection = createConnection(ProposedFeatures.all); const documents = new TextDocuments(TextDocument); let manifestStateInitialized: Promise | undefined = undefined; let ui5yamlStateInitialized: Promise | undefined = undefined; +let resourceBundletStateInitialized: Promise | undefined = undefined; let initializationOptions: ServerInitializationOptions | undefined; let hasConfigurationCapability = false; @@ -66,6 +73,9 @@ connection.onInitialize((params: InitializeParams) => { const workspaceFolderAbsPath = URI.parse(workspaceFolderUri).fsPath; manifestStateInitialized = initializeManifestData(workspaceFolderAbsPath); ui5yamlStateInitialized = initializeUI5YamlData(workspaceFolderAbsPath); + resourceBundletStateInitialized = initializeResourceBundleData( + workspaceFolderAbsPath + ); } // Does the client support the `workspace/configuration` request? @@ -92,6 +102,7 @@ connection.onInitialize((params: InitializeParams) => { commands: [ commands.QUICK_FIX_STABLE_ID_ERROR.name, commands.QUICK_FIX_STABLE_ID_FILE_ERRORS.name, + commands.QUICK_FIX_HARDCODED_I18N_STRING_ERROR.name, ], }, }, @@ -198,6 +209,8 @@ connection.onDidChangeWatchedFiles(async (changeEvent) => { await updateManifestData(uri, change.type); } else if (isUI5YamlDoc(uri)) { await updateUI5YamlData(uri, change.type); + } else if (isResourceBundleDoc(uri)) { + await updateResourceBundleData(uri, change.type); } }); }); @@ -207,12 +220,17 @@ documents.onDidChangeContent(async (changeEvent) => { if ( manifestStateInitialized === undefined || ui5yamlStateInitialized === undefined || + resourceBundletStateInitialized === undefined || !isXMLView(changeEvent.document.uri) ) { return; } - await Promise.all([manifestStateInitialized, ui5yamlStateInitialized]); + await Promise.all([ + manifestStateInitialized, + ui5yamlStateInitialized, + resourceBundletStateInitialized, + ]); const documentUri = changeEvent.document.uri; const document = documents.get(documentUri); if (document !== undefined) { @@ -263,11 +281,13 @@ connection.onCodeAction(async (params) => { version: ui5Model.version, }); + const resourceBundle = getResourceBundleData(documentPath); const diagnostics = params.context.diagnostics; const codeActions = diagnosticToCodeActionFix( textDocument, diagnostics, - ui5Model + ui5Model, + resourceBundle ); getLogger().trace("`computed codeActions", { codeActions }); return codeActions; diff --git a/packages/language-server/src/swa.ts b/packages/language-server/src/swa.ts index a03821502..2f3b4a03f 100644 --- a/packages/language-server/src/swa.ts +++ b/packages/language-server/src/swa.ts @@ -44,6 +44,7 @@ export function initSwa( export const TRACK_EVENTS = { MANIFEST_STABLE_ID: "manifest stable ID fix", XML_UI5_DOC_HOVER: "XML UI5 Doc Hover", + MANIFEST_HARDCODED_I18N_STRING: "hardcoded i18n string fix", }; Object.freeze(TRACK_EVENTS); diff --git a/packages/language-server/src/xml-view-diagnostics.ts b/packages/language-server/src/xml-view-diagnostics.ts index 52dd8660b..86844a134 100644 --- a/packages/language-server/src/xml-view-diagnostics.ts +++ b/packages/language-server/src/xml-view-diagnostics.ts @@ -73,6 +73,11 @@ function validationIssuesToLspDiagnostics( ...commonDiagnosticPros, code: validations.NON_STABLE_ID.code, }; + case "UseOfHardcodedI18nString": + return { + ...commonDiagnosticPros, + code: validations.HARDCODED_I18N_STRING.code, + }; case "UseOfDeprecatedClass": case "UseOfDeprecatedProperty": case "UseOfDeprecatedEvent": diff --git a/packages/language-server/test/snapshots/xml-view-diagnostics/use-of-hardcoded-string-i18n/input.xml b/packages/language-server/test/snapshots/xml-view-diagnostics/use-of-hardcoded-string-i18n/input.xml new file mode 100644 index 000000000..d8276857e --- /dev/null +++ b/packages/language-server/test/snapshots/xml-view-diagnostics/use-of-hardcoded-string-i18n/input.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/packages/language-server/test/snapshots/xml-view-diagnostics/use-of-hardcoded-string-i18n/output-lsp-response.json b/packages/language-server/test/snapshots/xml-view-diagnostics/use-of-hardcoded-string-i18n/output-lsp-response.json new file mode 100644 index 000000000..b7475e5b3 --- /dev/null +++ b/packages/language-server/test/snapshots/xml-view-diagnostics/use-of-hardcoded-string-i18n/output-lsp-response.json @@ -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 + } +] diff --git a/packages/user-facing-text/api.d.ts b/packages/user-facing-text/api.d.ts index 729c8ba99..cc2bb4917 100644 --- a/packages/user-facing-text/api.d.ts +++ b/packages/user-facing-text/api.d.ts @@ -30,11 +30,13 @@ type Validations = { UNKNOWN_TAG_NAME_IN_NS: validationText; UNKNOWN_TAG_NAME_IN_NS_UNDER_CLASS: validationText; UNKNOWN_TAG_NAME_NO_NS: validationText; + HARDCODED_I18N_STRING: validationText; }; type Commands = { QUICK_FIX_STABLE_ID_ERROR: commandText; QUICK_FIX_STABLE_ID_FILE_ERRORS: commandText; + QUICK_FIX_HARDCODED_I18N_STRING_ERROR: commandText; }; /** diff --git a/packages/user-facing-text/src/commands.ts b/packages/user-facing-text/src/commands.ts index aa8f24f5d..35c9647be 100644 --- a/packages/user-facing-text/src/commands.ts +++ b/packages/user-facing-text/src/commands.ts @@ -13,4 +13,8 @@ export const commands: Commands = { name: "ui5_lang.quick_fix_file_stable_id", title: "Generate IDs for the entire file", }, + QUICK_FIX_HARDCODED_I18N_STRING_ERROR: { + name: "ui5_lang.quick_fix_hardcoded_i18n_string", + title: "Replace with externalized string", + }, }; diff --git a/packages/user-facing-text/src/validations.ts b/packages/user-facing-text/src/validations.ts index fbb5b46d1..3214f7ed5 100644 --- a/packages/user-facing-text/src/validations.ts +++ b/packages/user-facing-text/src/validations.ts @@ -61,4 +61,8 @@ export const validations: Validations = { msg: `The "{0}" class can't have an empty ID attribute when flexEnabled is "true".`, code: 1012, }, + HARDCODED_I18N_STRING: { + msg: `Consider externalizing UI texts to a resource bundle or other model: "{0}".`, + code: 1013, + }, }; diff --git a/packages/vscode-ui5-language-assistant/README.md b/packages/vscode-ui5-language-assistant/README.md index ede258d6b..7b976f33a 100644 --- a/packages/vscode-ui5-language-assistant/README.md +++ b/packages/vscode-ui5-language-assistant/README.md @@ -78,6 +78,7 @@ and cannot be configured by the end user. - Use of deprecated properties - Use of deprecated events - Use of deprecated associations + - Use of hardcoded UI texts that could be externalized to a resource bundle (i18n.properties) or other model ### XML View Quick Fix @@ -90,6 +91,7 @@ Quick Fix will be shown for some validations when hovering over a diagnostic or - Missing or empty ID when `flexEnabled` is true (stableID). - Will add a generated ID. - Supports both fixing a single missing ID or all missing IDs in an entire file. +- Suggestions to replace a hardcoded UI text by a reference from one or more matches found in resource bundle (i18n.properties). ### XML View Hover Tooltips diff --git a/packages/vscode-ui5-language-assistant/src/extension.ts b/packages/vscode-ui5-language-assistant/src/extension.ts index 52b85cfb8..74debc4ea 100644 --- a/packages/vscode-ui5-language-assistant/src/extension.ts +++ b/packages/vscode-ui5-language-assistant/src/extension.ts @@ -82,6 +82,7 @@ function createLanguageClient(context: ExtensionContext): LanguageClient { fileEvents: [ workspace.createFileSystemWatcher("**/manifest.json"), workspace.createFileSystemWatcher("**/ui5.yaml"), + workspace.createFileSystemWatcher("**/i18n.properties"), ], }, outputChannelName: meta.displayName, diff --git a/packages/xml-views-quick-fix/api.d.ts b/packages/xml-views-quick-fix/api.d.ts index 125c6bdda..16dea4735 100644 --- a/packages/xml-views-quick-fix/api.d.ts +++ b/packages/xml-views-quick-fix/api.d.ts @@ -1,8 +1,16 @@ import { XMLDocument } from "@xml-tools/ast"; import { OffsetRange } from "@ui5-language-assistant/logic-utils"; import { QuickFixStableIdInfo } from "./src/quick-fix-stable-id"; +import { QuickFixHardcodedI18nStringInfo } from "./src/quick-fix-hardcoded-i18n-string"; +import { Property } from "properties-file"; export declare function computeQuickFixStableIdInfo( xmlDoc: XMLDocument, errorOffset: OffsetRange[] ): QuickFixStableIdInfo[]; + +export declare function computeQuickFixHardcodedI18nStringInfo( + xmlDoc: XMLDocument, + errorOffset: OffsetRange[], + resourceBundle: Property[] +): QuickFixHardcodedI18nStringInfo[]; diff --git a/packages/xml-views-quick-fix/src/api.ts b/packages/xml-views-quick-fix/src/api.ts index 7823cb62d..30bf19966 100644 --- a/packages/xml-views-quick-fix/src/api.ts +++ b/packages/xml-views-quick-fix/src/api.ts @@ -1 +1,2 @@ export { computeQuickFixStableIdInfo } from "./quick-fix-stable-id"; +export { computeQuickFixHardcodedI18nStringInfo } from "./quick-fix-hardcoded-i18n-string"; diff --git a/packages/xml-views-quick-fix/src/quick-fix-hardcoded-i18n-string.ts b/packages/xml-views-quick-fix/src/quick-fix-hardcoded-i18n-string.ts new file mode 100644 index 000000000..07bdb901e --- /dev/null +++ b/packages/xml-views-quick-fix/src/quick-fix-hardcoded-i18n-string.ts @@ -0,0 +1,90 @@ +import { compact, map } from "lodash"; +import { astPositionAtOffset } from "@xml-tools/ast-position"; +import { XMLDocument } from "@xml-tools/ast"; +import { OffsetRange } from "@ui5-language-assistant/logic-utils"; +import { Property } from "properties-file"; + +export type QuickFixHardcodedI18nStringInfo = { + newTextSuggestions: QuickFixHardcodedI18nSuggestion[]; + replaceRange: OffsetRange; +}; + +export type QuickFixHardcodedI18nSuggestion = { + suggestionKey: string; + suggestionValue: string; + newText: string; +}; + +export function computeQuickFixHardcodedI18nStringInfo( + xmlDoc: XMLDocument, + errorOffset: OffsetRange[], + resourceBundle: Property[] +): QuickFixHardcodedI18nStringInfo[] { + const quickFixHardcodedI18nStringInfo = compact( + map(errorOffset, (_) => { + const astNode = astPositionAtOffset(xmlDoc, _.start); + if (astNode?.kind !== "XMLAttributeValue") { + return undefined; + } + + const xmlAttribute = astNode.astNode; + if ( + xmlAttribute.key === null || + xmlAttribute.key === undefined || + xmlAttribute.value === null || + xmlAttribute.value === undefined + ) { + return undefined; + } + + const newTextSuggestions: QuickFixHardcodedI18nSuggestion[] = []; + + //If there are keys to iterate from the resource bundle, check for quick fixes + if (resourceBundle.length > 0) { + // Text value for xmlAttribute.value without spaces, tabs, new lines + const escapedXmlAttributeValue = xmlAttribute.value + .trim() + .replace(/[\n\t]/g, "") + .replace(/\s+(?=\s)/g, ""); + // Possible i18n key replacements to suggest (only 100% matches are returned) + const i18nReplacementSuggestions = resourceBundle.filter((property) => { + return property.escapedValue === escapedXmlAttributeValue; + }); + + // If i18n key replacements are found, suggest them as possible fixes + i18nReplacementSuggestions.forEach((property) => { + if (xmlAttribute.key) { + const newTextSuggestion = computeQuickFixI18nSuggestion( + xmlAttribute.key, + property.escapedValue, + property.escapedKey + ); + newTextSuggestions.push(newTextSuggestion); + } + }); + } + + const replaceRange = { + start: xmlAttribute.position.startOffset, + end: xmlAttribute.position.endOffset, + }; + + return { newTextSuggestions, replaceRange }; + }) + ); + + return quickFixHardcodedI18nStringInfo; +} + +function computeQuickFixI18nSuggestion( + attributeKey: string, + i18nReplacementSuggestionValue: string, + i18nReplacementSuggestionKey: string +): QuickFixHardcodedI18nSuggestion { + const newTextSuggestion = { + suggestionKey: i18nReplacementSuggestionKey, + suggestionValue: i18nReplacementSuggestionValue, + newText: `${attributeKey}="{i18n>${i18nReplacementSuggestionValue}}"`, + }; + return newTextSuggestion; +} diff --git a/packages/xml-views-quick-fix/test/quick-fix-hardcoded-i18n-string-spec.ts b/packages/xml-views-quick-fix/test/quick-fix-hardcoded-i18n-string-spec.ts new file mode 100644 index 000000000..71c847d7e --- /dev/null +++ b/packages/xml-views-quick-fix/test/quick-fix-hardcoded-i18n-string-spec.ts @@ -0,0 +1,118 @@ +import { expect } from "chai"; +import { TextDocument } from "vscode-languageserver"; +import { parse, DocumentCstNode } from "@xml-tools/parser"; +import { buildAst, XMLDocument } from "@xml-tools/ast"; +import { expectExists } from "@ui5-language-assistant/test-utils"; +import { computeQuickFixHardcodedI18nStringInfo } from "../src/quick-fix-hardcoded-i18n-string"; +import * as propertiesParser from "properties-file/content"; + +describe("the UI5 language assistant QuickFix Service", () => { + context("true positive scenarios", () => { + it("will get quick fix info when i18n-able attribute key is hardcoded", () => { + const testXmlSnippet = ` + +