diff --git a/package-lock.json b/package-lock.json index 211a91ce8..33fbd62dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@vscode/l10n": "^0.0.18", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", + "ajv-i18n": "^4.2.0", "request-light": "^0.5.7", "vscode-json-languageservice": "4.1.8", "vscode-languageserver": "^9.0.0", @@ -1206,6 +1207,15 @@ } } }, + "node_modules/ajv-i18n": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ajv-i18n/-/ajv-i18n-4.2.0.tgz", + "integrity": "sha512-v/ei2UkCEeuKNXh8RToiFsUclmU+G57LO1Oo22OagNMENIw+Yb8eMwvHu7Vn9fmkjJyv6XclhJ8TbuigSglPkg==", + "license": "MIT", + "peerDependencies": { + "ajv": "^8.0.0-beta.0" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", diff --git a/package.json b/package.json index 8074c2062..a8e9b3005 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@vscode/l10n": "^0.0.18", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", + "ajv-i18n": "^4.2.0", "request-light": "^0.5.7", "vscode-json-languageservice": "4.1.8", "vscode-languageserver": "^9.0.0", diff --git a/src/languageservice/services/yamlSchemaService.ts b/src/languageservice/services/yamlSchemaService.ts index e15f8288f..5395c2909 100644 --- a/src/languageservice/services/yamlSchemaService.ts +++ b/src/languageservice/services/yamlSchemaService.ts @@ -25,10 +25,11 @@ import { SingleYAMLDocument } from '../parser/yamlParser07'; import { SchemaVersions } from '../yamlTypes'; import { getSchemaFromModeline } from './modelineUtil'; -import Ajv, { DefinedError, type AnySchemaObject, type ValidateFunction } from 'ajv'; +import Ajv, { DefinedError, type AnySchemaObject, type ErrorObject, type ValidateFunction } from 'ajv'; import Ajv4 from 'ajv-draft-04'; import Ajv2019 from 'ajv/dist/2019'; import Ajv2020 from 'ajv/dist/2020'; +import type { Localize } from 'ajv-i18n/localize/types'; import * as Json from 'jsonc-parser'; import { parse } from 'yaml'; import { CRD_CATALOG_URL, KUBERNETES_SCHEMA_URL } from '../utils/schemaUrls'; @@ -47,6 +48,8 @@ const jsonSchema07 = require('ajv/dist/refs/json-schema-draft-07.json'); const jsonSchema2019 = require('ajv/dist/refs/json-schema-2019-09/schema.json'); // eslint-disable-next-line @typescript-eslint/no-var-requires const jsonSchema2020 = require('ajv/dist/refs/json-schema-2020-12/schema.json'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const ajvLocalizers: Record = require('ajv-i18n'); const schema04Validator = ajv4.compile(jsonSchema04); const schema07Validator = ajv7.compile(jsonSchema07); @@ -56,6 +59,11 @@ const schema2020Validator = ajv2020.compile(jsonSchema2020); const schemaDialectCache = new Map(); const schemaDialectInFlight = new Map>(); +const AJV_LOCALE_ALIASES = new Map([ + ['zh-cn', 'zh'], + ['zh-tw', 'zh-TW'], +]); + // metadata/keywords that don't add constraints and thus don't count as $ref siblings const REF_SIBLING_NONCONSTRAINT_KEYS = new Set([ '$ref', @@ -264,6 +272,7 @@ export class YAMLSchemaService extends JSONSchemaService { * ---------------------------- */ const _loadSchema = this.loadSchema.bind(this); + const ajvErrorLocale = this.yamlSettings?.locale; async function _metaValidateSchemaNode(node: JSONSchema, hasNestedSchema: boolean): Promise { if (!node || typeof node !== 'object') return; const dialect = await pickSchemaDialect(node.$schema, _loadSchema); @@ -283,6 +292,7 @@ export class YAMLSchemaService extends JSONSchemaService { } if (!validator(toValidate)) { + localizeAjvErrors(validator.errors, ajvErrorLocale); const errs: string[] = []; for (const err of validator.errors as DefinedError[]) { errs.push(`${err.instancePath} : ${err.message}`); @@ -1433,6 +1443,20 @@ function knownDialectFromSchemaUri(schemaUri?: string): SchemaDialect { return undefined; } +function getAjvLocalizer(locale?: string): Localize | undefined { + if (!locale) return undefined; + + const lowerLocale = locale.trim().toLowerCase(); + const aliasedLocale = AJV_LOCALE_ALIASES.get(lowerLocale); + + return ajvLocalizers[locale] || ajvLocalizers[lowerLocale] || (aliasedLocale ? ajvLocalizers[aliasedLocale] : undefined); +} + +function localizeAjvErrors(errors: ErrorObject[] | null | undefined, locale?: string): void { + const localizer = getAjvLocalizer(locale) || ajvLocalizers.en; + localizer(errors); +} + async function pickSchemaDialect( $schema: string | undefined, loadSchema?: (uri: string) => Promise diff --git a/src/yamlServerInit.ts b/src/yamlServerInit.ts index a2c4d4548..876c582be 100644 --- a/src/yamlServerInit.ts +++ b/src/yamlServerInit.ts @@ -57,6 +57,7 @@ export class YAMLServerInit { // public for test setup async connectionInitialized(params: InitializeParams): Promise { + this.yamlSettings.locale = params.locale || 'en'; this.yamlSettings.capabilities = params.capabilities; this.languageService = getCustomLanguageService({ schemaRequestService: this.schemaRequestService, diff --git a/src/yamlSettings.ts b/src/yamlSettings.ts index fe524db01..b46075958 100644 --- a/src/yamlSettings.ts +++ b/src/yamlSettings.ts @@ -120,6 +120,7 @@ export class SettingsState { useSchemaSelectionRequests = false; hasWsChangeWatchedFileDynamicRegistration = false; fileExtensions: string[] = ['.yml', '.yaml']; + locale = 'en'; } export class TextDocumentTestManager extends TextDocuments {