August 7, 2012

Introducing Franslate: French/German/Polish/Russian vocabulary flash quiz

I had a few hours free on Monday, so I spent them throwing together a French quiz. Since then, I added support for 3 other languages (Russian, German & Polish). Here's the process, and the results.

Producing Content

First, I found a frequency list of French words. I had to do some light scripting to sort them in order of decreasing frequency, and patched together a file containing the first 20k or so words, filtering out those that were fewer than 3 letters. I ran this through google translate, and created a file with a french word and its english translation on the same line, separated by a space.

Server-side

Next, I made a Sinatra app to serve up a random word selection. The app loads the wordlist and creates three hashes from it: beginner, intermediate, and advanced wordlists corresponding to the first 100, 1000, and 10000 words in the files respectively.

A view in the app serves up json containing a random word from the chosen wordlist, the correct translation, and 4 other english translations for different words in the wordlist to use as (incorrect) options.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
require 'sinatra'
require 'json'
require 'mustache'
 
## INITIALIZATION ##########################
 
configure do
LANGS_AVAILABLE = {
'de' => 'languages/de_20k_trans.txt',
'fr' => 'languages/fr_20k_trans.txt',
'pl' => 'languages/pl_20k_trans.txt',
'ru' => 'languages/ru_20k_trans.txt'
}
 
LEVELS_AVAILABLE = {
'beginner' => 100,
'intermediate' => 1000,
'advanced' => 10000
}
 
@@langs = {}
 
LANGS_AVAILABLE.each { |lang, filename|
@@langs[lang] = {}
LEVELS_AVAILABLE.each { |diff, num|
@@langs[lang][diff] = {}
}
 
i = 0
puts 'Loading ' + filename
File.open(filename) { |f|
f.each { |line|
tr, en = line.split(' ', 2)
 
if tr.strip.casecmp(en.strip) == 0
next
end
 
if i < 100
@@langs[lang]['beginner'][tr] = en;
end
 
if i < 1000
@@langs[lang]['intermediate'][tr] = en;
end
 
@@langs[lang]['advanced'][tr] = en;
 
i += 1
if i > 10000
break
end
}
}
}
 
end
 
## HELPERS ##############################################
 
def get_question(lang, difficulty)
src_wordset = @@langs[lang][difficulty]
french_word = src_wordset.keys().sample
correct_option = src_wordset[french_word]
 
{
'language' => lang,
'word' => french_word,
'correct_option' => correct_option,
'options' => (src_wordset.values().sample(4) + [correct_option]).shuffle.map{|i| {'name' => i}}
}.to_json
 
end
 
def render_template(tmpl_name)
Dir::chdir('templates'){ |d|
File.open(tmpl_name + '.mustache'){|f|
Mustache.render(f.read)
}
}
end
 
## VIEWS ##################################
 
get '/' do
render_template('index')
end
 
get '/about/' do
render_template('about')
end
 
get '/:lang/:difficulty/' do |lang, diff|
content_type :json
get_question(lang, diff)
end

Client-side

Finally, some Coffeescript+Backbone code contains all the machinery necessary to display the word and the multiple choice answers, plus handle the response and repeat the process.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
jQuery ->
 
class QuestionView extends Backbone.View
el: $('#main')
tmpl: mustache_templates.question
url: ''
 
events:
"click .answer": "check_answer"
 
initialize: (language, difficulty) ->
@url = "/#{language}/#{difficulty}/"
current_html = $(@el).html()
window.history.pushState({name: "Homepage"}, "Homepage", "/")
window.onpopstate = =>
window.location = "/"
 
_.bindAll @
 
@fetch_q()
 
fetch_q: ->
uniqueness_token = (new Date).getTime().toString()
$.ajax(
method: 'get'
url: @url + "?" + uniqueness_token
success: (data) =>
@q = data
@render()
)
 
 
check_answer: (e) ->
$('.incorrect', @el).remove()
$('.correct', @el).remove()
if $(e.target).html() == @q['correct_option']
$('h2', @el).after($('<div class="correct">Correct!</div>'))
setTimeout(@fetch_q, 500)
else
$('h2', @el).after($('<div class="incorrect">Incorrect. Please try again.</div>'))
 
 
render: ->
console.log(@q)
$(@el).html(Mustache.render(@tmpl, @q))
 
$('.initiate-quiz').click ->
parts = @id.split('-')
window.question = new QuestionView(parts[0], parts[1])

All told, the application weighs in at 96 lines of Ruby and 49 of Coffescript. I threw the mess up on Heroku, whose amazingly generous free plan must surely be a passing phase. After some DNS fuckery, I had a working app up at http://www.franslate.me/. Go ahead, try it out!