diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ed80dad4..a8a25d41 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,7 @@ - [Contributions](#contributions) - [Pull request process](#pull-request-process) + - [Translation](#translation) - [Development](#development) - [Installation](#installation) - [Gulp Commands](#gulp-commands) @@ -34,6 +35,33 @@ Gitter. New pull requests will be reviewed by project maintainers as soon as possible and we will do our best to provide feedback and support potential contributors. +### Translation + +Baby Buddy has support for translation/localization to any language. A general +translation process will look something like this: + +1. Set up a development environment (see [Development](#development) below). +1. Run `gulp makemessages -l xx` where `xx` is a specific locale code (e.g. "fr" +for French or "es" for Spanish). This create a new translation file at +`locale/xx/LC_MESSAGES/django.po`, or update one if it exits. +1. Open the created/updated `django.po` file and update the header template with +license and contact info. +1. Start translating! Each translatable string will have a `msgid` value with +the string in English and a corresponding (empty) `msgstr` value where a +translated string can be filled in. +1. Once all strings have been translated, run `gulp compilemessages -l xx` to +compile an optimized translation file (`locale/xx/LC_MESSAGES/django.mo`). +1. To expose the new translation as a user setting, add the locale code to the +`LANGUAGES` array in the base settings file (`babybuddy/settings/base.py`). +1. Run the development server, log in, and update the user language to test the +newly translated strings. + +Once the translation is complete, commit the new files and changes to a fork and +[create a pull request](#pull-request-process) for review. + +For more information on the Django translation process, see Django's +documentation section: [Translation](https://docs.djangoproject.com/en/dev/topics/i18n/translation/). + ## Development ### Requirements @@ -92,10 +120,12 @@ in the [`babybuddy/management/commands`](babybuddy/management/commands) folder. - [`gulp build`](#build) - [`gulp clean`](#clean) - [`gulp collectstatic`](#collectstatic) +- [`gulp compilemessages`](#compilemessages) - [`gulp coverage`](#coverage) - [`gulp extras`](#extras) - [`gulp fake`](#fake) - [`gulp lint`](#lint) +- [`gulp makemessages`](#makemessages) - [`gulp makemigrations`](#makemigrations) - [`gulp migrate`](#migrate) - [`gulp reset`](#reset) @@ -126,6 +156,11 @@ the `babybuddy/static` folder, so generally `gulp build` should be run before this command for production deployments. Gulp also passes along non-overlapping arguments for this command, e.g. `--no-input`. +#### `compilemessages` + +Executes Django's `compilemessages` management task. See [django-admin compilemessages](https://docs.djangoproject.com/en/dev/ref/django-admin/#compilemessages) +for more details about other options and functionality of this command. + #### `coverage` Create a test coverage report. See [`.coveragerc`](.coveragerc) for default @@ -147,6 +182,14 @@ children and seven days of data for each. Executes Python and SASS linting for all relevant source files. +#### `makemessages` + +Executes Django's `makemessages` management task. See [django-admin makemessages](https://docs.djangoproject.com/en/dev/ref/django-admin/#makemessages) +for more details about other options and functionality of this command. When +working on a single translation, the `-l` flag is useful to make message for +only that language, e.g. `gulp makemessages -l fr` to make only a French +language translation file. + #### `makemigrations` Executes Django's `makemigrations` management task. Gulp also passes along diff --git a/babybuddy/admin.py b/babybuddy/admin.py index ee7e4fd0..0eb69815 100644 --- a/babybuddy/admin.py +++ b/babybuddy/admin.py @@ -2,16 +2,18 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.models import User +from django.utils.translation import gettext_lazy as _ from babybuddy import models class SettingsInline(admin.StackedInline): model = models.Settings - verbose_name_plural = 'Settings' + verbose_name = _('Settings') + verbose_name_plural = _('Settings') can_delete = False fieldsets = ( - ('Dashboard', { + (_('Dashboard'), { 'fields': ('dashboard_refresh_rate',) }), ) diff --git a/babybuddy/forms.py b/babybuddy/forms.py index 017a2191..004d7380 100644 --- a/babybuddy/forms.py +++ b/babybuddy/forms.py @@ -41,4 +41,4 @@ class UserPasswordForm(PasswordChangeForm): class UserSettingsForm(forms.ModelForm): class Meta: model = Settings - fields = ['dashboard_refresh_rate'] + fields = ['dashboard_refresh_rate', 'language'] diff --git a/babybuddy/migrations/0004_settings_language.py b/babybuddy/migrations/0004_settings_language.py new file mode 100644 index 00000000..d473b65b --- /dev/null +++ b/babybuddy/migrations/0004_settings_language.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2019-04-17 04:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('babybuddy', '0003_add_refresh_help_text'), + ] + + operations = [ + migrations.AddField( + model_name='settings', + name='language', + field=models.CharField(choices=[], default='en', max_length=255, verbose_name='Language'), + ), + ] diff --git a/babybuddy/models.py b/babybuddy/models.py index 1c36ed9b..97636c31 100644 --- a/babybuddy/models.py +++ b/babybuddy/models.py @@ -1,9 +1,14 @@ # -*- coding: utf-8 -*- +from django.conf import settings from django.contrib.auth.models import User +from django.contrib.auth.signals import user_logged_in from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver from django.utils.timezone import timedelta +from django.utils.text import format_lazy +from django.utils import translation +from django.utils.translation import gettext_lazy as _ from rest_framework.authtoken.models import Token @@ -11,26 +16,32 @@ from rest_framework.authtoken.models import Token class Settings(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) dashboard_refresh_rate = models.DurationField( - verbose_name='Refresh rate', - help_text='This setting will only be used when a browser does not ' - 'support refresh on focus.', + verbose_name=_('Refresh rate'), + help_text=_('This setting will only be used when a browser does not ' + 'support refresh on focus.'), blank=True, null=True, default=timedelta(minutes=1), choices=[ - (None, 'disabled'), - (timedelta(minutes=1), '1 min.'), - (timedelta(minutes=2), '2 min.'), - (timedelta(minutes=3), '3 min.'), - (timedelta(minutes=4), '4 min.'), - (timedelta(minutes=5), '5 min.'), - (timedelta(minutes=10), '10 min.'), - (timedelta(minutes=15), '15 min.'), - (timedelta(minutes=30), '30 min.'), + (None, _('disabled')), + (timedelta(minutes=1), _('1 min.')), + (timedelta(minutes=2), _('2 min.')), + (timedelta(minutes=3), _('3 min.')), + (timedelta(minutes=4), _('4 min.')), + (timedelta(minutes=5), _('5 min.')), + (timedelta(minutes=10), _('10 min.')), + (timedelta(minutes=15), _('15 min.')), + (timedelta(minutes=30), _('30 min.')), ]) + language = models.CharField( + choices=settings.LANGUAGES, + default=settings.LANGUAGE_CODE, + max_length=255, + verbose_name=_('Language') + ) def __str__(self): - return '{}\'s Settings'.format(self.user) + return str(format_lazy(_('{user}\'s Settings'), user=self.user)) def api_key(self, reset=False): """ @@ -63,3 +74,9 @@ def create_user_settings(sender, instance, created, **kwargs): @receiver(post_save, sender=User) def save_user_settings(sender, instance, **kwargs): instance.settings.save() + + +@receiver(user_logged_in) +def user_logged_in_callback(sender, request, user, **kwargs): + translation.activate(user.settings.language) + request.session[translation.LANGUAGE_SESSION_KEY] = user.settings.language diff --git a/babybuddy/settings/base.py b/babybuddy/settings/base.py index c49e19c8..94b9e126 100644 --- a/babybuddy/settings/base.py +++ b/babybuddy/settings/base.py @@ -1,5 +1,6 @@ import os +from django.utils.translation import gettext_lazy as _ from dotenv import load_dotenv, find_dotenv # Build paths inside the project like this: os.path.join(BASE_DIR, ...) @@ -53,10 +54,9 @@ INSTALLED_APPS = [ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', - 'whitenoise.middleware.WhiteNoiseMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', @@ -110,7 +110,7 @@ LOGOUT_REDIRECT_URL = '/login/' # Internationalization # https://docs.djangoproject.com/en/1.11/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = 'en' TIME_ZONE = os.environ.get('TIME_ZONE', 'Etc/UTC') @@ -120,6 +120,15 @@ USE_L10N = True USE_TZ = True +LOCALE_PATHS = [ + os.path.join(BASE_DIR, "locale"), +] + +LANGUAGES = [ + ('en', _('English')), + ('fr', _('French')), +] + # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.11/howto/static-files/ diff --git a/babybuddy/templates/403.html b/babybuddy/templates/403.html index 0eb0d4d5..530950a0 100644 --- a/babybuddy/templates/403.html +++ b/babybuddy/templates/403.html @@ -1,15 +1,15 @@ {% extends 'babybuddy/page.html' %} -{% load widget_tweaks %} +{% load i18n widget_tweaks %} -{% block title %}403 Permission Denied{% endblock %} +{% block title %}403 {% trans "Permission Denied" %}{% endblock %} {% block breadcrumbs %} - + {% endblock %} {% block content %} {% endblock %} \ No newline at end of file diff --git a/babybuddy/templates/babybuddy/base.html b/babybuddy/templates/babybuddy/base.html index 04878c35..a17188aa 100644 --- a/babybuddy/templates/babybuddy/base.html +++ b/babybuddy/templates/babybuddy/base.html @@ -1,4 +1,4 @@ -{% load static %} +{% load i18n static %} @@ -31,7 +31,7 @@ {% block breadcrumb_nav %} diff --git a/babybuddy/templates/babybuddy/filter.html b/babybuddy/templates/babybuddy/filter.html index 0379d6a9..84765a40 100644 --- a/babybuddy/templates/babybuddy/filter.html +++ b/babybuddy/templates/babybuddy/filter.html @@ -1,4 +1,4 @@ -{% load widget_tweaks %} +{% load i18n widget_tweaks %}
@@ -15,8 +15,8 @@
{% endfor %}
- - Reset + + {% trans "Reset" %}
@@ -29,6 +29,6 @@ role="button" aria-expanded="false" aria-controls="filter_form"> - Filters + {% trans "Filters" %}

diff --git a/babybuddy/templates/babybuddy/form.html b/babybuddy/templates/babybuddy/form.html index 83dc2555..25bf20dc 100644 --- a/babybuddy/templates/babybuddy/form.html +++ b/babybuddy/templates/babybuddy/form.html @@ -1,4 +1,4 @@ -{% load widget_tweaks %} +{% load i18n widget_tweaks %}
@@ -8,6 +8,6 @@ {% include 'babybuddy/form_field.html' %}
{% endfor %} - +
diff --git a/babybuddy/templates/babybuddy/messages.html b/babybuddy/templates/babybuddy/messages.html index 45627ffe..c751a753 100644 --- a/babybuddy/templates/babybuddy/messages.html +++ b/babybuddy/templates/babybuddy/messages.html @@ -1,3 +1,5 @@ +{% load i18n %} + {% block messages %} {% if messages %} {% for message in messages %} @@ -13,12 +15,12 @@ {% if form.non_field_errors %} {% for error in form.non_field_errors %} {% endfor %} {% elif form.errors %} {% endif %} {% endif %} diff --git a/babybuddy/templates/babybuddy/nav-dropdown.html b/babybuddy/templates/babybuddy/nav-dropdown.html index 43d4013b..1bace63c 100644 --- a/babybuddy/templates/babybuddy/nav-dropdown.html +++ b/babybuddy/templates/babybuddy/nav-dropdown.html @@ -1,5 +1,5 @@ {% extends 'babybuddy/base.html' %} -{% load babybuddy_tags static timers %} +{% load babybuddy_tags i18n static timers %} {% block nav %}