From 17d9f5dd9e536fb5458dd505a235e9e89cbc499f Mon Sep 17 00:00:00 2001 From: Richard Knoll Date: Mon, 22 Jun 2026 11:27:24 -0700 Subject: [PATCH 1/2] add translatable ariaLabel to enums --- localtypings/pxtarget.d.ts | 2 ++ pxtblocks/loader.ts | 3 ++- pxtcompiler/emitter/service.ts | 5 +++++ pxtlib/service.ts | 15 ++++++++++++++- webapp/src/compiler.ts | 5 +++++ 5 files changed, 28 insertions(+), 2 deletions(-) diff --git a/localtypings/pxtarget.d.ts b/localtypings/pxtarget.d.ts index ec720c93c6ad..53145e146d2f 100644 --- a/localtypings/pxtarget.d.ts +++ b/localtypings/pxtarget.d.ts @@ -980,6 +980,7 @@ declare namespace ts.pxtc { enumIsHash?: boolean; // if true, the name of the enum is normalized, then hashed to generate the value enumPromptHint?: string; // The hint that will be displayed in the member creation prompt enumInitialMembers?: string[]; // The initial enum values which will be given the lowest values available + ariaLabel?: string; // The aria label for the enum value if the screen reader text should differ from the block value in dropdown items /* end enum-only attributes */ @@ -1017,6 +1018,7 @@ declare namespace ts.pxtc { _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 + _untranslatedAriaLabel?: string; // the aria label before it was translated _translatedLanguageCode?: string // the language this block has been translated into _shadowOverrides?: pxt.Map; jsDoc?: string; diff --git a/pxtblocks/loader.ts b/pxtblocks/loader.ts index e394006197ea..8f665f31cd15 100644 --- a/pxtblocks/loader.ts +++ b/pxtblocks/loader.ts @@ -538,7 +538,8 @@ function initBlock(block: Blockly.Block, info: pxtc.BlocksInfo, fn: pxtc.SymbolI height: 36, value: v.name } : k, - v.namespace + "." + v.name + v.namespace + "." + v.name, + v.attributes.ariaLabel ]; }); // if a value is provided, move it first diff --git a/pxtcompiler/emitter/service.ts b/pxtcompiler/emitter/service.ts index a1060683827e..1c7cad533666 100644 --- a/pxtcompiler/emitter/service.ts +++ b/pxtcompiler/emitter/service.ts @@ -366,6 +366,10 @@ namespace ts.pxtc { } } } + + if (si.attributes.ariaLabel) { + locStrings[`${si.qName}|ariaLabel`] = si.attributes.ariaLabel; + } } const mapLocs = (m: pxt.Map, name: string) => { if (!options.locs) return; @@ -385,6 +389,7 @@ namespace ts.pxtc { if (options.locs) enumMembers.forEach(em => { if (em.attributes.block) locStrings[`${em.qName}|block`] = em.attributes.block; + if (em.attributes.ariaLabel) locStrings[`${em.qName}|ariaLabel`] = em.attributes.ariaLabel; if (em.attributes.jsDoc) locStrings[em.qName] = em.attributes.jsDoc; }); mapLocs(locStrings, ""); diff --git a/pxtlib/service.ts b/pxtlib/service.ts index ebce7a7548ef..6e49c7c9fbdc 100644 --- a/pxtlib/service.ts +++ b/pxtlib/service.ts @@ -706,6 +706,7 @@ namespace ts.pxtc { const langLower = lang.toLowerCase(); const attrJsLocsKey = langLower + "|jsdoc"; const attrBlockLocsKey = langLower + "|block"; + const attrAriaLabelLocsKey = langLower + "|ariaLabel"; const loc = await mainPkg.localizationStringsAsync(lang); if (apiLocalizationStrings) @@ -717,7 +718,8 @@ namespace ts.pxtc { const altLocSrcFn = altLocSrc && apis.byQName[altLocSrc]; if (fn.attributes._untranslatedJsDoc) fn.attributes.jsDoc = fn.attributes._untranslatedJsDoc; - if (fn.attributes._untranslatedBlock) fn.attributes.jsDoc = fn.attributes._untranslatedBlock; + if (fn.attributes._untranslatedBlock) fn.attributes.block = fn.attributes._untranslatedBlock; + if (fn.attributes._untranslatedAriaLabel) fn.attributes.ariaLabel = fn.attributes._untranslatedAriaLabel; if (fn.attributes._untranslatedParamDefl) { fn.attributes.paramDefl = U.clone(fn.attributes._untranslatedParamDefl); syncParameterDefaults(fn); @@ -747,6 +749,7 @@ namespace ts.pxtc { const nsDoc = loc['{id:category}' + Util.capitalize(fn.qName)]; let locBlock = loc[`${fn.qName}|block`] || fn.attributes.locs?.[attrBlockLocsKey]; + const locAriaLabel = loc[`${fn.qName}|ariaLabel`] || fn.attributes.locs?.[attrAriaLabelLocsKey]; if (fn.attributes.block) { const comp = pxt.blocks.compileInfo(fn); @@ -848,6 +851,13 @@ namespace ts.pxtc { } else { updateBlockDef(fn.attributes); } + + if (locAriaLabel) { + if (!fn.attributes._untranslatedAriaLabel) { + fn.attributes._untranslatedAriaLabel = fn.attributes.ariaLabel; + } + fn.attributes.ariaLabel = locAriaLabel; + } fn.attributes._translatedLanguageCode = lang; }); @@ -990,6 +1000,9 @@ namespace ts.pxtc { } else if (U.startsWith(n, "jsdoc.loc.")) { if (!res.locs) res.locs = {}; res.locs[n.slice("jsdoc.loc.".length).toLowerCase() + "|jsdoc"] = v; + } else if (U.startsWith(n, "ariaLabel.loc.")) { + if (!res.locs) res.locs = {}; + res.locs[n.slice("ariaLabel.loc.".length).toLowerCase() + "|ariaLabel"] = v; } else if (U.contains(n, ".loc.")) { if (!res.locs) res.locs = {}; const p = n.slice(0, n.indexOf('.loc.')); diff --git a/webapp/src/compiler.ts b/webapp/src/compiler.ts index 218814ed1a47..a152d895a497 100644 --- a/webapp/src/compiler.ts +++ b/webapp/src/compiler.ts @@ -837,6 +837,11 @@ function cleanApiForCache(apiInfo: pxtc.SymbolInfo) { delete cachedAttrs._untranslatedJsDoc; defChanged = true; } + if (cachedAttrs._untranslatedAriaLabel) { + cachedAttrs.ariaLabel = cachedAttrs._untranslatedAriaLabel; + delete cachedAttrs._untranslatedAriaLabel; + defChanged = true; + } if (defChanged) { ts.pxtc.updateBlockDef(cachedAttrs); } From ddd5213e2d21f47b355bdfee6b583e27f7debce4 Mon Sep 17 00:00:00 2001 From: Richard Knoll Date: Mon, 22 Jun 2026 11:48:50 -0700 Subject: [PATCH 2/2] update docs --- docs/defining-blocks.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/defining-blocks.md b/docs/defining-blocks.md index 1dfb851025ad..4195ed18bd63 100644 --- a/docs/defining-blocks.md +++ b/docs/defining-blocks.md @@ -106,7 +106,7 @@ parameter = string * each `field` is mapped to a field name on the block. * the function parameters are mapped to the `$parameter` argument with an identical name. The loader automatically builds a mapping between the block field names and the function names. * the block will automatically switch to external inputs (wrapping) when there are four or more parameters. -* the `|` indicates where to start a new line if the block is in external inputs mode. +* the `|` indicates where to start a new line if the block is in external inputs mode. ## Custom block localization @@ -124,11 +124,12 @@ For example, export function square(x: number): number {} ``` -You can also override the ``jsdoc`` description and parameter info. +You can also override the ``jsdoc`` description, parameter info, and ariaLabel (for dropdown values). ``` jsdoc.loc.LOCALE = translated jsdoc PARAM.loc.LOCALE = parameter jsdoc +ariaLabel.loc.LOCALE = translated aria label ``` ```typescript-ignore @@ -141,6 +142,14 @@ PARAM.loc.LOCALE = parameter jsdoc //% jsdoc.loc.fr="Calcule le carré de x" //% x.loc.fr="le nombre" export function square(x: number): number {} + + +export enum MyEnum { + //% block="x" + //% ariaLabel="x axis" + //% ariaLabel.loc.fr="axe des x" + X +} ``` @@ -339,14 +348,16 @@ Enum is supported and will automatically be represented by a dropdown in blocks. enum Button { A = 1, B = 2, - //% blockId="ApB" block="A+B" + //% block="A+B" ariaLabel="A and B" AB = 3, } ``` * the initializer can be used to map the value -* the `blockId` attribute can be used to override the block id * the `block` attribute can be used to override the rendered string +* the `ariaLabel` attribute can be used to override the aria label on the dropdown value if the `block` value is not read correctly by screen readers + +The `block` and `ariaLabel` values can both be localized, either by including a strings JSON file or locally in the comments (see custom block localization section above). ### Tip: dropdown for non-enum parameters