diff --git a/localtypings/pxtarget.d.ts b/localtypings/pxtarget.d.ts index f3cbe35a88b8..ec720c93c6ad 100644 --- a/localtypings/pxtarget.d.ts +++ b/localtypings/pxtarget.d.ts @@ -1016,6 +1016,7 @@ declare namespace ts.pxtc { _expandedDef?: ParsedBlockDef; _untranslatedBlock?: string; // The block definition before it was translated _untranslatedJsDoc?: string // the jsDoc before it was translated + _untranslatedParamDefl?: pxt.Map; // the parameter defaults before they were translated _translatedLanguageCode?: string // the language this block has been translated into _shadowOverrides?: pxt.Map; jsDoc?: string; diff --git a/pxtcompiler/emitter/service.ts b/pxtcompiler/emitter/service.ts index def9229882f6..a1060683827e 100644 --- a/pxtcompiler/emitter/service.ts +++ b/pxtcompiler/emitter/service.ts @@ -353,6 +353,12 @@ namespace ts.pxtc { if (param.labelLocalizationKey && param.label) { locStrings[param.labelLocalizationKey] = param.label; } + + const defaultString = pxt.blocks.parameterDefaultToLocalizationString(param.defaultValue, param.type); + const defaultLocalizationKey = pxt.blocks.parameterDefaultLocalizationKey(si.qName, param.actualName); + if (defaultLocalizationKey && defaultString !== undefined) { + locStrings[defaultLocalizationKey] = defaultString; + } } if (comp.handlerArgs?.length) { for (const arg of comp.handlerArgs) { diff --git a/pxtlib/blocks.ts b/pxtlib/blocks.ts index 2422dd5a6648..751d45484ebd 100644 --- a/pxtlib/blocks.ts +++ b/pxtlib/blocks.ts @@ -91,6 +91,28 @@ namespace pxt.blocks { localizationKey: string; } + export function parameterDefaultLocalizationKey(qName: string, actualName: string) { + return qName ? `${qName}|param|${actualName}|defl` : undefined; + } + + export function parameterDefaultToLocalizationString(defaultValue: string, type?: string) { + if (!defaultValue) return undefined; + if (type === "string" && defaultValue.charAt(0) !== "\"") return defaultValue; + if (defaultValue.charAt(0) !== "\"") return undefined; + + try { + const value = JSON.parse(defaultValue); + return typeof value === "string" ? value : undefined; + } + catch (e) { + return undefined; + } + } + + export function localizationStringToParameterDefault(value: string) { + return JSON.stringify(value); + } + // Information for blocks that compile to function calls but are defined by vanilla Blockly // and not dynamically by BlocklyLoader export const builtinFunctionInfo: pxt.Map<{ params: string[]; blockId: string; }> = { diff --git a/pxtlib/service.ts b/pxtlib/service.ts index 45a7ab71c8a4..ebce7a7548ef 100644 --- a/pxtlib/service.ts +++ b/pxtlib/service.ts @@ -718,6 +718,10 @@ namespace ts.pxtc { if (fn.attributes._untranslatedJsDoc) fn.attributes.jsDoc = fn.attributes._untranslatedJsDoc; if (fn.attributes._untranslatedBlock) fn.attributes.jsDoc = fn.attributes._untranslatedBlock; + if (fn.attributes._untranslatedParamDefl) { + fn.attributes.paramDefl = U.clone(fn.attributes._untranslatedParamDefl); + syncParameterDefaults(fn); + } const lookupLoc = (locSuff: string, attrKey: string) => { return loc[fn.qName + locSuff] || fn.attributes.locs?.[attrKey] @@ -758,15 +762,30 @@ namespace ts.pxtc { } const paramsWithLabels = comp.thisParameter ? [comp.thisParameter, ...comp.parameters] : comp.parameters; for (const param of paramsWithLabels) { - if (!param.labelLocalizationKey) continue; - - const locSuff = param.labelLocalizationKey.slice(fn.qName.length); - const paramLabel = lookupLoc(locSuff, langLower + locSuff); - if (paramLabel) { - setBlockTranslationCacheKey(param.labelLocalizationKey, paramLabel); + if (param.labelLocalizationKey) { + const locSuff = param.labelLocalizationKey.slice(fn.qName.length); + const paramLabel = lookupLoc(locSuff, langLower + locSuff); + if (paramLabel) { + setBlockTranslationCacheKey(param.labelLocalizationKey, paramLabel); + } + else { + clearBlockTranslationCacheKey(param.labelLocalizationKey); + } } - else { - clearBlockTranslationCacheKey(param.labelLocalizationKey); + + const defaultString = pxt.blocks.parameterDefaultToLocalizationString(param.defaultValue, param.type); + const defaultLocalizationKey = pxt.blocks.parameterDefaultLocalizationKey(fn.qName, param.actualName); + if (defaultLocalizationKey && defaultString !== undefined) { + const locSuff = defaultLocalizationKey.slice(fn.qName.length); + const paramDefault = lookupLoc(locSuff, langLower + locSuff); + if (paramDefault !== undefined) { + if (!fn.attributes._untranslatedParamDefl) { + fn.attributes._untranslatedParamDefl = U.clone(fn.attributes.paramDefl || {}); + } + if (!fn.attributes.paramDefl) fn.attributes.paramDefl = {}; + fn.attributes.paramDefl[param.actualName] = pxt.blocks.localizationStringToParameterDefault(paramDefault); + syncParameterDefaults(fn); + } } } } @@ -835,6 +854,14 @@ namespace ts.pxtc { return cleanLocalizations(apis); } + function syncParameterDefaults(fn: SymbolInfo) { + if (!fn.parameters) return; + + for (const param of fn.parameters) { + param.default = fn.attributes.paramDefl && fn.attributes.paramDefl[param.name]; + } + } + function cleanLocalizations(apis: ApisInfo) { Util.values(apis.byQName) .filter(fb => fb.attributes.block && /^{[^:]+:[^}]+}/.test(fb.attributes.block)) diff --git a/tests/blocklycompiler-test/commentparsing.spec.ts b/tests/blocklycompiler-test/commentparsing.spec.ts index 4a0dbb0f2049..7d997a5d3251 100644 --- a/tests/blocklycompiler-test/commentparsing.spec.ts +++ b/tests/blocklycompiler-test/commentparsing.spec.ts @@ -512,6 +512,55 @@ describe("comment attribute parser", () => { chai.expect(strings["pins.DigitalPin.digitalRead|param|this|label"]).to.equal("pin"); }); + it("should compile and extract parameter default strings", () => { + const docDefault = testSymbolInfo("display.showString", "show string $text", [testParameter("text", "string")], ` + /** + * Show text on the screen. + * @param text the text to print on the screen, eg: "name" + */ + `); + const explicitDefault = testSymbolInfo("game.setGameOverMessage", "use message $message", [testParameter("message", "string")], ` + /** + * Set the message that displays when the game is over. + * @param message the message, eg: "Try again" + */ + //% message.defl="winner" + `); + const numericDefault = testSymbolInfo("display.showNumber", "show number $value", [testParameter("value", "number")], ` + /** + * Show a number on the screen. + * @param value the number to show, eg: 42 + */ + `); + + const docs = pxtc.genDocs("test", testApisInfo([docDefault, explicitDefault, numericDefault]), { locs: true }); + const strings = JSON.parse(docs["test-strings.json"]); + chai.expect(strings["display.showString|param|text|defl"]).to.equal("name"); + chai.expect(strings["game.setGameOverMessage|param|message|defl"]).to.equal("winner"); + chai.expect(strings["display.showNumber|param|value|defl"]).to.equal(undefined); + }); + + it("should apply localized parameter default strings", async () => { + const explicitDefault = testSymbolInfo("game.setGameOverMessage", "use message $message", [testParameter("message", "string")], ` + //% message.defl="GAME OVER!" + `); + + pxt.Util.setUserLanguage("es"); + try { + await pxtc.localizeApisAsync(testApisInfo([explicitDefault]), { + localizationStringsAsync: () => Promise.resolve({ + "game.setGameOverMessage|param|message|defl": "FIN DEL JUEGO" + }) + } as unknown as pxt.MainPackage); + + chai.expect(explicitDefault.attributes.paramDefl["message"]).to.equal("\"FIN DEL JUEGO\""); + chai.expect(explicitDefault.parameters[0].default).to.equal("\"FIN DEL JUEGO\""); + } + finally { + pxt.Util.setUserLanguage("en"); + } + }); + it("should parse parameter snippets", () => { const parsed = ts.pxtc.parseCommentString(` /** @@ -539,6 +588,7 @@ function testSymbolInfo(qName: string, block: string, parameters: pxtc.Parameter //% block="${block}" ${attributes} `); + parameters.forEach(parameter => parameter.default = attrs.paramDefl[parameter.name]); const qNameParts = qName.split("."); return { attributes: attrs,