diff --git a/gf.cabal b/gf.cabal
index f1bf61b98..4be421edf 100644
--- a/gf.cabal
+++ b/gf.cabal
@@ -27,6 +27,9 @@ data-files: www/index.html
www/TransQuiz/*.css
www/TransQuiz/*.js
www/TransQuiz/*.png
+ www/translator/*.html
+ www/translator/*.css
+ www/translator/*.js
source-repository head
type: darcs
diff --git a/src/www/index.html b/src/www/index.html
index 499f4be37..0c1261590 100644
--- a/src/www/index.html
+++ b/src/www/index.html
@@ -15,6 +15,7 @@
Minibar
Translation Quiz
GF online editor for simple multilingual grammars
+ Simple Translation Tool
Some Documentation
diff --git a/src/www/translator/about.html b/src/www/translator/about.html
new file mode 100644
index 000000000..395173162
--- /dev/null
+++ b/src/www/translator/about.html
@@ -0,0 +1,55 @@
+
+
+About: Simple Translation Tool
+
+
+
+
+
+
+
+
+
+
+
+About the Simple Translation Tool
+
+
+This is a simple bilingual document editor. Documents consist of a sequence
+of segments that are translated independently. The user can add segments
+in the source language and obtain automatically translated segments in
+the target language. If an unsatisfactory automatic translation is
+obtained, the user can click on it and replace it with a manual translation.
+
+
+The GF web service is used for automatic translation. The user picks which
+grammar to use from a menu of available grammars. Through menu options,
+the user also sets the source and target language for the document.
+
+
+The tool handles a set of documents. Documents can be named, saved (locally),
+closed and reopened later.
+
+
TODO
+
+ Test for browser compatibility (Safari & Firefox tested so far).
+ Use GF lexer/unlexer to allow for more natural looking text.
+ Import/export text.
+ Cloud service.
+ Interface to other translation services.
+ Interface to grammar editor for grammar extension.
+ ...
+ ...
+
+
+
+
+
+ Last modified: Tue May 15 17:35:39 CEST 2012
+
+
+TH
+
+
+
+
diff --git a/src/www/translator/index.html b/src/www/translator/index.html
new file mode 100644
index 000000000..7f3d46a22
--- /dev/null
+++ b/src/www/translator/index.html
@@ -0,0 +1,85 @@
+
+
+Simple Translation Tool
+
+
+
+
+
+
+
+
+
Simple Translation Tool
+
+
+
+
+...
+ This document translation editor requires JavaScript to work
+
+
+
+
+ Last modified: Tue May 15 16:17:32 CEST 2012
+
+About
+
+
+
+
+
+
+
+
diff --git a/src/www/translator/translator.css b/src/www/translator/translator.css
new file mode 100644
index 000000000..dff8d103a
--- /dev/null
+++ b/src/www/translator/translator.css
@@ -0,0 +1,54 @@
+body { margin: 5px; }
+h1 { float: right; margin: 0; font-size: 150%; }
+h2 { font-size: 120%; }
+div.pagehead { font-family: sans-serif;
+ background-color: #ccc;
+}
+table.menubar td { padding: 5px; }
+table.menubar dl, td.options > div > dl {
+ z-index: 1;
+ display: none; position: absolute;
+ background: white; color: black;
+ border: 1px solid black;
+ margin: 0;
+ box-shadow: 5px 5px 5px rgba(0,0,0,0.25);
+}
+table.menubar td:hover > dl { display: block; }
+table.menubar dt { margin: 0; padding: 5px; }
+table.submenu dt { padding: 0; }
+table.menubar td:hover, table.menubar dt:hover { background-color: #36f; color: white; }
+table table dl { left: 6em; }
+table.menubar dt { white-space: nowrap; }
+div.document {
+ clear: both;
+ background: white;
+ border: 2px solid #009;
+ padding: 0.6ex;
+}
+
+div.document h2 { color: #009; }
+
+table.segments { margin-left: auto; margin-right: auto; }
+tr.segment:hover { background: #ffc; }
+
+td.source, td.options, td.target {
+ padding: 1ex;
+}
+td.source, td.target {
+ border-bottom: 2px solid #ccc;
+}
+td.options > div { position: relative; margin: 0; }
+td.options:hover > div > dl { display: block; }
+td.options > div > dl {
+ left: 0.8em;
+ padding: 0.6ex;
+ font-family: sans-serif;
+ white-space: nowrap;
+}
+
+td.target input[name=it] {
+ width: 100%; font-family: inherit; font-size: inherit;
+}
+
+span.arrow { color: blue; }
+span.error { color: red; }
\ No newline at end of file
diff --git a/src/www/translator/translator.js b/src/www/translator/translator.js
new file mode 100644
index 000000000..a599b8d4a
--- /dev/null
+++ b/src/www/translator/translator.js
@@ -0,0 +1,429 @@
+
+
+/* --- Translator object ---------------------------------------------------- */
+
+function Translator() {
+ this.local=tr_local();
+ this.view=element("document")
+ this.current=this.local.get("current")
+ this.document=this.current && this.current!="/" && this.local.get("/"+this.current) || empty_document()
+ this.server=pgf_online({})
+ this.server.get_grammarlist(bind(this.extend_methods,this))
+ update_language_menu(this,"source")
+ update_language_menu(this,"target")
+ this.redraw();
+}
+
+function update_language_menu(t,id) {
+ var dl=element(id);
+ clear(dl);
+ for(var i in languages) {
+ var l=languages[i]
+ dl.appendChild(wrap("dt",radiobutton(id,l.code,l.name,bind(t.change,t))))
+ }
+}
+
+Translator.prototype.redraw=function() {
+ var t=this;
+ if(t.current=="/") t.browse()
+ else {
+ t.drawing=t.draw_document()
+ clear(t.view)
+ appendChildren(t.view,t.drawing.doc)
+ var o=t.document.options
+ update_radiobutton("method",o.method)
+ update_radiobutton("source",o.from)
+ update_radiobutton("target",o.to)
+ if(o.method!="Manual") {
+ function cont2(gr_info) {
+ t.grammar_info=gr_info
+ t.update_translations()
+ }
+ function cont1() { t.server.grammar_info(cont2)}
+ t.server.switch_grammar(o.method,cont1)
+ }
+ else
+ t.update_translations()
+ }
+}
+
+Translator.prototype.update_translations=function() {
+ var t=this
+ var doc=t.document
+ var o=doc.options
+ var ss=doc.segments
+ var ds=t.drawing.segments
+
+ function supported(concname) {
+ var l=t.grammar_info.languages
+ for(var i in l) if(l[i].name==concname) return true
+ return false
+ }
+
+ function update_translation(i) {
+ var segment=ss[i]
+ function replace(sd) {
+ var old=ds[i]
+ ds[i]=sd
+ replaceNode(sd,old)
+ }
+ function upd2(ts) {
+ switch(ts.length) {
+ case 1: segment.target=ts[0]; break;
+ case 0: segment.target="[no translation]";break;
+ default: segment.target="[ambiguous translation]"
+ }
+ segment.options=JSON.parse(JSON.stringify(o)) // no sharing!
+ replace(t.draw_segment(segment,i))
+ }
+ function upd(translate_output) {
+ //console.log(translate_output)
+ var ts=collect_texts(translate_output[0].translations)
+ upd2(ts)
+ }
+ var fs=supported(gfrom)
+ var ts=supported(gto)
+ if(fs && ts) {
+ if(segment.options.method!="Manual"
+ && JSON.stringify(segment.options)!=JSON.stringify(o))
+ t.server.translate({from:gfrom,to:gto,input:segment.source},upd)
+ }
+ else {
+ var fn=concname(o.from)
+ var tn=concname(o.to)
+ var unsup=" is not supported by the grammar"
+ var sup=" is supported by the grammar"
+ var msg= fs ? tn+unsup : ts ? fn+unsup :
+ "Neither "+fn+" nor "+tn+sup
+ upd2(["["+msg+"]"])
+ }
+ }
+
+ if(doc.options.method!="Manual") {
+ var gname=t.grammar_info.name
+ var gfrom=gname+o.from
+ var gto=gname+o.to
+ for(var i in ss) update_translation(i)
+ }
+}
+
+Translator.prototype.extend_methods=function(grammars) {
+ this.grammars=grammars
+ var dl=element("methods")
+ if(dl) for(var i in grammars) {
+ dl.appendChild(wrap("dt",radiobutton("method",grammars[i],
+ "GF: "+grammars[i],
+ bind(this.change,this))))
+ }
+ update_radiobutton("method",this.document.options.method)
+}
+
+Translator.prototype.change=function(el) {
+ var t=this
+ var o=t.document.options;
+ function update(field) {
+ if(el.value!=o[field]) {
+ o[field]=el.value
+ t.redraw()
+ }
+ }
+ switch(el.name) {
+ case "method": update("method"); break;
+ case "source": update("from"); break;
+ case "target": update("to"); break;
+ }
+}
+
+Translator.prototype.new=function(el) {
+ hide_menu(el);
+ this.current=null;
+ this.local.put("current",null)
+ this.document=empty_document()
+ this.redraw();
+}
+
+Translator.prototype.browse=function(el) {
+ hide_menu(el);
+ var t=this
+ function browse() {
+ var ul=empty_class("ul","files")
+ var pre=t.local.prefix+"/"
+ for(var i in localStorage) {
+ if(hasPrefix(i,pre)) {
+ var name=i.substr(pre.length)
+ var link=a(jsurl("translator.open('"+name+"')"),[text(name)])
+ ul.appendChild(li(link))
+ }
+ }
+ clear(t.view)
+ t.view.appendChild(wrap("h2",text("Your translator documents")))
+ t.view.appendChild(ul)
+ t.current="/"
+ t.local.put("current","/")
+ }
+ setTimeout(browse,100) // leave time to hide the menu first
+}
+
+Translator.prototype.open=function(name) {
+ var t=this
+ if(name) {
+ var path="/"+name
+ var document=t.local.get(path);
+ if(document) {
+ t.current=name;
+ t.local.put("current",name)
+ t.document=document;
+ t.redraw();
+ }
+ else alert("No such document: "+name)
+ }
+}
+
+Translator.prototype.save=function(el) {
+ hide_menu(el);
+ if(this.current!="/") {
+ if(this.current) this.local.put("/"+this.current,this.document)
+ else this.saveAs()
+ }
+}
+
+Translator.prototype.saveAs=function(el) {
+ hide_menu(el);
+ if(this.current!="/") {
+ var name=prompt("File name?")
+ if(name) {
+ this.current=this.document.name=name;
+ this.local.put("current",name)
+ this.save();
+ this.redraw();
+ }
+ }
+}
+
+Translator.prototype.close=function(el) {
+ hide_menu(el);
+ this.browse();
+}
+
+Translator.prototype.import=function(el) {
+ hide_menu(el);
+ var t=this
+ function imp() {
+ var text=prompt("Text segment to import?")
+ if(text) {
+ t.document.segments.push(new_segment(text))
+ t.redraw();
+ }
+ }
+ setTimeout(imp,100)
+}
+Translator.prototype.remove=function(el) {
+ hide_menu(el);
+ var t=this
+ function rm() {
+ if(t.document && t.document.segments.length>0) {
+ t.document.segments.pop();
+ t.redraw();
+ }
+ }
+ setTimeout(rm,100)
+}
+
+Translator.prototype.edit_translation=function(i) {
+ var t=this
+ var ds=t.drawing.segments
+
+ function replace_segment(sd) {
+ var old=ds[i]
+ ds[i]=sd
+ replaceNode(sd,old)
+ }
+
+ function edit_segment(s) {
+ function restore() { replace_segment(t.draw_segment(s,i)) }
+ function done() {
+ s.options.method="Manual"
+ s.options.from=t.document.options.from
+ s.options.to=t.document.options.to
+ s.target=inp.value // side effect, updating the document in-place
+ restore();
+ return false;
+ }
+ var inp=node("input",{name:"it",value:s.target})
+ var e=wrap("form",[inp, submit(), button("Cancel",restore)])
+ var target=wrap_class("td","target",e)
+ var edit=t.draw_segment_given_target(s,target)
+ replace_segment(edit)
+ e.onsubmit=done
+ inp.focus();
+ }
+ edit_segment(t.document.segments[i])
+}
+
+function hide_menu(el) {
+ function disp(s) { el.parentNode.style.display=s||""; }
+ if(el) {
+ disp("none")
+ setTimeout(disp,500)
+ }
+}
+
+/* --- Documents ------------------------------------------------------------ */
+
+/*
+type Document = { name:String, options: Options, segments:[Segment] }
+type Segment = { source:String, target:String, options:Options }
+type Options = {from: Lang, to: Lang, method:Method}
+type Lang = String // Eng, Swe, Ita, etc
+type Method = "Manual" | GrammarName
+type GrammarName = String // e.g. "Foods.pgf"
+*/
+
+Translator.prototype.draw_document=function() {
+ var t=this
+ var doc=t.document
+ var o=doc.options;
+ var segments=mapix(bind(t.draw_segment,t),doc.segments)
+ var drawing=[node("h2",{},[text(doc.name),text(" "),
+ wrap("small",draw_translation(o))]),
+ wrap_class("table","segments",segments)]
+ return {doc:drawing,segments:segments}
+}
+
+Translator.prototype.draw_segment=function(s,i) {
+ var t=this
+ var dopt=t.document.options
+ var opt=s.options
+ var txt=text(s.target)
+ if(opt.from!=dopt.from || opt.to!=dopt.to) txt=span_class("error",txt)
+ var target=wrap_class("td","target",txt)
+ function edit() { t.edit_translation(i) }
+ target.onclick=edit
+ return t.draw_segment_given_target(s,target)
+}
+
+Translator.prototype.draw_segment_given_target=function(s,target) {
+ var t=this
+
+ function draw_options2(o) {
+ function change(el) {
+ o.method=el.value // side effect, updating the document in-place
+ t.update_translations()
+ }
+ var manual=o.method=="Manual"
+ var autoB=radiobutton("method","Auto","Auto",change,!manual)
+ var manualB=radiobutton("method","Manual","Manual",change,manual)
+ return wrap("form",
+ [wrap("dt",autoB),
+ wrap("dt",manualB),
+ wrap("dt",draw_translation(o))])
+ }
+
+ function draw_options(o) {
+ return wrap("div",[span_class("arrow",text(" ⇒ ")),
+ wrap("dl",draw_options2(o))])
+ }
+
+ return wrap_class("tr","segment",
+ [wrap_class("td","source",text(s.source)),
+ wrap_class("td","options",draw_options(s.options)),
+ target])
+}
+
+function empty_document() {
+ return { name:"Unnamed",
+ options: {from:"Eng", to:"Swe", method:"Manual"},
+ segments:[] }
+}
+
+function new_segment(source) {
+ return { source:source, target:"", options:{} } // leave options empty
+}
+
+function draw_translation(o) {
+ return text("("+concname(o.from||"?")+" → "+concname(o.to||"?")+")")
+}
+
+/* --- Auxiliary functions -------------------------------------------------- */
+
+function lang(code,name) { return { code:code, name:name} }
+function lang1(name) {
+ var ws=name.split("/");
+ return ws.length==1 ? lang(name.substr(0,3),name) : lang(ws[0],ws[1]);
+}
+var languages =
+ map(lang1,"Amharic Arabic Bulgarian Catalan Danish Dutch English Finnish French German Hindi Ina/Interlingua Italian Japanese Latin Norwegian Polish Ron/Romanian Russian Spanish Swedish Thai Turkish Urdu".split(" "));
+
+var langname={};
+for(var i in languages)
+ langname[languages[i].code]=languages[i].name
+
+function concname(code) { return langname[code] || code; }
+
+
+function tr_local() {
+ var prefix="gf.translator."
+ function dummy() {
+ return {
+ prefix: prefix,
+ get: function(name,def) { return def },
+ put: function(name,value) { }
+ }
+ }
+ function real() {
+ return {
+ prefix: prefix,
+ get: function (name,def) {
+ var id=prefix+name
+ return localStorage[id] ? JSON.parse(localStorage[id]) : def;
+ },
+ put: function (name,value) {
+ var id=prefix+name;
+ localStorage[id]=JSON.stringify(value);
+ }
+ }
+ }
+ return window.localStorage ? real() : dummy()
+}
+
+// Collect alternative texts in the output from PGF service translate command
+function collect_texts(ts) {
+ var list=[]
+ for(var i in ts)
+ for(var j in ts[i].linearizations) {
+ var t=ts[i].linearizations[j].text
+ if(!elem(t,list)) list.push(t) // avoid duplicates
+ }
+ return list
+}
+
+function mapix(f,xs) {
+ var ys=[];
+ for(var i in xs) ys.push(f(xs[i],i))
+ return ys;
+}
+
+/* --- DOM Support ---------------------------------------------------------- */
+
+function a(url,linked) { return node("a",{href:url},linked); }
+function li(xs) { return wrap("li",xs); }
+function jsurl(js) { return "javascript:"+js; }
+
+function replaceNode(node,ref) { ref.parentNode.replaceChild(node,ref) }
+
+function radiobutton(name,value,label,change,checked) {
+ var b=node("input",{type:"radio",name:name,value:value})
+ if(change) b.onchange=function(event) { return change(event.target) }
+ if(checked) b.checked=true
+ return wrap("label",[b,text(label)])
+}
+
+function update_radiobutton(name,value) {
+ var bs=document.forms.options[name]
+ if(bs.length)
+ for(var i in bs) if(bs[i].value==value) bs[i].checked=true
+}
+
+function submit(label) {
+ return node("input",{type:"submit",value:label||"OK"})
+}