diff --git a/.gitignore b/.gitignore
index 8e89ed52c..b257f71cf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
*.pyo
*.o
*.psp
+_build
build-*
.DS_Store
runtime/.qmake.cache
diff --git a/docs/en_US/index.rst b/docs/en_US/index.rst
index 9bc65fc3a..52674d590 100644
--- a/docs/en_US/index.rst
+++ b/docs/en_US/index.rst
@@ -75,6 +75,7 @@ learn how pgAdmin works, and how to develop improvements and new features.
coding-standards
code-overview
submitting-patches
+ translations
*******
Website
diff --git a/docs/en_US/translations.rst b/docs/en_US/translations.rst
new file mode 100644
index 000000000..f3f4faede
--- /dev/null
+++ b/docs/en_US/translations.rst
@@ -0,0 +1,134 @@
+Translations
+============
+
+pgAdmin supports multiple languages using the `Flask-Babel
+`_ Python module. A list of supported
+languages is included in the **web/config.py** configuration file and must be
+updated whenever langauges are added or removed.
+
+Translation Marking
+-------------------
+
+Strings can be marked for translation in either Python code (using **gettext()**)
+or Jinja templates (using **_()**). Here are some examples that show how this
+is achieved.
+
+Python::
+
+ errormsg = gettext('No server group name was specified')
+
+Jinja:
+
+.. code-block:: html
+
+
+
+.. code-block:: html
+
+
{{ _('%(appname)s Password Change', appname=config.APP_NAME) }}
+
+.. code-block:: javascript
+
+ var alert = alertify.prompt(
+ '{{ _('Add a server group') }}',
+ '{{ _('Enter a name for the new server group') }}',
+ ''
+ ...
+ )
+
+Updating and Merging
+--------------------
+
+Whenever new strings are added to the application, the template catalogues
+(**web/pgadmin/messages.pot**) must be updated and the existing catalogues
+merged with the updated template and compiled. This can be achieved using the
+following command from the **web** directory, in the Python virtual environment
+used for pgAdmin:
+
+.. code-block:: bash
+
+ (pgadmin4)piranha:web dpage$ pybabel extract -F babel.cfg -o pgadmin/messages.pot pgadmin
+
+For example:
+
+.. code-block:: bash
+
+ (pgadmin4)piranha:web dpage$ pybabel extract -F babel.cfg -o pgadmin/messages.pot pgadmin
+ extracting messages from pgadmin/__init__.py
+ extracting messages from pgadmin/about/__init__.py
+ extracting messages from pgadmin/about/hooks.py
+ extracting messages from pgadmin/about/views.py
+ extracting messages from pgadmin/about/templates/about/index.html (extensions="jinja2.ext.autoescape,jinja2.ext.with_")
+ extracting messages from pgadmin/browser/__init__.py
+ extracting messages from pgadmin/browser/hooks.py
+ extracting messages from pgadmin/browser/views.py
+ extracting messages from pgadmin/browser/nodes/CollectionNode.py
+ extracting messages from pgadmin/browser/nodes/ObjectNode.py
+ extracting messages from pgadmin/browser/nodes/__init__.py
+ extracting messages from pgadmin/browser/nodes/server_groups/__init__.py
+ extracting messages from pgadmin/browser/nodes/server_groups/hooks.py
+ extracting messages from pgadmin/browser/nodes/server_groups/views.py
+ extracting messages from pgadmin/browser/templates/browser/body.html (extensions="jinja2.ext.autoescape,jinja2.ext.with_")
+ extracting messages from pgadmin/browser/templates/browser/index.html (extensions="jinja2.ext.autoescape,jinja2.ext.with_")
+ extracting messages from pgadmin/browser/templates/browser/messages.html (extensions="jinja2.ext.autoescape,jinja2.ext.with_")
+ extracting messages from pgadmin/help/__init__.py
+ extracting messages from pgadmin/help/hooks.py
+ extracting messages from pgadmin/help/views.py
+ extracting messages from pgadmin/redirects/__init__.py
+ extracting messages from pgadmin/redirects/views.py
+ extracting messages from pgadmin/settings/__init__.py
+ extracting messages from pgadmin/settings/hooks.py
+ extracting messages from pgadmin/settings/settings_model.py
+ extracting messages from pgadmin/settings/views.py
+ extracting messages from pgadmin/templates/base.html (extensions="jinja2.ext.autoescape,jinja2.ext.with_")
+ extracting messages from pgadmin/templates/security/change_password.html (extensions="jinja2.ext.autoescape,jinja2.ext.with_")
+ extracting messages from pgadmin/templates/security/fields.html (extensions="jinja2.ext.autoescape,jinja2.ext.with_")
+ extracting messages from pgadmin/templates/security/forgot_password.html (extensions="jinja2.ext.autoescape,jinja2.ext.with_")
+ extracting messages from pgadmin/templates/security/login_user.html (extensions="jinja2.ext.autoescape,jinja2.ext.with_")
+ extracting messages from pgadmin/templates/security/messages.html (extensions="jinja2.ext.autoescape,jinja2.ext.with_")
+ extracting messages from pgadmin/templates/security/panel.html (extensions="jinja2.ext.autoescape,jinja2.ext.with_")
+ extracting messages from pgadmin/templates/security/reset_password.html (extensions="jinja2.ext.autoescape,jinja2.ext.with_")
+ extracting messages from pgadmin/templates/security/watermark.html (extensions="jinja2.ext.autoescape,jinja2.ext.with_")
+ extracting messages from pgadmin/test/__init__.py
+ extracting messages from pgadmin/test/hooks.py
+ extracting messages from pgadmin/test/views.py
+ extracting messages from pgadmin/utils/__init__.py
+ extracting messages from pgadmin/utils/views.py
+ writing PO template file to pgadmin/messages.pot
+
+Once the template has been updated, it needs to be merged into the existing
+message catalogues, for example:
+
+.. code-block:: bash
+
+ (pgadmin4)piranha:web dpage$ pybabel update -i pgadmin/messages.pot -d pgadmin/translations
+ updating catalog 'pgadmin/translations/fr/LC_MESSAGES/messages.po' based on 'pgadmin/messages.pot'
+
+Finally, the message catalogues can be compiled for use:
+
+.. code-block:: bash
+
+ (pgadmin4)piranha:web dpage$ pybabel compile -d pgadmin/translations
+ compiling catalog 'pgadmin/translations/fr/LC_MESSAGES/messages.po' to 'pgadmin/translations/fr/LC_MESSAGES/messages.mo'
+
+Adding a new Language
+---------------------
+
+Adding a new language is simple. First, add the language name and identifier to
+**web/config.py**::
+
+ # Languages we support in the UI
+ LANGUAGES = {
+ 'en': 'English',
+ 'fr': 'Français'
+ }
+
+Then, create the new message catalogue from the **web** directory in the source
+tree, in the Python virtual environment used for pgAdmin:
+
+.. code-block:: bash
+
+ (pgadmin4)piranha:web dpage$ pybabel init -i pgadmin/messages.pot -d pgadmin/translations -l fr
+
+This will initialise a new catalogue for a French translation.
+
diff --git a/requirements.txt b/requirements.txt
index d573d6b85..f1eb3dae8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,6 @@
+Babel==1.3
Flask==0.10.1
+Flask-Babel==0.9
Flask-Gravatar==0.4.1
Flask-Login==0.2.11
Flask-Mail==0.9.1
@@ -19,5 +21,7 @@ html5lib==1.0b3
itsdangerous==0.24
passlib==1.6.2
psycopg2==2.5.2
+pytz==2014.10
six==1.9.0
+speaklater==1.3
wsgiref==0.1.2
diff --git a/web/babel.cfg b/web/babel.cfg
new file mode 100644
index 000000000..f0234b326
--- /dev/null
+++ b/web/babel.cfg
@@ -0,0 +1,3 @@
+[python: **.py]
+[jinja2: **/templates/**.html]
+extensions=jinja2.ext.autoescape,jinja2.ext.with_
diff --git a/web/config.py b/web/config.py
index 445f49c09..f2f3a980d 100644
--- a/web/config.py
+++ b/web/config.py
@@ -1,3 +1,5 @@
+# -*- coding: utf-8 -*-
+
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
@@ -34,6 +36,12 @@ APP_COPYRIGHT = 'Copyright 2014 - 2015, The pgAdmin Development Team'
# Path to the online help.
HELP_PATH = '../../../docs/en_US/_build/html/'
+# Languages we support in the UI
+LANGUAGES = {
+ 'en': 'English',
+ 'fr': 'Français'
+}
+
# DO NOT CHANGE!
# The application version string, constructed from the components
APP_VERSION = '%s.%s.%s-%s' % (APP_MAJOR, APP_MINOR, APP_REVISION, APP_SUFFIX)
diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py
index 74620e62d..c440e6651 100644
--- a/web/pgadmin/__init__.py
+++ b/web/pgadmin/__init__.py
@@ -10,7 +10,8 @@
"""The main pgAdmin module. This handles the application initialisation tasks,
such as setup of logging, dynamic loading of modules etc."""
-from flask import Flask, abort
+from flask import Flask, abort, request
+from flask.ext.babel import Babel
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.security import Security, SQLAlchemyUserDatastore, login_required
from flask_security.utils import login_user
@@ -66,6 +67,23 @@ def create_app(app_name=config.APP_NAME):
app.logger.info('Starting %s v%s...', config.APP_NAME, config.APP_VERSION)
app.logger.info('################################################################################')
+ ##########################################################################
+ # Setup i18n
+ ##########################################################################
+
+ # Initialise i18n
+ babel = Babel(app)
+
+ app.logger.debug('Available translations: %s' % babel.list_translations())
+
+ @babel.localeselector
+ def get_locale():
+ """Get the best language for the user."""
+ language = request.accept_languages.best_match(config.LANGUAGES.keys())
+ app.logger.info('Using language: %s', language)
+
+ return language
+
##########################################################################
# Setup authentication
##########################################################################
diff --git a/web/pgadmin/about/templates/about/index.html b/web/pgadmin/about/templates/about/index.html
index 09a846e5f..80c299a34 100644
--- a/web/pgadmin/about/templates/about/index.html
+++ b/web/pgadmin/about/templates/about/index.html
@@ -1,25 +1,25 @@
-
Version
+
{{ _('Version') }}
{{ config.APP_VERSION }}
-
Copyright
+
{{ _('Copyright') }}
{{ config.APP_COPYRIGHT }}
-
Python Version
+
{{ _('Python Version') }}
{{ info.python_version }}
-
Flask Version
+
{{ _('Flask Version') }}
{{ info.flask_version }}
-
Application Mode
+
{{ _('Application Mode') }}
{{ info.app_mode }}
-
Current User
+
{{ _('Current User') }}
{{ info.current_user }}
diff --git a/web/pgadmin/about/views.py b/web/pgadmin/about/views.py
index 44732df26..0feed10cd 100644
--- a/web/pgadmin/about/views.py
+++ b/web/pgadmin/about/views.py
@@ -11,6 +11,7 @@
MODULE_NAME = 'about'
from flask import Blueprint, Response, current_app, render_template, __version__
+from flask.ext.babel import gettext
from flask.ext.security import current_user, login_required
import sys
@@ -31,9 +32,9 @@ def index():
info['python_version'] = sys.version
info['flask_version'] = __version__
if config.SERVER_MODE == True:
- info['app_mode'] = 'Server'
+ info['app_mode'] = gettext('Server')
else:
- info['app_mode'] = 'Desktop'
+ info['app_mode'] = gettext('Desktop')
info['current_user'] = current_user.email
return render_template(MODULE_NAME + '/index.html', info=info)
diff --git a/web/pgadmin/browser/nodes/server_groups/hooks.py b/web/pgadmin/browser/nodes/server_groups/hooks.py
index a1095c46e..183623d0e 100644
--- a/web/pgadmin/browser/nodes/server_groups/hooks.py
+++ b/web/pgadmin/browser/nodes/server_groups/hooks.py
@@ -10,6 +10,7 @@
"""Integration hooks for server groups."""
from flask import render_template, url_for
+from flask.ext.babel import gettext
from flask.ext.security import current_user
from pgadmin.settings.settings_model import db, ServerGroup
@@ -30,17 +31,17 @@ def get_file_menu_items():
"""Return a (set) of dicts of file menu items, with name, priority, URL,
target and onclick code."""
return [
- {'name': 'mnu_add_server_group', 'label': 'Add a server group...', 'priority': 10, 'url': '#', 'onclick': 'add_server_group()'},
- {'name': 'mnu_delete_server_group', 'label': 'Delete server group', 'priority': 20, 'url': '#', 'onclick': 'delete_server_group()'},
- {'name': 'mnu_rename_server_group', 'label': 'Rename server group...', 'priority': 30, 'url': '#', 'onclick': 'rename_server_group()'}
+ {'name': 'mnu_add_server_group', 'label': gettext('Add a server group...'), 'priority': 10, 'url': '#', 'onclick': 'add_server_group()'},
+ {'name': 'mnu_delete_server_group', 'label': gettext('Delete server group'), 'priority': 20, 'url': '#', 'onclick': 'delete_server_group()'},
+ {'name': 'mnu_rename_server_group', 'label': gettext('Rename server group...'), 'priority': 30, 'url': '#', 'onclick': 'rename_server_group()'}
]
def get_context_menu_items():
"""Return a (set) of dicts of content menu items with name, label, priority and JS"""
return [
- {'name': 'delete', 'label': 'Delete server group', 'priority': 100, 'onclick': 'delete_server_group(item);'},
- {'name': 'rename', 'label': 'Rename server group...', 'priority': 200, 'onclick': 'rename_server_group(item);'}
+ {'name': 'delete', 'label': gettext('Delete server group'), 'priority': 100, 'onclick': 'delete_server_group(item);'},
+ {'name': 'rename', 'label': gettext('Rename server group...'), 'priority': 200, 'onclick': 'rename_server_group(item);'}
]
diff --git a/web/pgadmin/browser/nodes/server_groups/templates/server_groups/server_groups.js b/web/pgadmin/browser/nodes/server_groups/templates/server_groups/server_groups.js
index de40ee3c2..ee107c84d 100644
--- a/web/pgadmin/browser/nodes/server_groups/templates/server_groups/server_groups.js
+++ b/web/pgadmin/browser/nodes/server_groups/templates/server_groups/server_groups.js
@@ -1,8 +1,8 @@
// Add a server group
function add_server_group() {
var alert = alertify.prompt(
- 'Add a server group',
- 'Enter a name for the new server group',
+ '{{ _('Add a server group') }}',
+ '{{ _('Enter a name for the new server group') }}',
'',
function(evt, value) {
$.post("{{ url_for('NODE-server-group.add') }}", { name: value })
@@ -34,8 +34,8 @@ function add_server_group() {
// Delete a server group
function delete_server_group(item) {
alertify.confirm(
- 'Delete server group?',
- 'Are you sure you wish to delete the server group "{0}"?'.replace('{0}', tree.getLabel(item)),
+ '{{ _('Delete server group?') }}',
+ '{{ _('Are you sure you wish to delete the server group "{0}"?') }}'.replace('{0}', tree.getLabel(item)),
function() {
var id = tree.getId(item)
$.post("{{ url_for('NODE-server-group.delete') }}", { id: id })
@@ -62,8 +62,8 @@ function delete_server_group(item) {
// Rename a server group
function rename_server_group(item) {
alertify.prompt(
- 'Rename server group',
- 'Enter a new name for the server group',
+ '{{ _('Rename server group') }}',
+ '{{ _('Enter a new name for the server group') }}',
tree.getLabel(item),
function(evt, value) {
var id = tree.getId(item)
diff --git a/web/pgadmin/browser/nodes/server_groups/views.py b/web/pgadmin/browser/nodes/server_groups/views.py
index 9fbda176b..531700118 100644
--- a/web/pgadmin/browser/nodes/server_groups/views.py
+++ b/web/pgadmin/browser/nodes/server_groups/views.py
@@ -15,6 +15,7 @@ NODE_PATH = '/browser/' + NODE_NAME
import traceback
from flask import Blueprint, Response, current_app, request
+from flask.ext.babel import gettext
from flask.ext.security import current_user, login_required
from utils.ajax import make_json_response
@@ -44,7 +45,7 @@ def add():
else:
success = 0
- errormsg = "No server group name was specified"
+ errormsg = gettext('No server group name was specified')
if success == 1:
data['id'] = servergroup.id
@@ -69,7 +70,7 @@ def delete():
if servergroup is None:
success = 0
- errormsg = 'The specified server group could not be found.'
+ errormsg = gettext('The specified server group could not be found.')
else:
try:
db.session.delete(servergroup)
@@ -80,7 +81,7 @@ def delete():
else:
success = 0
- errormsg = "No server group was specified."
+ errormsg = gettext('No server group was specified.')
return make_json_response(success=success,
errormsg=errormsg,
@@ -100,7 +101,7 @@ def rename():
if servergroup is None:
success = 0
- errormsg = 'The specified server group could not be found.'
+ errormsg = gettext('The specified server group could not be found.')
else:
try:
servergroup.name = request.form['name']
@@ -111,9 +112,10 @@ def rename():
else:
success = 0
- errormsg = "No server group was specified."
+ errormsg = gettext('No server group was specified.')
return make_json_response(success=success,
errormsg=errormsg,
info=traceback.format_exc(),
- result=request.form)
\ No newline at end of file
+ result=request.form)
+
\ No newline at end of file
diff --git a/web/pgadmin/browser/static/js/utils.js b/web/pgadmin/browser/static/js/utils.js
deleted file mode 100644
index 7550fa62c..000000000
--- a/web/pgadmin/browser/static/js/utils.js
+++ /dev/null
@@ -1,39 +0,0 @@
-function report_error(message, info) {
-
- text = '