diff --git a/src/Scratch.as b/src/Scratch.as index ed8cd43c8..525da90c9 100644 --- a/src/Scratch.as +++ b/src/Scratch.as @@ -746,7 +746,7 @@ public class Scratch extends Sprite { m.addLine(); m.addItem('Import experimental extension', function():void { function loadJSExtension(dialog:DialogBox):void { - var url:String = dialog.fields['URL'].text.replace(/^\s+|\s+$/g, ''); + var url:String = dialog.getField('URL').replace(/^\s+|\s+$/g, ''); if (url.length == 0) return; externalCall('ScratchExtensions.loadExternalJS', null, url); } diff --git a/src/scratch/BlockMenus.as b/src/scratch/BlockMenus.as index 89d787a7e..9276effd6 100644 --- a/src/scratch/BlockMenus.as +++ b/src/scratch/BlockMenus.as @@ -640,7 +640,7 @@ public class BlockMenus implements DragClient { private function renameVar():void { function doVarRename(dialog:DialogBox):void { - var newName:String = dialog.fields['New name'].text.replace(/^\s+|\s+$/g, ''); + var newName:String = dialog.getField('New name').replace(/^\s+|\s+$/g, ''); if (newName.length == 0 || app.viewedObj().lookupVar(newName)) return; if (block.op != Specs.GET_VAR) return; var oldName:String = blockVarOrListName(); @@ -759,7 +759,7 @@ public class BlockMenus implements DragClient { private function newBroadcast():void { function changeBroadcast(dialog:DialogBox):void { - var newName:String = dialog.fields['Message Name'].text; + var newName:String = dialog.getField('Message Name'); if (newName.length == 0) return; setBlockArg(newName); } diff --git a/src/scratch/PaletteBuilder.as b/src/scratch/PaletteBuilder.as index a97188a23..db899709f 100644 --- a/src/scratch/PaletteBuilder.as +++ b/src/scratch/PaletteBuilder.as @@ -192,7 +192,7 @@ public class PaletteBuilder { private function makeVariable():void { function makeVar2():void { - var n:String = d.fields['Variable name'].text.replace(/^\s+|\s+$/g, ''); + var n:String = d.getField('Variable name').replace(/^\s+|\s+$/g, ''); if (n.length == 0) return; createVar(n, varSettings); @@ -209,7 +209,7 @@ public class PaletteBuilder { private function makeList():void { function makeList2(d:DialogBox):void { - var n:String = d.fields['List name'].text.replace(/^\s+|\s+$/g, ''); + var n:String = d.getField('List name').replace(/^\s+|\s+$/g, ''); if (n.length == 0) return; createVar(n, varSettings); diff --git a/src/translation/Translator.as b/src/translation/Translator.as index 8a311f8cf..be7e80290 100644 --- a/src/translation/Translator.as +++ b/src/translation/Translator.as @@ -18,12 +18,16 @@ */ package translation { - import flash.events.Event; - import flash.net.*; - import flash.utils.ByteArray; - import blocks.Block; - import uiwidgets.Menu; - import util.*; +import flash.events.Event; +import flash.net.*; +import flash.utils.ByteArray; +import flash.utils.Dictionary; +import blocks.Block; + +import mx.utils.StringUtil; + +import uiwidgets.Menu; +import util.*; public class Translator { @@ -36,8 +40,7 @@ public class Translator { private static const font12:Array = ['fa', 'he','ja','ja_HIRA', 'zh_CN']; private static const font13:Array = ['ar']; - private static var dictionary:Object = new Object(); - private static var isEnglish:Boolean = true; + private static var dictionary:Object = {}; public static function initializeLanguageList():void { // Get a list of language names for the languages menu from the server. @@ -46,7 +49,7 @@ public class Translator { for each (var line:String in data.split('\n')) { var fields:Array = line.split(','); if (fields.length >= 2) { - languages.push([trimWhitespace(fields[0]), trimWhitespace(fields[1])]); + languages.push([StringUtil.trim(fields[0]), StringUtil.trim(fields[1])]); } } } @@ -66,8 +69,7 @@ public class Translator { if ('import translation file' == lang) { importTranslationFromFile(); return; } if ('set font size' == lang) { fontSizeMenu(); return; } - dictionary = new Object(); // default to English (empty dictionary) if there's no .po file - isEnglish = true; + dictionary = {}; // default to English (empty dictionary) if there's no .po file setFontsFor('en'); if ('en' == lang) Scratch.app.translationChanged(); // there is no .po file English else Scratch.app.server.getPOFile(lang, gotPOFile); @@ -82,7 +84,6 @@ public class Translator { var langName:String = file.name.slice(0, i); var data:ByteArray = file.data; if (data) { - dictionary = new Object(); // default to English dictionary = parsePOData(data); setFontsFor(langName); checkBlockTranslations(); @@ -109,7 +110,6 @@ public class Translator { // Set the rightToLeft flag and font sizes the given language. currentLang = lang; - isEnglish = (lang == 'en'); const rtlLanguages:Array = ['ar', 'fa', 'he']; rightToLeft = rtlLanguages.indexOf(lang) > -1; @@ -119,12 +119,10 @@ public class Translator { if (font13.indexOf(lang) > -1) Block.setFonts(13, 12, false, 0); } - public static function map(s:String):String { -//return TranslatableStrings.has(s) ? s.toUpperCase() : s; -//return (s.indexOf('%') > -1) ? s : s.toUpperCase(); // xxx testing only + public static function map(s:String, context:Dictionary=null):String { var result:* = dictionary[s]; -//if (!isEnglish && (s.indexOf('%') == -1)) return s.toUpperCase(); // xxx for testing only; comment out before pushing! - if ((result == null) || (result.length == 0)) return s; + if ((result == null) || (result.length == 0)) result = s; + if (context) result = StringUtils.substitute(result, context); return result; } @@ -133,7 +131,7 @@ public class Translator { skipBOM(bytes); var lines:Array = []; while (bytes.bytesAvailable > 0) { - var s:String = trimWhitespace(nextLine(bytes)); + var s:String = StringUtil.trim(nextLine(bytes)); if ((s.length > 0) && (s.charAt(0) != '#')) lines.push(s); } return makeDictionary(lines); @@ -150,29 +148,18 @@ public class Translator { bytes.position = bytes.position - 3; // BOM not found; back up } - private static function trimWhitespace(s:String):String { - // Remove leading and trailing whitespace characters. - if (s.length == 0) return ''; // empty - var i:int = 0; - while ((i < s.length) && (s.charCodeAt(i) <= 32)) i++; - if (i == s.length) return ''; // all whitespace - var j:int = s.length - 1; - while ((j > i) && (s.charCodeAt(j) <= 32)) j--; - return s.slice(i, j + 1); - } - private static function nextLine(bytes:ByteArray):String { // Read the next line from the given ByteArray. A line ends with CR, LF, or CR-LF. var buf:ByteArray = new ByteArray(); while (bytes.bytesAvailable > 0) { - var byte:int = bytes.readUnsignedByte(); - if (byte == 13) { // CR + var nextByte:int = bytes.readUnsignedByte(); + if (nextByte == 13) { // CR // line could end in CR or CR-LF if (bytes.readUnsignedByte() != 10) bytes.position--; // try to read LF, but backup if not LF break; } - if (byte == 10) break; // LF - buf.writeByte(byte); // append anything else + if (nextByte == 10) break; // LF + buf.writeByte(nextByte); // append anything else } buf.position = 0; return buf.readUTFBytes(buf.length); @@ -180,7 +167,7 @@ public class Translator { private static function makeDictionary(lines:Array):Object { // Return a dictionary mapping original strings to their translations. - var dict:Object = new Object(); + var dict:Object = {}; var mode:String = 'none'; // none, key, val var key:String = ''; var val:String = ''; @@ -219,34 +206,6 @@ public class Translator { return result; } - private static function recordPairIn(key:String, val:String, dict:Object):void { - // Handle some special cases where block specs changed for Scratch 2.0. - // Note: No longer needed now that translators are starting with current block specs. - switch (key) { - case '%a of %m': - val = val.replace('%a', '%m.attribute'); - val = val.replace('%m', '%m.sprite'); - dict['%m.attribute of %m.sprite'] = val; - break; - case 'stop all': - dict['@stop stop all'] = '@stop ' + val; - break; - case 'touching %m?': - dict['touching %m.touching?'] = val.replace('%m', '%m.touching'); - break; - case 'turn %n degrees': - dict['turn @turnRight %n degrees'] = val.replace('%n', '@turnRight %n'); - dict['turn @turnLeft %n degrees'] = val.replace('%n', '@turnLeft %n'); - break; - case 'when %m clicked': - dict['when @greenFlag clicked'] = val.replace('%m', '@greenFlag'); - dict['when I am clicked'] = val.replace('%m', 'I am'); - break; - default: - dict[key] = val; - } - } - private static function checkBlockTranslations():void { for each (var entry:Array in Specs.commands) checkBlockSpec(entry[0]); for each (var spec:String in Specs.extensionSpecs) checkBlockSpec(spec); @@ -255,7 +214,6 @@ public class Translator { private static function checkBlockSpec(spec:String):void { var translatedSpec:String = map(spec); if (translatedSpec == spec) return; // not translated - var origArgs:Array = extractArgs(spec); if (!argsMatch(extractArgs(spec), extractArgs(translatedSpec))) { Scratch.app.log('Block argument mismatch:'); Scratch.app.log(' ' + spec); diff --git a/src/uiwidgets/DialogBox.as b/src/uiwidgets/DialogBox.as index db7a08af5..132c43954 100644 --- a/src/uiwidgets/DialogBox.as +++ b/src/uiwidgets/DialogBox.as @@ -28,12 +28,13 @@ package uiwidgets { public class DialogBox extends Sprite { - public var fields:Dictionary = new Dictionary(); - public var booleanFields:Dictionary = new Dictionary(); + private var fields:Dictionary = new Dictionary(); + private var booleanFields:Dictionary = new Dictionary(); public var widget:DisplayObject; - public var w:int, h:int; + private var w:int, h:int; public var leftJustify:Boolean; + private var context:Dictionary; private var title:TextField; protected var buttons:Array = []; private var labelsAndFields:Array = []; @@ -58,31 +59,50 @@ public class DialogBox extends Sprite { addEventListener(FocusEvent.KEY_FOCUS_CHANGE, focusChange); } - public static function ask(question:String, defaultAnswer:String, stage:Stage = null, resultFunction:Function = null):void { + public static function ask(question:String, defaultAnswer:String, stage:Stage = null, resultFunction:Function = null, context:Dictionary = null):void { function done():void { if (resultFunction != null) resultFunction(d.fields['answer'].text) } var d:DialogBox = new DialogBox(done); d.addTitle(question); d.addField('answer', 120, defaultAnswer, false); d.addButton('OK', d.accept); + if (context) d.updateContext(context); d.showOnStage(stage ? stage : Scratch.app.stage); } - public static function confirm(question:String, stage:Stage = null, okFunction:Function = null, cancelFunction:Function = null):void { + public static function confirm(question:String, stage:Stage = null, okFunction:Function = null, cancelFunction:Function = null, context:Dictionary = null):void { var d:DialogBox = new DialogBox(okFunction, cancelFunction); d.addTitle(question); d.addAcceptCancelButtons('OK'); + if (context) d.updateContext(context); d.showOnStage(stage ? stage : Scratch.app.stage); } - public static function notify(title:String, msg:String, stage:Stage = null, leftJustify:Boolean = false, okFunction:Function = null, cancelFunction:Function = null):void { + public static function notify(title:String, msg:String, stage:Stage = null, leftJustify:Boolean = false, okFunction:Function = null, cancelFunction:Function = null, context:Dictionary = null):void { var d:DialogBox = new DialogBox(okFunction, cancelFunction); d.leftJustify = leftJustify; d.addTitle(title); d.addText(msg); d.addButton('OK', d.accept); + if (context) d.updateContext(context); d.showOnStage(stage ? stage : Scratch.app.stage); } + // Updates the context for variable substitution in the dialog's text, or sets it if there was none before. + // Make sure any text values in the context are already translated: they will not be translated here. + // Calling this will update the text of the dialog immediately. + public function updateContext(c:Dictionary):void { + if (!context) context = new Dictionary(); + for (var key:String in c) { + context[key] = c[key]; + } + for (var i:int = 0; i < numChildren; ++i) { + var f:VariableTextField = getChildAt(i) as VariableTextField; + if (f) { + f.applyContext(context); + } + } + } + public function addTitle(s:String):void { title = makeLabel(Translator.map(s), true); addChild(title); @@ -126,23 +146,23 @@ public class DialogBox extends Sprite { booleanLabelsAndFields.push([l, f]); } -private function getCheckMark(b:Boolean):Sprite{ - var spr:Sprite = new Sprite(); - var g:Graphics = spr.graphics; - g.clear(); - g.beginFill(0xFFFFFF); - g.lineStyle(1, 0x929497, 1, true); - g.drawRoundRect(0, 0, 17, 17, 3, 3); - g.endFill(); - if (b) { - g.lineStyle(2, 0x4c4d4f, 1, true); - g.moveTo(3,7); - g.lineTo(5,7); - g.lineTo(8,13); - g.lineTo(14,3); + private function getCheckMark(b:Boolean):Sprite{ + var spr:Sprite = new Sprite(); + var g:Graphics = spr.graphics; + g.clear(); + g.beginFill(0xFFFFFF); + g.lineStyle(1, 0x929497, 1, true); + g.drawRoundRect(0, 0, 17, 17, 3, 3); + g.endFill(); + if (b) { + g.lineStyle(2, 0x4c4d4f, 1, true); + g.moveTo(3,7); + g.lineTo(5,7); + g.lineTo(8,13); + g.lineTo(14,3); + } + return spr; } - return spr; -} public function addAcceptCancelButtons(acceptLabel:String = null):void { // Add a cancel button and an optional accept button with the given label. @@ -225,11 +245,11 @@ private function getCheckMark(b:Boolean):Sprite{ private function makeLabel(s:String, forTitle:Boolean = false):TextField { const normalFormat:TextFormat = new TextFormat(CSS.font, 14, CSS.textColor); - var result:TextField = new TextField(); + var result:VariableTextField = new VariableTextField(); result.autoSize = TextFieldAutoSize.LEFT; result.selectable = false; result.background = false; - result.text = s; + result.setText(s, context); result.setTextFormat(forTitle ? CSS.titleFormat : normalFormat); return result; } diff --git a/src/uiwidgets/VariableTextField.as b/src/uiwidgets/VariableTextField.as new file mode 100644 index 000000000..27f880d6c --- /dev/null +++ b/src/uiwidgets/VariableTextField.as @@ -0,0 +1,47 @@ +/* + * Scratch Project Editor and Player + * Copyright (C) 2014 Massachusetts Institute of Technology + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package uiwidgets { +import flash.text.TextField; +import flash.text.TextFormat; +import flash.utils.Dictionary; + +import util.StringUtils; + +public class VariableTextField extends TextField { + private var originalText:String; + + override public function set text(value:String):void { + throw Error('Call setText() instead'); + } + + public function setText(t: String, context:Dictionary = null):void { + originalText = t; + applyContext(context); + } + + // Re-substitutes values from this new context into the original text. + // This context must be a complete context, not just the fields that have changed. + public function applyContext(context: Dictionary):void { + // Assume that the whole text field uses the same format since there's no guarantee how indices will map. + var oldFormat:TextFormat = this.getTextFormat(); + super.text = context ? StringUtils.substitute(originalText, context) : originalText; + setTextFormat(oldFormat); + } +} +} diff --git a/src/util/StringUtils.as b/src/util/StringUtils.as new file mode 100644 index 000000000..5e4cc7e2b --- /dev/null +++ b/src/util/StringUtils.as @@ -0,0 +1,32 @@ +/* + * Scratch Project Editor and Player + * Copyright (C) 2014 Massachusetts Institute of Technology + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package util { +import flash.utils.Dictionary; + +public class StringUtils { + // format('My {animal} is named {name}.', {animal:'goat',name:'Eric'}) => 'My goat is named Eric.' + // Tokens not contained in the dictionary will not be modified. + public static function substitute(s:String, context:Dictionary):String { + for (var token:String in context) { + s = s.replace('{'+token+'}', context[token]); + } + return s; + } +} +} diff --git a/src/watchers/Watcher.as b/src/watchers/Watcher.as index 1ebdb91df..16534bb61 100644 --- a/src/watchers/Watcher.as +++ b/src/watchers/Watcher.as @@ -383,8 +383,8 @@ public class Watcher extends Sprite implements DragClient { private function sliderMinMaxDialog():void { function setMinMax():void { - var min:String = d.fields['Min'].text; - var max:String = d.fields['Max'].text; + var min:String = d.getField('Min'); + var max:String = d.getField('Max'); var minVal:Number = Number(min); var maxVal:Number = Number(max); if (isNaN(minVal) || isNaN(maxVal)) return;