Skip to content


feat(i18n): make the strings translatable
Browse files Browse the repository at this point in the history
  • Loading branch information
Jérôme Deuchnord committed Jan 16, 2020
1 parent 29adfef commit c4f07a1
Show file tree
Hide file tree
Showing 18 changed files with 551 additions and 141 deletions.
13 changes: 13 additions & 0 deletions .github/workflows/pythonapp.yml
Expand Up @@ -26,4 +26,17 @@ jobs:
- name: Lint
run: |
pipenv run pylint kosmorro *.py kosmorrolib/*.py
- name: Check i18n
run: |
pipenv run python extract_messages --output-file=/tmp/kosmorro-messages.pot > /dev/null
n=$(diff -y --suppress-common-lines kosmorrolib/locales/messages.pot /tmp/kosmorro-messages.pot | grep -v -E '^"POT-Creation-Date: ' | wc -l)
if [ "$n" -ne "0" ]; then
echo "❌ The messages file is not up-to-date!"
echo " Please run the following command to fix this:"
echo " pipenv run python extract_messages --output-file=kosmorrolib/locales/messages.pot"
exit 1
echo "✔ Messages file up-to-date."
5 changes: 4 additions & 1 deletion .github/workflows/release.yml
Expand Up @@ -16,11 +16,14 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine skyfield numpy tabulate
pip install setuptools wheel twine skyfield numpy tabulate Babel requests
- name: Build and publish
run: |
python .scripts/build/
python sdist bdist_wheel
twine upload dist/*
4 changes: 4 additions & 0 deletions .gitignore
Expand Up @@ -6,3 +6,7 @@ kosmorro.egg-info

# Translation files are taken care on
52 changes: 52 additions & 0 deletions .scripts/build/
@@ -0,0 +1,52 @@
#!/usr/bin/env python3

# This script's purpose is to retrieve the translations from POEditor (
# It is mainly used in the release process.
# (c) Jérôme Deuchnord - MIT License

import os
import requests


languages ='%s/languages/list' % POEDITOR_URL,
data={'api_token': API_TOKEN,
'id': PROJECT_ID})

json = languages.json()

if languages.status_code != 200:
raise AssertionError(json['response']['message'])

for language in json['result']['languages']:
if language['percentage'] < 100:
# Ignore unfinished translations

print('Importing finished translation for %s... ' % language['name'], end='')

translations ='%s/projects/export' % POEDITOR_URL,
data={'api_token': API_TOKEN,
'language': language['code'],
'type': 'po'})

if translations.status_code != 200:
raise AssertionError(translations.json()['response']['message'])

translations = requests.get(translations.json()['result']['url'])

if translations.status_code != 200:
raise AssertionError('URL given by the API returned a %d status code' % translations.status_code)

os.makedirs('kosmorrolib/locales/%s/LC_MESSAGES' % language['code'], exist_ok=True)

with open('kosmorrolib/locales/%s/LC_MESSAGES/messages.po' % language['code'], 'w') as file:


31 changes: 31 additions & 0 deletions
Expand Up @@ -15,13 +15,44 @@ If it is not, [create a bug report](
Have an idea of feature you think would be nice on Kosmorro? Time to suggest it!
First, please check someone didn't suggest your next revolution in the _Issues_ tab. If it's not, [create a feature request]( and fill in the templace that offers to you.

## Translating

If you speak another language than English, another nice way to enhance Kosmorro is to translate its messages. The recommended way to begin translating Kosmorro is to [join the POEditor team](

## Writing code

First of all, if you are fixing an opened issue, check that nobody is already working on it — if someone seems to be but their Pull Request seems stuck, please ask them first if you can continue the development. If you retake the code they produced, **don't change the author of the commits**.

Before writing the code, first create a fork of the repository and clone it. You may also want to add the original repository (`Deuchnord/kosmorro`), so you can update your fork with the last upstream commits.
Then create a new branch and start coding. Finally, commit and push, then open a PR on this project. If your project is not complete, feel free to open it as Draft (if you forgot to activate the Draft status, just edit the first comment to say it), then mark it as ready for review when you're done.

### Dealing with the translations

The messages file contains all the messages Kosmorro can display, in order to make them translatable. When you change code, you may change also the messages displayed by the software.

When you add a new string that will be displayed to the end user, please pass it to the `_()` function made available in the `kosmorrolib.i18n` package, for instance:

# Dont:
print('Note: All the hours are given in UTC.')

# Do:
from kosmorrolib.i18n import _
print(_('Note: All the hours are given in UTC.'))

This will allow Python's internationalization tool to translate the string in any available language.

Once you have done your work, please remember to tell [Babel]( to extract the new strings:

$ pipenv run python extract_messages --output-file=kosmorrolib/locales/messages.pot

> If the `` script tells you that the `extract_messages` command does not exist, then run `kosmorro sync` to ensure all the dev dependencies are installed and try again.
Note that if you forget to update the messages file, the CI will fail.

### Matching the coding standards

Kosmorro's source code follows the major coding standards of Python (PEPs). Before marking your Pull Request as ready for review, don't forget to check that the code respects the coding standards with PyLint (it is run on the CI, but feel free to run it on your local machine too). Your PR must have a global note of 10/10 to be elligible to merge.
Expand Down
1 change: 1 addition & 0 deletions
@@ -0,0 +1 @@
recursive-include kosmorrolib/locales *
1 change: 1 addition & 0 deletions Pipfile
Expand Up @@ -7,6 +7,7 @@ verify_ssl = true
pylintfileheader = "*"
pylint = "*"
codecov = "*"
babel = "*"

skyfield = ">=1.13.0,<2.0.0"
Expand Down
19 changes: 17 additions & 2 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

105 changes: 3 additions & 102 deletions kosmorro
Expand Up @@ -16,110 +16,11 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <>.

import argparse
import sys
from datetime import date

from kosmorrolib.version import VERSION
from kosmorrolib import dumper
from kosmorrolib import core
from kosmorrolib.ephemerides import EphemeridesComputer, Position
from kosmorrolib import events

def main():
output_formats = get_dumpers()
args = get_args(list(output_formats.keys()))

if args.special_action is not None:
return 0 if args.special_action() else 1

year = args.year
month = args.month
day =

compute_date = date(year, month, day)

if day is not None and month is None:
month =

if args.latitude is None or args.longitude is None:
position = None
position = Position(args.latitude, args.longitude)

ephemeris = EphemeridesComputer(position)
ephemerides = ephemeris.compute_ephemerides(year, month, day)

events_list = events.search_events(compute_date)

dump = output_formats[args.format](ephemerides, events_list, compute_date)

return 0

def get_dumpers() -> {str: dumper.Dumper}:
return {
'text': dumper.TextDumper,
'json': dumper.JsonDumper

def output_version() -> bool:
python_version = '%d.%d.%d' % (sys.version_info[0], sys.version_info[1], sys.version_info[2])
print('Kosmorro %s' % VERSION)
print('Running on Python %s' % python_version)

return True

def clear_cache() -> bool:
confirm = input("Do you really want to clear Kosmorro's cache? [yN] ").upper()
if confirm == 'Y':
except FileNotFoundError:
elif confirm not in ('N', ''):
print('Answer did not match expected options, cache not cleared.')
return False

return True

def get_args(output_formats: [str]):
today =

parser = argparse.ArgumentParser(description='Compute the ephemerides and the events for a given date,'
' at a given position on Earth.',
epilog='By default, only the events will be computed for today (%s).\n'
'To compute also the ephemerides, latitude and longitude arguments'
' are needed.'
% today.strftime('%a %b %d, %Y'))

parser.add_argument('--version', '-v', dest='special_action', action='store_const', const=output_version,
default=None, help='Show the program version')
parser.add_argument('--clear-cache', dest='special_action', action='store_const', const=clear_cache, default=None,
help='Delete all the files Kosmorro stored in the cache.')
parser.add_argument('--format', '-f', type=str, default=output_formats[0], choices=output_formats,
help='The format under which the information have to be output')
parser.add_argument('--latitude', '-lat', type=float, default=None,
help="The observer's latitude on Earth")
parser.add_argument('--longitude', '-lon', type=float, default=None,
help="The observer's longitude on Earth")
parser.add_argument('--day', '-d', type=int,,
help='A number between 1 and 28, 29, 30 or 31 (depending on the month). The day you want to '
' compute the ephemerides for. Defaults to %d (the current day).' %
parser.add_argument('--month', '-m', type=int, default=today.month,
help='A number between 1 and 12. The month you want to compute the ephemerides for. Defaults to'
' %d (the current month).' % today.month)
parser.add_argument('--year', '-y', type=int, default=today.year,
help='The year you want to compute the ephemerides for.'
' Defaults to %d (the current year).' % today.year)

return parser.parse_args()
import locale
from kosmorrolib.main import main

locale.setlocale(locale.LC_ALL, '')

if __name__ == '__main__':
21 changes: 11 additions & 10 deletions kosmorrolib/
Expand Up @@ -25,21 +25,22 @@
from skyfield.nutationlib import iau2000b

from .data import Star, Planet, Satellite, MOON_PHASES, MoonPhase
from .i18n import _

CACHE_FOLDER = str(Path.home()) + '/.kosmorro-cache'

MONTHS = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']

ASTERS = [Star('Sun', 'SUN'),
Satellite('Moon', 'MOON'),
Planet('Mercury', 'MERCURY'),
Planet('Venus', 'VENUS'),
Planet('Mars', 'MARS'),
Planet('Jupiter', 'JUPITER BARYCENTER'),
Planet('Saturn', 'SATURN BARYCENTER'),
Planet('Uranus', 'URANUS BARYCENTER'),
Planet('Neptune', 'NEPTUNE BARYCENTER'),
Planet('Pluto', 'PLUTO BARYCENTER')]
ASTERS = [Star(_('Sun'), 'SUN'),
Satellite(_('Moon'), 'MOON'),
Planet(_('Mercury'), 'MERCURY'),
Planet(_('Venus'), 'VENUS'),
Planet(_('Mars'), 'MARS'),
Planet(_('Jupiter'), 'JUPITER BARYCENTER'),
Planet(_('Saturn'), 'SATURN BARYCENTER'),
Planet(_('Uranus'), 'URANUS BARYCENTER'),
Planet(_('Neptune'), 'NEPTUNE BARYCENTER'),
Planet(_('Pluto'), 'PLUTO BARYCENTER')]

def get_loader():
Expand Down
22 changes: 12 additions & 10 deletions kosmorrolib/
Expand Up @@ -22,20 +22,22 @@
from skyfield.api import Topos
from skyfield.timelib import Time

from .i18n import _

'NEW_MOON': 'New Moon',
'WAXING_CRESCENT': 'Waxing crescent',
'FIRST_QUARTER': 'First Quarter',
'WAXING_GIBBOUS': 'Waxing gibbous',
'FULL_MOON': 'Full Moon',
'WANING_GIBBOUS': 'Waning gibbous',
'LAST_QUARTER': 'Last Quarter',
'WANING_CRESCENT': 'Waning crescent'
'NEW_MOON': _('New Moon'),
'WAXING_CRESCENT': _('Waxing crescent'),
'FIRST_QUARTER': _('First Quarter'),
'WAXING_GIBBOUS': _('Waxing gibbous'),
'FULL_MOON': _('Full Moon'),
'WANING_GIBBOUS': _('Waning gibbous'),
'LAST_QUARTER': _('Last Quarter'),
'WANING_CRESCENT': _('Waning crescent')

'OPPOSITION': {'message': '%s is in opposition'},
'CONJUNCTION': {'message': '%s and %s are in conjunction'}
'OPPOSITION': {'message': _('%s is in opposition')},
'CONJUNCTION': {'message': _('%s and %s are in conjunction')}

Expand Down

0 comments on commit c4f07a1

Please sign in to comment.