ajax translations

This commit is contained in:
Miguel Grinberg 2013-02-19 22:59:54 -08:00
parent 22ba5eed17
commit 4081db19e1
11 changed files with 232 additions and 72 deletions

View File

@ -84,6 +84,7 @@ class Post(db.Model):
body = db.Column(db.String(140)) body = db.Column(db.String(140))
timestamp = db.Column(db.DateTime) timestamp = db.Column(db.DateTime)
user_id = db.Column(db.Integer, db.ForeignKey('user.id')) user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
language = db.Column(db.String(5))
def __repr__(self): def __repr__(self):
return '<Post %r>' % (self.body) return '<Post %r>' % (self.body)

BIN
app/static/img/loading.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

View File

@ -15,6 +15,25 @@
<script src="/static/js/moment-{{g.locale}}.min.js"></script> <script src="/static/js/moment-{{g.locale}}.min.js"></script>
{% endif %} {% endif %}
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<script>
function translate(sourceLang, destLang, sourceId, destId, loadingId) {
$(destId).hide();
$(loadingId).show();
$.post('/translate', {
text: $(sourceId).text(),
sourceLang: sourceLang,
destLang: destLang
}).done(function(translated) {
$(destId).text(translated['text'])
$(loadingId).hide();
$(destId).show();
}).fail(function() {
$(destId).text("{{ _('Error: Could not contact server.') }}");
$(loadingId).hide();
$(destId).show();
});
}
</script>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
@ -48,4 +67,3 @@
</div> </div>
</body> </body>
</html> </html>

View File

@ -5,7 +5,15 @@
{% autoescape false %} {% autoescape false %}
<p>{{ _('%(nickname)s said %(when)s:', nickname = '<a href="%s">%s</a>' % (url_for('user', nickname = post.author.nickname), post.author.nickname), when = momentjs(post.timestamp).fromNow()) }}</p> <p>{{ _('%(nickname)s said %(when)s:', nickname = '<a href="%s">%s</a>' % (url_for('user', nickname = post.author.nickname), post.author.nickname), when = momentjs(post.timestamp).fromNow()) }}</p>
{% endautoescape %} {% endautoescape %}
<p><strong>{{post.body}}</strong></p> <p><strong><span id="post{{post.id}}">{{post.body}}</span></strong></p>
{% if post.language != None and post.language != '' and post.language != g.locale %}
<div>
<span id="translation{{post.id}}">
<a href="javascript:translate('{{post.language}}', '{{g.locale}}', '#post{{post.id}}', '#translation{{post.id}}', '#loading{{post.id}}');">{{ _('Translate') }}</a>
</span>
<img id="loading{{post.id}}" style="display: none" src="/static/img/loading.gif">
</div>
{% endif %}
</td> </td>
</tr> </tr>
</table> </table>

56
app/translate.py Executable file
View File

@ -0,0 +1,56 @@
import urllib, httplib
import json
from app import app
from flask.ext.babel import gettext
from config import MS_TRANSLATOR_CLIENT_ID, MS_TRANSLATOR_CLIENT_SECRET
def microsoft_translate(text, sourceLang, destLang):
if MS_TRANSLATOR_CLIENT_ID == "" or MS_TRANSLATOR_CLIENT_SECRET == "":
return gettext('Error: translation service not configured.')
try:
# get access token
params = urllib.urlencode({
'client_id': MS_TRANSLATOR_CLIENT_ID,
'client_secret': MS_TRANSLATOR_CLIENT_SECRET,
'scope': 'http://api.microsofttranslator.com',
'grant_type': 'client_credentials'
})
conn = httplib.HTTPSConnection("datamarket.accesscontrol.windows.net")
conn.request("POST", "/v2/OAuth2-13", params)
response = json.loads (conn.getresponse().read())
token = response[u'access_token']
# translate
conn = httplib.HTTPConnection('api.microsofttranslator.com')
params = {
'appId': 'Bearer ' + token,
'from': sourceLang,
'to': destLang,
'text': text.encode("utf-8")
}
conn.request("GET", '/V2/Ajax.svc/Translate?' + urllib.urlencode(params))
response = json.loads("{\"response\":" + conn.getresponse().read().decode('utf-8-sig') + "}")
return response["response"]
except:
#return gettext('Error: Unexpected error.')
raise
def google_translate(text, sourceLang, destLang):
if not app.debug:
return gettext('Error: translation service not available.')
try:
params = urllib.urlencode({
'client': 't',
'text': text.encode("utf-8"),
'sl': sourceLang,
'tl': destLang,
'ie': 'UTF-8',
'oe': 'UTF-8'
})
conn = httplib.HTTPSConnection("translate.google.com")
conn.request("GET", "/translate_a/t?" + params, headers = { 'User-Agent': 'Mozilla/5.0' })
httpresponse = conn.getresponse().read().replace(",,,", ",\"\",\"\",").replace(",,", ",\"\",")
response = json.loads("{\"response\":" + httpresponse + "}")
return response["response"][0][0][0]
except:
return gettext('Error: Unexpected error.')

View File

@ -7,8 +7,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2013-01-31 23:19-0800\n" "POT-Creation-Date: 2013-02-19 22:11-0800\n"
"PO-Revision-Date: 2013-01-31 23:19-0800\n" "PO-Revision-Date: 2013-02-19 22:12-0800\n"
"Last-Translator: Miguel Grinberg <miguel.grinberg@gmail.com>\n" "Last-Translator: Miguel Grinberg <miguel.grinberg@gmail.com>\n"
"Language-Team: es <LL@li.org>\n" "Language-Team: es <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@ -34,47 +34,59 @@ msgstr ""
msgid "This nickname is already in use. Please choose another one." msgid "This nickname is already in use. Please choose another one."
msgstr "Este nombre de usuario ya esta usado. Por favor elije otro." msgstr "Este nombre de usuario ya esta usado. Por favor elije otro."
#: app/views.py:49 #: app/translate.py:9
msgid "Error: translation service not configured."
msgstr "Error: el servicio de traducción no está configurado."
#: app/translate.py:35 app/translate.py:55
msgid "Error: Unexpected error."
msgstr "Error: Un error inesperado ha ocurrido."
#: app/translate.py:39
msgid "Error: translation service not available."
msgstr "Error: servicio de traducción no disponible."
#: app/views.py:56
msgid "Your post is now live!" msgid "Your post is now live!"
msgstr "¡Tu artículo ha sido publicado!" msgstr "¡Tu artículo ha sido publicado!"
#: app/views.py:74 #: app/views.py:81
msgid "Invalid login. Please try again." msgid "Invalid login. Please try again."
msgstr "Credenciales inválidas. Por favor intenta de nuevo." msgstr "Credenciales inválidas. Por favor intenta de nuevo."
#: app/views.py:107 #: app/views.py:114
#, python-format #, python-format
msgid "User %(nickname)s not found." msgid "User %(nickname)s not found."
msgstr "El usuario %(nickname)s no existe." msgstr "El usuario %(nickname)s no existe."
#: app/views.py:123 #: app/views.py:130
msgid "Your changes have been saved." msgid "Your changes have been saved."
msgstr "Tus cambios han sido guardados." msgstr "Tus cambios han sido guardados."
#: app/views.py:139 #: app/views.py:146
msgid "You can't follow yourself!" msgid "You can't follow yourself!"
msgstr "¡No te puedes seguir a tí mismo!" msgstr "¡No te puedes seguir a tí mismo!"
#: app/views.py:143 #: app/views.py:150
#, python-format #, python-format
msgid "Cannot follow %(nickname)s." msgid "Cannot follow %(nickname)s."
msgstr "No se pudo seguir a %(nickname)s." msgstr "No se pudo seguir a %(nickname)s."
#: app/views.py:147 #: app/views.py:154
#, python-format #, python-format
msgid "You are now following %(nickname)s!" msgid "You are now following %(nickname)s!"
msgstr "¡Ya estás siguiendo a %(nickname)s!" msgstr "¡Ya estás siguiendo a %(nickname)s!"
#: app/views.py:159 #: app/views.py:166
msgid "You can't unfollow yourself!" msgid "You can't unfollow yourself!"
msgstr "¡No te puedes dejar de seguir a tí mismo!" msgstr "¡No te puedes dejar de seguir a tí mismo!"
#: app/views.py:163 #: app/views.py:170
#, python-format #, python-format
msgid "Cannot unfollow %(nickname)s." msgid "Cannot unfollow %(nickname)s."
msgstr "No se pudo dejar de seguir a %(nickname)s." msgstr "No se pudo dejar de seguir a %(nickname)s."
#: app/views.py:167 #: app/views.py:174
#, python-format #, python-format
msgid "You have stopped following %(nickname)s." msgid "You have stopped following %(nickname)s."
msgstr "Ya no sigues más a %(nickname)s." msgstr "Ya no sigues más a %(nickname)s."
@ -95,19 +107,23 @@ msgstr "Un error inesperado ha ocurrido"
msgid "The administrator has been notified. Sorry for the inconvenience!" msgid "The administrator has been notified. Sorry for the inconvenience!"
msgstr "El administrador ha sido notificado. ¡Lo lamento!" msgstr "El administrador ha sido notificado. ¡Lo lamento!"
#: app/templates/base.html:30 #: app/templates/base.html:31
msgid "Error: Could not contact server."
msgstr "Error: No es posible contactar al servidor."
#: app/templates/base.html:49
msgid "Home" msgid "Home"
msgstr "Inicio" msgstr "Inicio"
#: app/templates/base.html:32 #: app/templates/base.html:51
msgid "Your Profile" msgid "Your Profile"
msgstr "Tu Perfil" msgstr "Tu Perfil"
#: app/templates/base.html:33 #: app/templates/base.html:52
msgid "Logout" msgid "Logout"
msgstr "Desconectarse" msgstr "Desconectarse"
#: app/templates/base.html:38 #: app/templates/base.html:57
msgid "Search" msgid "Search"
msgstr "Buscar" msgstr "Buscar"
@ -188,6 +204,10 @@ msgstr "Ingresar"
msgid "%(nickname)s said %(when)s:" msgid "%(nickname)s said %(when)s:"
msgstr "%(nickname)s dijo %(when)s:" msgstr "%(nickname)s dijo %(when)s:"
#: app/templates/post.html:12
msgid "Translate"
msgstr "Traducir"
#: app/templates/search_results.html:5 #: app/templates/search_results.html:5
#, python-format #, python-format
msgid "Search results for \"%(query)s\":" msgid "Search results for \"%(query)s\":"

View File

@ -1,4 +1,4 @@
from flask import render_template, flash, redirect, session, url_for, request, g from flask import render_template, flash, redirect, session, url_for, request, g, jsonify
from flask.ext.login import login_user, logout_user, current_user, login_required from flask.ext.login import login_user, logout_user, current_user, login_required
from flask.ext.babel import gettext from flask.ext.babel import gettext
from app import app, db, lm, oid, babel from app import app, db, lm, oid, babel
@ -6,8 +6,9 @@ from forms import LoginForm, EditForm, PostForm, SearchForm
from models import User, ROLE_USER, ROLE_ADMIN, Post from models import User, ROLE_USER, ROLE_ADMIN, Post
from datetime import datetime from datetime import datetime
from emails import follower_notification from emails import follower_notification
from config import POSTS_PER_PAGE, MAX_SEARCH_RESULTS from guess_language import guessLanguage
from config import LANGUAGES from translate import microsoft_translate
from config import POSTS_PER_PAGE, MAX_SEARCH_RESULTS, LANGUAGES
@lm.user_loader @lm.user_loader
def load_user(id): def load_user(id):
@ -43,7 +44,13 @@ def internal_error(error):
def index(page = 1): def index(page = 1):
form = PostForm() form = PostForm()
if form.validate_on_submit(): if form.validate_on_submit():
post = Post(body = form.post.data, timestamp = datetime.utcnow(), author = g.user) language = guessLanguage(form.post.data)
if language == 'UNKNOWN' or len(language) > 5:
language = ''
post = Post(body = form.post.data,
timestamp = datetime.utcnow(),
author = g.user,
language = language)
db.session.add(post) db.session.add(post)
db.session.commit() db.session.commit()
flash(gettext('Your post is now live!')) flash(gettext('Your post is now live!'))
@ -182,3 +189,12 @@ def search_results(query):
query = query, query = query,
results = results) results = results)
@app.route('/translate', methods = ['POST'])
@login_required
def translate():
return jsonify({
'text': microsoft_translate(
request.form['text'],
request.form['sourceLang'],
request.form['destLang']) })

View File

@ -30,6 +30,10 @@ LANGUAGES = {
'es': 'Español' 'es': 'Español'
} }
# microsoft translation service
MS_TRANSLATOR_CLIENT_ID = '' # enter your MS translator app id here
MS_TRANSLATOR_CLIENT_SECRET = '' # enter your MS translator app secret here
# administrator list # administrator list
ADMINS = ['you@example.com'] ADMINS = ['you@example.com']

View File

@ -0,0 +1,29 @@
from sqlalchemy import *
from migrate import *
from migrate.changeset import schema
pre_meta = MetaData()
post_meta = MetaData()
post = Table('post', post_meta,
Column('id', Integer, primary_key=True, nullable=False),
Column('body', String(length=140)),
Column('timestamp', DateTime),
Column('user_id', Integer),
Column('language', String(length=5)),
)
def upgrade(migrate_engine):
# Upgrade operations go here. Don't create your own engine; bind
# migrate_engine to your metadata
pre_meta.bind = migrate_engine
post_meta.bind = migrate_engine
post_meta.tables['post'].columns['language'].create()
def downgrade(migrate_engine):
# Operations to reverse the above upgrade go here.
pre_meta.bind = migrate_engine
post_meta.bind = migrate_engine
post_meta.tables['post'].columns['language'].drop()

View File

@ -18,4 +18,5 @@ subprocess.call([os.path.join('flask', bin, 'pip'), 'install', 'sqlalchemy-migra
subprocess.call([os.path.join('flask', bin, 'pip'), 'install', 'flask-whooshalchemy']) subprocess.call([os.path.join('flask', bin, 'pip'), 'install', 'flask-whooshalchemy'])
subprocess.call([os.path.join('flask', bin, 'pip'), 'install', 'flask-wtf']) subprocess.call([os.path.join('flask', bin, 'pip'), 'install', 'flask-wtf'])
subprocess.call([os.path.join('flask', bin, 'pip'), 'install', 'flask-babel']) subprocess.call([os.path.join('flask', bin, 'pip'), 'install', 'flask-babel'])
subprocess.call([os.path.join('flask', bin, 'pip'), 'install', 'guess-language'])
subprocess.call([os.path.join('flask', bin, 'pip'), 'install', 'flup']) subprocess.call([os.path.join('flask', bin, 'pip'), 'install', 'flup'])

103
tests.py
View File

@ -1,4 +1,6 @@
#!flask/bin/python #!flask/bin/python
# -*- coding: utf8 -*-
import os import os
import unittest import unittest
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -6,6 +8,7 @@ from datetime import datetime, timedelta
from config import basedir from config import basedir
from app import app, db from app import app, db
from app.models import User, Post from app.models import User, Post
from app.translate import microsoft_translate
class TestCase(unittest.TestCase): class TestCase(unittest.TestCase):
def setUp(self): def setUp(self):
@ -64,54 +67,58 @@ class TestCase(unittest.TestCase):
assert u1.followed.count() == 0 assert u1.followed.count() == 0
assert u2.followers.count() == 0 assert u2.followers.count() == 0
def test_follow_posts(self): def test_follow_posts(self):
# make four users # make four users
u1 = User(nickname = 'john', email = 'john@example.com') u1 = User(nickname = 'john', email = 'john@example.com')
u2 = User(nickname = 'susan', email = 'susan@example.com') u2 = User(nickname = 'susan', email = 'susan@example.com')
u3 = User(nickname = 'mary', email = 'mary@example.com') u3 = User(nickname = 'mary', email = 'mary@example.com')
u4 = User(nickname = 'david', email = 'david@example.com') u4 = User(nickname = 'david', email = 'david@example.com')
db.session.add(u1) db.session.add(u1)
db.session.add(u2) db.session.add(u2)
db.session.add(u3) db.session.add(u3)
db.session.add(u4) db.session.add(u4)
# make four posts # make four posts
utcnow = datetime.utcnow() utcnow = datetime.utcnow()
p1 = Post(body = "post from john", author = u1, timestamp = utcnow + timedelta(seconds = 1)) p1 = Post(body = "post from john", author = u1, timestamp = utcnow + timedelta(seconds = 1))
p2 = Post(body = "post from susan", author = u2, timestamp = utcnow + timedelta(seconds = 2)) p2 = Post(body = "post from susan", author = u2, timestamp = utcnow + timedelta(seconds = 2))
p3 = Post(body = "post from mary", author = u3, timestamp = utcnow + timedelta(seconds = 3)) p3 = Post(body = "post from mary", author = u3, timestamp = utcnow + timedelta(seconds = 3))
p4 = Post(body = "post from david", author = u4, timestamp = utcnow + timedelta(seconds = 4)) p4 = Post(body = "post from david", author = u4, timestamp = utcnow + timedelta(seconds = 4))
db.session.add(p1) db.session.add(p1)
db.session.add(p2) db.session.add(p2)
db.session.add(p3) db.session.add(p3)
db.session.add(p4) db.session.add(p4)
db.session.commit() db.session.commit()
# setup the followers # setup the followers
u1.follow(u1) # john follows himself u1.follow(u1) # john follows himself
u1.follow(u2) # john follows susan u1.follow(u2) # john follows susan
u1.follow(u4) # john follows david u1.follow(u4) # john follows david
u2.follow(u2) # susan follows herself u2.follow(u2) # susan follows herself
u2.follow(u3) # susan follows mary u2.follow(u3) # susan follows mary
u3.follow(u3) # mary follows herself u3.follow(u3) # mary follows herself
u3.follow(u4) # mary follows david u3.follow(u4) # mary follows david
u4.follow(u4) # david follows himself u4.follow(u4) # david follows himself
db.session.add(u1) db.session.add(u1)
db.session.add(u2) db.session.add(u2)
db.session.add(u3) db.session.add(u3)
db.session.add(u4) db.session.add(u4)
db.session.commit() db.session.commit()
# check the followed posts of each user # check the followed posts of each user
f1 = u1.followed_posts().all() f1 = u1.followed_posts().all()
f2 = u2.followed_posts().all() f2 = u2.followed_posts().all()
f3 = u3.followed_posts().all() f3 = u3.followed_posts().all()
f4 = u4.followed_posts().all() f4 = u4.followed_posts().all()
assert len(f1) == 3 assert len(f1) == 3
assert len(f2) == 2 assert len(f2) == 2
assert len(f3) == 2 assert len(f3) == 2
assert len(f4) == 1 assert len(f4) == 1
assert f1 == [p4, p2, p1] assert f1 == [p4, p2, p1]
assert f2 == [p3, p2] assert f2 == [p3, p2]
assert f3 == [p4, p3] assert f3 == [p4, p3]
assert f4 == [p4] assert f4 == [p4]
def test_translation(self):
assert microsoft_translate(u'English', 'en', 'es') == u'Inglés'
assert microsoft_translate(u'Español', 'es', 'en') == u'Spanish'
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()