mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-23 14:32:59 -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 BlockType = require('../extension-support/block-type');
|
||||
const Sequencer = require('./sequencer');
|
||||
const TargetType = require('../extension-support/target-type');
|
||||
const Thread = require('./thread');
|
||||
const Profiler = require('./profiler');
|
||||
const log = require('../util/log');
|
||||
const maybeFormatMessage = require('../util/maybe-format-message');
|
||||
|
||||
// Virtual I/O devices.
|
||||
const Clock = require('../io/clock');
|
||||
|
@ -495,6 +497,19 @@ class Runtime extends EventEmitter {
|
|||
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.
|
||||
* @param {ExtensionMetadata} extensionInfo - information about the extension (id, blocks, etc.)
|
||||
|
@ -503,7 +518,7 @@ class Runtime extends EventEmitter {
|
|||
_registerExtensionPrimitives (extensionInfo) {
|
||||
const categoryInfo = {
|
||||
id: extensionInfo.id,
|
||||
name: extensionInfo.name,
|
||||
name: maybeFormatMessage(extensionInfo.name),
|
||||
blockIconURI: extensionInfo.blockIconURI,
|
||||
menuIconURI: extensionInfo.menuIconURI,
|
||||
color1: '#FF6680',
|
||||
|
@ -591,14 +606,16 @@ class Runtime extends EventEmitter {
|
|||
if (typeof menuItems === 'function') {
|
||||
options = menuItems;
|
||||
} else {
|
||||
const extensionMessageContext = this.makeMessageContextForTarget();
|
||||
options = menuItems.map(item => {
|
||||
switch (typeof item) {
|
||||
const formattedItem = maybeFormatMessage(item, extensionMessageContext);
|
||||
switch (typeof formattedItem) {
|
||||
case 'string':
|
||||
return [item, item];
|
||||
return [formattedItem, formattedItem];
|
||||
case 'object':
|
||||
return [item.text, item.value];
|
||||
return [maybeFormatMessage(item.text, extensionMessageContext), item.value];
|
||||
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 outLineNum = 0; // used for scratch-blocks `message${outLineNum}` and `args${outLineNum}`
|
||||
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
|
||||
while (inTextNum < blockText.length || inBranchNum < blockInfo.branchCount) {
|
||||
if (inTextNum < blockText.length) {
|
||||
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}`]) {
|
||||
blockJSON[`message${outLineNum}`] += convertedText;
|
||||
} else {
|
||||
|
@ -784,7 +803,7 @@ class Runtime extends EventEmitter {
|
|||
const argTypeInfo = ArgumentTypeMap[argInfo.type] || {};
|
||||
const defaultValue = (typeof argInfo.defaultValue === 'undefined' ?
|
||||
'' :
|
||||
escapeHtml(argInfo.defaultValue.toString()));
|
||||
escapeHtml(maybeFormatMessage(argInfo.defaultValue, this.makeMessageContextForTarget()).toString()));
|
||||
|
||||
if (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 log = require('../util/log');
|
||||
const maybeFormatMessage = require('../util/maybe-format-message');
|
||||
|
||||
const BlockType = require('./block-type');
|
||||
|
||||
|
@ -286,12 +287,23 @@ class ExtensionManager {
|
|||
_getExtensionMenuItems (extensionObject, menuName) {
|
||||
// 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.
|
||||
const editingTarget = this.runtime.getEditingTarget();
|
||||
const editingTarget = this.runtime.getEditingTarget() || this.runtime.getTargetForStage();
|
||||
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.
|
||||
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) {
|
||||
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