├── 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("^