mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-01-08 14:01:58 -05:00
Preliminary localization (#777)
* localize the block and menu strings in the pen extension * adds .tx/config to be able to push translations to transifex * includes format-message to localize strings and extracting them. * add setLocale function to VM to allow GUI to pass in locale data. * refresh block definitions when the locale changes. ### Still to be decided For now just extracting messages from the pen extension into their own file. We’ll need to decide if each category gets its own file, or group all the strings into one resource.
This commit is contained in:
parent
f33c6294bc
commit
f51cf9877e
7 changed files with 210 additions and 22 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -16,3 +16,6 @@ npm-*
|
|||
# Build
|
||||
/dist
|
||||
/playground
|
||||
|
||||
# Localization
|
||||
/translations
|
||||
|
|
8
.tx/config
Normal file
8
.tx/config
Normal file
|
@ -0,0 +1,8 @@
|
|||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[experimental-scratch.pen]
|
||||
file_filter = translations/pen/<lang>.json
|
||||
source_file = translations/pen/en.json
|
||||
source_lang = en
|
||||
type = CHROME
|
|
@ -15,7 +15,9 @@
|
|||
"build": "webpack --progress --colors --bail",
|
||||
"coverage": "tap ./test/{unit,integration}/*.js --coverage --coverage-report=lcov",
|
||||
"deploy": "touch playground/.nojekyll && gh-pages -t -d playground -m \"Build for $(git log --pretty=format:%H -n1)\"",
|
||||
"lint": "eslint .",
|
||||
"extract:pen": "mkdirp translations/pen && format-message extract --out-file translations/pen/en.json src/extensions/scratch3_pen/index.js",
|
||||
"i18n:src": "npm run extract:pen",
|
||||
"lint": "eslint . && format-message lint src/**/*.js",
|
||||
"prepublish": "in-publish && npm run build || not-in-publish",
|
||||
"start": "webpack-dev-server",
|
||||
"tap": "tap ./test/{unit,integration}/*.js",
|
||||
|
@ -37,6 +39,8 @@
|
|||
"eslint": "^4.5.0",
|
||||
"eslint-config-scratch": "^5.0.0",
|
||||
"expose-loader": "0.7.4",
|
||||
"format-message": "5.2.1",
|
||||
"format-message-cli": "5.2.1",
|
||||
"gh-pages": "^1.1.0",
|
||||
"highlightjs": "^9.8.0",
|
||||
"htmlparser2": "3.9.2",
|
||||
|
|
|
@ -385,6 +385,14 @@ class Runtime extends EventEmitter {
|
|||
return 'EXTENSION_ADDED';
|
||||
}
|
||||
|
||||
/**
|
||||
* Event name for reporting that blocksInfo was updated.
|
||||
* @const {string}
|
||||
*/
|
||||
static get BLOCKSINFO_UPDATE () {
|
||||
return 'BLOCKSINFO_UPDATE';
|
||||
}
|
||||
|
||||
/**
|
||||
* How rapidly we try to step threads by default, in ms.
|
||||
*/
|
||||
|
@ -497,6 +505,41 @@ class Runtime extends EventEmitter {
|
|||
this.emit(Runtime.EXTENSION_ADDED, categoryInfo.blocks.concat(categoryInfo.menus));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reregister the primitives for an extension
|
||||
* @param {ExtensionInfo} extensionInfo - new info (results of running getInfo)
|
||||
* for an extension
|
||||
* @private
|
||||
*/
|
||||
_refreshExtensionPrimitives (extensionInfo) {
|
||||
let extensionBlocks = [];
|
||||
for (const categoryInfo of this._blockInfo) {
|
||||
if (extensionInfo.id === categoryInfo.id) {
|
||||
categoryInfo.blocks = [];
|
||||
categoryInfo.menus = [];
|
||||
for (const menuName in extensionInfo.menus) {
|
||||
if (extensionInfo.menus.hasOwnProperty(menuName)) {
|
||||
const menuItems = extensionInfo.menus[menuName];
|
||||
const convertedMenu = this._buildMenuForScratchBlocks(menuName, menuItems, categoryInfo);
|
||||
categoryInfo.menus.push(convertedMenu);
|
||||
}
|
||||
}
|
||||
for (const blockInfo of extensionInfo.blocks) {
|
||||
const convertedBlock = this._convertForScratchBlocks(blockInfo, categoryInfo);
|
||||
const opcode = convertedBlock.json.type;
|
||||
categoryInfo.blocks.push(convertedBlock);
|
||||
this._primitives[opcode] = convertedBlock.info.func;
|
||||
if (blockInfo.blockType === BlockType.HAT) {
|
||||
this._hats[opcode] = {edgeActivated: true}; /** @TODO let extension specify this */
|
||||
}
|
||||
}
|
||||
extensionBlocks = extensionBlocks.concat(categoryInfo.blocks, categoryInfo.menus);
|
||||
}
|
||||
}
|
||||
|
||||
this.emit(Runtime.BLOCKSINFO_UPDATE, extensionBlocks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the scratch-blocks JSON for a menu. Note that scratch-blocks treats menus as a special kind of block.
|
||||
* @param {string} menuName - the name of the menu
|
||||
|
|
|
@ -78,7 +78,7 @@ class ExtensionManager {
|
|||
* @type {Set.<string>}
|
||||
* @private
|
||||
*/
|
||||
this._loadedExtensions = new Set();
|
||||
this._loadedExtensions = new Map();
|
||||
|
||||
/**
|
||||
* Keep a reference to the runtime so we can construct internal extension objects.
|
||||
|
@ -119,8 +119,8 @@ class ExtensionManager {
|
|||
|
||||
const extension = builtinExtensions[extensionURL];
|
||||
const extensionInstance = new extension(this.runtime);
|
||||
return this._registerInternalExtension(extensionInstance).then(() => {
|
||||
this._loadedExtensions.add(extensionURL);
|
||||
return this._registerInternalExtension(extensionInstance).then(serviceName => {
|
||||
this._loadedExtensions.set(extensionURL, serviceName);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -133,6 +133,21 @@ class ExtensionManager {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* regenerate blockinfo for any loaded extensions
|
||||
*/
|
||||
refreshBlocks () {
|
||||
this._loadedExtensions.forEach(serviceName => {
|
||||
dispatch.call(serviceName, 'getInfo')
|
||||
.then(info => {
|
||||
dispatch.call('runtime', '_refreshExtensionPrimitives', info);
|
||||
})
|
||||
.catch(e => {
|
||||
log.error(`Failed to refresh buildtin extension primitives: ${JSON.stringify(e)}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
allocateWorker () {
|
||||
const id = this.nextExtensionWorker++;
|
||||
const workerInfo = this.pendingExtensions.shift();
|
||||
|
@ -175,7 +190,10 @@ class ExtensionManager {
|
|||
const fakeWorkerId = this.nextExtensionWorker++;
|
||||
const serviceName = `extension.${fakeWorkerId}.${extensionInfo.id}`;
|
||||
return dispatch.setService(serviceName, extensionObject)
|
||||
.then(() => dispatch.call('extensions', 'registerExtensionService', serviceName));
|
||||
.then(() => {
|
||||
dispatch.call('extensions', 'registerExtensionService', serviceName);
|
||||
return serviceName;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,6 +3,7 @@ const BlockType = require('../../extension-support/block-type');
|
|||
const Cast = require('../../util/cast');
|
||||
const Clone = require('../../util/clone');
|
||||
const Color = require('../../util/color');
|
||||
const formatMessage = require('format-message');
|
||||
const MathUtil = require('../../util/math-util');
|
||||
const RenderedTarget = require('../../sprites/rendered-target');
|
||||
const log = require('../../util/log');
|
||||
|
@ -15,7 +16,7 @@ const log = require('../../util/log');
|
|||
const iconURI = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+cGVuLWljb248L3RpdGxlPjxnIHN0cm9rZT0iIzU3NUU3NSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik04Ljc1MyAzNC42MDJsLTQuMjUgMS43OCAxLjc4My00LjIzN2MxLjIxOC0yLjg5MiAyLjkwNy01LjQyMyA1LjAzLTcuNTM4TDMxLjA2NiA0LjkzYy44NDYtLjg0MiAyLjY1LS40MSA0LjAzMi45NjcgMS4zOCAxLjM3NSAxLjgxNiAzLjE3My45NyA0LjAxNUwxNi4zMTggMjkuNTljLTIuMTIzIDIuMTE2LTQuNjY0IDMuOC03LjU2NSA1LjAxMiIgZmlsbD0iI0ZGRiIvPjxwYXRoIGQ9Ik0yOS40MSA2LjExcy00LjQ1LTIuMzc4LTguMjAyIDUuNzcyYy0xLjczNCAzLjc2Ni00LjM1IDEuNTQ2LTQuMzUgMS41NDYiLz48cGF0aCBkPSJNMzYuNDIgOC44MjVjMCAuNDYzLS4xNC44NzMtLjQzMiAxLjE2NGwtOS4zMzUgOS4zYy4yODItLjI5LjQxLS42NjguNDEtMS4xMiAwLS44NzQtLjUwNy0xLjk2My0xLjQwNi0yLjg2OC0xLjM2Mi0xLjM1OC0zLjE0Ny0xLjgtNC4wMDItLjk5TDMwLjk5IDUuMDFjLjg0NC0uODQgMi42NS0uNDEgNC4wMzUuOTYuODk4LjkwNCAxLjM5NiAxLjk4MiAxLjM5NiAyLjg1NU0xMC41MTUgMzMuNzc0Yy0uNTczLjMwMi0xLjE1Ny41Ny0xLjc2NC44M0w0LjUgMzYuMzgybDEuNzg2LTQuMjM1Yy4yNTgtLjYwNC41My0xLjE4Ni44MzMtMS43NTcuNjkuMTgzIDEuNDQ4LjYyNSAyLjEwOCAxLjI4Mi42Ni42NTggMS4xMDIgMS40MTIgMS4yODcgMi4xMDIiIGZpbGw9IiM0Qzk3RkYiLz48cGF0aCBkPSJNMzYuNDk4IDguNzQ4YzAgLjQ2NC0uMTQuODc0LS40MzMgMS4xNjVsLTE5Ljc0MiAxOS42OGMtMi4xMyAyLjExLTQuNjczIDMuNzkzLTcuNTcyIDUuMDFMNC41IDM2LjM4bC45NzQtMi4zMTYgMS45MjUtLjgwOGMyLjg5OC0xLjIxOCA1LjQ0LTIuOSA3LjU3LTUuMDFsMTkuNzQzLTE5LjY4Yy4yOTItLjI5Mi40MzItLjcwMi40MzItMS4xNjUgMC0uNjQ2LS4yNy0xLjQtLjc4LTIuMTIyLjI1LjE3Mi41LjM3Ny43MzcuNjE0Ljg5OC45MDUgMS4zOTYgMS45ODMgMS4zOTYgMi44NTYiIGZpbGw9IiM1NzVFNzUiIG9wYWNpdHk9Ii4xNSIvPjxwYXRoIGQ9Ik0xOC40NSAxMi44M2MwIC41LS40MDQuOTA1LS45MDQuOTA1cy0uOTA1LS40MDUtLjkwNS0uOTA0YzAtLjUuNDA3LS45MDMuOTA2LS45MDMuNSAwIC45MDQuNDA0LjkwNC45MDR6IiBmaWxsPSIjNTc1RTc1Ii8+PC9nPjwvc3ZnPg==';
|
||||
|
||||
/**
|
||||
* Enum for pen color parameters.
|
||||
* Enum for pen color parameter values.
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
|
@ -201,6 +202,49 @@ class Scratch3PenBlocks {
|
|||
return MathUtil.wrapClamp(value, 0, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize color parameters menu with localized strings
|
||||
* @returns {array} of the localized text and values for each menu element
|
||||
* @private
|
||||
*/
|
||||
_initColorParam () {
|
||||
return [
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'pen.colorMenu.color',
|
||||
default: 'color',
|
||||
description: 'label for color element in color picker for pen extension'
|
||||
}),
|
||||
value: ColorParam.COLOR
|
||||
},
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'pen.colorMenu.saturation',
|
||||
default: 'saturation',
|
||||
description: 'label for saturation element in color picker for pen extension'
|
||||
}),
|
||||
value: ColorParam.SATURATION
|
||||
},
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'pen.colorMenu.brightness',
|
||||
default: 'brightness',
|
||||
description: 'label for brightness element in color picker for pen extension'
|
||||
}),
|
||||
value: ColorParam.BRIGHTNESS
|
||||
},
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'pen.colorMenu.transparency',
|
||||
default: 'transparency',
|
||||
description: 'label for transparency element in color picker for pen extension'
|
||||
}),
|
||||
value: ColorParam.TRANSPARENCY
|
||||
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp a pen color parameter to the range (0,100).
|
||||
* @param {number} value - the value to be clamped.
|
||||
|
@ -246,26 +290,48 @@ class Scratch3PenBlocks {
|
|||
blocks: [
|
||||
{
|
||||
opcode: 'clear',
|
||||
blockType: BlockType.COMMAND
|
||||
blockType: BlockType.COMMAND,
|
||||
text: formatMessage({
|
||||
id: 'pen.clear',
|
||||
default: 'clear',
|
||||
description: 'erase all pen trails and stamps'
|
||||
})
|
||||
},
|
||||
{
|
||||
opcode: 'stamp',
|
||||
blockType: BlockType.COMMAND
|
||||
blockType: BlockType.COMMAND,
|
||||
text: formatMessage({
|
||||
id: 'pen.stamp',
|
||||
default: 'stamp',
|
||||
description: 'render current costume on the background'
|
||||
})
|
||||
},
|
||||
{
|
||||
opcode: 'penDown',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'pen down'
|
||||
text: formatMessage({
|
||||
id: 'pen.penDown',
|
||||
default: 'pen down',
|
||||
description: 'start leaving a trail when the sprite moves'
|
||||
})
|
||||
},
|
||||
{
|
||||
opcode: 'penUp',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'pen up'
|
||||
text: formatMessage({
|
||||
id: 'pen.penUp',
|
||||
default: 'pen up',
|
||||
description: 'stop leaving a trail behind the sprite'
|
||||
})
|
||||
},
|
||||
{
|
||||
opcode: 'setPenColorToColor',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'set pen color to [COLOR]',
|
||||
text: formatMessage({
|
||||
id: 'pen.setColor',
|
||||
default: 'set pen color to [COLOR]',
|
||||
description: 'set the pen color to a particular (RGB) value'
|
||||
}),
|
||||
arguments: {
|
||||
COLOR: {
|
||||
type: ArgumentType.COLOR
|
||||
|
@ -275,7 +341,11 @@ class Scratch3PenBlocks {
|
|||
{
|
||||
opcode: 'changePenColorParamBy',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'change pen [COLOR_PARAM] by [VALUE]',
|
||||
text: formatMessage({
|
||||
id: 'pen.changeColorParam',
|
||||
default: 'change pen [COLOR_PARAM] by [VALUE]',
|
||||
description: 'change the state of a pen color parameter'
|
||||
}),
|
||||
arguments: {
|
||||
COLOR_PARAM: {
|
||||
type: ArgumentType.STRING,
|
||||
|
@ -291,7 +361,11 @@ class Scratch3PenBlocks {
|
|||
{
|
||||
opcode: 'setPenColorParamTo',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'set pen [COLOR_PARAM] to [VALUE]',
|
||||
text: formatMessage({
|
||||
id: 'pen.setColorParam',
|
||||
default: 'set pen [COLOR_PARAM] to [VALUE]',
|
||||
description: 'set the state for a pen color parameter e.g. saturation'
|
||||
}),
|
||||
arguments: {
|
||||
COLOR_PARAM: {
|
||||
type: ArgumentType.STRING,
|
||||
|
@ -307,7 +381,11 @@ class Scratch3PenBlocks {
|
|||
{
|
||||
opcode: 'changePenSizeBy',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'change pen size by [SIZE]',
|
||||
text: formatMessage({
|
||||
id: 'pen.changeSize',
|
||||
default: 'change pen size by [SIZE]',
|
||||
description: 'change the diameter of the trail left by a sprite'
|
||||
}),
|
||||
arguments: {
|
||||
SIZE: {
|
||||
type: ArgumentType.NUMBER,
|
||||
|
@ -318,7 +396,11 @@ class Scratch3PenBlocks {
|
|||
{
|
||||
opcode: 'setPenSizeTo',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'set pen size to [SIZE]',
|
||||
text: formatMessage({
|
||||
id: 'pen.setSize',
|
||||
default: 'set pen size to [SIZE]',
|
||||
description: 'set the diameter of a trail left by a sprite'
|
||||
}),
|
||||
arguments: {
|
||||
SIZE: {
|
||||
type: ArgumentType.NUMBER,
|
||||
|
@ -330,7 +412,11 @@ class Scratch3PenBlocks {
|
|||
{
|
||||
opcode: 'setPenShadeToNumber',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'set pen shade to [SHADE]',
|
||||
text: formatMessage({
|
||||
id: 'pen.setShade',
|
||||
default: 'set pen shade to [SHADE]',
|
||||
description: 'legacy pen blocks - set pen shade'
|
||||
}),
|
||||
arguments: {
|
||||
SHADE: {
|
||||
type: ArgumentType.NUMBER,
|
||||
|
@ -342,7 +428,11 @@ class Scratch3PenBlocks {
|
|||
{
|
||||
opcode: 'changePenShadeBy',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'change pen shade by [SHADE]',
|
||||
text: formatMessage({
|
||||
id: 'pen.changeShade',
|
||||
default: 'change pen shade by [SHADE]',
|
||||
description: 'legacy pen blocks - change pen shade'
|
||||
}),
|
||||
arguments: {
|
||||
SHADE: {
|
||||
type: ArgumentType.NUMBER,
|
||||
|
@ -354,7 +444,11 @@ class Scratch3PenBlocks {
|
|||
{
|
||||
opcode: 'setPenHueToNumber',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'set pen hue to [HUE]',
|
||||
text: formatMessage({
|
||||
id: 'pen.setHue',
|
||||
default: 'set pen hue to [HUE]',
|
||||
description: 'legacy pen blocks - set pen color to number'
|
||||
}),
|
||||
arguments: {
|
||||
HUE: {
|
||||
type: ArgumentType.NUMBER,
|
||||
|
@ -366,7 +460,11 @@ class Scratch3PenBlocks {
|
|||
{
|
||||
opcode: 'changePenHueBy',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'change pen hue by [HUE]',
|
||||
text: formatMessage({
|
||||
id: 'pen.changeHue',
|
||||
default: 'change pen hue by [HUE]',
|
||||
description: 'legacy pen blocks - change pen color'
|
||||
}),
|
||||
arguments: {
|
||||
HUE: {
|
||||
type: ArgumentType.NUMBER,
|
||||
|
@ -377,9 +475,7 @@ class Scratch3PenBlocks {
|
|||
}
|
||||
],
|
||||
menus: {
|
||||
colorParam:
|
||||
[ColorParam.COLOR, ColorParam.SATURATION,
|
||||
ColorParam.BRIGHTNESS, ColorParam.TRANSPARENCY]
|
||||
colorParam: this._initColorParam()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ const Runtime = require('./engine/runtime');
|
|||
const sb2 = require('./serialization/sb2');
|
||||
const sb3 = require('./serialization/sb3');
|
||||
const StringUtil = require('./util/string-util');
|
||||
const formatMessage = require('format-message');
|
||||
|
||||
const {loadCostume} = require('./import/load-costume.js');
|
||||
const {loadSound} = require('./import/load-sound.js');
|
||||
|
@ -67,6 +68,9 @@ class VirtualMachine extends EventEmitter {
|
|||
this.runtime.on(Runtime.EXTENSION_ADDED, blocksInfo => {
|
||||
this.emit(Runtime.EXTENSION_ADDED, blocksInfo);
|
||||
});
|
||||
this.runtime.on(Runtime.BLOCKSINFO_UPDATE, blocksInfo => {
|
||||
this.emit(Runtime.BLOCKSINFO_UPDATE, blocksInfo);
|
||||
});
|
||||
|
||||
this.extensionManager = new ExtensionManager(this.runtime);
|
||||
|
||||
|
@ -552,6 +556,18 @@ class VirtualMachine extends EventEmitter {
|
|||
this.runtime.attachStorage(storage);
|
||||
}
|
||||
|
||||
/**
|
||||
* set the current locale and builtin messages for the VM
|
||||
* @param {[type]} locale current locale
|
||||
* @param {[type]} messages builtin messages map for current locale
|
||||
*/
|
||||
setLocale (locale, messages) {
|
||||
if (locale !== formatMessage.setup().locale) {
|
||||
formatMessage.setup({locale: locale, translations: {[locale]: messages}});
|
||||
this.extensionManager.refreshBlocks();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a Blockly event for the current editing target.
|
||||
* @param {!Blockly.Event} e Any Blockly event.
|
||||
|
|
Loading…
Reference in a new issue