mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-01-11 10:39:56 -05:00
Support extension translation
The new `maybeFormatMessage` function detects whether its argument looks like a message descriptor object and, if so, will call `formatMessage` on it. This is now used for all user-visible text fields in extensions. Also, messages may use "select" to check the target type with a message like this: '{targetType, select, stage {text for stage} sprite {text for sprite} other {text for other}'. Note that the "other" clause is required by `formatMessage`.
This commit is contained in:
parent
defdd42c47
commit
f8db6c3f02
4 changed files with 76 additions and 9 deletions
|
@ -6,9 +6,11 @@ const ArgumentType = require('../extension-support/argument-type');
|
||||||
const Blocks = require('./blocks');
|
const Blocks = require('./blocks');
|
||||||
const BlockType = require('../extension-support/block-type');
|
const BlockType = require('../extension-support/block-type');
|
||||||
const Sequencer = require('./sequencer');
|
const Sequencer = require('./sequencer');
|
||||||
|
const TargetType = require('../extension-support/target-type');
|
||||||
const Thread = require('./thread');
|
const Thread = require('./thread');
|
||||||
const Profiler = require('./profiler');
|
const Profiler = require('./profiler');
|
||||||
const log = require('../util/log');
|
const log = require('../util/log');
|
||||||
|
const maybeFormatMessage = require('../util/maybe-format-message');
|
||||||
|
|
||||||
// Virtual I/O devices.
|
// Virtual I/O devices.
|
||||||
const Clock = require('../io/clock');
|
const Clock = require('../io/clock');
|
||||||
|
@ -495,6 +497,19 @@ class Runtime extends EventEmitter {
|
||||||
return `${extensionId}.menu.${escapeHtml(menuName)}`;
|
return `${extensionId}.menu.${escapeHtml(menuName)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a context ("args") object for use with `formatMessage` on messages which might be target-specific.
|
||||||
|
* @param {Target} [target] - the target to use as context. If a target is not provided, default to the current
|
||||||
|
* editing target or the stage.
|
||||||
|
*/
|
||||||
|
makeMessageContextForTarget (target) {
|
||||||
|
const context = {};
|
||||||
|
target = target || this.getEditingTarget() || this.getTargetForStage();
|
||||||
|
if (target) {
|
||||||
|
context.targetType = (target.isStage ? TargetType.STAGE : TargetType.SPRITE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register the primitives provided by an extension.
|
* Register the primitives provided by an extension.
|
||||||
* @param {ExtensionMetadata} extensionInfo - information about the extension (id, blocks, etc.)
|
* @param {ExtensionMetadata} extensionInfo - information about the extension (id, blocks, etc.)
|
||||||
|
@ -503,7 +518,7 @@ class Runtime extends EventEmitter {
|
||||||
_registerExtensionPrimitives (extensionInfo) {
|
_registerExtensionPrimitives (extensionInfo) {
|
||||||
const categoryInfo = {
|
const categoryInfo = {
|
||||||
id: extensionInfo.id,
|
id: extensionInfo.id,
|
||||||
name: extensionInfo.name,
|
name: maybeFormatMessage(extensionInfo.name),
|
||||||
blockIconURI: extensionInfo.blockIconURI,
|
blockIconURI: extensionInfo.blockIconURI,
|
||||||
menuIconURI: extensionInfo.menuIconURI,
|
menuIconURI: extensionInfo.menuIconURI,
|
||||||
color1: '#FF6680',
|
color1: '#FF6680',
|
||||||
|
@ -591,14 +606,16 @@ class Runtime extends EventEmitter {
|
||||||
if (typeof menuItems === 'function') {
|
if (typeof menuItems === 'function') {
|
||||||
options = menuItems;
|
options = menuItems;
|
||||||
} else {
|
} else {
|
||||||
|
const extensionMessageContext = this.makeMessageContextForTarget();
|
||||||
options = menuItems.map(item => {
|
options = menuItems.map(item => {
|
||||||
switch (typeof item) {
|
const formattedItem = maybeFormatMessage(item, extensionMessageContext);
|
||||||
|
switch (typeof formattedItem) {
|
||||||
case 'string':
|
case 'string':
|
||||||
return [item, item];
|
return [formattedItem, formattedItem];
|
||||||
case 'object':
|
case 'object':
|
||||||
return [item.text, item.value];
|
return [maybeFormatMessage(item.text, extensionMessageContext), item.value];
|
||||||
default:
|
default:
|
||||||
throw new Error(`Can't interpret menu item: ${item}`);
|
throw new Error(`Can't interpret menu item: ${JSON.stringify(item)}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -713,12 +730,14 @@ class Runtime extends EventEmitter {
|
||||||
let inBranchNum = 0; // how many branches have we placed into the JSON so far?
|
let inBranchNum = 0; // how many branches have we placed into the JSON so far?
|
||||||
let outLineNum = 0; // used for scratch-blocks `message${outLineNum}` and `args${outLineNum}`
|
let outLineNum = 0; // used for scratch-blocks `message${outLineNum}` and `args${outLineNum}`
|
||||||
const convertPlaceholders = this._convertPlaceholders.bind(this, context);
|
const convertPlaceholders = this._convertPlaceholders.bind(this, context);
|
||||||
|
const extensionMessageContext = this.makeMessageContextForTarget();
|
||||||
|
|
||||||
// alternate between a block "arm" with text on it and an open slot for a substack
|
// alternate between a block "arm" with text on it and an open slot for a substack
|
||||||
while (inTextNum < blockText.length || inBranchNum < blockInfo.branchCount) {
|
while (inTextNum < blockText.length || inBranchNum < blockInfo.branchCount) {
|
||||||
if (inTextNum < blockText.length) {
|
if (inTextNum < blockText.length) {
|
||||||
context.outLineNum = outLineNum;
|
context.outLineNum = outLineNum;
|
||||||
const convertedText = blockText[inTextNum].replace(/\[(.+?)]/g, convertPlaceholders);
|
const lineText = maybeFormatMessage(blockText[inTextNum], extensionMessageContext);
|
||||||
|
const convertedText = lineText.replace(/\[(.+?)]/g, convertPlaceholders);
|
||||||
if (blockJSON[`message${outLineNum}`]) {
|
if (blockJSON[`message${outLineNum}`]) {
|
||||||
blockJSON[`message${outLineNum}`] += convertedText;
|
blockJSON[`message${outLineNum}`] += convertedText;
|
||||||
} else {
|
} else {
|
||||||
|
@ -784,7 +803,7 @@ class Runtime extends EventEmitter {
|
||||||
const argTypeInfo = ArgumentTypeMap[argInfo.type] || {};
|
const argTypeInfo = ArgumentTypeMap[argInfo.type] || {};
|
||||||
const defaultValue = (typeof argInfo.defaultValue === 'undefined' ?
|
const defaultValue = (typeof argInfo.defaultValue === 'undefined' ?
|
||||||
'' :
|
'' :
|
||||||
escapeHtml(argInfo.defaultValue.toString()));
|
escapeHtml(maybeFormatMessage(argInfo.defaultValue, this.makeMessageContextForTarget()).toString()));
|
||||||
|
|
||||||
if (argTypeInfo.check) {
|
if (argTypeInfo.check) {
|
||||||
argJSON.check = argTypeInfo.check;
|
argJSON.check = argTypeInfo.check;
|
||||||
|
|
18
src/extension-support/define-messages.js
Normal file
18
src/extension-support/define-messages.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
/**
|
||||||
|
* @typedef {object} MessageDescriptor
|
||||||
|
* @property {string} id - the translator-friendly unique ID of this message.
|
||||||
|
* @property {string} default - the message text in the default language (English).
|
||||||
|
* @property {string} [description] - a description of this message to help translators understand the context.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a hook for extracting messages from extension source files.
|
||||||
|
* This function simply returns the message descriptor map object that's passed in.
|
||||||
|
* @param {object.<MessageDescriptor>} messages - the messages to be defined
|
||||||
|
* @return {object.<MessageDescriptor>} - the input, unprocessed
|
||||||
|
*/
|
||||||
|
const defineMessages = function (messages) {
|
||||||
|
return messages;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = defineMessages;
|
|
@ -1,5 +1,6 @@
|
||||||
const dispatch = require('../dispatch/central-dispatch');
|
const dispatch = require('../dispatch/central-dispatch');
|
||||||
const log = require('../util/log');
|
const log = require('../util/log');
|
||||||
|
const maybeFormatMessage = require('../util/maybe-format-message');
|
||||||
|
|
||||||
const BlockType = require('./block-type');
|
const BlockType = require('./block-type');
|
||||||
|
|
||||||
|
@ -286,12 +287,23 @@ class ExtensionManager {
|
||||||
_getExtensionMenuItems (extensionObject, menuName) {
|
_getExtensionMenuItems (extensionObject, menuName) {
|
||||||
// Fetch the items appropriate for the target currently being edited. This assumes that menus only
|
// Fetch the items appropriate for the target currently being edited. This assumes that menus only
|
||||||
// collect items when opened by the user while editing a particular target.
|
// collect items when opened by the user while editing a particular target.
|
||||||
const editingTarget = this.runtime.getEditingTarget();
|
const editingTarget = this.runtime.getEditingTarget() || this.runtime.getTargetForStage();
|
||||||
const editingTargetID = editingTarget ? editingTarget.id : null;
|
const editingTargetID = editingTarget ? editingTarget.id : null;
|
||||||
|
const extensionMessageContext = this.runtime.makeMessageContextForTarget(editingTarget);
|
||||||
|
|
||||||
// TODO: Fix this to use dispatch.call when extensions are running in workers.
|
// TODO: Fix this to use dispatch.call when extensions are running in workers.
|
||||||
const menuFunc = extensionObject[menuName];
|
const menuFunc = extensionObject[menuName];
|
||||||
const menuItems = menuFunc.call(extensionObject, editingTargetID);
|
const menuItems = menuFunc.call(extensionObject, editingTargetID).map(
|
||||||
|
item => {
|
||||||
|
item = maybeFormatMessage(item, extensionMessageContext);
|
||||||
|
if (typeof item === 'object') {
|
||||||
|
return [
|
||||||
|
maybeFormatMessage(item.text, extensionMessageContext),
|
||||||
|
item.value
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
if (!menuItems || menuItems.length < 1) {
|
if (!menuItems || menuItems.length < 1) {
|
||||||
throw new Error(`Extension menu returned no items: ${menuName}`);
|
throw new Error(`Extension menu returned no items: ${menuName}`);
|
||||||
|
|
18
src/util/maybe-format-message.js
Normal file
18
src/util/maybe-format-message.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
const formatMessage = require('format-message');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if `maybeMessage` looks like a message object, and if so pass it to `formatMessage`.
|
||||||
|
* Otherwise, return `maybeMessage` as-is.
|
||||||
|
* @param {*} maybeMessage - something that might be a message descriptor object.
|
||||||
|
* @param {object} [args] - the arguments to pass to `formatMessage` if it gets called.
|
||||||
|
* @param {string} [locale] - the locale to pass to `formatMessage` if it gets called.
|
||||||
|
* @return {string|*} - the formatted message OR the original `maybeMessage` input.
|
||||||
|
*/
|
||||||
|
const maybeFormatMessage = function (maybeMessage, args, locale) {
|
||||||
|
if (maybeMessage.id && maybeMessage.default) {
|
||||||
|
return formatMessage(maybeMessage, args, locale);
|
||||||
|
}
|
||||||
|
return maybeMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = maybeFormatMessage;
|
Loading…
Reference in a new issue