diff options
-rw-r--r-- | conf.py | 6 | ||||
-rw-r--r-- | messages.pot | 154 | ||||
-rw-r--r-- | pyaggr3g470r/__init__.py | 3 | ||||
-rw-r--r-- | pyaggr3g470r/templates/home.html | 12 | ||||
-rw-r--r-- | pyaggr3g470r/translations/fr/LC_MESSAGES/messages.mo | bin | 0 -> 2717 bytes | |||
-rw-r--r-- | pyaggr3g470r/translations/fr/LC_MESSAGES/messages.po | 155 | ||||
-rw-r--r-- | pyaggr3g470r/views.py | 71 |
7 files changed, 364 insertions, 37 deletions
@@ -11,6 +11,12 @@ import os, sys basedir = os.path.abspath(os.path.dirname(__file__)) PATH = os.path.abspath(".") +# available languages +LANGUAGES = { + 'en': 'English', + 'fr': 'French' +} + ON_HEROKU = int(os.environ.get('HEROKU', 0)) == 1 if not ON_HEROKU: diff --git a/messages.pot b/messages.pot new file mode 100644 index 00000000..6e487268 --- /dev/null +++ b/messages.pot @@ -0,0 +1,154 @@ +# Translations template for PROJECT. +# Copyright (C) 2014 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2014. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2014-05-01 11:10+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" + +#: pyaggr3g470r/views.py:93 +msgid "Authentication required." +msgstr "" + +#: pyaggr3g470r/views.py:98 +msgid "Forbidden." +msgstr "" + +#: pyaggr3g470r/views.py:141 +msgid "Logged in successfully." +msgstr "" + +#: pyaggr3g470r/views.py:161 +msgid "Logged out successfully." +msgstr "" + +#: pyaggr3g470r/views.py:193 +msgid "Downloading articles..." +msgstr "" + +#: pyaggr3g470r/views.py:281 +msgid "Articles marked as read." +msgstr "" + +#: pyaggr3g470r/views.py:284 +msgid "All articles marked as read" +msgstr "" + +#: pyaggr3g470r/views.py:315 +msgid "Article" +msgstr "" + +#: pyaggr3g470r/views.py:315 +msgid "deleted." +msgstr "" + +#: pyaggr3g470r/views.py:318 +msgid "This article do not exist." +msgstr "" + +#: pyaggr3g470r/views.py:411 +msgid "Database indexed." +msgstr "" + +#: pyaggr3g470r/views.py:413 pyaggr3g470r/views.py:482 +msgid "An error occured" +msgstr "" + +#: pyaggr3g470r/views.py:416 +msgid "Option not available on Heroku." +msgstr "" + +#: pyaggr3g470r/views.py:431 pyaggr3g470r/views.py:441 +msgid "Error when exporting articles." +msgstr "" + +#: pyaggr3g470r/views.py:447 +msgid "Export format not supported." +msgstr "" + +#: pyaggr3g470r/views.py:470 +msgid "Full text search is not yet implemented for Heroku." +msgstr "" + +#: pyaggr3g470r/views.py:508 +msgid "File not allowed." +msgstr "" + +#: pyaggr3g470r/views.py:514 +msgid "feeds imported." +msgstr "" + +#: pyaggr3g470r/views.py:516 +msgid "Impossible to import the new feeds." +msgstr "" + +#: pyaggr3g470r/views.py:552 pyaggr3g470r/views.py:564 +#: pyaggr3g470r/views.py:567 pyaggr3g470r/views.py:590 +msgid "Feed" +msgstr "" + +#: pyaggr3g470r/views.py:552 pyaggr3g470r/views.py:608 +#: pyaggr3g470r/views.py:652 +msgid "successfully updated." +msgstr "" + +#: pyaggr3g470r/views.py:564 pyaggr3g470r/views.py:663 +msgid "successfully created." +msgstr "" + +#: pyaggr3g470r/views.py:567 +msgid "already in the database." +msgstr "" + +#: pyaggr3g470r/views.py:590 pyaggr3g470r/views.py:703 +msgid "successfully deleted." +msgstr "" + +#: pyaggr3g470r/views.py:608 pyaggr3g470r/views.py:652 +#: pyaggr3g470r/views.py:663 pyaggr3g470r/views.py:703 +msgid "User" +msgstr "" + +#: pyaggr3g470r/views.py:689 pyaggr3g470r/views.py:705 +msgid "This user does not exist." +msgstr "" + +#: pyaggr3g470r/templates/home.html:5 +msgid "You are not subscribed to any feed." +msgstr "" + +#: pyaggr3g470r/templates/home.html:5 +msgid "Fix this" +msgstr "" + +#: pyaggr3g470r/templates/home.html:11 +msgid "More articles" +msgstr "" + +#: pyaggr3g470r/templates/home.html:12 +msgid "Details" +msgstr "" + +#: pyaggr3g470r/templates/home.html:13 +msgid "Edit this feed" +msgstr "" + +#: pyaggr3g470r/templates/home.html:15 +msgid "Fetch this feed" +msgstr "" + +#: pyaggr3g470r/templates/home.html:17 +msgid "Mark all as read" +msgstr "" + diff --git a/pyaggr3g470r/__init__.py b/pyaggr3g470r/__init__.py index 9ba33849..da978128 100644 --- a/pyaggr3g470r/__init__.py +++ b/pyaggr3g470r/__init__.py @@ -4,6 +4,7 @@ import os from flask import Flask from flask.ext.sqlalchemy import SQLAlchemy +from flask.ext.babel import Babel from flask.ext.gravatar import Gravatar import conf @@ -27,6 +28,8 @@ def allowed_file(filename): return '.' in filename and \ filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS +babel = Babel(app) + # Gravatar gravatar = Gravatar(app, size=100, rating='g', default='retro', force_default=False, use_ssl=False, base_url=None) diff --git a/pyaggr3g470r/templates/home.html b/pyaggr3g470r/templates/home.html index 6448fc15..43fdbb97 100644 --- a/pyaggr3g470r/templates/home.html +++ b/pyaggr3g470r/templates/home.html @@ -2,19 +2,19 @@ {% block content %} <div class="container"> {% if result|count == 0 %} - <h1>You are not subscribed to any feed. <a href="/create_feed/">Fix this</a>.</h1> + <h1>{{ _('You are not subscribed to any feed.') }} <a href="/create_feed/">{{ _('Fix this') }}</a>.</h1> {% else %} {% for feed in result|sort(attribute="title") %} <div class="row"> <div class="col-md-6 col-md-offset-3"> <h1>{{ feed.title|safe }}</h1> - <a href="/articles/{{ feed.id }}/100"><i class="glyphicon glyphicon-th-list" title="More articles"></i></a> - <a href="/feed/{{ feed.id }}"><i class="glyphicon glyphicon-info-sign" title="Details"></i></a> - <a href="/edit_feed/{{ feed.id }}"><i class="glyphicon glyphicon-edit" title="Edit this feed"></i></a> + <a href="/articles/{{ feed.id }}/100"><i class="glyphicon glyphicon-th-list" title="{{ _('More articles') }}"></i></a> + <a href="/feed/{{ feed.id }}"><i class="glyphicon glyphicon-info-sign" title="{{ _('Details') }}"></i></a> + <a href="/edit_feed/{{ feed.id }}"><i class="glyphicon glyphicon-edit" title="{{ _('Edit this feed') }}"></i></a> {% if feed.enabled %} - <a href="/fetch/{{ feed.id }}"><i class="glyphicon glyphicon-cloud-download" title="Fetch this feed"></i></a> + <a href="/fetch/{{ feed.id }}"><i class="glyphicon glyphicon-cloud-download" title="{{ _('Fetch this feed') }}"></i></a> {% endif %} - <a href="/mark_as_read/{{ feed.id }}"><i class="glyphicon glyphicon-check" title="Mark all as read"></i></a> + <a href="/mark_as_read/{{ feed.id }}"><i class="glyphicon glyphicon-check" title="{{ _('Mark all as read') }}"></i></a> </div> </div> {% for number in range(0, feed.articles.all()|count-(feed.articles.all()|count % 3), 3) %} diff --git a/pyaggr3g470r/translations/fr/LC_MESSAGES/messages.mo b/pyaggr3g470r/translations/fr/LC_MESSAGES/messages.mo Binary files differnew file mode 100644 index 00000000..e2f373d0 --- /dev/null +++ b/pyaggr3g470r/translations/fr/LC_MESSAGES/messages.mo diff --git a/pyaggr3g470r/translations/fr/LC_MESSAGES/messages.po b/pyaggr3g470r/translations/fr/LC_MESSAGES/messages.po new file mode 100644 index 00000000..bcb85625 --- /dev/null +++ b/pyaggr3g470r/translations/fr/LC_MESSAGES/messages.po @@ -0,0 +1,155 @@ +# French translations for PROJECT. +# Copyright (C) 2014 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2014. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2014-05-01 11:10+0200\n" +"PO-Revision-Date: 2014-05-01 11:12+0100\n" +"Last-Translator: Cédric Bonhomme <cedric@cedricbonhomme.org>\n" +"Language-Team: fr <LL@li.org>\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"Generated-By: Babel 1.3\n" +"X-Generator: Poedit 1.5.4\n" + +#: pyaggr3g470r/views.py:93 +msgid "Authentication required." +msgstr "Authentification requise." + +#: pyaggr3g470r/views.py:98 +msgid "Forbidden." +msgstr "Interdit." + +#: pyaggr3g470r/views.py:141 +msgid "Logged in successfully." +msgstr "Connecté avec succès." + +#: pyaggr3g470r/views.py:161 +msgid "Logged out successfully." +msgstr "Déconnecté avec succès." + +#: pyaggr3g470r/views.py:193 +msgid "Downloading articles..." +msgstr "Téléchargement des articles." + +#: pyaggr3g470r/views.py:281 +msgid "Articles marked as read." +msgstr "Articles marqués comme lus." + +#: pyaggr3g470r/views.py:284 +msgid "All articles marked as read" +msgstr "Tous les articles marqués comme lus." + +#: pyaggr3g470r/views.py:315 +msgid "Article" +msgstr "Article" + +#: pyaggr3g470r/views.py:315 +msgid "deleted." +msgstr "supprimé." + +#: pyaggr3g470r/views.py:318 +msgid "This article do not exist." +msgstr "Cet article n'existe pas." + +#: pyaggr3g470r/views.py:411 +msgid "Database indexed." +msgstr "Base de données indexée." + +#: pyaggr3g470r/views.py:413 pyaggr3g470r/views.py:482 +msgid "An error occured" +msgstr "Une erreur est survenue." + +#: pyaggr3g470r/views.py:416 +msgid "Option not available on Heroku." +msgstr "Option non disponible sur Heroku." + +#: pyaggr3g470r/views.py:431 pyaggr3g470r/views.py:441 +msgid "Error when exporting articles." +msgstr "Erreur lors de l'export des articles." + +#: pyaggr3g470r/views.py:447 +msgid "Export format not supported." +msgstr "Ce format d'export n'est pas supporté." + +#: pyaggr3g470r/views.py:470 +msgid "Full text search is not yet implemented for Heroku." +msgstr "La recherche rapide n'est pas supporté sur Heroku." + +#: pyaggr3g470r/views.py:508 +msgid "File not allowed." +msgstr "Fichier non autorisé." + +#: pyaggr3g470r/views.py:514 +msgid "feeds imported." +msgstr "flux importés." + +#: pyaggr3g470r/views.py:516 +msgid "Impossible to import the new feeds." +msgstr "Impossible d'importer les nouveaux flux." + +#: pyaggr3g470r/views.py:552 pyaggr3g470r/views.py:564 +#: pyaggr3g470r/views.py:567 pyaggr3g470r/views.py:590 +msgid "Feed" +msgstr "Flux" + +#: pyaggr3g470r/views.py:552 pyaggr3g470r/views.py:608 +#: pyaggr3g470r/views.py:652 +msgid "successfully updated." +msgstr "mis à jour avec succès." + +#: pyaggr3g470r/views.py:564 pyaggr3g470r/views.py:663 +msgid "successfully created." +msgstr "créé avec succès." + +#: pyaggr3g470r/views.py:567 +msgid "already in the database." +msgstr "déjà dans la base de données." + +#: pyaggr3g470r/views.py:590 pyaggr3g470r/views.py:703 +msgid "successfully deleted." +msgstr "supprimé avec succès." + +#: pyaggr3g470r/views.py:608 pyaggr3g470r/views.py:652 +#: pyaggr3g470r/views.py:663 pyaggr3g470r/views.py:703 +msgid "User" +msgstr "Utilisateur" + +#: pyaggr3g470r/views.py:689 pyaggr3g470r/views.py:705 +msgid "This user does not exist." +msgstr "Cet utilisateur n'existe pas." + +#: pyaggr3g470r/templates/home.html:5 +msgid "You are not subscribed to any feed." +msgstr "Vous êtes abonné à aucun flux." + +#: pyaggr3g470r/templates/home.html:5 +msgid "Fix this" +msgstr "Résolvez ce problème." + +#: pyaggr3g470r/templates/home.html:11 +msgid "More articles" +msgstr "Plus d'articles" + +#: pyaggr3g470r/templates/home.html:12 +msgid "Details" +msgstr "Détails" + +#: pyaggr3g470r/templates/home.html:13 +msgid "Edit this feed" +msgstr "Éditer ce flux" + +#: pyaggr3g470r/templates/home.html:15 +msgid "Fetch this feed" +msgstr "Récupérer ce flux" + +#: pyaggr3g470r/templates/home.html:17 +msgid "Mark all as read" +msgstr "Marquer tout comme lu" diff --git a/pyaggr3g470r/views.py b/pyaggr3g470r/views.py index 6d2a3b99..05b7e0f6 100644 --- a/pyaggr3g470r/views.py +++ b/pyaggr3g470r/views.py @@ -32,6 +32,7 @@ import datetime from flask import render_template, request, flash, session, url_for, redirect, g, current_app, make_response from flask.ext.login import LoginManager, login_user, logout_user, login_required, current_user, AnonymousUserMixin from flask.ext.principal import Principal, Identity, AnonymousIdentity, identity_changed, identity_loaded, Permission, RoleNeed, UserNeed +from flask.ext.babel import gettext from sqlalchemy import desc from werkzeug import generate_password_hash @@ -41,7 +42,7 @@ import export if not conf.ON_HEROKU: import search as fastsearch from forms import SigninForm, AddFeedForm, ProfileForm -from pyaggr3g470r import app, db, allowed_file +from pyaggr3g470r import app, db, allowed_file, babel from pyaggr3g470r.models import User, Feed, Article, Role from pyaggr3g470r.decorators import feed_access_required @@ -89,12 +90,12 @@ def load_user(email): # @app.errorhandler(401) def authentication_required(e): - flash('Authentication required.', 'info') + flash(gettext('Authentication required.'), 'info') return redirect(url_for('login')) @app.errorhandler(403) def authentication_failed(e): - flash('Forbidden.', 'danger') + flash(gettext('Forbidden.'), 'danger') return redirect(url_for('home')) @app.errorhandler(404) @@ -112,6 +113,14 @@ def redirect_url(default='home'): url_for(default) +@babel.localeselector +def get_locale(): + """ + Called before each request to give us a chance to choose + the language to use when producing its response. + """ + return request.accept_languages.best_match(conf.LANGUAGES.keys()) + # # Views. @@ -129,7 +138,7 @@ def login(): login_user(user) g.user = user identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) - flash("Logged in successfully.", 'success') + flash(gettext("Logged in successfully."), 'success') return redirect(url_for('home')) return render_template('login.html', form=form) @@ -149,7 +158,7 @@ def logout(): # Tell Flask-Principal the user is anonymous identity_changed.send(current_app._get_current_object(), identity=AnonymousIdentity()) - flash("Logged out successfully.", 'success') + flash(gettext("Logged out successfully."), 'success') return redirect(url_for('login')) @app.route('/') @@ -181,7 +190,7 @@ def fetch(feed_id=None): """ cmd = ['python', conf.basedir+'/fetch.py', g.user.email, str(feed_id)] p = subprocess.Popen(cmd, stdout=subprocess.PIPE) - flash("Downloading articles...", 'success') + flash(gettext("Downloading articles..."), 'success') return redirect(redirect_url()) @app.route('/about/', methods=['GET']) @@ -269,10 +278,10 @@ def mark_as_read(feed_id=None): if feed_id is not None: Article.query.filter(Article.user_id == g.user.id, Article.feed_id == feed_id, Article.readed == False).update({"readed": True}) - flash('Articles marked as read.', 'info') + flash(gettext('Articles marked as read.'), 'info') else: Article.query.filter(Article.user_id == g.user.id, Article.readed == False).update({"readed": True}) - flash("All articles marked as read", 'info') + flash(gettext("All articles marked as read"), 'info') db.session.commit() return redirect(redirect_url()) @@ -303,10 +312,10 @@ def delete(article_id=None): fastsearch.delete_article(g.user.id, article.feed_id, article.id) except: pass - flash('Article "' + article.title + '" deleted.', 'success') + flash(gettext('Article') + ' ' + article.title + ' ' + gettext('deleted.'), 'success') return redirect(url_for('home')) else: - flash('This article do not exist.', 'danger') + flash(gettext('This article do not exist.'), 'danger') return redirect(url_for('home')) @app.route('/articles/<feed_id>/', methods=['GET']) @@ -399,12 +408,12 @@ def index_database(): user = User.query.filter(User.id == g.user.id).first() try: fastsearch.create_index(user) - flash('Database indexed.', 'success') + flash(gettext('Database indexed.'), 'success') except Exception as e: - flash('An error occured (%s).' % e, 'danger') + flash(gettext('An error occured') + ' (%s).' % e, 'danger') return redirect(url_for('home')) else: - flash('Option not available on Heroku.', 'success') + flash(gettext('Option not available on Heroku.'), 'success') return redirect(url_for('home')) @app.route('/export/', methods=['GET']) @@ -419,7 +428,7 @@ def export_articles(): try: archive_file, archive_file_name = export.export_html(user) except: - flash("Error when exporting articles.", 'danger') + flash(gettext("Error when exporting articles."), 'danger') return redirect(redirect_url()) response = make_response(archive_file) response.headers['Content-Type'] = 'application/x-compressed' @@ -429,13 +438,13 @@ def export_articles(): try: json_result = export.export_json(user) except: - flash("Error when exporting articles.", 'danger') + flash(gettext("Error when exporting articles."), 'danger') return redirect(redirect_url()) response = make_response(json_result) response.mimetype = 'application/json' response.headers["Content-Disposition"] = 'attachment; filename=articles.json' else: - flash('Export format not supported.', 'warning') + flash(gettext('Export format not supported.'), 'warning') return redirect(redirect_url()) return response @@ -458,7 +467,7 @@ def search(): Search articles corresponding to the query. """ if conf.ON_HEROKU: - flash("Full text search is not yet implemented for Heroku.", "warning") + flash(gettext("Full text search is not yet implemented for Heroku."), "warning") return redirect(url_for('home')) user = User.query.filter(User.id == g.user.id).first() @@ -470,7 +479,7 @@ def search(): try: search_result, nb_articles = fastsearch.search(user.id, query) except Exception as e: - flash('An error occured (%s).' % e, 'danger') + flash(gettext('An error occured') + ' (%s).' % e, 'danger') for feed_id in search_result: for feed in user.feeds: if feed.id == feed_id: @@ -496,15 +505,15 @@ def management(): # Import an OPML file data = request.files.get('opmlfile', None) if None == data or not allowed_file(data.filename): - flash('File not allowed.', 'danger') + flash(gettext('File not allowed.'), 'danger') else: opml_path = os.path.join("./pyaggr3g470r/var/", data.filename) data.save(opml_path) try: nb = utils.import_opml(g.user.email, opml_path) - flash(str(nb) + " feeds imported.", "success") + flash(str(nb) + ' ' + gettext('feeds imported.'), "success") except Exception as e: - flash("Impossible to import the new feeds.", "danger") + flash(gettext("Impossible to import the new feeds."), "danger") form = AddFeedForm() @@ -540,7 +549,7 @@ def edit_feed(feed_id=None): # Edit an existing feed form.populate_obj(feed) db.session.commit() - flash('Feed "' + feed.title + '" successfully updated.', 'success') + flash(gettext('Feed') + ' ' + feed.title + ' ' + gettext('successfully updated.'), 'success') return redirect('/edit_feed/' + str(feed_id)) else: # Create a new feed @@ -552,10 +561,10 @@ def edit_feed(feed_id=None): g.user.feeds.append(new_feed) #user.feeds = sorted(user.feeds, key=lambda t: t.title.lower()) db.session.commit() - flash('Feed "' + new_feed.title + '" successfully created.', 'success') + flash(gettext('Feed') + ' ' + new_feed.title + ' ' + gettext('successfully created.'), 'success') return redirect('/edit_feed/' + str(new_feed.id)) else: - flash('Feed "' + existing_feed[0].title + '" already in the database.', 'warning') + flash(gettext('Feed') + ' ' + existing_feed[0].title + ' ' + gettext('already in the database.'), 'warning') return redirect('/edit_feed/' + str(existing_feed[0].id)) if request.method == 'GET': @@ -578,7 +587,7 @@ def delete_feed(feed_id=None): feed = Feed.query.filter(Feed.id == feed_id).first() db.session.delete(feed) db.session.commit() - flash('Feed "' + feed.title + '" successfully deleted.', 'success') + flash(gettext('Feed') + ' ' + feed.title + ' ' + gettext('successfully deleted.'), 'success') return redirect(redirect_url()) @app.route('/profile/', methods=['GET', 'POST']) @@ -596,7 +605,7 @@ def profile(): if form.password.data != "": user.set_password(form.password.data) db.session.commit() - flash('User "' + user.firstname + '" successfully updated.', 'success') + flash(gettext('User') + ' ' + user.firstname + ' ' + gettext('successfully updated.'), 'success') return redirect(url_for('profile')) else: return render_template('profile.html', form=form) @@ -640,7 +649,7 @@ def create_user(user_id=None): if form.password.data != "": user.set_password(form.password.data) db.session.commit() - flash('User "' + user.firstname + '" successfully updated.', 'success') + flash(gettext('User') + ' ' + user.firstname + ' ' + gettext('successfully updated.'), 'success') else: # Create a new user role_user = Role.query.filter(Role.name == "user").first() @@ -651,7 +660,7 @@ def create_user(user_id=None): user.roles.extend([role_user]) db.session.add(user) db.session.commit() - flash('User "' + user.firstname + '" successfully created.', 'success') + flash(gettext('User') + ' ' + user.firstname + ' ' + gettext('successfully created.'), 'success') return redirect("/admin/edit_user/"+str(user.id)+"/") else: return render_template('profile.html', form=form) @@ -677,7 +686,7 @@ def user(user_id=None): if user is not None: return render_template('/admin/user.html', user=user) else: - flash('This user does not exist.', 'danger') + flash(gettext('This user does not exist.'), 'danger') return redirect(redirect_url()) @app.route('/admin/delete_user/<int:user_id>/', methods=['GET']) @@ -691,7 +700,7 @@ def delete_user(user_id=None): if user is not None: db.session.delete(user) db.session.commit() - flash('User "' + user.firstname + '" successfully deleted.', 'success') + flash(gettext('User') + ' ' + user.firstname + ' ' + gettext('successfully deleted.'), 'success') else: - flash('This user does not exist.', 'danger') + flash(gettext('This user does not exist.'), 'danger') return redirect(redirect_url()) |