├── tests
├── __init__.py
├── test_project
│ ├── project
│ │ ├── __init__.py
│ │ ├── static
│ │ │ └── test.js
│ │ ├── locale
│ │ │ ├── en
│ │ │ │ └── LC_MESSAGES
│ │ │ │ │ ├── djangojs.mo
│ │ │ │ │ └── djangojs.po
│ │ │ ├── fr
│ │ │ │ └── LC_MESSAGES
│ │ │ │ │ ├── djangojs.mo
│ │ │ │ │ └── djangojs.po
│ │ │ ├── ko_KR
│ │ │ │ └── LC_MESSAGES
│ │ │ │ │ ├── djangojs.mo
│ │ │ │ │ └── djangojs.po
│ │ │ └── zh_Hans
│ │ │ │ └── LC_MESSAGES
│ │ │ │ ├── djangojs.mo
│ │ │ │ └── djangojs.po
│ │ ├── urls.py
│ │ ├── templates
│ │ │ └── base.html
│ │ └── settings.py
│ └── manage.py
├── conftest.py
├── test_utils.py
└── test_app.py
├── src
└── statici18n
│ ├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ └── compilejsi18n.py
│ ├── templatetags
│ ├── __init__.py
│ └── statici18n.py
│ ├── __init__.py
│ ├── apps.py
│ ├── conf.py
│ └── utils.py
├── .python-version
├── MANIFEST.in
├── .gitignore
├── docs
├── index.rst
├── Makefile
├── make.bat
├── templatetags.rst
├── troubleshooting.rst
├── commands.rst
├── conf.py
├── settings.rst
├── faq.rst
├── changelog.rst
└── _ext
│ └── djangodocs.py
├── setup.cfg
├── .readthedocs.yaml
├── .github
└── workflows
│ ├── codecov.yml
│ └── build.yml
├── tox.ini
├── setup.py
├── LICENSE
└── README.rst
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/statici18n/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/test_project/project/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/statici18n/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/statici18n/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
1 | 3.13.0
2 | 3.12.7
3 | 3.11.10
4 | 3.10.15
5 | 3.9.20
6 | 3.8.20
7 |
--------------------------------------------------------------------------------
/tests/test_project/project/static/test.js:
--------------------------------------------------------------------------------
1 | document.write(gettext('Hello world!'));
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.rst LICENSE
2 | recursive-exclude statici18n *.pyc
3 | recursive-exclude tests *
4 |
--------------------------------------------------------------------------------
/tests/test_project/project/locale/en/LC_MESSAGES/djangojs.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zyegfryed/django-statici18n/HEAD/tests/test_project/project/locale/en/LC_MESSAGES/djangojs.mo
--------------------------------------------------------------------------------
/tests/test_project/project/locale/fr/LC_MESSAGES/djangojs.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zyegfryed/django-statici18n/HEAD/tests/test_project/project/locale/fr/LC_MESSAGES/djangojs.mo
--------------------------------------------------------------------------------
/tests/test_project/project/locale/ko_KR/LC_MESSAGES/djangojs.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zyegfryed/django-statici18n/HEAD/tests/test_project/project/locale/ko_KR/LC_MESSAGES/djangojs.mo
--------------------------------------------------------------------------------
/tests/test_project/project/locale/zh_Hans/LC_MESSAGES/djangojs.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zyegfryed/django-statici18n/HEAD/tests/test_project/project/locale/zh_Hans/LC_MESSAGES/djangojs.mo
--------------------------------------------------------------------------------
/src/statici18n/__init__.py:
--------------------------------------------------------------------------------
1 | # following PEP 386
2 | __version__ = "2.6.0"
3 |
4 | import django
5 |
6 | if django.VERSION < (3, 2):
7 | default_app_config = "statici18n.apps.StaticI18NConfig"
8 |
--------------------------------------------------------------------------------
/src/statici18n/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class StaticI18NConfig(AppConfig):
5 | name = "statici18n"
6 |
7 | def ready(self):
8 | from . import conf # noqa
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .coverage
2 | .tox
3 | .DS_Store
4 | *.egg-info
5 | *.pyc
6 | *.egg
7 | *.swp
8 | pip-log.txt
9 | /htmlcov
10 | /cover
11 | /build
12 | /dist
13 | /docs/_build
14 | .cache
15 | .pytest_cache
16 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../README.rst
2 |
3 | Documentation
4 | -------------
5 |
6 | .. toctree::
7 | :maxdepth: 2
8 |
9 | commands
10 | templatetags
11 | settings
12 | troubleshooting
13 | faq
14 | changelog
15 |
--------------------------------------------------------------------------------
/tests/test_project/project/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from django.views.generic.base import TemplateView
3 |
4 | urlpatterns = [
5 | "",
6 | path("", TemplateView.as_view(template_name="base.html")),
7 | path("jsi18n/", "django.views.i18n.javascript_catalog", name="jsi18n"),
8 | ]
9 |
--------------------------------------------------------------------------------
/tests/test_project/project/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | A test page
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/tests/test_project/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
7 |
8 | from django.core.management import execute_from_command_line
9 |
10 | execute_from_command_line(sys.argv)
11 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bumpversion]
2 | current_version = 2.6.0
3 |
4 | [bumpversion:file:setup.py]
5 |
6 | [bumpversion:file:src/statici18n/__init__.py]
7 |
8 | [bumpversion:file:docs/conf.py]
9 |
10 | [tool:pytest]
11 | python_files = test*.py
12 | addopts = --tb=short -x
13 |
14 | [flake8]
15 | ignore = F999,E501,E128,E124
16 | max-line-length = 100
17 | exclude = .tox,.git,docs
18 |
19 | [wheel]
20 | universal = 1
21 |
--------------------------------------------------------------------------------
/tests/test_project/project/locale/en/LC_MESSAGES/djangojs.po:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr ""
3 | "Project-Id-Version: PACKAGE VERSION\n"
4 | "Report-Msgid-Bugs-To: \n"
5 | "POT-Creation-Date: 2018-02-11 17:06+0000\n"
6 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
7 | "Last-Translator: FULL NAME \n"
8 | "Language-Team: LANGUAGE \n"
9 | "Language: en\n"
10 | "MIME-Version: 1.0\n"
11 | "Content-Type: text/plain; charset=UTF-8\n"
12 | "Content-Transfer-Encoding: 8bit\n"
13 |
14 | #: project/static/test.js:1
15 | msgid "Hello world!"
16 | msgstr ""
17 |
--------------------------------------------------------------------------------
/tests/test_project/project/locale/zh_Hans/LC_MESSAGES/djangojs.po:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr ""
3 | "Project-Id-Version: PACKAGE VERSION\n"
4 | "Report-Msgid-Bugs-To: \n"
5 | "POT-Creation-Date: 2018-02-11 17:06+0000\n"
6 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
7 | "Last-Translator: FULL NAME \n"
8 | "Language-Team: LANGUAGE \n"
9 | "Language: zh_Hans\n"
10 | "MIME-Version: 1.0\n"
11 | "Content-Type: text/plain; charset=UTF-8\n"
12 | "Content-Transfer-Encoding: 8bit\n"
13 | "Plural-Forms: nplurals=1; plural=0;\n"
14 |
15 | #: project/static/test.js:1
16 | msgid "Hello world!"
17 | msgstr "大家好!"
18 |
--------------------------------------------------------------------------------
/tests/test_project/project/locale/fr/LC_MESSAGES/djangojs.po:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr ""
3 | "Project-Id-Version: PACKAGE VERSION\n"
4 | "Report-Msgid-Bugs-To: \n"
5 | "POT-Creation-Date: 2018-02-11 17:06+0000\n"
6 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
7 | "Last-Translator: FULL NAME \n"
8 | "Language-Team: LANGUAGE \n"
9 | "Language: fr\n"
10 | "MIME-Version: 1.0\n"
11 | "Content-Type: text/plain; charset=UTF-8\n"
12 | "Content-Transfer-Encoding: 8bit\n"
13 | "Plural-Forms: nplurals=2; plural=(n > 1);\n"
14 |
15 | #: project/static/test.js:1
16 | msgid "Hello world!"
17 | msgstr "Bonjour à tous !"
18 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | # import os
2 | # import sys
3 | import shutil
4 | import pytest
5 |
6 |
7 | # FIXME: wait for #216 to be merged
8 | # See: https://github.com/pytest-dev/pytest-django/pull/216
9 | # def pytest_configure():
10 | # BASE_DIR = os.path.join(os.path.dirname(__file__))
11 | # sys.path.append(os.path.realpath(os.path.join(BASE_DIR, "test_project")))
12 | # os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
13 |
14 |
15 | @pytest.fixture
16 | def cleandir(request, settings):
17 | def teardown():
18 | shutil.rmtree(settings.STATICI18N_ROOT)
19 |
20 | request.addfinalizer(teardown)
21 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | # Read the Docs configuration file
2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
3 |
4 | # Required
5 | version: 2
6 |
7 | # Set the version of Python and other tools you might need
8 | build:
9 | os: ubuntu-20.04
10 | tools:
11 | python: "3.9"
12 |
13 | # Build documentation in the docs/ directory with Sphinx
14 | sphinx:
15 | configuration: docs/conf.py
16 |
17 | # If using Sphinx, optionally build your docs in additional formats such as PDF
18 | # formats:
19 | # - pdf
20 |
21 | # Optionally declare the Python requirements required to build your docs
22 | # python:
23 | # install:
24 | # - requirements: docs/requirements.txt
25 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/src/statici18n/conf.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings # noqa
2 |
3 | from appconf import AppConf
4 |
5 |
6 | class StaticFilesConf(AppConf):
7 | # The gettext domain to use when generating catalog files.
8 | DOMAIN = "djangojs"
9 | # A list of packages to check for translations.
10 | PACKAGES = "django.conf"
11 | # Controls the file path that generated catalog will be written into.
12 | ROOT = settings.STATIC_ROOT
13 | # Controls the directory inside STATICI18N_ROOT
14 | # that generated files will be written to.
15 | OUTPUT_DIR = "jsi18n"
16 | # The dotted path to the function that creates the filename
17 | FILENAME_FUNCTION = "statici18n.utils.default_filename"
18 | # Javascript identifier to use as namespace.
19 | NAMESPACE = None
20 |
--------------------------------------------------------------------------------
/tests/test_project/project/locale/ko_KR/LC_MESSAGES/djangojs.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the PACKAGE package.
4 | # FIRST AUTHOR , YEAR.
5 | #
6 | #, fuzzy
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: PACKAGE VERSION\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2018-02-11 17:06+0000\n"
12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13 | "Last-Translator: FULL NAME \n"
14 | "Language-Team: LANGUAGE \n"
15 | "Language: ko-KR\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=UTF-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 |
20 | #: project/static/test.js:1
21 | msgid "Hello world!"
22 | msgstr "안녕하세요!"
23 |
--------------------------------------------------------------------------------
/.github/workflows/codecov.yml:
--------------------------------------------------------------------------------
1 | name: Code coverage
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v4
12 | - name: Set up Python
13 | uses: actions/setup-python@v5
14 | with:
15 | python-version: '3.12'
16 | - name: Install dependencies
17 | run: |
18 | python -m pip install --upgrade pip wheel
19 | pip install tox
20 | - name: Generate coverage report
21 | run: tox -e coverage
22 | - name: Upload coverage to Codecov
23 | uses: codecov/codecov-action@v4
24 | env:
25 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
26 | with:
27 | fail_ci_if_error: true
28 | files: ./coverage.xml
29 | flags: unittests
30 | verbose: true
31 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist =
3 | py{38,39,310,311,312}-django42,
4 | py{310,311,312}-django50,
5 | py{310,311,312,313}-django51,
6 | coverage
7 |
8 | [gh-actions]
9 | python =
10 | 3.8: py38
11 | 3.9: py39
12 | 3.10: py310
13 | 3.11: py311
14 | 3.12: py312
15 | 3.13: py313
16 |
17 | [testenv]
18 | passenv =
19 | CI
20 | setenv =
21 | PYTHONPATH={toxinidir}/tests/test_project
22 | DJANGO_SETTINGS_MODULE=project.settings
23 | commands = pytest -q tests
24 | deps =
25 | pytest
26 | pytest-django
27 | django42: Django>=4.2,<4.3
28 | django50: Django>=5.0,<5.1
29 | django51: Django>=5.1,<5.2
30 |
31 | [testenv:coverage]
32 | basepython = python3.12
33 | commands =
34 | pytest -q --cov=statici18n --cov-report=xml tests
35 | deps =
36 | Django>=4.2,<5.0
37 | pytest
38 | pytest-cov
39 | pytest-django
40 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 |
13 | if "%1" == "" goto help
14 |
15 | %SPHINXBUILD% >NUL 2>NUL
16 | if errorlevel 9009 (
17 | echo.
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19 | echo.installed, then set the SPHINXBUILD environment variable to point
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
21 | echo.may add the Sphinx directory to PATH.
22 | echo.
23 | echo.If you don't have Sphinx installed, grab it from
24 | echo.http://sphinx-doc.org/
25 | exit /b 1
26 | )
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs/templatetags.rst:
--------------------------------------------------------------------------------
1 | .. module:: statici18n
2 | :synopsis: An app for handling JavaScript catalog.
3 |
4 | Template tags
5 | =============
6 |
7 | .. highlight:: html+django
8 |
9 | statici18n
10 | ----------
11 |
12 | .. function:: templatetags.statici18n.statici18n(locale)
13 |
14 | .. versionadded:: 0.4
15 |
16 | Builds the full JavaScript catalog URL for the given locale by joining the
17 | :attr:`~django.conf.settings.STATICI18N_OUTPUT_DIR` and
18 | :attr:`~django.conf.settings.STATICI18N_FILENAME_FUNCTION` settings::
19 |
20 | {% load statici18n %}
21 |
22 |
23 | This is especially useful when using a non-local storage backend to
24 | :ref:`deploy files to a CDN ` or when using :class:`~django.contrib.staticfiles.storage.CachedStaticFilesStorage` storage to serve files.
25 |
26 | .. note::
27 |
28 | Behind the scenes, it's a thin wrapper around the :django:ttag:`static`
29 | template tag. Therefore, ensure that :mod:`django.contrib.staticfiles` is
30 | configured before proceeding. See :ref:`staticfiles-configuration` for more
31 | information.
32 |
--------------------------------------------------------------------------------
/src/statici18n/templatetags/statici18n.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from django import template
4 | from django.utils.safestring import mark_safe
5 | from django.templatetags.static import static
6 | from django.contrib.staticfiles.storage import staticfiles_storage
7 |
8 | from statici18n.conf import settings
9 | from statici18n.utils import get_filename
10 |
11 | register = template.Library()
12 |
13 |
14 | def get_path(locale):
15 | return os.path.join(
16 | settings.STATICI18N_OUTPUT_DIR, get_filename(locale, settings.STATICI18N_DOMAIN)
17 | )
18 |
19 |
20 | @register.simple_tag
21 | def statici18n(locale):
22 | """
23 | A template tag that returns the URL to a Javascript catalog
24 | for the selected locale.
25 |
26 | Behind the scenes, this is a thin wrapper around staticfiles's static
27 | template tag.
28 | """
29 | return static(get_path(locale))
30 |
31 |
32 | @register.simple_tag
33 | def inlinei18n(locale):
34 | """
35 | A template tag that returns the Javascript catalog content
36 | for the selected locale to be inlined in a block.
37 |
38 | Behind the scenes, this is a thin wrapper around staticfiles's configred
39 | storage
40 | """
41 | return mark_safe(staticfiles_storage.open(get_path(locale)).read().decode())
42 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on: [push,pull_request]
4 |
5 | jobs:
6 | build:
7 |
8 | name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
9 | runs-on: ${{ matrix.os }}
10 | strategy:
11 | matrix:
12 | python-version: [3.8, 3.9, "3.10", "3.11", "3.12"]
13 | os: [ ubuntu-latest]
14 |
15 | # Python 3.6 & 3.7 are no longer available in ubuntu-latest
16 | include:
17 | - python-version: '3.6'
18 | os: ubuntu-20.04
19 | - python-version: '3.7'
20 | os: ubuntu-20.04
21 |
22 | steps:
23 | - uses: actions/checkout@v4
24 | - name: Set up Python ${{ matrix.python-version }}
25 | uses: actions/setup-python@v5
26 | with:
27 | python-version: ${{ matrix.python-version }}
28 | - name: Install dependencies
29 | run: |
30 | python -m pip install --upgrade pip wheel
31 | pip install flake8 tox tox-gh-actions
32 | - name: Lint with flake8
33 | run: |
34 | # stop the build if there are Python syntax errors or undefined names
35 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
36 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
37 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
38 | - name: Test with Tox
39 | run: tox
40 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | setup(
4 | name="django-statici18n",
5 | version="2.6.0",
6 | author="Sebastien Fievet",
7 | author_email="zyegfryed@gmail.com",
8 | url="http://django-statici18n.readthedocs.org/",
9 | description=("A Django app that compiles i18n JavaScript catalogs "
10 | "to static files."),
11 | long_description=open("README.rst").read(),
12 | package_dir={"": "src"},
13 | packages=find_packages("src"),
14 | include_package_data=True,
15 | zip_safe=False,
16 | install_requires=[
17 | "Django>=3.2",
18 | "django-appconf>=1.0",
19 | ],
20 | license="BSD",
21 | classifiers=[
22 | "Development Status :: 5 - Production/Stable",
23 | "Environment :: Web Environment",
24 | "Framework :: Django",
25 | "Intended Audience :: Developers",
26 | "License :: OSI Approved :: BSD License",
27 | "Operating System :: OS Independent",
28 | "Programming Language :: Python",
29 | "Programming Language :: Python :: 3",
30 | "Programming Language :: Python :: 3.6",
31 | "Programming Language :: Python :: 3.7",
32 | "Programming Language :: Python :: 3.8",
33 | "Programming Language :: Python :: 3.9",
34 | "Programming Language :: Python :: 3.10",
35 | "Programming Language :: Python :: 3.11",
36 | ],
37 | project_urls={
38 | "Source": "https://github.com/zyegfryed/django-statici18n",
39 | },
40 | )
41 |
--------------------------------------------------------------------------------
/docs/troubleshooting.rst:
--------------------------------------------------------------------------------
1 | Troubleshooting
2 | ===============
3 |
4 | Files are not served during development
5 | ---------------------------------------
6 |
7 | By default ``django-statici18n`` doesn't rely on
8 | :mod:`django.contrib.staticfiles`, so you have to serve the generated catalogs
9 | files with the Django dev server. For example::
10 |
11 | # urls.py
12 | from django.conf import settings
13 | from django.conf.urls.static import static
14 |
15 | urlpatterns = patterns('',
16 | # ... the rest of your URLconf goes here ...
17 | ) + static(settings.STATIC_URL, document_root=settings.STATICI18N_ROOT)
18 |
19 | However, when using the :mod:`statici18n` template tag you should first
20 | integrate ``django-static18n`` with :mod:`django.contrib.staticfiles`. See
21 | :ref:`staticfiles-configuration` for more information.
22 |
23 | .. note::
24 |
25 | Even if the setup looks a bit more tedious at first sight, using the
26 | :mod:`statici18n` template tag is the recommended way and it will make
27 | your life easier in the long run.
28 |
29 |
30 | Catalog is empty
31 | ----------------
32 |
33 | ``django-statici18n`` requires that the locale paths are available in the settings.
34 | So just add :django:setting:`LOCALE_PATHS=('/path/to/your/locale/directory',)` to the settings file.
35 |
36 | For more information on how Django discovers translations, refer to the `official documentation`_.
37 |
38 | .. _official documentation: https://docs.djangoproject.com/en/1.7/topics/i18n/translation/#how-django-discovers-translations
39 |
40 |
--------------------------------------------------------------------------------
/docs/commands.rst:
--------------------------------------------------------------------------------
1 | Management commands
2 | ===================
3 |
4 | .. highlight:: console
5 |
6 | .. _compilejsi18n:
7 |
8 | compilejsi18n
9 | -------------
10 |
11 | Collect JavaScript catalog files in a single location.
12 |
13 | Some commonly used options are:
14 |
15 | ``-l LOCALE`` or ``--locale=LOCALE``
16 | The locale to process. Default is to process all but if for some reason I18N
17 | features are disabled, only `settings.LANGUAGE_CODE` will be processed.
18 |
19 | ``-d DOMAIN`` or ``--domain=DOMAIN``
20 | Override the gettext domain. By default, the command uses the ``djangojs``
21 | gettext domain.
22 |
23 | ``-p PACKAGES`` or ``-packages=PACKAGES``
24 | A list of packages to check for translations. Default is ``'django.conf'``.
25 | Use multiple times to add more.
26 |
27 | ``-o OUPUT_DIR`` or ``--output=OUTPUT_DIR``
28 | Output directory to store generated catalogs. Defaults to the joining path
29 | of :attr:`~django.conf.settings.STATICI18N_ROOT` and
30 | :attr:`~django.conf.settings.STATICI18N_OUTPUT_DIR`.
31 |
32 | ``-f OUTPUT_FORMAT`` or ``--format=OUTPUT_FORMAT``
33 | Format of the output catalog. Options are:
34 | * ``js``,
35 | * ``json``.
36 |
37 | Defaults to ``js``.
38 |
39 | ``-n NAMESPACE`` or ``--namespace=NAMESPACE``
40 | The final gettext will be put with window.SpecialBlock.gettext rather
41 | than the window.gettext. This is useful for pluggable modules which
42 | need Javascript i18n.
43 |
44 | Defaults to ``None``.
45 |
46 | For a full list of options, refer to the ``compilejsi18n`` management command
47 | help by running::
48 |
49 | $ python manage.py compilejsi18n --help
50 |
51 |
52 | .. note::
53 |
54 | Missing directories will be created on-the-fly by the command when invoked.
55 |
--------------------------------------------------------------------------------
/src/statici18n/utils.py:
--------------------------------------------------------------------------------
1 | import os
2 | from collections.abc import Sequence
3 | from importlib import import_module
4 |
5 | from django.utils.translation import get_supported_language_variant
6 | from django.utils.translation.trans_real import to_language
7 |
8 | from statici18n.conf import settings
9 |
10 |
11 | def get_mod_func(callback):
12 | """
13 | Converts 'django.views.news.stories.story_detail' to
14 | ('django.views.news.stories', 'story_detail')
15 | """
16 | try:
17 | dot = callback.rindex(".")
18 | except ValueError:
19 | return callback, ""
20 | return callback[:dot], callback[dot + 1 :]
21 |
22 |
23 | def get_filename(*args, **kwargs):
24 | try:
25 | mod_name, func_name = get_mod_func(settings.STATICI18N_FILENAME_FUNCTION)
26 | _filename_func = getattr(import_module(mod_name), func_name)
27 | except (AttributeError, ImportError) as e:
28 | raise ImportError(
29 | "Couldn't import filename function %s: %s"
30 | % (settings.STATICI18N_FILENAME_FUNCTION, e)
31 | )
32 | return _filename_func(*args, **kwargs)
33 |
34 |
35 | def default_filename(locale, domain, output_format="js"):
36 | language_code = to_language(locale)
37 | language_code = get_supported_language_variant(language_code)
38 | return os.path.join(language_code, "%s.%s" % (domain, output_format))
39 |
40 |
41 | def legacy_filename(locale, domain, output_format="js"):
42 | return os.path.join(to_language(locale), "%s.%s" % (domain, output_format))
43 |
44 |
45 | def get_packages(packages):
46 | if packages == "django.conf":
47 | return None
48 |
49 | if isinstance(packages, str):
50 | return packages
51 |
52 | if isinstance(packages, Sequence):
53 | return "+".join(packages)
54 |
--------------------------------------------------------------------------------
/tests/test_project/project/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for project project.
3 |
4 | For more information on this file, see
5 | https://docs.djangoproject.com/en/1.6/topics/settings/
6 |
7 | For the full list of settings and their values, see
8 | https://docs.djangoproject.com/en/1.6/ref/settings/
9 | """
10 |
11 | import os
12 |
13 | BASE_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), os.pardir))
14 | PROJECT_ROOT = os.path.normpath(os.path.dirname(__file__))
15 |
16 |
17 | # Quick-start development settings - unsuitable for production
18 | # See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/
19 |
20 | # SECURITY WARNING: keep the secret key used in production secret!
21 | SECRET_KEY = "m!aji^@#s#bh9j8v0ct#fl1&9$a^pqq1d6f5ti49=unv3z3bn("
22 |
23 | # SECURITY WARNING: don't run with debug turned on in production!
24 | DEBUG = True
25 |
26 | TEMPLATE_DEBUG = True
27 |
28 | ALLOWED_HOSTS = []
29 |
30 |
31 | # Application definition
32 |
33 | INSTALLED_APPS = (
34 | "django.contrib.staticfiles",
35 | "statici18n",
36 | )
37 |
38 | MIDDLEWARE_CLASSES = ("django.middleware.common.CommonMiddleware",)
39 |
40 | ROOT_URLCONF = "project.urls"
41 |
42 | WSGI_APPLICATION = "project.wsgi.application"
43 |
44 |
45 | # Database
46 | # https://docs.djangoproject.com/en/1.6/ref/settings/#databases
47 |
48 | DATABASES = {
49 | "default": {
50 | "ENGINE": "django.db.backends.sqlite3",
51 | "NAME": "db.sqlite3",
52 | }
53 | }
54 |
55 | # Internationalization
56 | # https://docs.djangoproject.com/en/1.6/topics/i18n/
57 |
58 | LANGUAGE_CODE = "en-us"
59 |
60 | LANGUAGES = (
61 | ("en", "English"),
62 | ("fr", "French"),
63 | ("zh-Hans", "Simplified Chinese"),
64 | ("ko-KR", "Korean"),
65 | )
66 |
67 | LOCALE_PATHS = (os.path.join(PROJECT_ROOT, "locale"),)
68 |
69 | TIME_ZONE = "UTC"
70 |
71 | USE_I18N = True
72 |
73 | USE_L10N = True
74 |
75 | USE_TZ = True
76 |
77 |
78 | # Static files (CSS, JavaScript, Images)
79 | # https://docs.djangoproject.com/en/1.6/howto/static-files/
80 | STATIC_ROOT = os.path.realpath(os.path.join(BASE_DIR, "static"))
81 | STATIC_URL = "/static/"
82 |
--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from statici18n import utils
4 |
5 |
6 | def test_default_filename():
7 | filename = utils.get_filename("en", "djangojs")
8 | assert filename == "en/djangojs.js"
9 |
10 | filename = utils.get_filename("zh_Hans", "djangojs")
11 | assert filename == "zh-hans/djangojs.js"
12 |
13 | filename = utils.get_filename("ko_KR", "djangojs")
14 | assert filename == "ko-kr/djangojs.js"
15 |
16 |
17 | @pytest.mark.parametrize("locale", ["en", "en-gb"])
18 | def test_default_filename_coerce_locale(settings, locale):
19 | settings.LANGUAGES = [("en", "English")]
20 |
21 | filename = utils.default_filename(locale, "django")
22 | assert filename == "en/django.js"
23 |
24 |
25 | @pytest.mark.parametrize("fmt", ["js", "json", "yaml"])
26 | def test_default_filename_with_outputformat(fmt):
27 | filename = utils.get_filename("en", "djangojs", fmt)
28 | assert filename == "en/djangojs.%s" % fmt
29 |
30 |
31 | def test_legacy_filename(settings):
32 | settings.STATICI18N_FILENAME_FUNCTION = "statici18n.utils.legacy_filename"
33 |
34 | filename = utils.get_filename("en_GB", "djangojs")
35 | assert filename == "en-gb/djangojs.js"
36 |
37 | filename = utils.get_filename("zh-Hans", "djangojs")
38 | assert filename == "zh-hans/djangojs.js"
39 |
40 |
41 | def custom_func(locale, domain):
42 | return "{0}-{1}.js".format(locale, domain)
43 |
44 |
45 | def test_filename_with_custom_func(settings):
46 | settings.STATICI18N_FILENAME_FUNCTION = ".".join([__name__, "custom_func"])
47 |
48 | filename = utils.get_filename("es", "djangojs")
49 | assert filename == "es-djangojs.js"
50 |
51 |
52 | def test_filename_with_no_func(settings):
53 | settings.STATICI18N_FILENAME_FUNCTION = "no_func"
54 |
55 | with pytest.raises(ImportError):
56 | utils.get_filename("es", "djangojs")
57 |
58 |
59 | @pytest.mark.parametrize(
60 | "packages", ["mypackage1+mypackage2", ["mypackage1", "mypackage2"]]
61 | )
62 | def test_get_packages(packages):
63 | assert utils.get_packages(packages) == "mypackage1+mypackage2"
64 |
65 |
66 | def test_get_packages_None():
67 | assert utils.get_packages("django.conf") is None
68 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # This file only contains a selection of the most common options. For a full
4 | # list see the documentation:
5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
6 |
7 | # -- Path setup --------------------------------------------------------------
8 |
9 | # If extensions (or modules to document with autodoc) are in another directory,
10 | # add these directories to sys.path here. If the directory is relative to the
11 | # documentation root, use os.path.abspath to make it absolute, like shown here.
12 |
13 | import os
14 | import sys
15 |
16 | sys.path.insert(0, os.path.abspath("_ext"))
17 |
18 |
19 | # -- Project information -----------------------------------------------------
20 |
21 | project = "django-statici18n"
22 | copyright = "2012-2024, Sébastien Fievet"
23 | author = "Sébastien Fievet"
24 |
25 | # The full version, including alpha/beta/rc tags
26 | release = "2.6.0"
27 |
28 |
29 | # -- General configuration ---------------------------------------------------
30 |
31 | # Add any Sphinx extension module names here, as strings. They can be
32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
33 | # ones.
34 | extensions = ["djangodocs", "sphinx.ext.intersphinx"]
35 |
36 | intersphinx_mapping = {
37 | "python": ("http://docs.python.org/3.8", None),
38 | "django": (
39 | "https://docs.djangoproject.com/en/2.2/",
40 | "https://docs.djangoproject.com/en/2.2/_objects",
41 | ),
42 | }
43 |
44 | # Add any paths that contain templates here, relative to this directory.
45 | templates_path = ["_templates"]
46 |
47 | # The master toctree document.
48 | master_doc = "index"
49 |
50 | # List of patterns, relative to source directory, that match files and
51 | # directories to ignore when looking for source files.
52 | # This pattern also affects html_static_path and html_extra_path.
53 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
54 |
55 |
56 | # -- Options for HTML output -------------------------------------------------
57 |
58 | # The theme to use for HTML and HTML Help pages. See the documentation for
59 | # a list of builtin themes.
60 | #
61 | html_theme = "alabaster"
62 |
63 | # Add any paths that contain custom static files (such as style sheets) here,
64 | # relative to this directory. They are copied after the builtin static files,
65 | # so a file named "default.css" will overwrite the builtin "default.css".
66 | html_static_path = ["_static"]
67 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | django-statici18n
2 | -----------------
3 | Copyright (c) 2012, Sebastien Fievet
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are
8 | met:
9 |
10 | * Redistributions of source code must retain the above copyright
11 | notice, this list of conditions and the following disclaimer.
12 | * Redistributions in binary form must reproduce the above
13 | copyright notice, this list of conditions and the following
14 | disclaimer in the documentation and/or other materials provided
15 | with the distribution.
16 | * Neither the name of the author nor the names of other
17 | contributors may be used to endorse or promote products derived
18 | from this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 |
32 |
33 | django-statici18n contains code from Jannis Leidel's django_compressor
34 | --------------------------------------------------------------------
35 | Copyright (c) 2009-2012 Django Compressor authors (see AUTHORS file)
36 |
37 | Permission is hereby granted, free of charge, to any person obtaining a copy
38 | of this software and associated documentation files (the "Software"), to deal
39 | in the Software without restriction, including without limitation the rights
40 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
41 | copies of the Software, and to permit persons to whom the Software is
42 | furnished to do so, subject to the following conditions:
43 |
44 | The above copyright notice and this permission notice shall be included in
45 | all copies or substantial portions of the Software.
46 |
47 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
48 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
49 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
50 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
51 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
52 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
53 | THE SOFTWARE.
54 |
--------------------------------------------------------------------------------
/docs/settings.rst:
--------------------------------------------------------------------------------
1 | Settings
2 | ========
3 |
4 | .. currentmodule:: django.conf.settings
5 |
6 | .. attribute:: STATICI18N_DOMAIN
7 |
8 | :default: ``'djangojs'``
9 |
10 | The gettext domain to use when generating static files.
11 |
12 | Can be overrided with the ``-d/--domain`` option of ``compilejsi18n`` command.
13 |
14 | Usually you don't want to do that, as JavaScript messages go to the
15 | ``djangojs`` domain. But this might be needed if you deliver your JavaScript
16 | source from Django templates.
17 |
18 | .. attribute:: STATICI18N_PACKAGES
19 |
20 | :default: ``('django.conf')``
21 |
22 | A list of packages to check for translations.
23 |
24 | Can be overrided with the ``-p/--package`` option of :ref:`compilejsi18n`
25 | command.
26 |
27 | Each string in packages should be in Python dotted-package syntax (the
28 | same format as the strings in ``INSTALLED_APPS``) and should refer to a
29 | package that contains a locale directory. If you specify multiple
30 | packages, all those catalogs are merged into one catalog. This is useful
31 | if you have JavaScript that uses strings from different applications.
32 |
33 | .. attribute:: STATICI18N_ROOT
34 |
35 | :default: ``STATIC_ROOT``
36 |
37 | Controls the file path that catalog files will be written into.
38 |
39 | .. attribute:: STATICI18N_OUTPUT_DIR
40 |
41 | :Default: ``'jsi18n'``
42 |
43 | Controls the directory inside :attr:`STATICI18N_ROOT` that generated files
44 | will be written into.
45 |
46 | .. attribute:: STATICI18N_FILENAME_FUNCTION
47 |
48 | :default: ``'statici18n.utils.default_filename'``
49 |
50 | The dotted path to the function that creates the filename.
51 |
52 | The function receives two parameters:
53 |
54 | * ``locale``: a string representation of the locale currently processed
55 |
56 | * ``domain``: a string representation of the gettext domain used to check
57 | for translations
58 |
59 | By default, the function returns the path ``'/.js'``.
60 |
61 | The final filename is resulted by joining :attr:`STATICI18N_ROOT`,
62 | :attr:`STATICI18N_OUTPUT_DIR` and :attr:`STATICI18N_FILENAME_FUNCTION`.
63 |
64 | For example, with default settings in place and ``STATIC_ROOT = 'static'``,
65 | the JavaScript catalog generated for the ``en_GB`` locale is:
66 | ``'static/jsi18n/en_GB/djangojs.js'``.
67 |
68 | Use the legacy function ``statici18n.utils.legacy_filename`` to
69 | generate a filename with the language code derived from the
70 | ``django.utils.translation.trans_real import to_language``.
71 |
72 | .. attribute:: STATICI18N_NAMESPACE
73 |
74 | :default: ``None``
75 |
76 | Javascript identifier to use as namespace. This is useful when we want to
77 | have separate translations for the global and the namespaced contexts.
78 | The final gettext will be put under `window..gettext` rather
79 | than the `window.gettext`. Useful for pluggable modules that need JS i18n.
80 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | django-statici18n
2 | =================
3 |
4 | .. image:: https://github.com/zyegfryed/django-statici18n/actions/workflows/build.yml/badge.svg?branch=main
5 | :alt: Build Status
6 | :target: https://github.com/zyegfryed/django-statici18n/actions
7 |
8 | .. image:: https://codecov.io/gh/zyegfryed/django-statici18n/branch/main/graph/badge.svg?token=xiaDYAr30F
9 | :target: https://codecov.io/gh/zyegfryed/django-statici18n
10 |
11 | Overview
12 | --------
13 |
14 | When dealing with internationalization in JavaScript code, Django provides
15 | the `JSONCatalog view`_ which sends out a JavaScript code library with
16 | functions that mimic the gettext interface, plus an array of translation
17 | strings.
18 |
19 | At first glance, it works well and everything is fine. But, because
20 | `JSONCatalog view`_ is generating JavaScript catalog dynamically on each
21 | and every request, it's `adding an overhead`_ that can be an issue with
22 | site growth.
23 |
24 | That's what ``django-statici18n`` is for:
25 |
26 | Collecting JavaScript catalogs from each of your Django apps (and any
27 | other place you specify) into a single location that can easily be
28 | served in production.
29 |
30 | The main website for ``django-statici18n`` is
31 | `github.com/zyegfryed/django-statici18n`_ where you can also file tickets.
32 |
33 | .. _JSONCatalog view: https://docs.djangoproject.com/en/3.2/topics/i18n/translation/#the-jsoncatalog-view
34 | .. _adding an overhead: https://docs.djangoproject.com/en/3.2/topics/i18n/translation/#note-on-performance
35 | .. _github.com/zyegfryed/django-statici18n: https://github.com/zyegfryed/django-statici18n
36 |
37 | Supported Django Versions
38 | -------------------------
39 |
40 | ``django-statici18n`` works with all the Django versions officially
41 | supported by the Django project. At this time of writing, these are the
42 | 4.2 (LTS), 5.0 and 5.1 series.
43 |
44 | Installation
45 | ------------
46 |
47 | 1. Use your favorite Python packaging tool to install ``django-statici18n``
48 | from `PyPI`_, e.g.::
49 |
50 | pip install django-statici18n
51 |
52 | 2. Add ``'statici18n'`` to your ``INSTALLED_APPS`` setting::
53 |
54 | INSTALLED_APPS = [
55 | # ...
56 | 'statici18n',
57 | ]
58 |
59 | 3. Once you have `translated`_ and `compiled`_ your messages, use the
60 | ``compilejsi18n`` management command::
61 |
62 | python manage.py compilejsi18n
63 |
64 | 4. Add the `django.core.context_processors.i18n`_ context processor to the
65 | ``context_processors`` section for your backend in the ``TEMPLATES``
66 | setting - it should have already been set by Django::
67 |
68 | TEMPLATES = [
69 | {
70 | # ...
71 | 'OPTIONS': {
72 | 'context_processors': {
73 | # ...
74 | 'django.template.context_processors.i18n',
75 | },
76 | },
77 | },
78 | ]
79 |
80 | 5. Edit your template(s) and replace the `dynamically generated script`_ by the
81 | statically generated one:
82 |
83 | .. code-block:: html+django
84 |
85 |
86 |
87 | .. note::
88 |
89 | By default, the generated catalogs are stored to ``STATIC_ROOT/jsi18n``.
90 | You can modify the output path and more options by tweaking
91 | ``django-statici18n`` settings.
92 |
93 | **(Optional)**
94 |
95 | The following step assumes you're using `django.contrib.staticfiles`_.
96 |
97 | 5. Edit your template(s) and use the provided template tag:
98 |
99 | .. code-block:: html+django
100 |
101 | {% load statici18n %}
102 |
103 |
104 | 6. Or inline the JavaScript directly in your template:
105 |
106 | .. code-block:: html+django
107 |
108 | {% load statici18n %}
109 |
110 |
111 | .. _PyPI: http://pypi.python.org/pypi/django-statici18n
112 | .. _translated: https://docs.djangoproject.com/en/4.2/topics/i18n/translation/#message-files
113 | .. _compiled: https://docs.djangoproject.com/en/4.2/topics/i18n/translation/#compiling-message-files
114 | .. _django.core.context_processors.i18n: https://docs.djangoproject.com/en/4.2/ref/templates/api/#django-template-context-processors-i18n
115 | .. _Upgrading templates to Django 1.8: https://docs.djangoproject.com/en/2.2/ref/templates/upgrading/
116 | .. _dynamically generated script: https://docs.djangoproject.com/en/4.2/topics/i18n/translation/#using-the-javascript-translation-catalog
117 | .. _django.contrib.staticfiles: https://docs.djangoproject.com/en/4.2/ref/contrib/staticfiles/
118 |
--------------------------------------------------------------------------------
/docs/faq.rst:
--------------------------------------------------------------------------------
1 | FAQ
2 | ===
3 |
4 | .. _staticfiles-configuration:
5 |
6 | How to configure static files with ``django-statici18n``?
7 | ---------------------------------------------------------
8 |
9 | Due to the modularity of :mod:`django.contrib.staticfiles` it's easy to use
10 | the storage facility provided by tweaking some settings.
11 |
12 | There's two solution leveraging the :django:setting:`STATICFILES_FINDERS`
13 | setting:
14 |
15 | * using a dedicated application, or,
16 |
17 | * using a dedicated directory to hold the catalog files.
18 |
19 | In the next sections, we'll detail with examples how to use both solutions.
20 | Choose the one that best fits your needs and/or taste.
21 |
22 | See `static files management`_ for more information.
23 |
24 | Once setup is in place, run the :djadmin:`compilejsi18n` command to
25 | compile/update the Javascript catalog files followed by the
26 | :djadmin:`collectstatic` command to generate the static files::
27 |
28 | # compile/update Javascript catalog files...
29 | $ python manage.py compilejsi18n
30 |
31 | # then, collect static files.
32 | $ python manage.py collectstatic
33 |
34 |
35 | .. _static files management: http://django.readthedocs.org/en/1.6.x/ref/contrib/staticfiles/
36 |
37 |
38 | .. _staticfiles-app-configuration:
39 |
40 | Using a placeholder app
41 | ~~~~~~~~~~~~~~~~~~~~~~~
42 |
43 | You need to have the ``AppDirectoriesFinder`` finder enabled (the default).
44 |
45 | Create a minimal app with a ``static`` subdirectory. For example, let's create
46 | an app named **ì18n** to hold the generated catalogs::
47 |
48 | cd /path/to/your/django/project
49 | mkdir -p i18n/static
50 | touch i18n/__init__.py i18n/models.py
51 |
52 | Your project layout should then looks like the following::
53 |
54 | example_project
55 | |-- app
56 | | |-- __init__.py
57 | | |-- admin.py
58 | | |-- locale
59 | | |-- models.py
60 | | |-- static
61 | | |-- templates
62 | | |-- tests.py
63 | | `-- views.py
64 | |-- i18n <-- Your dedicated app
65 | | |-- __init__.py
66 | | |-- models.py <-- A placeholder file to enable app loading
67 | | `-- static <-- The output directory of catalog files
68 | | `-- jsi18n
69 | |-- manage.py
70 | |-- project
71 | | |-- __init__.py
72 | | |-- locale
73 | | |-- settings.py
74 | | |-- templates
75 | | `-- urls.py
76 | `-- public
77 | `-- static <-- The output directory of collected
78 | `-- jsi18n static files for deployment
79 |
80 | Then update your settings accordingly. Following the previous example::
81 |
82 | # project/settings.py
83 |
84 | # ... the rest of your settings here ...
85 |
86 | INSTALLED_APPS = (
87 | 'django.contrib.staticfiles',
88 | # ...
89 | 'statici18n',
90 | 'i18n',
91 | )
92 |
93 | STATIC_ROOT = os.path.join(BASE_DIR, "public", "static")
94 | STATICI18N_ROOT = os.path.join(BASE_DIR, "i18n", "static")
95 |
96 |
97 | .. _staticfiles-directory-configuration:
98 |
99 | Using a placeholder directory
100 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
101 |
102 | This approach extends the :django:setting:`STATICFILES_DIRS` setting.
103 | You need to have the ``FileSystemFinder`` finder enabled (the default).
104 |
105 | Following is an example project layout::
106 |
107 | example_project
108 | |-- app
109 | | |-- __init__.py
110 | | |-- admin.py
111 | | |-- locale
112 | | |-- models.py
113 | | |-- tests.py
114 | | `-- views.py
115 | |-- manage.py
116 | |-- project
117 | | |-- __init__.py
118 | | |-- locale
119 | | |-- settings.py
120 | | |-- static <-- Directory holding catalog files
121 | | | `-- jsi18n
122 | | |-- templates
123 | | `-- urls.py
124 | `-- public
125 | `-- static <-- The output directory of collected
126 | static files for deployment
127 |
128 | Then update your settings accordingly. Following the previous example::
129 |
130 | # project/settings.py
131 |
132 | # ... the rest of your settings here ...
133 |
134 | INSTALLED_APPS = (
135 | 'django.contrib.staticfiles',
136 | # ...
137 | 'statici18n',
138 | )
139 |
140 | STATIC_ROOT = os.path.join(BASE_DIR, "public", "static")
141 | STATICI18N_ROOT = os.path.join(BASE_DIR, "project", "static")
142 | STATICFILES_DIRS += (STATICI18N_ROOT,)
143 |
144 |
145 | Can I use the generated catalog with RequireJS_?
146 | ------------------------------------------------
147 |
148 | Yes. You just need some boilerplate configuration to export the object
149 | reference, like the following::
150 |
151 | # settings.py
152 | STATICI18N_ROOT = os.path.join(BASE_DIR, "project", "static")
153 | STATICFILES_DIRS += (STATICI18N_ROOT,)
154 |
155 | # app.js
156 | require.config({
157 | baseUrl: "static/js",
158 | paths: {
159 | "jsi18n": "../jsi18n/{{ LANGUAGE_CODE }}/djangojs",
160 | },
161 | shim: {
162 | "jsi18n":
163 | {
164 | exports: 'django'
165 | },
166 | }
167 | })
168 |
169 | // Usage
170 | require(["jquery", "jsi18n"], function($, jsi18n) {
171 | console.log(jsi18n.gettext('Internationalization is fun !'));
172 | // > "L’internationalisation, c'est cool !"
173 | })
174 |
175 | .. _RequireJS: http://requirejs.org/
176 |
--------------------------------------------------------------------------------
/tests/test_app.py:
--------------------------------------------------------------------------------
1 | import io
2 | import os
3 | import pytest
4 | import re
5 |
6 | from django.core import management
7 | from django.template import Context, Engine
8 | from django.utils.translation import to_language
9 |
10 | from statici18n.utils import default_filename
11 |
12 |
13 | LOCALES = ["en", "fr", "zh_Hans", "ko_KR"]
14 | LANGUAGES = [to_language(locale) for locale in LOCALES]
15 |
16 |
17 | def get_template_from_string(template_code):
18 | engine_options = {
19 | "libraries": {
20 | "statici18n": "statici18n.templatetags.statici18n",
21 | },
22 | }
23 | return Engine(**engine_options).from_string(template_code)
24 |
25 |
26 | @pytest.mark.usefixtures("cleandir")
27 | def test_compile_all(settings):
28 | out = io.StringIO()
29 | management.call_command("compilejsi18n", verbosity=1, stdout=out)
30 | out.seek(0)
31 | lines = [line.strip() for line in out.readlines()]
32 |
33 | assert len(lines) == len(settings.LANGUAGES)
34 | for locale, _ in settings.LANGUAGES:
35 | assert "processing language %s" % locale in lines
36 |
37 |
38 | LOCALIZED_CONTENT = {
39 | "en": "django",
40 | "fr": '"Hello world!": "Bonjour \\u00e0 tous !"',
41 | "zh_Hans": '"Hello world!": "\\u5927\\u5bb6\\u597d\\uff01"',
42 | "ko_KR": '"Hello world!": "\\uc548\\ub155\\ud558\\uc138\\uc694!"',
43 | }
44 |
45 |
46 | @pytest.mark.usefixtures("cleandir")
47 | @pytest.mark.parametrize("locale", LOCALES)
48 | def test_compile(settings, locale):
49 | out = io.StringIO()
50 | management.call_command("compilejsi18n", verbosity=1, stdout=out, locale=locale)
51 | out.seek(0)
52 | lines = [line.strip() for line in out.readlines()]
53 |
54 | assert len(lines) == 1
55 | assert lines[0] == "processing language %s" % locale
56 | basename = default_filename(locale, settings.STATICI18N_DOMAIN)
57 | filename = os.path.join(settings.STATICI18N_ROOT, "jsi18n", basename)
58 | assert os.path.exists(filename)
59 | with io.open(filename, "r", encoding="utf-8") as fp:
60 | content = fp.read()
61 | assert LOCALIZED_CONTENT[locale] in content
62 |
63 |
64 | @pytest.mark.parametrize("locale", LOCALES)
65 | def test_compile_no_use_i18n(settings, locale):
66 | """Tests compilation when `USE_I18N = False`.
67 |
68 | In this scenario, only the `settings.LANGUAGE_CODE` locale is processed
69 | (it defaults to `en-us` for Django projects).
70 | """
71 | settings.USE_I18N = False
72 |
73 | out = io.StringIO()
74 | management.call_command("compilejsi18n", verbosity=1, stdout=out, locale=locale)
75 | out.seek(0)
76 | lines = [line.strip() for line in out.readlines()]
77 | assert len(lines) == 1
78 | assert lines[0] == "processing language %s" % locale
79 | basename = default_filename(settings.LANGUAGE_CODE, settings.STATICI18N_DOMAIN)
80 | assert os.path.exists(os.path.join(settings.STATIC_ROOT, "jsi18n", basename))
81 |
82 |
83 | @pytest.mark.parametrize("locale", ["en"])
84 | @pytest.mark.parametrize("output_format", ["js", "json"])
85 | def test_compile_with_output_format(settings, locale, output_format):
86 | out = io.StringIO()
87 | management.call_command(
88 | "compilejsi18n",
89 | verbosity=1,
90 | stdout=out,
91 | locale=locale,
92 | outputformat=output_format,
93 | )
94 | out.seek(0)
95 | lines = [line.strip() for line in out.readlines()]
96 | assert len(lines) == 1
97 | assert lines[0] == "processing language %s" % locale
98 | basename = default_filename(locale, settings.STATICI18N_DOMAIN, output_format)
99 | assert os.path.exists(os.path.join(settings.STATIC_ROOT, "jsi18n", basename))
100 |
101 |
102 | @pytest.mark.parametrize("locale", ["en"])
103 | @pytest.mark.parametrize("namespace", ["MyBlock"])
104 | def test_compile_with_namespace(settings, locale, namespace):
105 | out = io.StringIO()
106 | management.call_command(
107 | "compilejsi18n",
108 | verbosity=1,
109 | stdout=out,
110 | locale=locale,
111 | outputformat="js",
112 | namespace=namespace,
113 | )
114 | out.seek(0)
115 | lines = [line.strip() for line in out.readlines()]
116 | assert len(lines) == 1
117 | assert lines[0] == "processing language %s" % locale
118 | basename = default_filename(locale, settings.STATICI18N_DOMAIN, "js")
119 | filename = os.path.join(settings.STATIC_ROOT, "jsi18n", basename)
120 | assert os.path.exists(filename)
121 | generated_content = open(filename).read()
122 | assert "global.MyBlock = MyBlock;" in generated_content
123 |
124 |
125 | @pytest.mark.usefixtures("cleandir")
126 | def test_compile_locale_not_exists():
127 | out = io.StringIO()
128 | management.call_command("compilejsi18n", locale="ar", verbosity=1, stderr=out)
129 | assert out.getvalue() == ""
130 |
131 |
132 | @pytest.mark.parametrize("language", LANGUAGES)
133 | def test_statici18n_templatetag(language):
134 | template = """
135 | {% load statici18n %}
136 |
137 | """
138 | template = get_template_from_string(template)
139 | txt = template.render(Context({"LANGUAGE_CODE": language})).strip()
140 | assert txt == '' % language
141 |
142 |
143 | @pytest.mark.usefixtures("cleandir")
144 | @pytest.mark.parametrize("language", LANGUAGES)
145 | def test_inlinei18n_templatetag(language):
146 | template = """
147 | {% load statici18n %}
148 |
149 | """
150 | management.call_command("compilejsi18n")
151 | template = get_template_from_string(template)
152 | rendered = template.render(Context({"LANGUAGE_CODE": language})).strip()
153 | assert "django = globals.django || (globals.django = {});" in rendered
154 | assert """ not in rendered
155 | assert re.match("^