diff --git a/docs/zh-CN/game/config-system.md b/docs/zh-CN/game/config-system.md index a133ff63..2f25fcb4 100644 --- a/docs/zh-CN/game/config-system.md +++ b/docs/zh-CN/game/config-system.md @@ -200,6 +200,7 @@ var hotReload = loader.EnableHotReload( - 浏览 `config/` 目录 - 打开 raw YAML 文件 - 打开匹配的 schema 文件 +- 根据 VS Code 当前界面语言在英文和简体中文之间切换主要工具界面文本 - 对嵌套对象中的必填字段、未知字段、基础标量类型、标量数组和对象数组元素做轻量校验 - 对嵌套对象字段、对象数组、顶层标量字段和顶层标量数组提供轻量表单入口 - 对同一配置域内的多份 YAML 文件执行批量字段更新 diff --git a/tools/gframework-config-tool/README.md b/tools/gframework-config-tool/README.md index 8a7c9672..9a316fee 100644 --- a/tools/gframework-config-tool/README.md +++ b/tools/gframework-config-tool/README.md @@ -7,6 +7,7 @@ VS Code extension for the GFramework AI-First config workflow. - Browse config files from the workspace `config/` directory - Open raw YAML files - Open matching schema files from `schemas/` +- Localize extension UI text in English and Simplified Chinese, including the form preview, prompts, and notifications - Run lightweight schema validation for nested required fields, unknown nested fields, scalar types, scalar arrays, and arrays of objects - Open a lightweight form preview for nested object fields, object arrays, top-level scalar fields, and scalar arrays diff --git a/tools/gframework-config-tool/package.json b/tools/gframework-config-tool/package.json index cbc751f3..688c9601 100644 --- a/tools/gframework-config-tool/package.json +++ b/tools/gframework-config-tool/package.json @@ -1,8 +1,8 @@ { "name": "gframework-config-tool", - "displayName": "GFramework Config Tool", - "description": "VS Code tooling for browsing, validating, and editing AI-First config files in GFramework projects.", - "version": "0.0.1", + "displayName": "%extension.displayName%", + "description": "%extension.description%", + "version": "0.0.2", "publisher": "GeWuYou", "license": "Apache-2.0", "repository": { @@ -54,34 +54,34 @@ "explorer": [ { "id": "gframeworkConfigExplorer", - "name": "GFramework Config" + "name": "%view.gframeworkConfig.name%" } ] }, "commands": [ { "command": "gframeworkConfig.refresh", - "title": "GFramework Config: Refresh" + "title": "%command.refresh.title%" }, { "command": "gframeworkConfig.openRaw", - "title": "GFramework Config: Open Raw File" + "title": "%command.openRaw.title%" }, { "command": "gframeworkConfig.openSchema", - "title": "GFramework Config: Open Schema" + "title": "%command.openSchema.title%" }, { "command": "gframeworkConfig.openFormPreview", - "title": "GFramework Config: Open Form Preview" + "title": "%command.openFormPreview.title%" }, { "command": "gframeworkConfig.batchEditDomain", - "title": "GFramework Config: Batch Edit Domain" + "title": "%command.batchEditDomain.title%" }, { "command": "gframeworkConfig.validateAll", - "title": "GFramework Config: Validate All" + "title": "%command.validateAll.title%" } ], "menus": { @@ -121,17 +121,17 @@ ] }, "configuration": { - "title": "GFramework Config", + "title": "%configuration.title%", "properties": { "gframeworkConfig.configPath": { "type": "string", "default": "config", - "description": "Relative path from the workspace root to the config directory." + "description": "%configuration.configPath.description%" }, "gframeworkConfig.schemasPath": { "type": "string", "default": "schemas", - "description": "Relative path from the workspace root to the schema directory." + "description": "%configuration.schemasPath.description%" } } } diff --git a/tools/gframework-config-tool/package.nls.json b/tools/gframework-config-tool/package.nls.json new file mode 100644 index 00000000..a7acaa0f --- /dev/null +++ b/tools/gframework-config-tool/package.nls.json @@ -0,0 +1,14 @@ +{ + "extension.displayName": "GFramework Config Tool", + "extension.description": "VS Code tooling for browsing, validating, and editing AI-First config files in GFramework projects.", + "view.gframeworkConfig.name": "GFramework Config", + "command.refresh.title": "GFramework Config: Refresh", + "command.openRaw.title": "GFramework Config: Open Raw File", + "command.openSchema.title": "GFramework Config: Open Schema", + "command.openFormPreview.title": "GFramework Config: Open Form Preview", + "command.batchEditDomain.title": "GFramework Config: Batch Edit Domain", + "command.validateAll.title": "GFramework Config: Validate All", + "configuration.title": "GFramework Config", + "configuration.configPath.description": "Relative path from the workspace root to the config directory.", + "configuration.schemasPath.description": "Relative path from the workspace root to the schema directory." +} diff --git a/tools/gframework-config-tool/package.nls.zh-cn.json b/tools/gframework-config-tool/package.nls.zh-cn.json new file mode 100644 index 00000000..bd067a59 --- /dev/null +++ b/tools/gframework-config-tool/package.nls.zh-cn.json @@ -0,0 +1,14 @@ +{ + "extension.displayName": "GFramework 配置工具", + "extension.description": "为 GFramework 项目中的 AI-First 配置文件提供浏览、校验和编辑能力的 VS Code 扩展。", + "view.gframeworkConfig.name": "GFramework 配置", + "command.refresh.title": "GFramework 配置:刷新", + "command.openRaw.title": "GFramework 配置:打开原始文件", + "command.openSchema.title": "GFramework 配置:打开 Schema", + "command.openFormPreview.title": "GFramework 配置:打开表单预览", + "command.batchEditDomain.title": "GFramework 配置:批量编辑配置域", + "command.validateAll.title": "GFramework 配置:校验全部", + "configuration.title": "GFramework 配置", + "configuration.configPath.description": "从工作区根目录到配置目录的相对路径。", + "configuration.schemasPath.description": "从工作区根目录到 Schema 目录的相对路径。" +} diff --git a/tools/gframework-config-tool/src/configValidation.js b/tools/gframework-config-tool/src/configValidation.js index 301e3d67..17ee2816 100644 --- a/tools/gframework-config-tool/src/configValidation.js +++ b/tools/gframework-config-tool/src/configValidation.js @@ -100,11 +100,12 @@ function parseTopLevelYaml(text) { * * @param {{type: "object", required: string[], properties: Record}} schemaInfo Parsed schema. * @param {YamlNode} parsedYaml Parsed YAML tree. + * @param {{isChinese?: boolean} | undefined} localizer Optional runtime localizer. * @returns {Array<{severity: "error" | "warning", message: string}>} Validation diagnostics. */ -function validateParsedConfig(schemaInfo, parsedYaml) { +function validateParsedConfig(schemaInfo, parsedYaml, localizer) { const diagnostics = []; - validateNode(schemaInfo, parsedYaml, "", diagnostics); + validateNode(schemaInfo, parsedYaml, "", diagnostics, localizer); return diagnostics; } @@ -353,10 +354,11 @@ function parseSchemaNode(rawNode, displayPath) { * @param {YamlNode} yamlNode YAML node. * @param {string} displayPath Current logical path. * @param {Array<{severity: "error" | "warning", message: string}>} diagnostics Diagnostic sink. + * @param {{isChinese?: boolean} | undefined} localizer Optional runtime localizer. */ -function validateNode(schemaNode, yamlNode, displayPath, diagnostics) { +function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer) { if (schemaNode.type === "object") { - validateObjectNode(schemaNode, yamlNode, displayPath, diagnostics); + validateObjectNode(schemaNode, yamlNode, displayPath, diagnostics, localizer); return; } @@ -364,13 +366,15 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics) { if (!yamlNode || yamlNode.kind !== "array") { diagnostics.push({ severity: "error", - message: `Property '${displayPath}' is expected to be an array.` + message: localizeValidationMessage("expectedArray", localizer, { + displayPath + }) }); return; } for (let index = 0; index < yamlNode.items.length; index += 1) { - validateNode(schemaNode.items, yamlNode.items[index], `${displayPath}[${index}]`, diagnostics); + validateNode(schemaNode.items, yamlNode.items[index], `${displayPath}[${index}]`, diagnostics, localizer); } return; } @@ -378,7 +382,11 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics) { if (!yamlNode || yamlNode.kind !== "scalar") { diagnostics.push({ severity: "error", - message: `Property '${displayPath}' is expected to be '${schemaNode.type}', but the current YAML shape is '${yamlNode ? yamlNode.kind : "missing"}'.` + message: localizeValidationMessage("expectedScalarShape", localizer, { + displayPath, + schemaType: schemaNode.type, + yamlKind: yamlNode ? yamlNode.kind : "missing" + }) }); return; } @@ -386,7 +394,10 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics) { if (!isScalarCompatible(schemaNode.type, yamlNode.value)) { diagnostics.push({ severity: "error", - message: `Property '${displayPath}' is expected to be '${schemaNode.type}', but the current scalar value is incompatible.` + message: localizeValidationMessage("expectedScalarValue", localizer, { + displayPath, + schemaType: schemaNode.type + }) }); return; } @@ -396,7 +407,10 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics) { !schemaNode.enumValues.includes(unquoteScalar(yamlNode.value))) { diagnostics.push({ severity: "error", - message: `Property '${displayPath}' must be one of: ${schemaNode.enumValues.join(", ")}.` + message: localizeValidationMessage("enumMismatch", localizer, { + displayPath, + values: schemaNode.enumValues.join(", ") + }) }); } } @@ -408,13 +422,17 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics) { * @param {YamlNode} yamlNode YAML node. * @param {string} displayPath Current logical path. * @param {Array<{severity: "error" | "warning", message: string}>} diagnostics Diagnostic sink. + * @param {{isChinese?: boolean} | undefined} localizer Optional runtime localizer. */ -function validateObjectNode(schemaNode, yamlNode, displayPath, diagnostics) { +function validateObjectNode(schemaNode, yamlNode, displayPath, diagnostics, localizer) { if (!yamlNode || yamlNode.kind !== "object") { const subject = displayPath.length === 0 ? "Root object" : `Property '${displayPath}'`; diagnostics.push({ severity: "error", - message: `${subject} is expected to be an object.` + message: localizeValidationMessage("expectedObject", localizer, { + subject, + displayPath + }) }); return; } @@ -423,7 +441,9 @@ function validateObjectNode(schemaNode, yamlNode, displayPath, diagnostics) { if (!yamlNode.map.has(requiredProperty)) { diagnostics.push({ severity: "error", - message: `Required property '${combinePath(displayPath, requiredProperty)}' is missing.` + message: localizeValidationMessage("missingRequired", localizer, { + displayPath: combinePath(displayPath, requiredProperty) + }) }); } } @@ -432,7 +452,9 @@ function validateObjectNode(schemaNode, yamlNode, displayPath, diagnostics) { if (!Object.prototype.hasOwnProperty.call(schemaNode.properties, entry.key)) { diagnostics.push({ severity: "error", - message: `Property '${combinePath(displayPath, entry.key)}' is not declared in the matching schema.` + message: localizeValidationMessage("unknownProperty", localizer, { + displayPath: combinePath(displayPath, entry.key) + }) }); continue; } @@ -441,7 +463,60 @@ function validateObjectNode(schemaNode, yamlNode, displayPath, diagnostics) { schemaNode.properties[entry.key], entry.node, combinePath(displayPath, entry.key), - diagnostics); + diagnostics, + localizer); + } +} + +/** + * Format one validation message in either English or Simplified Chinese. + * + * @param {"expectedArray" | "expectedScalarShape" | "expectedScalarValue" | "enumMismatch" | "expectedObject" | "missingRequired" | "unknownProperty"} key Message key. + * @param {{isChinese?: boolean} | undefined} localizer Optional runtime localizer. + * @param {Record} params Message parameters. + * @returns {string} Localized validation message. + */ +function localizeValidationMessage(key, localizer, params) { + if (localizer && localizer.isChinese) { + switch (key) { + case "expectedArray": + return `属性“${params.displayPath}”应为数组。`; + case "expectedScalarShape": + return `属性“${params.displayPath}”应为“${params.schemaType}”,但当前 YAML 结构是“${params.yamlKind}”。`; + case "expectedScalarValue": + return `属性“${params.displayPath}”应为“${params.schemaType}”,但当前标量值不兼容。`; + case "enumMismatch": + return `属性“${params.displayPath}”必须是以下值之一:${params.values}。`; + case "expectedObject": + return params.displayPath && params.displayPath.length > 0 + ? `属性“${params.displayPath}”应为对象。` + : "根对象应为对象。"; + case "missingRequired": + return `缺少必填属性“${params.displayPath}”。`; + case "unknownProperty": + return `属性“${params.displayPath}”未在匹配的 schema 中声明。`; + default: + return key; + } + } + + switch (key) { + case "expectedArray": + return `Property '${params.displayPath}' is expected to be an array.`; + case "expectedScalarShape": + return `Property '${params.displayPath}' is expected to be '${params.schemaType}', but the current YAML shape is '${params.yamlKind}'.`; + case "expectedScalarValue": + return `Property '${params.displayPath}' is expected to be '${params.schemaType}', but the current scalar value is incompatible.`; + case "enumMismatch": + return `Property '${params.displayPath}' must be one of: ${params.values}.`; + case "expectedObject": + return `${params.subject} is expected to be an object.`; + case "missingRequired": + return `Required property '${params.displayPath}' is missing.`; + case "unknownProperty": + return `Property '${params.displayPath}' is not declared in the matching schema.`; + default: + return key; } } diff --git a/tools/gframework-config-tool/src/extension.js b/tools/gframework-config-tool/src/extension.js index 22fceb82..62eb2b7f 100644 --- a/tools/gframework-config-tool/src/extension.js +++ b/tools/gframework-config-tool/src/extension.js @@ -10,6 +10,9 @@ const { unquoteScalar, validateParsedConfig } = require("./configValidation"); +const {createLocalizer} = require("./localization"); + +const localizer = createLocalizer(vscode.env.language); /** * Activate the GFramework config extension. @@ -132,11 +135,11 @@ class ConfigTreeDataProvider { if (!configRoot || !fs.existsSync(configRoot.fsPath)) { return [ new ConfigTreeItem( - "No config directory", + localizer.t("tree.noConfigDirectory.label"), "info", vscode.TreeItemCollapsibleState.None, undefined, - "Set gframeworkConfig.configPath or create the directory.") + localizer.t("tree.noConfigDirectory.description")) ]; } @@ -171,8 +174,8 @@ class ConfigTreeDataProvider { const fileUri = vscode.Uri.joinPath(domainUri, entry.name); const schemaUri = getSchemaUriForConfigFile(fileUri, workspaceRoot); const description = schemaUri && fs.existsSync(schemaUri.fsPath) - ? "schema" - : "schema missing"; + ? localizer.t("tree.fileDescription.schema") + : localizer.t("tree.fileDescription.schemaMissing"); const item = new ConfigTreeItem( entry.name, "file", @@ -183,7 +186,7 @@ class ConfigTreeDataProvider { item.contextValue = "gframeworkConfigFile"; item.command = { command: "gframeworkConfig.openRaw", - title: "Open Raw", + title: localizer.t("command.openRaw.title"), arguments: [item] }; @@ -243,7 +246,7 @@ async function openSchemaFile(item) { const schemaUri = getSchemaUriForConfigFile(configUri, workspaceRoot); if (!schemaUri || !fs.existsSync(schemaUri.fsPath)) { - void vscode.window.showWarningMessage("Matching schema file was not found."); + void vscode.window.showWarningMessage(localizer.t("message.schemaNotFound")); return; } @@ -273,7 +276,7 @@ async function openFormPreview(item, diagnostics) { const panel = vscode.window.createWebviewPanel( "gframeworkConfigFormPreview", - `Config Form: ${path.basename(configUri.fsPath)}`, + localizer.t("webview.panelTitle", {fileName: path.basename(configUri.fsPath)}), vscode.ViewColumn.Beside, {enableScripts: true}); @@ -294,7 +297,7 @@ async function openFormPreview(item, diagnostics) { const document = await vscode.workspace.openTextDocument(configUri); await document.save(); await validateConfigFile(configUri, diagnostics); - void vscode.window.showInformationMessage("Config file saved from form preview."); + void vscode.window.showInformationMessage(localizer.t("message.formSaved")); } if (message.type === "openRaw") { @@ -353,13 +356,13 @@ async function validateConfigFile(configUri, diagnostics) { if (!schemaInfo.exists) { fileDiagnostics.push(new vscode.Diagnostic( new vscode.Range(0, 0, 0, 1), - `Matching schema file not found: ${schemaInfo.schemaPath}`, + localizer.t("diagnostic.schemaMissing", {schemaPath: schemaInfo.schemaPath}), vscode.DiagnosticSeverity.Warning)); diagnostics.set(configUri, fileDiagnostics); return; } - for (const diagnostic of validateParsedConfig(schemaInfo, parsedYaml)) { + for (const diagnostic of validateParsedConfig(schemaInfo, parsedYaml, localizer)) { fileDiagnostics.push(new vscode.Diagnostic( new vscode.Range(0, 0, 0, 1), diagnostic.message, @@ -403,14 +406,14 @@ async function openBatchEdit(item, diagnostics, provider) { }); if (fileItems.length === 0) { - void vscode.window.showWarningMessage("No YAML config files were found in the selected domain."); + void vscode.window.showWarningMessage(localizer.t("message.noYamlFilesInDomain")); return; } const selectedFiles = await vscode.window.showQuickPick(fileItems, { canPickMany: true, - title: `Batch Edit: ${path.basename(domainUri.fsPath)}`, - placeHolder: "Select the config files to update." + title: localizer.t("quickPick.batchEdit.title", {domain: path.basename(domainUri.fsPath)}), + placeHolder: localizer.t("quickPick.batchEdit.placeholder") }); if (!selectedFiles || selectedFiles.length === 0) { return; @@ -418,14 +421,13 @@ async function openBatchEdit(item, diagnostics, provider) { const schemaInfo = await loadSchemaInfoForConfig(selectedFiles[0].fileUri, workspaceRoot); if (!schemaInfo.exists) { - void vscode.window.showWarningMessage("Batch edit requires a matching schema file for the selected domain."); + void vscode.window.showWarningMessage(localizer.t("message.batchEditNeedsSchema")); return; } const editableFields = getEditableSchemaFields(schemaInfo); if (editableFields.length === 0) { - void vscode.window.showWarningMessage( - "No top-level scalar or scalar-array fields were found in the matching schema."); + void vscode.window.showWarningMessage(localizer.t("message.batchEditNoEditableFields")); return; } @@ -433,19 +435,19 @@ async function openBatchEdit(item, diagnostics, provider) { editableFields.map((field) => ({ label: field.title || field.key, description: field.inputKind === "array" - ? `array<${field.itemType}>` + ? localizer.t("detail.arrayType", {itemType: field.itemType}) : field.type, detail: [ - field.required ? "required" : "", + field.required ? localizer.t("detail.required") : "", field.description || "", - field.refTable ? `ref: ${field.refTable}` : "" + field.refTable ? localizer.t("detail.refTable", {refTable: field.refTable}) : "" ].filter((part) => part.length > 0).join(" · ") || undefined, field })), { canPickMany: true, - title: `Batch Edit Fields: ${path.basename(domainUri.fsPath)}`, - placeHolder: "Select the fields to apply across the chosen files." + title: localizer.t("quickPick.batchEditFields.title", {domain: path.basename(domainUri.fsPath)}), + placeHolder: localizer.t("quickPick.batchEditFields.placeholder") }); if (!selectedFields || selectedFields.length === 0) { return; @@ -492,13 +494,15 @@ async function openBatchEdit(item, diagnostics, provider) { } if (changedFileCount === 0) { - void vscode.window.showInformationMessage("Batch edit did not change any selected config files."); + void vscode.window.showInformationMessage(localizer.t("message.batchEditNoChanges")); return; } const applied = await vscode.workspace.applyEdit(edit); if (!applied) { - throw new Error("VS Code rejected the batch edit workspace update."); + throw new Error(localizer.isChinese + ? "VS Code 拒绝了这次批量编辑工作区更新。" + : "VS Code rejected the batch edit workspace update."); } for (const document of touchedDocuments) { @@ -507,8 +511,10 @@ async function openBatchEdit(item, diagnostics, provider) { } provider.refresh(); - void vscode.window.showInformationMessage( - `Batch updated ${changedFileCount} config file(s) in '${path.basename(domainUri.fsPath)}'.`); + void vscode.window.showInformationMessage(localizer.t("message.batchEditUpdated", { + count: changedFileCount, + domain: path.basename(domainUri.fsPath) + })); } /** @@ -570,6 +576,9 @@ async function loadSchemaInfoForConfig(configUri, workspaceRoot) { */ function renderFormHtml(fileName, schemaInfo, parsedYaml) { const formModel = buildFormModel(schemaInfo, parsedYaml); + const saveButtonLabel = escapeHtml(localizer.t("webview.button.save")); + const openRawButtonLabel = escapeHtml(localizer.t("webview.button.openRaw")); + const objectArrayItemLabel = localizer.t("webview.objectArray.item"); const renderedFields = formModel.fields .map((field) => renderFormField(field)) .join("\n"); @@ -583,8 +592,8 @@ function renderFormHtml(fileName, schemaInfo, parsedYaml) { .join("\n"); const schemaStatus = schemaInfo.exists - ? `Schema: ${escapeHtml(schemaInfo.schemaPath)}` - : `Schema missing: ${escapeHtml(schemaInfo.schemaPath)}`; + ? escapeHtml(localizer.t("webview.meta.schema", {schemaPath: schemaInfo.schemaPath})) + : escapeHtml(localizer.t("webview.meta.schemaMissing", {schemaPath: schemaInfo.schemaPath})); const editableContent = renderedFields; const unsupportedSection = unsupportedFields.length > 0 @@ -592,10 +601,10 @@ function renderFormHtml(fileName, schemaInfo, parsedYaml) { : ""; const emptyState = editableContent.length > 0 ? `${editableContent}${unsupportedSection}` - : "

No editable schema-bound fields were detected. Use raw YAML for unsupported shapes.

"; + : `

${escapeHtml(localizer.t("webview.emptyState"))}

`; return ` - + @@ -739,16 +748,17 @@ function renderFormHtml(fileName, schemaInfo, parsedYaml) {
- - + +
-
File: ${escapeHtml(fileName)}
+
${escapeHtml(localizer.t("webview.meta.file", {fileName}))}
${schemaStatus}
${emptyState}