Localization support for custom blocks (experimental)

upd4.2
Jens Mönig 2017-12-01 12:55:15 +01:00
rodzic bf3eedab75
commit 3584c13258
3 zmienionych plików z 134 dodań i 10 usunięć

126
byob.js
Wyświetl plik

@ -104,11 +104,11 @@ contains, InputSlotMorph, ToggleButtonMorph, IDE_Morph, MenuMorph, copy,
ToggleElementMorph, Morph, fontHeight, StageMorph, SyntaxElementMorph,
SnapSerializer, CommentMorph, localize, CSlotMorph, MorphicPreferences,
SymbolMorph, isNil, CursorMorph, VariableFrame, WatcherMorph, Variable,
BooleanSlotMorph, XML_Serializer*/
BooleanSlotMorph, XML_Serializer, SnapTranslator*/
// Global stuff ////////////////////////////////////////////////////////
modules.byob = '2017-October-09';
modules.byob = '2017-December-01';
// Declarations
@ -146,11 +146,13 @@ function CustomBlockDefinition(spec, receiver) {
this.comment = null;
this.codeMapping = null; // experimental, generate text code
this.codeHeader = null; // experimental, generate text code
this.translations = {}; // experimental, format: {lang : spec}
// don't serialize (not needed for functionality):
this.receiver = receiver || null; // for serialization only (pointer)
this.editorDimensions = null; // a rectangle, last bounds of the editor
this.cachedIsRecursive = null; // for automatic yielding
this.cachedTranslation = null; // for localized block specs
}
// CustomBlockDefinition instantiating blocks
@ -383,6 +385,82 @@ CustomBlockDefinition.prototype.isDirectlyRecursive = function () {
return this.cachedIsRecursive;
};
// CustomBlockDefinition localizing, highly experimental
CustomBlockDefinition.prototype.localizedSpec = function () {
if (this.cachedTranslation) {return this.cachedTranslation; }
var loc = this.translations[SnapTranslator.language],
sem = this.blockSpec(),
locParts,
inputs,
i = -1;
function isInput(str) {
return (str.length > 1) && (str[0] === '%');
}
if (isNil(loc)) {return sem; }
inputs = BlockMorph.prototype.parseSpec(sem).filter(function (str) {
return (isInput(str));
});
locParts = BlockMorph.prototype.parseSpec(loc);
// perform a bunch of sanity checks on the localized spec
if (locParts.some(function (str) {return isInput(str); }) ||
(locParts.filter(function (str) {return str === '_'; }).length !==
inputs.length)
) {
this.cachedTranslation = sem;
} else {
// substitute each input place holder with its semantic spec part
locParts = locParts.map(function (str) {
if (str === '_') {
i += 1;
return inputs[i];
}
return str;
});
this.cachedTranslation = locParts.join(' ');
}
return this.cachedTranslation;
};
CustomBlockDefinition.prototype.abstractBlockSpec = function () {
// answer the semantic block spec substituting each input
// with an underscore
return BlockMorph.prototype.parseSpec(this.blockSpec()).map(
function (str) {
return (str.length > 1 && (str[0]) === '%') ? '_' : str;
}
).join(' ');
};
CustomBlockDefinition.prototype.translationsAsText = function () {
var myself = this,
txt = '';
Object.keys(this.translations).forEach(function (lang) {
txt += (lang + ':' + myself.translations[lang] + '\n');
});
return txt;
};
CustomBlockDefinition.prototype.updateTranslations = function (text) {
var myself = this,
lines = text.split('\n').filter(function (txt) {
return txt.length;
});
this.translations = {};
lines.forEach(function (txt) {
var idx = txt.indexOf(':'),
key = txt.slice(0, idx).trim(),
val = txt.slice(idx + 1).trim();
if (idx) {
myself.translations[key] = val;
}
});
};
// CustomBlockDefinition picturing
CustomBlockDefinition.prototype.scriptsPicture = function () {
@ -465,6 +543,7 @@ function CustomCommandBlockMorph(definition, isProto) {
CustomCommandBlockMorph.prototype.init = function (definition, isProto) {
this.definition = definition; // mandatory
this.semanticSpec = '';
this.isGlobal = definition ? definition.isGlobal : false;
this.isPrototype = isProto || false; // optional
CustomCommandBlockMorph.uber.init.call(this, true); // silently
@ -495,9 +574,11 @@ CustomCommandBlockMorph.prototype.initializeVariables = function (oldVars) {
CustomCommandBlockMorph.prototype.refresh = function (aDefinition, silently) {
var def = aDefinition || this.definition,
newSpec = this.isPrototype ?
def.spec : def.blockSpec(),
def.spec : def.localizedSpec(),
oldInputs;
this.semanticSpec = def.blockSpec();
// make sure local custom blocks don't hold on to a method.
// future performance optimization plan:
// null out the definition for local blocks here,
@ -809,7 +890,7 @@ CustomCommandBlockMorph.prototype.edit = function () {
this.duplicateBlockDefinition();
return;
}
def = rcvr.getMethod(this.blockSpec);
def = rcvr.getMethod(this.semanticSpec);
}
Morph.prototype.trackChanges = false;
editor = new BlockEditorMorph(def, rcvr);
@ -946,6 +1027,13 @@ CustomCommandBlockMorph.prototype.userMenu = function () {
},
'open a new window\nwith a picture of this script'
);
menu.addItem(
"translations...",
function () {
hat.parentThatIsA(BlockEditorMorph).editTranslations();
},
'experimental -\nunder construction'
);
if (this.isGlobal) {
if (hat.inputs().length < 2) {
menu.addItem(
@ -1193,6 +1281,7 @@ CustomReporterBlockMorph.prototype.init = function (
isProto
) {
this.definition = definition; // mandatory
this.semanticSpec = ''; // used for translations
this.isGlobal = definition ? definition.isGlobal : false;
this.isPrototype = isProto || false; // optional
CustomReporterBlockMorph.uber.init.call(this, isPredicate, true); // sil.
@ -1942,6 +2031,7 @@ BlockEditorMorph.prototype.init = function (definition, target) {
// additional properties:
this.definition = definition;
this.translations = definition.translationsAsText();
this.handle = null;
// initialize inherited properties:
@ -2161,6 +2251,8 @@ BlockEditorMorph.prototype.updateDefinition = function () {
this.definition.declarations = this.prototypeSlots();
this.definition.variableNames = this.variableNames();
this.definition.scripts = [];
this.definition.updateTranslations(this.translations);
this.definition.cachedTranslation = null;
this.definition.editorDimensions = this.bounds.copy();
this.definition.cachedIsRecursive = null; // flush the cache, don't update
@ -2247,6 +2339,32 @@ BlockEditorMorph.prototype.variableNames = function () {
).variableNames();
};
// BlockEditorMorph translation
BlockEditorMorph.prototype.editTranslations = function () {
var myself = this,
block = this.definition.blockInstance();
block.addShadow(new Point(3, 3));
new DialogBoxMorph(
myself,
function (text) {
myself.translations = text;
},
myself
).promptCode(
'Custom Block Translations',
myself.translations,
myself.world(),
block.fullImage(),
myself.definition.abstractBlockSpec() +
'\n\n' +
localize('Enter one translation per line. ' +
'use colon (":") as lang/spec delimiter\n' +
'and underscore ("_") as placeholder for an input, ' +
'e.g.:\n\nen:say _ for _ secs')
);
};
// BlockEditorMorph layout
BlockEditorMorph.prototype.setInitialDimensions = function () {

Wyświetl plik

@ -61,7 +61,7 @@ normalizeCanvas, contains*/
// Global stuff ////////////////////////////////////////////////////////
modules.store = '2017-November-26';
modules.store = '2017-December-01';
// XML_Serializer ///////////////////////////////////////////////////////
@ -875,7 +875,7 @@ SnapSerializer.prototype.loadCustomBlocks = function (
// private
var myself = this;
element.children.forEach(function (child) {
var definition, names, inputs, vars, header, code, comment, i;
var definition, names, inputs, vars, header, code, trans, comment, i;
if (child.tag !== 'block-definition') {
return;
}
@ -942,6 +942,11 @@ SnapSerializer.prototype.loadCustomBlocks = function (
definition.codeMapping = code.contents;
}
trans = child.childNamed('translations');
if (trans) {
definition.updateTranslations(trans.contents);
}
comment = child.childNamed('comment');
if (comment) {
definition.comment = myself.loadComment(comment);
@ -1928,7 +1933,7 @@ CustomCommandBlockMorph.prototype.toBlockXML = function (serializer) {
var scope = this.isGlobal ? undefined : 'local';
return serializer.format(
'<custom-block s="@"%>%%%</custom-block>',
this.blockSpec,
this.semanticSpec,
this.isGlobal ?
'' : serializer.format(' scope="@"', scope),
serializer.store(this.inputs()),
@ -1949,7 +1954,6 @@ CustomReporterBlockMorph.prototype.toBlockXML
CustomBlockDefinition.prototype.toXML = function (serializer) {
var myself = this;
function encodeScripts(array) {
return array.reduce(function (xml, element) {
if (element instanceof BlockMorph) {
@ -1968,6 +1972,7 @@ CustomBlockDefinition.prototype.toXML = function (serializer) {
(this.variableNames.length ? '<variables>%</variables>' : '@') +
'<header>@</header>' +
'<code>@</code>' +
'<translations>@</translations>' +
'<inputs>%</inputs>%%' +
'</block-definition>',
this.spec,
@ -1978,6 +1983,7 @@ CustomBlockDefinition.prototype.toXML = function (serializer) {
serializer.store(new List(this.variableNames)) : ''),
this.codeHeader || '',
this.codeMapping || '',
this.translationsAsText(),
Object.keys(this.declarations).reduce(function (xml, decl) {
return xml + serializer.format(
'<input type="@"$>$%</input>',

Wyświetl plik

@ -61,7 +61,7 @@ StageMorph, SpriteMorph, StagePrompterMorph, Note, modules, isString, copy,
isNil, WatcherMorph, List, ListWatcherMorph, alert, console, TableMorph,
TableFrameMorph, ColorSlotMorph, isSnapObject*/
modules.threads = '2017-November-16';
modules.threads = '2017-December-01';
var ThreadManager;
var Process;
@ -1332,7 +1332,7 @@ Process.prototype.evaluateCustomBlock = function () {
var caller = this.context.parentContext,
block = this.context.expression,
method = block.isGlobal ? block.definition
: this.blockReceiver().getMethod(block.blockSpec),
: this.blockReceiver().getMethod(block.semanticSpec),
context = method.body,
declarations = method.declarations,
args = new List(this.context.inputs),