Chapter 13: I18n and L10n (v0.13)
This commit is contained in:
		
							parent
							
								
									ce9875cbd8
								
							
						
					
					
						commit
						6aa0ad2de6
					
				|  | @ -1,13 +1,14 @@ | |||
| import logging | ||||
| from logging.handlers import SMTPHandler, RotatingFileHandler | ||||
| import os | ||||
| from flask import Flask | ||||
| from flask import Flask, request | ||||
| from flask_sqlalchemy import SQLAlchemy | ||||
| from flask_migrate import Migrate | ||||
| from flask_login import LoginManager | ||||
| from flask_mail import Mail | ||||
| from flask_bootstrap import Bootstrap | ||||
| from flask_moment import Moment | ||||
| from flask_babel import Babel, lazy_gettext as _l | ||||
| from config import Config | ||||
| 
 | ||||
| app = Flask(__name__) | ||||
|  | @ -16,9 +17,11 @@ db = SQLAlchemy(app) | |||
| migrate = Migrate(app, db) | ||||
| login = LoginManager(app) | ||||
| login.login_view = 'login' | ||||
| login.login_message = _l('Please log in to access this page.') | ||||
| mail = Mail(app) | ||||
| bootstrap = Bootstrap(app) | ||||
| moment = Moment(app) | ||||
| babel = Babel(app) | ||||
| 
 | ||||
| if not app.debug: | ||||
|     if app.config['MAIL_SERVER']: | ||||
|  | @ -48,4 +51,10 @@ if not app.debug: | |||
|     app.logger.setLevel(logging.INFO) | ||||
|     app.logger.info('Microblog startup') | ||||
| 
 | ||||
| 
 | ||||
| @babel.localeselector | ||||
| def get_locale(): | ||||
|     return request.accept_languages.best_match(app.config['LANGUAGES']) | ||||
| 
 | ||||
| 
 | ||||
| from app import routes, models, errors | ||||
|  |  | |||
|  | @ -0,0 +1,38 @@ | |||
| import os | ||||
| import click | ||||
| from app import app | ||||
| 
 | ||||
| 
 | ||||
| @app.cli.group() | ||||
| def translate(): | ||||
|     """Translation and localization commands.""" | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| @translate.command() | ||||
| @click.argument('lang') | ||||
| def init(lang): | ||||
|     """Initialize a new language.""" | ||||
|     if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'): | ||||
|         raise RuntimeError('extract command failed') | ||||
|     if os.system( | ||||
|             'pybabel init -i messages.pot -d app/translations -l ' + lang): | ||||
|         raise RuntimeError('init command failed') | ||||
|     os.remove('messages.pot') | ||||
| 
 | ||||
| 
 | ||||
| @translate.command() | ||||
| def update(): | ||||
|     """Update all languages.""" | ||||
|     if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'): | ||||
|         raise RuntimeError('extract command failed') | ||||
|     if os.system('pybabel update -i messages.pot -d app/translations'): | ||||
|         raise RuntimeError('update command failed') | ||||
|     os.remove('messages.pot') | ||||
| 
 | ||||
| 
 | ||||
| @translate.command() | ||||
| def compile(): | ||||
|     """Compile all languages.""" | ||||
|     if os.system('pybabel compile -d app/translations'): | ||||
|         raise RuntimeError('compile command failed') | ||||
|  | @ -1,6 +1,7 @@ | |||
| from threading import Thread | ||||
| from flask import render_template | ||||
| from flask_mail import Message | ||||
| from flask_babel import _ | ||||
| from app import app, mail | ||||
| 
 | ||||
| 
 | ||||
|  | @ -18,7 +19,7 @@ def send_email(subject, sender, recipients, text_body, html_body): | |||
| 
 | ||||
| def send_password_reset_email(user): | ||||
|     token = user.get_reset_password_token() | ||||
|     send_email('[Microblog] Reset Your Password', | ||||
|     send_email(_('[Microblog] Reset Your Password'), | ||||
|                sender=app.config['ADMINS'][0], | ||||
|                recipients=[user.email], | ||||
|                text_body=render_template('email/reset_password.txt', | ||||
|  |  | |||
							
								
								
									
										48
									
								
								app/forms.py
								
								
								
								
							
							
						
						
									
										48
									
								
								app/forms.py
								
								
								
								
							|  | @ -3,51 +3,55 @@ from wtforms import StringField, PasswordField, BooleanField, SubmitField, \ | |||
|     TextAreaField | ||||
| from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, \ | ||||
|     Length | ||||
| from flask_babel import _, lazy_gettext as _l | ||||
| from app.models import User | ||||
| 
 | ||||
| 
 | ||||
| class LoginForm(FlaskForm): | ||||
|     username = StringField('Username', validators=[DataRequired()]) | ||||
|     password = PasswordField('Password', validators=[DataRequired()]) | ||||
|     remember_me = BooleanField('Remember Me') | ||||
|     submit = SubmitField('Sign In') | ||||
|     username = StringField(_l('Username'), validators=[DataRequired()]) | ||||
|     password = PasswordField(_l('Password'), validators=[DataRequired()]) | ||||
|     remember_me = BooleanField(_l('Remember Me')) | ||||
|     submit = SubmitField(_l('Sign In')) | ||||
| 
 | ||||
| 
 | ||||
| class RegistrationForm(FlaskForm): | ||||
|     username = StringField('Username', validators=[DataRequired()]) | ||||
|     email = StringField('Email', validators=[DataRequired(), Email()]) | ||||
|     password = PasswordField('Password', validators=[DataRequired()]) | ||||
|     username = StringField(_l('Username'), validators=[DataRequired()]) | ||||
|     email = StringField(_l('Email'), validators=[DataRequired(), Email()]) | ||||
|     password = PasswordField(_l('Password'), validators=[DataRequired()]) | ||||
|     password2 = PasswordField( | ||||
|         'Repeat Password', validators=[DataRequired(), EqualTo('password')]) | ||||
|     submit = SubmitField('Register') | ||||
|         _l('Repeat Password'), validators=[DataRequired(), | ||||
|                                            EqualTo('password')]) | ||||
|     submit = SubmitField(_l('Register')) | ||||
| 
 | ||||
|     def validate_username(self, username): | ||||
|         user = User.query.filter_by(username=username.data).first() | ||||
|         if user is not None: | ||||
|             raise ValidationError('Please use a different username.') | ||||
|             raise ValidationError(_('Please use a different username.')) | ||||
| 
 | ||||
|     def validate_email(self, email): | ||||
|         user = User.query.filter_by(email=email.data).first() | ||||
|         if user is not None: | ||||
|             raise ValidationError('Please use a different email address.') | ||||
|             raise ValidationError(_('Please use a different email address.')) | ||||
| 
 | ||||
| 
 | ||||
| class ResetPasswordRequestForm(FlaskForm): | ||||
|     email = StringField('Email', validators=[DataRequired(), Email()]) | ||||
|     submit = SubmitField('Request Password Reset') | ||||
|     email = StringField(_l('Email'), validators=[DataRequired(), Email()]) | ||||
|     submit = SubmitField(_l('Request Password Reset')) | ||||
| 
 | ||||
| 
 | ||||
| class ResetPasswordForm(FlaskForm): | ||||
|     password = PasswordField('Password', validators=[DataRequired()]) | ||||
|     password = PasswordField(_l('Password'), validators=[DataRequired()]) | ||||
|     password2 = PasswordField( | ||||
|         'Repeat Password', validators=[DataRequired(), EqualTo('password')]) | ||||
|     submit = SubmitField('Request Password Reset') | ||||
|         _l('Repeat Password'), validators=[DataRequired(), | ||||
|                                            EqualTo('password')]) | ||||
|     submit = SubmitField(_l('Request Password Reset')) | ||||
| 
 | ||||
| 
 | ||||
| class EditProfileForm(FlaskForm): | ||||
|     username = StringField('Username', validators=[DataRequired()]) | ||||
|     about_me = TextAreaField('About me', validators=[Length(min=0, max=140)]) | ||||
|     submit = SubmitField('Submit') | ||||
|     username = StringField(_l('Username'), validators=[DataRequired()]) | ||||
|     about_me = TextAreaField(_l('About me'), | ||||
|                              validators=[Length(min=0, max=140)]) | ||||
|     submit = SubmitField(_l('Submit')) | ||||
| 
 | ||||
|     def __init__(self, original_username, *args, **kwargs): | ||||
|         super(EditProfileForm, self).__init__(*args, **kwargs) | ||||
|  | @ -57,9 +61,9 @@ class EditProfileForm(FlaskForm): | |||
|         if username.data != self.original_username: | ||||
|             user = User.query.filter_by(username=self.username.data).first() | ||||
|             if user is not None: | ||||
|                 raise ValidationError('Please use a different username.') | ||||
|                 raise ValidationError(_('Please use a different username.')) | ||||
| 
 | ||||
| 
 | ||||
| class PostForm(FlaskForm): | ||||
|     post = TextAreaField('Say something', validators=[DataRequired()]) | ||||
|     submit = SubmitField('Submit') | ||||
|     post = TextAreaField(_l('Say something'), validators=[DataRequired()]) | ||||
|     submit = SubmitField(_l('Submit')) | ||||
|  |  | |||
|  | @ -1,7 +1,8 @@ | |||
| from datetime import datetime | ||||
| from flask import render_template, flash, redirect, url_for, request | ||||
| from flask import render_template, flash, redirect, url_for, request, g | ||||
| from flask_login import login_user, logout_user, current_user, login_required | ||||
| from werkzeug.urls import url_parse | ||||
| from flask_babel import _, get_locale | ||||
| from app import app, db | ||||
| from app.forms import LoginForm, RegistrationForm, EditProfileForm, PostForm, \ | ||||
|     ResetPasswordRequestForm, ResetPasswordForm | ||||
|  | @ -14,6 +15,7 @@ def before_request(): | |||
|     if current_user.is_authenticated: | ||||
|         current_user.last_seen = datetime.utcnow() | ||||
|         db.session.commit() | ||||
|     g.locale = str(get_locale()) | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/', methods=['GET', 'POST']) | ||||
|  | @ -25,7 +27,7 @@ def index(): | |||
|         post = Post(body=form.post.data, author=current_user) | ||||
|         db.session.add(post) | ||||
|         db.session.commit() | ||||
|         flash('Your post is now live!') | ||||
|         flash(_('Your post is now live!')) | ||||
|         return redirect(url_for('index')) | ||||
|     page = request.args.get('page', 1, type=int) | ||||
|     posts = current_user.followed_posts().paginate( | ||||
|  | @ -34,7 +36,7 @@ def index(): | |||
|         if posts.has_next else None | ||||
|     prev_url = url_for('explore', page=posts.prev_num) \ | ||||
|         if posts.has_prev else None | ||||
|     return render_template('index.html', title='Home', form=form, | ||||
|     return render_template('index.html', title=_('Home'), form=form, | ||||
|                            posts=posts.items, next_url=next_url, | ||||
|                            prev_url=prev_url) | ||||
| 
 | ||||
|  | @ -49,8 +51,9 @@ def explore(): | |||
|         if posts.has_next else None | ||||
|     prev_url = url_for('explore', page=posts.prev_num) \ | ||||
|         if posts.has_prev else None | ||||
|     return render_template('index.html', title='Explore', posts=posts.items, | ||||
|                            next_url=next_url, prev_url=prev_url) | ||||
|     return render_template('index.html', title=_('Explore'), | ||||
|                            posts=posts.items, next_url=next_url, | ||||
|                            prev_url=prev_url) | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/login', methods=['GET', 'POST']) | ||||
|  | @ -61,14 +64,14 @@ def login(): | |||
|     if form.validate_on_submit(): | ||||
|         user = User.query.filter_by(username=form.username.data).first() | ||||
|         if user is None or not user.check_password(form.password.data): | ||||
|             flash('Invalid username or password') | ||||
|             flash(_('Invalid username or password')) | ||||
|             return redirect(url_for('login')) | ||||
|         login_user(user, remember=form.remember_me.data) | ||||
|         next_page = request.args.get('next') | ||||
|         if not next_page or url_parse(next_page).netloc != '': | ||||
|             next_page = url_for('index') | ||||
|         return redirect(next_page) | ||||
|     return render_template('login.html', title='Sign In', form=form) | ||||
|     return render_template('login.html', title=_('Sign In'), form=form) | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/logout') | ||||
|  | @ -87,9 +90,9 @@ def register(): | |||
|         user.set_password(form.password.data) | ||||
|         db.session.add(user) | ||||
|         db.session.commit() | ||||
|         flash('Congratulations, you are now a registered user!') | ||||
|         flash(_('Congratulations, you are now a registered user!')) | ||||
|         return redirect(url_for('login')) | ||||
|     return render_template('register.html', title='Register', form=form) | ||||
|     return render_template('register.html', title=_('Register'), form=form) | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/reset_password_request', methods=['GET', 'POST']) | ||||
|  | @ -101,10 +104,11 @@ def reset_password_request(): | |||
|         user = User.query.filter_by(email=form.email.data).first() | ||||
|         if user: | ||||
|             send_password_reset_email(user) | ||||
|         flash('Check your email for the instructions to reset your password') | ||||
|         flash( | ||||
|             _('Check your email for the instructions to reset your password')) | ||||
|         return redirect(url_for('login')) | ||||
|     return render_template('reset_password_request.html', | ||||
|                            title='Reset Password', form=form) | ||||
|                            title=_('Reset Password'), form=form) | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/reset_password/<token>', methods=['GET', 'POST']) | ||||
|  | @ -118,7 +122,7 @@ def reset_password(token): | |||
|     if form.validate_on_submit(): | ||||
|         user.set_password(form.password.data) | ||||
|         db.session.commit() | ||||
|         flash('Your password has been reset.') | ||||
|         flash(_('Your password has been reset.')) | ||||
|         return redirect(url_for('login')) | ||||
|     return render_template('reset_password.html', form=form) | ||||
| 
 | ||||
|  | @ -146,12 +150,12 @@ def edit_profile(): | |||
|         current_user.username = form.username.data | ||||
|         current_user.about_me = form.about_me.data | ||||
|         db.session.commit() | ||||
|         flash('Your changes have been saved.') | ||||
|         flash(_('Your changes have been saved.')) | ||||
|         return redirect(url_for('edit_profile')) | ||||
|     elif request.method == 'GET': | ||||
|         form.username.data = current_user.username | ||||
|         form.about_me.data = current_user.about_me | ||||
|     return render_template('edit_profile.html', title='Edit Profile', | ||||
|     return render_template('edit_profile.html', title=_('Edit Profile'), | ||||
|                            form=form) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -160,14 +164,14 @@ def edit_profile(): | |||
| def follow(username): | ||||
|     user = User.query.filter_by(username=username).first() | ||||
|     if user is None: | ||||
|         flash('User {} not found.'.format(username)) | ||||
|         flash(_('User %(username)s not found.', username=username)) | ||||
|         return redirect(url_for('index')) | ||||
|     if user == current_user: | ||||
|         flash('You cannot follow yourself!') | ||||
|         flash(_('You cannot follow yourself!')) | ||||
|         return redirect(url_for('user', username=username)) | ||||
|     current_user.follow(user) | ||||
|     db.session.commit() | ||||
|     flash('You are following {}!'.format(username)) | ||||
|     flash(_('You are following %(username)s!', username=username)) | ||||
|     return redirect(url_for('user', username=username)) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -176,12 +180,12 @@ def follow(username): | |||
| def unfollow(username): | ||||
|     user = User.query.filter_by(username=username).first() | ||||
|     if user is None: | ||||
|         flash('User {} not found.'.format(username)) | ||||
|         flash(_('User %(username)s not found.', username=username)) | ||||
|         return redirect(url_for('index')) | ||||
|     if user == current_user: | ||||
|         flash('You cannot unfollow yourself!') | ||||
|         flash(_('You cannot unfollow yourself!')) | ||||
|         return redirect(url_for('user', username=username)) | ||||
|     current_user.unfollow(user) | ||||
|     db.session.commit() | ||||
|     flash('You are not following {}.'.format(username)) | ||||
|     flash(_('You are not following %(username)s.', username=username)) | ||||
|     return redirect(url_for('user', username=username)) | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| {% extends "base.html" %} | ||||
| 
 | ||||
| {% block app_content %} | ||||
|     <h1>Not Found</h1> | ||||
|     <p><a href="{{ url_for('index') }}">Back</a></p> | ||||
|     <h1>{{ _('Not Found') }}</h1> | ||||
|     <p><a href="{{ url_for('index') }}">{{ _('Back') }}</a></p> | ||||
| {% endblock %} | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| {% extends "base.html" %} | ||||
| 
 | ||||
| {% block app_content %} | ||||
|     <h1>An unexpected error has occurred</h1> | ||||
|     <p>The administrator has been notified. Sorry for the inconvenience!</p> | ||||
|     <p><a href="{{ url_for('index') }}">Back</a></p> | ||||
|     <h1>{{ _('An unexpected error has occurred') }}</h1> | ||||
|     <p>{{ _('The administrator has been notified. Sorry for the inconvenience!') }}</p> | ||||
|     <p><a href="{{ url_for('index') }}">{{ _('Back') }}</a></p> | ||||
| {% endblock %} | ||||
|  |  | |||
|  | @ -6,10 +6,13 @@ | |||
|                 </a> | ||||
|             </td> | ||||
|             <td> | ||||
|                 <a href="{{ url_for('user', username=post.author.username) }}"> | ||||
|                     {{ post.author.username }} | ||||
|                 </a> | ||||
|                 said {{ moment(post.timestamp).fromNow() }}: | ||||
|                 {% set user_link %} | ||||
|                     <a href="{{ url_for('user', username=post.author.username) }}"> | ||||
|                         {{ post.author.username }} | ||||
|                     </a> | ||||
|                 {% endset %} | ||||
|                 {{ _('%(username)s said %(when)s', | ||||
|                     username=user_link, when=moment(post.timestamp).fromNow()) }} | ||||
|                 <br> | ||||
|                 {{ post.body }} | ||||
|             </td> | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| {% extends 'bootstrap/base.html' %} | ||||
| 
 | ||||
| {% block title %} | ||||
|     {% if title %}{{ title }} - Microblog{% else %}Welcome to Microblog{% endif %} | ||||
|     {% if title %}{{ title }} - Microblog{% else %}{{ _('Welcome to Microblog') }}{% endif %} | ||||
| {% endblock %} | ||||
| 
 | ||||
| {% block navbar %} | ||||
|  | @ -18,15 +18,15 @@ | |||
|             </div> | ||||
|             <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> | ||||
|                 <ul class="nav navbar-nav"> | ||||
|                     <li><a href="{{ url_for('index') }}">Home</a></li> | ||||
|                     <li><a href="{{ url_for('explore') }}">Explore</a></li> | ||||
|                     <li><a href="{{ url_for('index') }}">{{ _('Home') }}</a></li> | ||||
|                     <li><a href="{{ url_for('explore') }}">{{ _('Explore') }}</a></li> | ||||
|                 </ul> | ||||
|                 <ul class="nav navbar-nav navbar-right"> | ||||
|                     {% if current_user.is_anonymous %} | ||||
|                     <li><a href="{{ url_for('login') }}">Login</a></li> | ||||
|                     <li><a href="{{ url_for('login') }}">{{ _('Login') }}</a></li> | ||||
|                     {% else %} | ||||
|                     <li><a href="{{ url_for('user', username=current_user.username) }}">Profile</a></li> | ||||
|                     <li><a href="{{ url_for('logout') }}">Logout</a></li> | ||||
|                     <li><a href="{{ url_for('user', username=current_user.username) }}">{{ _('Profile') }}</a></li> | ||||
|                     <li><a href="{{ url_for('logout') }}">{{ _('Logout') }}</a></li> | ||||
|                     {% endif %} | ||||
|                 </ul> | ||||
|             </div> | ||||
|  | @ -52,4 +52,5 @@ | |||
| {% block scripts %} | ||||
|     {{ super() }} | ||||
|     {{ moment.include_moment() }} | ||||
|     {{ moment.lang(g.locale) }} | ||||
| {% endblock %} | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| {% import 'bootstrap/wtf.html' as wtf %} | ||||
| 
 | ||||
| {% block app_content %} | ||||
|     <h1>Edit Profile</h1> | ||||
|     <h1>{{ _('Edit Profile') }}</h1> | ||||
|     <div class="row"> | ||||
|         <div class="col-md-4"> | ||||
|             {{ wtf.quick_form(form) }} | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| {% import 'bootstrap/wtf.html' as wtf %} | ||||
| 
 | ||||
| {% block app_content %} | ||||
|     <h1>Hi, {{ current_user.username }}!</h1> | ||||
|     <h1>{{ _('Hi, %(username)s!', username=current_user.username) }}</h1> | ||||
|     {% if form %} | ||||
|     {{ wtf.quick_form(form) }} | ||||
|     <br> | ||||
|  | @ -14,12 +14,12 @@ | |||
|         <ul class="pager"> | ||||
|             <li class="previous{% if not prev_url %} disabled{% endif %}"> | ||||
|                 <a href="{{ prev_url or '#' }}"> | ||||
|                     <span aria-hidden="true">←</span> Newer posts | ||||
|                     <span aria-hidden="true">←</span> {{ _('Newer posts') }} | ||||
|                 </a> | ||||
|             </li> | ||||
|             <li class="next{% if not next_url %} disabled{% endif %}"> | ||||
|                 <a href="{{ next_url or '#' }}"> | ||||
|                     Older posts <span aria-hidden="true">→</span> | ||||
|                     {{ _('Older posts') }} <span aria-hidden="true">→</span> | ||||
|                 </a> | ||||
|             </li> | ||||
|         </ul> | ||||
|  |  | |||
|  | @ -2,16 +2,16 @@ | |||
| {% import 'bootstrap/wtf.html' as wtf %} | ||||
| 
 | ||||
| {% block app_content %} | ||||
|     <h1>Sign In</h1> | ||||
|     <h1>{{ _('Sign In') }}</h1> | ||||
|     <div class="row"> | ||||
|         <div class="col-md-4"> | ||||
|             {{ wtf.quick_form(form) }} | ||||
|         </div> | ||||
|     </div> | ||||
|     <br> | ||||
|     <p>New User? <a href="{{ url_for('register') }}">Click to Register!</a></p> | ||||
|     <p>{{ _('New User?') }} <a href="{{ url_for('register') }}">{{ _('Click to Register!') }}</a></p> | ||||
|     <p> | ||||
|         Forgot Your Password? | ||||
|         <a href="{{ url_for('reset_password_request') }}">Click to Reset It</a> | ||||
|         {{ _('Forgot Your Password?') }} | ||||
|         <a href="{{ url_for('reset_password_request') }}">{{ _('Click to Reset It') }}</a> | ||||
|     </p> | ||||
| {% endblock %} | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| {% import 'bootstrap/wtf.html' as wtf %} | ||||
| 
 | ||||
| {% block app_content %} | ||||
|     <h1>Register</h1> | ||||
|     <h1>{{ _('Register') }}</h1> | ||||
|     <div class="row"> | ||||
|         <div class="col-md-4"> | ||||
|             {{ wtf.quick_form(form) }} | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| {% import 'bootstrap/wtf.html' as wtf %} | ||||
| 
 | ||||
| {% block app_content %} | ||||
|     <h1>Reset Your Password</h1> | ||||
|     <h1>{{ _('Reset Your Password') }}</h1> | ||||
|     <div class="row"> | ||||
|         <div class="col-md-4"> | ||||
|             {{ wtf.quick_form(form) }} | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| {% import 'bootstrap/wtf.html' as wtf %} | ||||
| 
 | ||||
| {% block app_content %} | ||||
|     <h1>Reset Password</h1> | ||||
|     <h1>{{ _('Reset Password') }}</h1> | ||||
|     <div class="row"> | ||||
|         <div class="col-md-4"> | ||||
|             {{ wtf.quick_form(form) }} | ||||
|  |  | |||
|  | @ -5,18 +5,18 @@ | |||
|         <tr> | ||||
|             <td width="256px"><img src="{{ user.avatar(256) }}"></td> | ||||
|             <td> | ||||
|                 <h1>User: {{ user.username }}</h1> | ||||
|                 <h1>{{ _('User') }}: {{ user.username }}</h1> | ||||
|                 {% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %} | ||||
|                 {% if user.last_seen %} | ||||
|                 <p>Last seen on: {{ moment(user.last_seen).format('LLL') }}</p> | ||||
|                 <p>{{ _('Last seen on') }}: {{ moment(user.last_seen).format('LLL') }}</p> | ||||
|                 {% endif %} | ||||
|                 <p>{{ user.followers.count() }} followers, {{ user.followed.count() }} following.</p> | ||||
|                 <p>{{ _('%(count)d followers', count=user.followers.count()) }}, {{ _('%(count)d following', count=user.followed.count()) }}</p> | ||||
|                 {% if user == current_user %} | ||||
|                 <p><a href="{{ url_for('edit_profile') }}">Edit your profile</a></p> | ||||
|                 <p><a href="{{ url_for('edit_profile') }}">{{ _('Edit your profile') }}</a></p> | ||||
|                 {% elif not current_user.is_following(user) %} | ||||
|                 <p><a href="{{ url_for('follow', username=user.username) }}">Follow</a></p> | ||||
|                 <p><a href="{{ url_for('follow', username=user.username) }}">{{ _('Follow') }}</a></p> | ||||
|                 {% else %} | ||||
|                 <p><a href="{{ url_for('unfollow', username=user.username) }}">Unfollow</a></p> | ||||
|                 <p><a href="{{ url_for('unfollow', username=user.username) }}">{{ _('Unfollow') }}</a></p> | ||||
|                 {% endif %} | ||||
|             </td> | ||||
|         </tr> | ||||
|  | @ -28,12 +28,12 @@ | |||
|         <ul class="pager"> | ||||
|             <li class="previous{% if not prev_url %} disabled{% endif %}"> | ||||
|                 <a href="{{ prev_url or '#' }}"> | ||||
|                     <span aria-hidden="true">←</span> Newer posts | ||||
|                     <span aria-hidden="true">←</span> {{ _('Newer posts') }} | ||||
|                 </a> | ||||
|             </li> | ||||
|             <li class="next{% if not next_url %} disabled{% endif %}"> | ||||
|                 <a href="{{ next_url or '#' }}"> | ||||
|                     Older posts <span aria-hidden="true">→</span> | ||||
|                     {{ _('Older posts') }} <span aria-hidden="true">→</span> | ||||
|                 </a> | ||||
|             </li> | ||||
|         </ul> | ||||
|  |  | |||
|  | @ -0,0 +1,259 @@ | |||
| # Spanish translations for PROJECT. | ||||
| # Copyright (C) 2017 ORGANIZATION | ||||
| # This file is distributed under the same license as the PROJECT project. | ||||
| # FIRST AUTHOR <EMAIL@ADDRESS>, 2017. | ||||
| # | ||||
| msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PROJECT VERSION\n" | ||||
| "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" | ||||
| "POT-Creation-Date: 2017-10-03 15:49-0700\n" | ||||
| "PO-Revision-Date: 2017-09-29 23:25-0700\n" | ||||
| "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | ||||
| "Language: es\n" | ||||
| "Language-Team: es <LL@li.org>\n" | ||||
| "Plural-Forms: nplurals=2; plural=(n != 1)\n" | ||||
| "MIME-Version: 1.0\n" | ||||
| "Content-Type: text/plain; charset=utf-8\n" | ||||
| "Content-Transfer-Encoding: 8bit\n" | ||||
| "Generated-By: Babel 2.5.1\n" | ||||
| 
 | ||||
| #: app/__init__.py:20 | ||||
| msgid "Please log in to access this page." | ||||
| msgstr "Por favor ingrese para acceder a esta página." | ||||
| 
 | ||||
| #: app/email.py:21 | ||||
| msgid "[Microblog] Reset Your Password" | ||||
| msgstr "[Microblog] Nueva Contraseña" | ||||
| 
 | ||||
| #: app/forms.py:12 app/forms.py:19 app/forms.py:50 | ||||
| msgid "Username" | ||||
| msgstr "Nombre de usuario" | ||||
| 
 | ||||
| #: app/forms.py:13 app/forms.py:21 app/forms.py:43 | ||||
| msgid "Password" | ||||
| msgstr "Contraseña" | ||||
| 
 | ||||
| #: app/forms.py:14 | ||||
| msgid "Remember Me" | ||||
| msgstr "Recordarme" | ||||
| 
 | ||||
| #: app/forms.py:15 app/templates/login.html:5 | ||||
| msgid "Sign In" | ||||
| msgstr "Ingresar" | ||||
| 
 | ||||
| #: app/forms.py:20 app/forms.py:38 | ||||
| msgid "Email" | ||||
| msgstr "Email" | ||||
| 
 | ||||
| #: app/forms.py:23 app/forms.py:45 | ||||
| msgid "Repeat Password" | ||||
| msgstr "Repetir Contraseña" | ||||
| 
 | ||||
| #: app/forms.py:24 app/templates/register.html:5 | ||||
| msgid "Register" | ||||
| msgstr "Registrarse" | ||||
| 
 | ||||
| #: app/forms.py:29 app/forms.py:62 | ||||
| msgid "Please use a different username." | ||||
| msgstr "Por favor use un nombre de usuario diferente." | ||||
| 
 | ||||
| #: app/forms.py:34 | ||||
| msgid "Please use a different email address." | ||||
| msgstr "Por favor use una dirección de email diferente." | ||||
| 
 | ||||
| #: app/forms.py:39 app/forms.py:46 | ||||
| msgid "Request Password Reset" | ||||
| msgstr "Pedir una nueva contraseña" | ||||
| 
 | ||||
| #: app/forms.py:51 | ||||
| msgid "About me" | ||||
| msgstr "Acerca de mí" | ||||
| 
 | ||||
| #: app/forms.py:52 app/forms.py:67 | ||||
| msgid "Submit" | ||||
| msgstr "Enviar" | ||||
| 
 | ||||
| #: app/forms.py:66 | ||||
| msgid "Say something" | ||||
| msgstr "Dí algo" | ||||
| 
 | ||||
| #: app/forms.py:71 | ||||
| msgid "Search" | ||||
| msgstr "Buscar" | ||||
| 
 | ||||
| #: app/routes.py:30 | ||||
| msgid "Your post is now live!" | ||||
| msgstr "¡Tu artículo ha sido publicado!" | ||||
| 
 | ||||
| #: app/routes.py:66 | ||||
| msgid "Invalid username or password" | ||||
| msgstr "Nombre de usuario o contraseña inválidos" | ||||
| 
 | ||||
| #: app/routes.py:92 | ||||
| msgid "Congratulations, you are now a registered user!" | ||||
| msgstr "¡Felicitaciones, ya eres un usuario registrado!" | ||||
| 
 | ||||
| #: app/routes.py:107 | ||||
| msgid "Check your email for the instructions to reset your password" | ||||
| msgstr "Busca en tu email las instrucciones para crear una nueva contraseña" | ||||
| 
 | ||||
| #: app/routes.py:124 | ||||
| msgid "Your password has been reset." | ||||
| msgstr "Tu contraseña ha sido cambiada." | ||||
| 
 | ||||
| #: app/routes.py:152 | ||||
| msgid "Your changes have been saved." | ||||
| msgstr "Tus cambios han sido salvados." | ||||
| 
 | ||||
| #: app/routes.py:157 app/templates/edit_profile.html:5 | ||||
| msgid "Edit Profile" | ||||
| msgstr "Editar Perfil" | ||||
| 
 | ||||
| #: app/routes.py:166 app/routes.py:182 | ||||
| #, python-format | ||||
| msgid "User %(username)s not found." | ||||
| msgstr "El usuario %(username)s no ha sido encontrado." | ||||
| 
 | ||||
| #: app/routes.py:169 | ||||
| msgid "You cannot follow yourself!" | ||||
| msgstr "¡No te puedes seguir a tí mismo!" | ||||
| 
 | ||||
| #: app/routes.py:173 | ||||
| #, python-format | ||||
| msgid "You are following %(username)s!" | ||||
| msgstr "¡Ahora estás siguiendo a %(username)s!" | ||||
| 
 | ||||
| #: app/routes.py:185 | ||||
| msgid "You cannot unfollow yourself!" | ||||
| msgstr "¡No te puedes dejar de seguir a tí mismo!" | ||||
| 
 | ||||
| #: app/routes.py:189 | ||||
| #, python-format | ||||
| msgid "You are not following %(username)s." | ||||
| msgstr "No estás siguiendo a %(username)s." | ||||
| 
 | ||||
| #: app/templates/404.html:4 | ||||
| msgid "Not Found" | ||||
| msgstr "Página No Encontrada" | ||||
| 
 | ||||
| #: app/templates/404.html:5 app/templates/500.html:6 | ||||
| msgid "Back" | ||||
| msgstr "Atrás" | ||||
| 
 | ||||
| #: app/templates/500.html:4 | ||||
| msgid "An unexpected error has occurred" | ||||
| msgstr "Ha ocurrido un error inesperado" | ||||
| 
 | ||||
| #: app/templates/500.html:5 | ||||
| msgid "The administrator has been notified. Sorry for the inconvenience!" | ||||
| msgstr "El administrador ha sido notificado. ¡Lamentamos la inconveniencia!" | ||||
| 
 | ||||
| #: app/templates/_post.html:9 | ||||
| #, python-format | ||||
| msgid "%(username)s said %(when)s" | ||||
| msgstr "%(username)s dijo %(when)s" | ||||
| 
 | ||||
| #: app/templates/base.html:4 | ||||
| msgid "Welcome to Microblog" | ||||
| msgstr "Bienvenido a Microblog" | ||||
| 
 | ||||
| #: app/templates/base.html:21 | ||||
| msgid "Home" | ||||
| msgstr "Inicio" | ||||
| 
 | ||||
| #: app/templates/base.html:22 | ||||
| msgid "Explore" | ||||
| msgstr "Explorar" | ||||
| 
 | ||||
| #: app/templates/base.html:33 | ||||
| msgid "Login" | ||||
| msgstr "Ingresar" | ||||
| 
 | ||||
| #: app/templates/base.html:35 | ||||
| msgid "Profile" | ||||
| msgstr "Perfil" | ||||
| 
 | ||||
| #: app/templates/base.html:36 | ||||
| msgid "Logout" | ||||
| msgstr "Salir" | ||||
| 
 | ||||
| #: app/templates/index.html:5 | ||||
| #, python-format | ||||
| msgid "Hi, %(username)s!" | ||||
| msgstr "¡Hola, %(username)s!" | ||||
| 
 | ||||
| #: app/templates/index.html:17 app/templates/user.html:31 | ||||
| msgid "Newer posts" | ||||
| msgstr "Artículos siguientes" | ||||
| 
 | ||||
| #: app/templates/index.html:22 app/templates/user.html:36 | ||||
| msgid "Older posts" | ||||
| msgstr "Artículos previos" | ||||
| 
 | ||||
| #: app/templates/login.html:12 | ||||
| msgid "New User?" | ||||
| msgstr "¿Usuario Nuevo?" | ||||
| 
 | ||||
| #: app/templates/login.html:12 | ||||
| msgid "Click to Register!" | ||||
| msgstr "¡Haz click aquí para registrarte!" | ||||
| 
 | ||||
| #: app/templates/login.html:14 | ||||
| msgid "Forgot Your Password?" | ||||
| msgstr "¿Te olvidaste tu contraseña?" | ||||
| 
 | ||||
| #: app/templates/login.html:15 | ||||
| msgid "Click to Reset It" | ||||
| msgstr "Haz click aquí para pedir una nueva" | ||||
| 
 | ||||
| #: app/templates/reset_password.html:5 | ||||
| msgid "Reset Your Password" | ||||
| msgstr "Nueva Contraseña" | ||||
| 
 | ||||
| #: app/templates/reset_password_request.html:5 | ||||
| msgid "Reset Password" | ||||
| msgstr "Nueva Contraseña" | ||||
| 
 | ||||
| #: app/templates/search.html:4 | ||||
| msgid "Search Results" | ||||
| msgstr "Resultados de Búsqueda" | ||||
| 
 | ||||
| #: app/templates/search.html:12 | ||||
| msgid "Previous results" | ||||
| msgstr "Resultados previos" | ||||
| 
 | ||||
| #: app/templates/search.html:17 | ||||
| msgid "Next results" | ||||
| msgstr "Resultados próximos" | ||||
| 
 | ||||
| #: app/templates/user.html:8 | ||||
| msgid "User" | ||||
| msgstr "Usuario" | ||||
| 
 | ||||
| #: app/templates/user.html:11 | ||||
| msgid "Last seen on" | ||||
| msgstr "Última visita" | ||||
| 
 | ||||
| #: app/templates/user.html:13 | ||||
| #, python-format | ||||
| msgid "%(count)d followers" | ||||
| msgstr "%(count)d seguidores" | ||||
| 
 | ||||
| #: app/templates/user.html:13 | ||||
| #, python-format | ||||
| msgid "%(count)d following" | ||||
| msgstr "siguiendo a %(count)d" | ||||
| 
 | ||||
| #: app/templates/user.html:15 | ||||
| msgid "Edit your profile" | ||||
| msgstr "Editar tu perfil" | ||||
| 
 | ||||
| #: app/templates/user.html:17 | ||||
| msgid "Follow" | ||||
| msgstr "Seguir" | ||||
| 
 | ||||
| #: app/templates/user.html:19 | ||||
| msgid "Unfollow" | ||||
| msgstr "Dejar de seguir" | ||||
| 
 | ||||
|  | @ -0,0 +1,3 @@ | |||
| [python: app/**.py] | ||||
| [jinja2: app/templates/**.html] | ||||
| extensions=jinja2.ext.autoescape,jinja2.ext.with_ | ||||
|  | @ -13,4 +13,5 @@ class Config(object): | |||
|     MAIL_USERNAME = os.environ.get('MAIL_USERNAME') | ||||
|     MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') | ||||
|     ADMINS = ['your-email@example.com'] | ||||
|     LANGUAGES = ['en', 'es'] | ||||
|     POSTS_PER_PAGE = 25 | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| from app import app, db | ||||
| from app import app, db, cli | ||||
| from app.models import User, Post | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue