django-lang 0.6.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. django_lang-0.6.3.data/data/share/django-lang/requirements/py310-django32.txt +30 -0
  2. django_lang-0.6.3.data/data/share/django-lang/requirements/py310-django42.txt +26 -0
  3. django_lang-0.6.3.data/data/share/django-lang/requirements/py310-django52.txt +26 -0
  4. django_lang-0.6.3.data/data/share/django-lang/requirements/py311-django42.txt +22 -0
  5. django_lang-0.6.3.data/data/share/django-lang/requirements/py311-django52.txt +22 -0
  6. django_lang-0.6.3.data/data/share/django-lang/requirements/py312-django42.txt +22 -0
  7. django_lang-0.6.3.data/data/share/django-lang/requirements/py312-django52.txt +22 -0
  8. django_lang-0.6.3.data/data/share/django-lang/requirements/py313-django52.txt +22 -0
  9. django_lang-0.6.3.data/data/share/django-lang/requirements/py314-django52.txt +22 -0
  10. django_lang-0.6.3.data/data/share/django-lang/requirements/py39-django42.txt +26 -0
  11. django_lang-0.6.3.dist-info/METADATA +207 -0
  12. django_lang-0.6.3.dist-info/RECORD +30 -0
  13. django_lang-0.6.3.dist-info/WHEEL +5 -0
  14. django_lang-0.6.3.dist-info/licenses/LICENSE +21 -0
  15. django_lang-0.6.3.dist-info/top_level.txt +1 -0
  16. lang/__init__.py +22 -0
  17. lang/conf.py +103 -0
  18. lang/context_processors.py +63 -0
  19. lang/defaults.py +53 -0
  20. lang/middleware.py +56 -0
  21. lang/static/lang/css/nav-link-standalone.css +27 -0
  22. lang/static/lang/css/nav-link.css +17 -0
  23. lang/static/lang/js/display-standalone-class.js +15 -0
  24. lang/templates/hreflang.html +8 -0
  25. lang/templates/lang/nav-link-standalone.html +6 -0
  26. lang/templates/lang/nav-link.html +14 -0
  27. lang/templatetags/__init__.py +0 -0
  28. lang/templatetags/languages_helpers.py +74 -0
  29. lang/templatetags/urls.py +83 -0
  30. lang/utils.py +35 -0
@@ -0,0 +1,30 @@
1
+ #
2
+ # This file is autogenerated by pip-compile with Python 3.10
3
+ # by the following command:
4
+ #
5
+ # pip-compile --allow-unsafe --generate-hashes --output-file=requirements/py310-django32.txt requirements/runtime.in
6
+ #
7
+ asgiref==3.11.1 \
8
+ --hash=sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce \
9
+ --hash=sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133
10
+ # via django
11
+ django==3.2.25 \
12
+ --hash=sha256:7ca38a78654aee72378594d63e51636c04b8e28574f5505dff630895b5472777 \
13
+ --hash=sha256:a52ea7fcf280b16f7b739cec38fa6d3f8953a5456986944c3ca97e79882b4e38
14
+ # via -r requirements/runtime.in
15
+ emoji-country-flag==2.1.0 \
16
+ --hash=sha256:224b19583210901b950a624ab0b0896cdf0764b36f8bd1463868b15fb8c10f6a \
17
+ --hash=sha256:3b4df6a9d9961ab7716090689c2e96405baff68e46849ab2704335d904589f2a
18
+ # via -r requirements/runtime.in
19
+ pytz==2026.2 \
20
+ --hash=sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126 \
21
+ --hash=sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a
22
+ # via django
23
+ sqlparse==0.5.5 \
24
+ --hash=sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba \
25
+ --hash=sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e
26
+ # via django
27
+ typing-extensions==4.15.0 \
28
+ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
29
+ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
30
+ # via asgiref
@@ -0,0 +1,26 @@
1
+ #
2
+ # This file is autogenerated by pip-compile with Python 3.10
3
+ # by the following command:
4
+ #
5
+ # pip-compile --allow-unsafe --generate-hashes --output-file=requirements/py310-django42.txt requirements/runtime.in
6
+ #
7
+ asgiref==3.11.1 \
8
+ --hash=sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce \
9
+ --hash=sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133
10
+ # via django
11
+ django==4.2.30 \
12
+ --hash=sha256:4d07aaf1c62f9984842b67c2874ebbf7056a17be253860299b93ae1881faad65 \
13
+ --hash=sha256:4ebc7a434e3819db6cf4b399fb5b3f536310a30e8486f08b66886840be84b37c
14
+ # via -r requirements/runtime.in
15
+ emoji-country-flag==2.1.0 \
16
+ --hash=sha256:224b19583210901b950a624ab0b0896cdf0764b36f8bd1463868b15fb8c10f6a \
17
+ --hash=sha256:3b4df6a9d9961ab7716090689c2e96405baff68e46849ab2704335d904589f2a
18
+ # via -r requirements/runtime.in
19
+ sqlparse==0.5.5 \
20
+ --hash=sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba \
21
+ --hash=sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e
22
+ # via django
23
+ typing-extensions==4.15.0 \
24
+ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
25
+ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
26
+ # via asgiref
@@ -0,0 +1,26 @@
1
+ #
2
+ # This file is autogenerated by pip-compile with Python 3.10
3
+ # by the following command:
4
+ #
5
+ # pip-compile --allow-unsafe --generate-hashes --output-file=requirements/py310-django52.txt requirements/runtime.in
6
+ #
7
+ asgiref==3.11.1 \
8
+ --hash=sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce \
9
+ --hash=sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133
10
+ # via django
11
+ django==5.2.15 \
12
+ --hash=sha256:0eb4a9bb1853a35b0286dbc6d916bd352c8c2687195a7f2d6f80cefd840e4970 \
13
+ --hash=sha256:5154a9bf84ac01dde011e367f355c07dbb329532e06810dcf3ef2af269e236e7
14
+ # via -r requirements/runtime.in
15
+ emoji-country-flag==2.1.0 \
16
+ --hash=sha256:224b19583210901b950a624ab0b0896cdf0764b36f8bd1463868b15fb8c10f6a \
17
+ --hash=sha256:3b4df6a9d9961ab7716090689c2e96405baff68e46849ab2704335d904589f2a
18
+ # via -r requirements/runtime.in
19
+ sqlparse==0.5.5 \
20
+ --hash=sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba \
21
+ --hash=sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e
22
+ # via django
23
+ typing-extensions==4.15.0 \
24
+ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
25
+ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
26
+ # via asgiref
@@ -0,0 +1,22 @@
1
+ #
2
+ # This file is autogenerated by pip-compile with Python 3.11
3
+ # by the following command:
4
+ #
5
+ # pip-compile --allow-unsafe --generate-hashes --output-file=requirements/py311-django42.txt requirements/runtime.in
6
+ #
7
+ asgiref==3.11.1 \
8
+ --hash=sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce \
9
+ --hash=sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133
10
+ # via django
11
+ django==4.2.30 \
12
+ --hash=sha256:4d07aaf1c62f9984842b67c2874ebbf7056a17be253860299b93ae1881faad65 \
13
+ --hash=sha256:4ebc7a434e3819db6cf4b399fb5b3f536310a30e8486f08b66886840be84b37c
14
+ # via -r requirements/runtime.in
15
+ emoji-country-flag==2.1.0 \
16
+ --hash=sha256:224b19583210901b950a624ab0b0896cdf0764b36f8bd1463868b15fb8c10f6a \
17
+ --hash=sha256:3b4df6a9d9961ab7716090689c2e96405baff68e46849ab2704335d904589f2a
18
+ # via -r requirements/runtime.in
19
+ sqlparse==0.5.5 \
20
+ --hash=sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba \
21
+ --hash=sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e
22
+ # via django
@@ -0,0 +1,22 @@
1
+ #
2
+ # This file is autogenerated by pip-compile with Python 3.11
3
+ # by the following command:
4
+ #
5
+ # pip-compile --allow-unsafe --generate-hashes --output-file=requirements/py311-django52.txt requirements/runtime.in
6
+ #
7
+ asgiref==3.11.1 \
8
+ --hash=sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce \
9
+ --hash=sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133
10
+ # via django
11
+ django==5.2.15 \
12
+ --hash=sha256:0eb4a9bb1853a35b0286dbc6d916bd352c8c2687195a7f2d6f80cefd840e4970 \
13
+ --hash=sha256:5154a9bf84ac01dde011e367f355c07dbb329532e06810dcf3ef2af269e236e7
14
+ # via -r requirements/runtime.in
15
+ emoji-country-flag==2.1.0 \
16
+ --hash=sha256:224b19583210901b950a624ab0b0896cdf0764b36f8bd1463868b15fb8c10f6a \
17
+ --hash=sha256:3b4df6a9d9961ab7716090689c2e96405baff68e46849ab2704335d904589f2a
18
+ # via -r requirements/runtime.in
19
+ sqlparse==0.5.5 \
20
+ --hash=sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba \
21
+ --hash=sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e
22
+ # via django
@@ -0,0 +1,22 @@
1
+ #
2
+ # This file is autogenerated by pip-compile with Python 3.12
3
+ # by the following command:
4
+ #
5
+ # pip-compile --allow-unsafe --generate-hashes --output-file=requirements/py312-django42.txt requirements/runtime.in
6
+ #
7
+ asgiref==3.11.1 \
8
+ --hash=sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce \
9
+ --hash=sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133
10
+ # via django
11
+ django==4.2.30 \
12
+ --hash=sha256:4d07aaf1c62f9984842b67c2874ebbf7056a17be253860299b93ae1881faad65 \
13
+ --hash=sha256:4ebc7a434e3819db6cf4b399fb5b3f536310a30e8486f08b66886840be84b37c
14
+ # via -r requirements/runtime.in
15
+ emoji-country-flag==2.1.0 \
16
+ --hash=sha256:224b19583210901b950a624ab0b0896cdf0764b36f8bd1463868b15fb8c10f6a \
17
+ --hash=sha256:3b4df6a9d9961ab7716090689c2e96405baff68e46849ab2704335d904589f2a
18
+ # via -r requirements/runtime.in
19
+ sqlparse==0.5.5 \
20
+ --hash=sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba \
21
+ --hash=sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e
22
+ # via django
@@ -0,0 +1,22 @@
1
+ #
2
+ # This file is autogenerated by pip-compile with Python 3.12
3
+ # by the following command:
4
+ #
5
+ # pip-compile --allow-unsafe --generate-hashes --output-file=requirements/py312-django52.txt requirements/runtime.in
6
+ #
7
+ asgiref==3.11.1 \
8
+ --hash=sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce \
9
+ --hash=sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133
10
+ # via django
11
+ django==5.2.15 \
12
+ --hash=sha256:0eb4a9bb1853a35b0286dbc6d916bd352c8c2687195a7f2d6f80cefd840e4970 \
13
+ --hash=sha256:5154a9bf84ac01dde011e367f355c07dbb329532e06810dcf3ef2af269e236e7
14
+ # via -r requirements/runtime.in
15
+ emoji-country-flag==2.1.0 \
16
+ --hash=sha256:224b19583210901b950a624ab0b0896cdf0764b36f8bd1463868b15fb8c10f6a \
17
+ --hash=sha256:3b4df6a9d9961ab7716090689c2e96405baff68e46849ab2704335d904589f2a
18
+ # via -r requirements/runtime.in
19
+ sqlparse==0.5.5 \
20
+ --hash=sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba \
21
+ --hash=sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e
22
+ # via django
@@ -0,0 +1,22 @@
1
+ #
2
+ # This file is autogenerated by pip-compile with Python 3.13
3
+ # by the following command:
4
+ #
5
+ # pip-compile --allow-unsafe --generate-hashes --output-file=requirements/py313-django52.txt requirements/runtime.in
6
+ #
7
+ asgiref==3.11.1 \
8
+ --hash=sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce \
9
+ --hash=sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133
10
+ # via django
11
+ django==5.2.15 \
12
+ --hash=sha256:0eb4a9bb1853a35b0286dbc6d916bd352c8c2687195a7f2d6f80cefd840e4970 \
13
+ --hash=sha256:5154a9bf84ac01dde011e367f355c07dbb329532e06810dcf3ef2af269e236e7
14
+ # via -r requirements/runtime.in
15
+ emoji-country-flag==2.1.0 \
16
+ --hash=sha256:224b19583210901b950a624ab0b0896cdf0764b36f8bd1463868b15fb8c10f6a \
17
+ --hash=sha256:3b4df6a9d9961ab7716090689c2e96405baff68e46849ab2704335d904589f2a
18
+ # via -r requirements/runtime.in
19
+ sqlparse==0.5.5 \
20
+ --hash=sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba \
21
+ --hash=sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e
22
+ # via django
@@ -0,0 +1,22 @@
1
+ #
2
+ # This file is autogenerated by pip-compile with Python 3.14
3
+ # by the following command:
4
+ #
5
+ # pip-compile --allow-unsafe --generate-hashes --output-file=requirements/py314-django52.txt requirements/runtime.in
6
+ #
7
+ asgiref==3.11.1 \
8
+ --hash=sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce \
9
+ --hash=sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133
10
+ # via django
11
+ django==5.2.15 \
12
+ --hash=sha256:0eb4a9bb1853a35b0286dbc6d916bd352c8c2687195a7f2d6f80cefd840e4970 \
13
+ --hash=sha256:5154a9bf84ac01dde011e367f355c07dbb329532e06810dcf3ef2af269e236e7
14
+ # via -r requirements/runtime.in
15
+ emoji-country-flag==2.1.0 \
16
+ --hash=sha256:224b19583210901b950a624ab0b0896cdf0764b36f8bd1463868b15fb8c10f6a \
17
+ --hash=sha256:3b4df6a9d9961ab7716090689c2e96405baff68e46849ab2704335d904589f2a
18
+ # via -r requirements/runtime.in
19
+ sqlparse==0.5.5 \
20
+ --hash=sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba \
21
+ --hash=sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e
22
+ # via django
@@ -0,0 +1,26 @@
1
+ #
2
+ # This file is autogenerated by pip-compile with Python 3.9
3
+ # by the following command:
4
+ #
5
+ # pip-compile --allow-unsafe --generate-hashes --output-file=requirements/py39-django42.txt requirements/runtime.in
6
+ #
7
+ asgiref==3.11.1 \
8
+ --hash=sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce \
9
+ --hash=sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133
10
+ # via django
11
+ django==4.2.30 \
12
+ --hash=sha256:4d07aaf1c62f9984842b67c2874ebbf7056a17be253860299b93ae1881faad65 \
13
+ --hash=sha256:4ebc7a434e3819db6cf4b399fb5b3f536310a30e8486f08b66886840be84b37c
14
+ # via -r requirements/runtime.in
15
+ emoji-country-flag==1.3.2 \
16
+ --hash=sha256:42d9fc1a8ec27b3ef2c18a39532dcdbf0527afbc95749be02a3afc1f1b165aa3 \
17
+ --hash=sha256:6ddcf8f3bc55b2f59b1d7d9dc830e90f9423f555dd37862c4c2e220d42726f73
18
+ # via -r requirements/runtime.in
19
+ sqlparse==0.5.5 \
20
+ --hash=sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba \
21
+ --hash=sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e
22
+ # via django
23
+ typing-extensions==4.15.0 \
24
+ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
25
+ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
26
+ # via asgiref
@@ -0,0 +1,207 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-lang
3
+ Version: 0.6.3
4
+ Summary: Django application to provide useful utils and reusable parts of code for multi-languages sites.
5
+ Author-email: DLRSP <dlrsp.dev@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/DLRSP/django-lang
8
+ Project-URL: Documentation, https://github.com/DLRSP/django-lang/
9
+ Project-URL: Repository, https://github.com/DLRSP/django-lang
10
+ Project-URL: Issues, https://github.com/DLRSP/django-lang/issues
11
+ Project-URL: Changelog, https://github.com/DLRSP/django-lang/blob/main/CHANGELOG.rst
12
+ Keywords: django,i18n,multilingual,hreflang,language
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Natural Language :: Italian
15
+ Classifier: Natural Language :: English
16
+ Classifier: Environment :: Web Environment
17
+ Classifier: Framework :: Django
18
+ Classifier: Framework :: Django :: 3.2
19
+ Classifier: Framework :: Django :: 4.2
20
+ Classifier: Framework :: Django :: 5.2
21
+ Classifier: Intended Audience :: Developers
22
+ Classifier: Operating System :: OS Independent
23
+ Classifier: Programming Language :: Python
24
+ Classifier: Programming Language :: Python :: 3
25
+ Classifier: Programming Language :: Python :: 3 :: Only
26
+ Classifier: Programming Language :: Python :: 3.9
27
+ Classifier: Programming Language :: Python :: 3.10
28
+ Classifier: Programming Language :: Python :: 3.11
29
+ Classifier: Programming Language :: Python :: 3.12
30
+ Classifier: Programming Language :: Python :: 3.13
31
+ Classifier: Programming Language :: Python :: 3.14
32
+ Classifier: Topic :: Internet :: WWW/HTTP
33
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
34
+ Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
35
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
36
+ Classifier: Topic :: Software Development :: Libraries
37
+ Requires-Python: >=3.9
38
+ Description-Content-Type: text/markdown
39
+ License-File: LICENSE
40
+ Requires-Dist: Django>=3.2
41
+ Requires-Dist: emoji-country-flag>=2.1.0; python_version >= "3.10"
42
+ Requires-Dist: emoji-country-flag<2,>=1.3.2; python_version < "3.10"
43
+ Provides-Extra: testing
44
+ Requires-Dist: coverage; extra == "testing"
45
+ Requires-Dist: codecov; extra == "testing"
46
+ Requires-Dist: pytest; extra == "testing"
47
+ Requires-Dist: pytest-django; extra == "testing"
48
+ Provides-Extra: docs
49
+ Requires-Dist: mkdocs>=1.5; extra == "docs"
50
+ Requires-Dist: mkdocs-material>=9.0; extra == "docs"
51
+ Requires-Dist: pymdown-extensions>=10.0; extra == "docs"
52
+ Requires-Dist: mkdocs-git-revision-date-plugin>=2.0; extra == "docs"
53
+ Provides-Extra: linting
54
+ Requires-Dist: flake8; extra == "linting"
55
+ Requires-Dist: flake8-pyproject; extra == "linting"
56
+ Requires-Dist: flake8-bugbear; extra == "linting"
57
+ Requires-Dist: flake8-comprehensions; extra == "linting"
58
+ Requires-Dist: flake8-tidy-imports; extra == "linting"
59
+ Requires-Dist: flake8-typing-imports; extra == "linting"
60
+ Requires-Dist: pylint; extra == "linting"
61
+ Dynamic: license-file
62
+
63
+ # django-lang [![PyPi license](https://img.shields.io/pypi/l/django-lang.svg)](https://pypi.python.org/pypi/django-lang)
64
+
65
+ [![PyPi status](https://img.shields.io/pypi/status/django-lang.svg)](https://pypi.python.org/pypi/django-lang)
66
+ [![PyPi version](https://img.shields.io/pypi/v/django-lang.svg)](https://pypi.python.org/pypi/django-lang)
67
+ [![PyPi python version](https://img.shields.io/pypi/pyversions/django-lang.svg)](https://pypi.python.org/pypi/django-lang)
68
+ [![PyPi downloads](https://img.shields.io/pypi/dm/django-lang.svg)](https://pypi.python.org/pypi/django-lang)
69
+ [![PyPi downloads](https://img.shields.io/pypi/dw/django-lang.svg)](https://pypi.python.org/pypi/django-lang)
70
+ [![PyPi downloads](https://img.shields.io/pypi/dd/django-lang.svg)](https://pypi.python.org/pypi/django-lang)
71
+
72
+ ## GitHub ![GitHub release](https://img.shields.io/github/tag/DLRSP/django-lang.svg) ![GitHub release](https://img.shields.io/github/release/DLRSP/django-lang.svg)
73
+
74
+ ## Test [![codecov.io](https://codecov.io/github/DLRSP/django-lang/coverage.svg?branch=main)](https://codecov.io/github/DLRSP/django-lang?branch=main) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/DLRSP/django-lang/main.svg)](https://results.pre-commit.ci/latest/github/DLRSP/django-lang/main) [![gitthub.com](https://github.com/DLRSP/django-lang/actions/workflows/ci.yaml/badge.svg)](https://github.com/DLRSP/django-lang/actions/workflows/ci.yaml)
75
+
76
+ ## Check Demo Project
77
+ * Check the demo repo on [GitHub](https://github.com/DLRSP/example/tree/django-lang)
78
+
79
+ ## Requirements
80
+ - Python 3.9+ supported.
81
+ - Django 3.2+ supported.
82
+
83
+ ## Setup
84
+ 1. Install from **pip**:
85
+ ```shell
86
+ pip install django-lang
87
+ ```
88
+ 2. Modify `settings.py` by adding the app to `INSTALLED_APPS`:
89
+ ```python
90
+ INSTALLED_APPS = [
91
+ # ...
92
+ "lang",
93
+ # ...
94
+ ]
95
+ ```
96
+ 3. Modify `settings.py` by adding the app to `INSTALLED_APPS`:
97
+ ``` python title="settings.py" hl_lines="12"
98
+ TEMPLATES = [
99
+ {
100
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
101
+ "DIRS": [os.path.join(PROJECT_DIR, "templates")],
102
+ "APP_DIRS": True,
103
+ "OPTIONS": {
104
+ "context_processors": [
105
+ "django.template.context_processors.debug",
106
+ "django.template.context_processors.request",
107
+ "django.contrib.auth.context_processors.auth",
108
+ "django.contrib.messages.context_processors.messages",
109
+ "lang.context_processors.from_settings",
110
+ "lang.context_processors.seo_i18n",
111
+ ],
112
+ },
113
+ },
114
+ ]
115
+ ```
116
+
117
+ Optional: ``lang.context_processors.language_switcher_next`` (fills ``redirect_to`` for the packaged language form).
118
+
119
+ ### Optional: ``SetLanguageNextPathMiddleware``
120
+
121
+ Only needed if you use **translated URL segments** (``gettext_lazy`` under ``i18n_patterns``) and see language switches that POST to ``set_language`` but stay on the wrong prefix. Short how-to: [docs/set_language_middleware.md](docs/set_language_middleware.md).
122
+
123
+ 4. Modify your project's base template `base.html` to include language's switcher styles:
124
+ ```html
125
+ <head>
126
+ ...
127
+ <link rel="stylesheet" type="text/css" href="{% static 'lang/css/nav-link.css' %}">
128
+ ...
129
+ </head>
130
+ ```
131
+ 5. Modify your project's base template `base.html` to include attributes using `translate_url` template's tag:
132
+ ```html
133
+ <head>
134
+ ...
135
+ <meta name="language" content="{{ LANGUAGE_CODE }}" />
136
+ {% include "hreflang.html" %}
137
+ ...
138
+ </head>
139
+ ```
140
+ 6. Modify your project's nav template `nav.html` to include language's switcher:
141
+ ```html
142
+ <nav class="navbar">
143
+ ...
144
+ <ul class="nav navbar-nav">
145
+ {% include "lang/nav-link.html" %}
146
+ </ul>
147
+ ...
148
+ </nav>
149
+ ```
150
+
151
+ ### Configuration: ``lang.conf`` and ``APP_CONFIG``
152
+
153
+ Built-in maps live in ``lang.defaults``. At runtime, :mod:`lang.conf` resolves values **lazily**:
154
+
155
+ 1. Top-level Django settings (``LANGUAGE_HREFLANG_MAP``, ``LANGUAGE_WIKIPEDIA_SAMEAS``, ``OG_LOCALE_BY_LANGUAGE``, ``HREFLANG_DEFAULT_LANGUAGE``, ``LANGUAGE_FLAG_MAP``), if set.
156
+ 2. Partial dicts under ``settings.APP_CONFIG["lang"]`` merged onto the package defaults (for the three ``LANGUAGE_*`` / ``OG_*`` maps and flag overrides).
157
+ 3. Otherwise the content of ``lang.defaults``.
158
+
159
+ You do **not** need to import ``lang.defaults`` from ``settings.py``. Typical site-only override for ``x-default``::
160
+
161
+ APP_CONFIG = {
162
+ "lang": {
163
+ "HREFLANG_DEFAULT_LANGUAGE": "it",
164
+ },
165
+ }
166
+
167
+ Or use the usual flat settings (full replacement for the dict keys, or ``HREFLANG_DEFAULT_LANGUAGE`` at top level) if you prefer.
168
+
169
+ ### Optional: language control beside the hamburger on **small viewports**
170
+
171
+ ``nav-link-standalone.css`` shows the extra switcher next to the menu toggle below the ``lg`` breakpoint (~992px) and hides the duplicate inside the collapsed drawer. Optional script ``display-standalone-class.js`` only adds class ``display-standalone`` on ``<html>`` for installed web apps; layout no longer depends on it.
172
+
173
+ 1. In `settings.py`, add the optional context processor so `redirect_to` is filled for `set_language`’s `next` (unless you pass `redirect_to` from each view):
174
+ ```python
175
+ "lang.context_processors.language_switcher_next",
176
+ ```
177
+ 2. In the base layout `<head>`, after `nav-link.css`, add:
178
+ ```html
179
+ <link rel="stylesheet" href="{% static 'lang/css/nav-link-standalone.css' %}">
180
+ <script src="{% static 'lang/js/display-standalone-class.js' %}"></script>
181
+ ```
182
+ 3. Next to your mobile menu button, include:
183
+ ```html
184
+ {% include "lang/nav-link-standalone.html" %}
185
+ ```
186
+
187
+ Packaged templates:
188
+
189
+ - ``hreflang.html`` (app template root) — ``<link rel="alternate" hreflang="…">`` for the current view.
190
+ - ``lang/nav-link.html`` — language ``<select>`` (optional context: ``lang_switcher_id``, ``lang_switcher_extra_class``).
191
+ - ``lang/nav-link-standalone.html`` — duplicate switcher beside the mobile toggle (small viewports / PWA).
192
+
193
+
194
+ ## Run Example Project
195
+
196
+ ```shell
197
+ git clone --depth=50 --branch=django-lang https://github.com/DLRSP/example.git DLRSP/example
198
+ cd DLRSP/example
199
+ python manage.py runserver
200
+ ```
201
+
202
+ Now browser the app @ http://127.0.0.1:8000
203
+
204
+ ## References
205
+
206
+ - [brainstorm.it](https://brainstorm.it/snippets/django-language-switching/) - Language's switching
207
+ - [hakibenita.com](https://hakibenita.com/django-multi-language-site-hreflang) - Url's translation for "hreflang" html's attributes
@@ -0,0 +1,30 @@
1
+ django_lang-0.6.3.data/data/share/django-lang/requirements/py310-django32.txt,sha256=Is5a75mDJwwbiTi_y2Ew0SNFnD_wI_mJv3qOVFlK5-s,1490
2
+ django_lang-0.6.3.data/data/share/django-lang/requirements/py310-django42.txt,sha256=hYkTt5hE91FrZ-LJH0dMUembnnuTEcjbk6POfVU9bf4,1290
3
+ django_lang-0.6.3.data/data/share/django-lang/requirements/py310-django52.txt,sha256=TWCY2bckrkhdP578QYvtZoICIWQVPISOKstdDg2r1ZE,1290
4
+ django_lang-0.6.3.data/data/share/django-lang/requirements/py311-django42.txt,sha256=roRqDD4Bkx1nqaVZ_7Iisy1Htku1VMzPv-DS2LPuYWk,1076
5
+ django_lang-0.6.3.data/data/share/django-lang/requirements/py311-django52.txt,sha256=RQYIjE0lPtVgurhNzMcoPZf2qDzwhXd7vkTLrn3bqkc,1076
6
+ django_lang-0.6.3.data/data/share/django-lang/requirements/py312-django42.txt,sha256=dUK-xWhdYCNu3NXSZK_aXFisZREqfxk1P_zM2kCIPek,1076
7
+ django_lang-0.6.3.data/data/share/django-lang/requirements/py312-django52.txt,sha256=rtKLPk_UEC5dXzTw-SsD4PYgL_QFP7j11dPcfjcGKHU,1076
8
+ django_lang-0.6.3.data/data/share/django-lang/requirements/py313-django52.txt,sha256=cghaLwOzYis19pbLcDFUwYqw4woVbLXba1Ia64EHZ8g,1076
9
+ django_lang-0.6.3.data/data/share/django-lang/requirements/py314-django52.txt,sha256=R6gYVOJzUQjehM-TFQtgHdP1lM0F5z4Pokm-EidMuxs,1076
10
+ django_lang-0.6.3.data/data/share/django-lang/requirements/py39-django42.txt,sha256=hli_6tnHOeH5BSDGPnBGF1x6ot4w088o3J-sraCLp7w,1288
11
+ django_lang-0.6.3.dist-info/licenses/LICENSE,sha256=lea6YyyICc5SM-8jvboUSCWiCpPwBrUPJAb8cHxAP5A,1114
12
+ lang/__init__.py,sha256=IIk7gZN3QmKyv2GEWFOd5D3qY04cMPb8Rr-flYq8Mwk,462
13
+ lang/conf.py,sha256=G5teoWe7Vl6fwX3GVWiSWXvcSkvPMWoD1wfeCrff2UA,3054
14
+ lang/context_processors.py,sha256=zefDpet4tXYlnBELC1HCK_VkU7jgbEKDijZOkdUJvL4,2155
15
+ lang/defaults.py,sha256=idhm3btcU-IZBmliby0q3sGY-6qVyQsN01q7FUZ5IG8,1866
16
+ lang/middleware.py,sha256=UP7gdMMwMs9pWjh2ZKqabBsJCz6-ovRt9d-KXngy1kA,2313
17
+ lang/utils.py,sha256=K67OAzyC4WJLz-ZGCbTSRkHNgYelmMZ1uNZEYmuXy-c,978
18
+ lang/static/lang/css/nav-link-standalone.css,sha256=bipwrkyKNgR0_Kv-q7rXyWdzym4JAf1GbcdpZK2w0G4,811
19
+ lang/static/lang/css/nav-link.css,sha256=tFhfGH6FkWZDWUaH6H4VFWwo3afBmkDwX2qKzO1hQcI,426
20
+ lang/static/lang/js/display-standalone-class.js,sha256=TUhv5gpWZJUBQtTzuv12WSym8sKi3zlJVkKKYuq1BVs,643
21
+ lang/templates/hreflang.html,sha256=BdciMglJ2xETeXc92YZl9wvmpzDBAYf7aCIIkgzY16A,373
22
+ lang/templates/lang/nav-link-standalone.html,sha256=wuaGJ3PmT2RaQCnxn6UKJ-CsIHjPuA3G4YojQOYGzjM,307
23
+ lang/templates/lang/nav-link.html,sha256=swHsDd3lOw7yfJfiAyOWCFPqgSt1J2seX281luxnsO8,781
24
+ lang/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
+ lang/templatetags/languages_helpers.py,sha256=_zhbFZ5IH7QUum7wXQJeJ9Qw3hn5a20u5CQNVhv6SxA,2242
26
+ lang/templatetags/urls.py,sha256=4bYpWmhBfHKHEAyrAj0wod1YSok1JqS4oIcCJtossYc,2842
27
+ django_lang-0.6.3.dist-info/METADATA,sha256=OvZ6Qhg3x3k6yG61GHzmdxsXrIET3OV1iOuk0SvQOCI,9491
28
+ django_lang-0.6.3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
29
+ django_lang-0.6.3.dist-info/top_level.txt,sha256=k1z7040BUci34YPBCxIsaMA0DGeL-qBF3vBS1z3Ro28,5
30
+ django_lang-0.6.3.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2010-present DLRSP (https://dlrsp.org) and other contributors.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ lang
lang/__init__.py ADDED
@@ -0,0 +1,22 @@
1
+ """
2
+ See PEP 386 (https://peps.python.org/pep-0386/)
3
+ """
4
+
5
+ __version__ = "0.6.3"
6
+ __version_info__ = tuple(
7
+ int(i) if i.isdigit() else i for i in __version__.split(".")
8
+ )
9
+ __license__ = "MIT"
10
+ __title__ = "lang"
11
+
12
+ __author__ = "DLRSP"
13
+ __copyright__ = "Copyright 2010-present DLRSP"
14
+
15
+ # Version synonym
16
+ VERSION = __version_info__
17
+
18
+ # Header encoding (see RFC5987)
19
+ HTTP_HEADER_ENCODING = "iso-8859-1"
20
+
21
+ # Default datetime input and output formats
22
+ ISO_8601 = "iso-8601"
lang/conf.py ADDED
@@ -0,0 +1,103 @@
1
+ """
2
+ Resolved configuration for ``lang`` (lazy reads from ``django.conf.settings``).
3
+
4
+ Priority for each value:
5
+
6
+ 1. Top-level Django setting (e.g. ``LANGUAGE_HREFLANG_MAP``), if set.
7
+ 2. Partial override inside ``settings.APP_CONFIG["lang"]`` merged onto
8
+ :mod:`lang.defaults`.
9
+ 3. Package defaults from :mod:`lang.defaults`.
10
+
11
+ Projects can rely on defaults only (no imports from ``lang.defaults`` in
12
+ ``settings.py``) and optionally set::
13
+
14
+ APP_CONFIG = {
15
+ "lang": {
16
+ "HREFLANG_DEFAULT_LANGUAGE": "it",
17
+ "LANGUAGE_HREFLANG_MAP": {"en": "en-GB"},
18
+ },
19
+ }
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from typing import Any
25
+
26
+ from django.conf import settings
27
+
28
+ from lang import defaults
29
+
30
+
31
+ def _app_config_lang() -> dict[str, Any]:
32
+ cfg = getattr(settings, "APP_CONFIG", None) or {}
33
+ lang = cfg.get("lang")
34
+ return dict(lang) if lang else {}
35
+
36
+
37
+ def _merged_lang_dict(
38
+ setting_name: str,
39
+ defaults_dict: dict[str, str],
40
+ ) -> dict[str, str]:
41
+ """
42
+ Full replacement if ``settings.<name>`` is set; else defaults merged with
43
+ ``APP_CONFIG['lang'][<name>]`` (partial).
44
+ """
45
+ explicit = getattr(settings, setting_name, None)
46
+ if explicit is not None:
47
+ return dict(explicit)
48
+ partial = _app_config_lang().get(setting_name)
49
+ if partial:
50
+ return {**defaults_dict, **dict(partial)}
51
+ return dict(defaults_dict)
52
+
53
+
54
+ def get_language_hreflang_map() -> dict[str, str]:
55
+ """Django language code → BCP 47 ``hreflang`` attribute value."""
56
+ return _merged_lang_dict(
57
+ "LANGUAGE_HREFLANG_MAP",
58
+ defaults.LANGUAGE_HREFLANG_MAP,
59
+ )
60
+
61
+
62
+ def get_language_wikipedia_sameas() -> dict[str, str]:
63
+ """Django language code → Wikipedia URL (e.g. JSON-LD ``sameAs``)."""
64
+ return _merged_lang_dict(
65
+ "LANGUAGE_WIKIPEDIA_SAMEAS",
66
+ defaults.LANGUAGE_WIKIPEDIA_SAMEAS,
67
+ )
68
+
69
+
70
+ def get_og_locale_by_language() -> dict[str, str]:
71
+ """Django language code → Open Graph locale string."""
72
+ return _merged_lang_dict(
73
+ "OG_LOCALE_BY_LANGUAGE",
74
+ defaults.OG_LOCALE_BY_LANGUAGE,
75
+ )
76
+
77
+
78
+ def get_hreflang_default_language() -> str | None:
79
+ """
80
+ Brand default for ``hreflang`` ``x-default`` (non-empty string or ``None``).
81
+
82
+ Reads ``HREFLANG_DEFAULT_LANGUAGE`` from top-level settings, then
83
+ ``APP_CONFIG['lang']['HREFLANG_DEFAULT_LANGUAGE']``.
84
+ """
85
+ top = getattr(settings, "HREFLANG_DEFAULT_LANGUAGE", None)
86
+ if top:
87
+ return str(top)
88
+ nested = _app_config_lang().get("HREFLANG_DEFAULT_LANGUAGE")
89
+ if nested:
90
+ return str(nested)
91
+ return None
92
+
93
+
94
+ def get_language_flag_map() -> dict[str, str]:
95
+ """
96
+ ISO region codes for emoji flags: merge package defaults with
97
+ ``settings.LANGUAGE_FLAG_MAP`` and ``APP_CONFIG['lang']['LANGUAGE_FLAG_MAP']``.
98
+ """
99
+ from lang.utils import DEFAULT_LANGUAGE_FLAG_MAP
100
+
101
+ user = getattr(settings, "LANGUAGE_FLAG_MAP", None) or {}
102
+ user2 = _app_config_lang().get("LANGUAGE_FLAG_MAP") or {}
103
+ return {**DEFAULT_LANGUAGE_FLAG_MAP, **dict(user), **dict(user2)}
@@ -0,0 +1,63 @@
1
+ """
2
+ Context processors for django-lang app
3
+ """
4
+
5
+ from typing import Any, Dict
6
+
7
+ from django.conf import settings
8
+
9
+ from lang.conf import (
10
+ get_hreflang_default_language,
11
+ get_language_wikipedia_sameas,
12
+ get_og_locale_by_language,
13
+ )
14
+
15
+
16
+ def from_settings(request) -> Dict[str, Any]:
17
+ return {
18
+ "DEFAULT_LANGUAGE_CODE": getattr(settings, "LANGUAGE_CODE", None),
19
+ }
20
+
21
+
22
+ def language_switcher_next(request) -> Dict[str, Any]:
23
+ """
24
+ Optional: set ``redirect_to`` to the current path for packaged language forms
25
+ (``set_language`` POST field ``next``). Add to ``TEMPLATES`` context_processors
26
+ when you use ``lang/nav-link.html`` without
27
+ passing ``redirect_to`` from each view.
28
+ """
29
+ if request is None:
30
+ return {}
31
+ return {"redirect_to": request.get_full_path()}
32
+
33
+
34
+ def seo_i18n(request) -> Dict[str, Any]:
35
+ """
36
+ SEO helpers for multilingual templates (list **after**
37
+ ``lang.context_processors.from_settings`` so it can override
38
+ ``DEFAULT_LANGUAGE_CODE`` when needed).
39
+
40
+ - ``DEFAULT_LANGUAGE_CODE``: set from ``settings.HREFLANG_DEFAULT_LANGUAGE``
41
+ when it is a non-empty string (drives ``hreflang.html`` ``x-default``).
42
+ If unset or empty, ``from_settings`` keeps ``LANGUAGE_CODE``.
43
+ - ``LANGUAGE_WIKIPEDIA_SAMEAS``: optional ``dict`` mapping Django language
44
+ code → URL (e.g. JSON-LD ``sameAs`` for languages).
45
+ - ``OG_LOCALE_BY_LANGUAGE``: optional ``dict`` mapping Django language code
46
+ → Open Graph locale string (e.g. ``it_IT``).
47
+
48
+ Configure on ``settings`` (all optional except the first, which is
49
+ opt-in via defining ``HREFLANG_DEFAULT_LANGUAGE``):
50
+
51
+ - ``HREFLANG_DEFAULT_LANGUAGE`` (top-level or ``APP_CONFIG['lang']``)
52
+ - ``LANGUAGE_WIKIPEDIA_SAMEAS`` / ``OG_LOCALE_BY_LANGUAGE`` (same)
53
+
54
+ See :mod:`lang.conf` for merge rules.
55
+ """
56
+ ctx: Dict[str, Any] = {
57
+ "LANGUAGE_WIKIPEDIA_SAMEAS": get_language_wikipedia_sameas(),
58
+ "OG_LOCALE_BY_LANGUAGE": get_og_locale_by_language(),
59
+ }
60
+ href_default = get_hreflang_default_language()
61
+ if href_default:
62
+ ctx["DEFAULT_LANGUAGE_CODE"] = href_default
63
+ return ctx
lang/defaults.py ADDED
@@ -0,0 +1,53 @@
1
+ """
2
+ Built-in defaults for i18n / SEO helpers.
3
+
4
+ Runtime resolution (settings + optional ``APP_CONFIG``) lives in
5
+ :mod:`lang.conf`; do **not** require projects to import this module from
6
+ ``settings.py`` unless they prefer explicit re-exports.
7
+
8
+ ``HREFLANG_DEFAULT_LANGUAGE`` is project-specific; set it via
9
+ ``settings.HREFLANG_DEFAULT_LANGUAGE`` or ``APP_CONFIG['lang']`` (see
10
+ :mod:`lang.conf`).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ # Google-style BCP 47 in <link hreflang="…"> (Django code → attribute value).
16
+ LANGUAGE_HREFLANG_MAP: dict[str, str] = {
17
+ "zh-hans": "zh-Hans",
18
+ "zh-hant": "zh-Hant",
19
+ }
20
+
21
+ # JSON-LD ``sameAs`` (or similar) → Wikipedia articles per language.
22
+ LANGUAGE_WIKIPEDIA_SAMEAS: dict[str, str] = {
23
+ "it": "https://en.wikipedia.org/wiki/Italian_language",
24
+ "en": "https://en.wikipedia.org/wiki/English_language",
25
+ "de": "https://en.wikipedia.org/wiki/German_language",
26
+ "fr": "https://en.wikipedia.org/wiki/French_language",
27
+ "es": "https://en.wikipedia.org/wiki/Spanish_language",
28
+ "pt": "https://en.wikipedia.org/wiki/Portuguese_language",
29
+ "nl": "https://en.wikipedia.org/wiki/Dutch_language",
30
+ "pl": "https://en.wikipedia.org/wiki/Polish_language",
31
+ "ru": "https://en.wikipedia.org/wiki/Russian_language",
32
+ "zh-hans": "https://en.wikipedia.org/wiki/Chinese_language",
33
+ "ja": "https://en.wikipedia.org/wiki/Japanese_language",
34
+ "ko": "https://en.wikipedia.org/wiki/Korean_language",
35
+ "ar": "https://en.wikipedia.org/wiki/Arabic",
36
+ }
37
+
38
+ # Open Graph ``og:locale``-style strings (underscore + region).
39
+ OG_LOCALE_BY_LANGUAGE: dict[str, str] = {
40
+ "it": "it_IT",
41
+ "en": "en_US",
42
+ "de": "de_DE",
43
+ "fr": "fr_FR",
44
+ "es": "es_ES",
45
+ "pt": "pt_PT",
46
+ "nl": "nl_NL",
47
+ "pl": "pl_PL",
48
+ "ru": "ru_RU",
49
+ "zh-hans": "zh_CN",
50
+ "ja": "ja_JP",
51
+ "ko": "ko_KR",
52
+ "ar": "ar_SA",
53
+ }
lang/middleware.py ADDED
@@ -0,0 +1,56 @@
1
+ """
2
+ Optional middleware for Django's ``set_language`` view and gettext_lazy URL paths.
3
+
4
+ ``django.views.i18n.set_language`` calls ``django.urls.translate_url``, which runs
5
+ ``resolve()`` while the **active** translation language is still the one chosen by
6
+ ``LocaleMiddleware`` for the current request. For POSTs to ``/i18n/setlang/`` there
7
+ is no language prefix in the URL, so the language often comes from the
8
+ ``django_language`` cookie (e.g. ``LANGUAGE_CODE`` default ``en``). If the user is
9
+ leaving a page whose path uses a **different** prefix (e.g. ``/it/...``) and
10
+ translated segments from ``gettext_lazy``, ``resolve()`` can fail under the wrong
11
+ active language and the redirect keeps the old prefix — while the cookie updates,
12
+ ``LocaleMiddleware`` still prefers the URL prefix, so the UI appears stuck.
13
+
14
+ This middleware activates the language parsed from the ``next`` URL path (when it
15
+ carries an i18n prefix) **before** ``set_language`` runs, so ``translate_url`` matches
16
+ the same patterns as for a normal request on that path.
17
+
18
+ Insert **immediately after** ``django.middleware.locale.LocaleMiddleware``::
19
+
20
+ MIDDLEWARE = [
21
+ ...
22
+ "django.middleware.locale.LocaleMiddleware",
23
+ "lang.middleware.SetLanguageNextPathMiddleware",
24
+ ...
25
+ ]
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from urllib.parse import urlsplit
31
+
32
+ from django.utils import translation
33
+ from django.utils.deprecation import MiddlewareMixin
34
+ from django.utils.translation import get_language_from_path
35
+
36
+
37
+ class SetLanguageNextPathMiddleware(MiddlewareMixin):
38
+ """Align active language with the i18n prefix in ``next`` for POST ``set_language``."""
39
+
40
+ def process_request(self, request):
41
+ if request.method != "POST":
42
+ return None
43
+ path_info = request.path_info or ""
44
+ if not path_info.startswith("/i18n/") or "setlang" not in path_info:
45
+ return None
46
+ next_url = request.POST.get("next") or request.GET.get("next")
47
+ if not next_url:
48
+ return None
49
+ path = urlsplit(next_url).path or "/"
50
+ path_lang = get_language_from_path(path)
51
+ if not path_lang:
52
+ return None
53
+ translation.activate(path_lang)
54
+ if hasattr(request, "LANGUAGE_CODE"):
55
+ request.LANGUAGE_CODE = path_lang
56
+ return None
@@ -0,0 +1,27 @@
1
+ /*-----------------------------------------------------------------------------------*/
2
+ /* Mobile / narrow navbar (< lg): language switch beside hamburger; hide duplicate */
3
+ /* in the collapsed drawer. (display-standalone-class.js is optional; not required.) */
4
+ /*-----------------------------------------------------------------------------------*/
5
+
6
+ .lang-switcher-standalone {
7
+ display: none !important;
8
+ }
9
+
10
+ @media (max-width: 991.98px) {
11
+ .lang-switcher-standalone {
12
+ display: flex !important;
13
+ align-items: center;
14
+ margin-right: 6px;
15
+ }
16
+
17
+ #language-switcher-standalone select {
18
+ font-size: 24px;
19
+ line-height: 1;
20
+ cursor: pointer;
21
+ padding: 0 2px 0 0;
22
+ }
23
+
24
+ .navbar-collapse #language-switcher {
25
+ display: none !important;
26
+ }
27
+ }
@@ -0,0 +1,17 @@
1
+ /* Menu Language (in-drawer + optional standalone bar id language-switcher-standalone) */
2
+ #language-switcher select,
3
+ #language-switcher-standalone select {
4
+ width: auto;
5
+ background-color: transparent;
6
+ font-size: 28px;
7
+ border: 0px solid;
8
+
9
+ /*remove arrow*/
10
+ -webkit-appearance: none;
11
+ -moz-appearance: none;
12
+ text-indent: 1px;
13
+ text-overflow: '';
14
+
15
+ /*remove focus border*/
16
+ outline: none;
17
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Adds class ``display-standalone`` on <html> when the page runs in a standalone
3
+ * display surface (CSS ``display-mode: standalone``) or as an iOS home-screen
4
+ * web app (``navigator.standalone``). Optional hook for future CSS/JS; navbar
5
+ * language placement no longer depends on it (see nav-link-standalone.css).
6
+ */
7
+ (function () {
8
+ try {
9
+ var dm = window.matchMedia && window.matchMedia("(display-mode: standalone)");
10
+ var ios = window.navigator.standalone === true;
11
+ if ((dm && dm.matches) || ios) {
12
+ document.documentElement.classList.add("display-standalone");
13
+ }
14
+ } catch (e) {}
15
+ })();
@@ -0,0 +1,8 @@
1
+ {% load i18n urls languages_helpers %}
2
+
3
+ <!-- hreflang -->
4
+ {% get_available_languages as LANGUAGES %}
5
+ {% for language_code, language_name in LANGUAGES %}
6
+ <link rel="alternate" hreflang="{{ language_code|hreflang_bcp47 }}" href="{% translate_url language_code %}" />
7
+ {% endfor %}
8
+ <link rel="alternate" hreflang="x-default" href="{% translate_url DEFAULT_LANGUAGE_CODE %}" />
@@ -0,0 +1,6 @@
1
+ {# Beside the mobile menu toggle when (display-mode: standalone) or iOS web app. #}
2
+ {# Use with nav-link-standalone.css + display-standalone-class.js #}
3
+ {% load i18n %}
4
+ <div class="lang-switcher-standalone">
5
+ {% include "lang/nav-link.html" with lang_switcher_id="language-switcher-standalone" %}
6
+ </div>
@@ -0,0 +1,14 @@
1
+ {% load i18n languages_helpers %}
2
+ <div id="{{ lang_switcher_id|default:'language-switcher' }}" class="nav-link{% if lang_switcher_extra_class %} {{ lang_switcher_extra_class }}{% endif %}">
3
+ <form action="{% url 'set_language' %}" method="post">
4
+ {% csrf_token %}
5
+ <input name="next" type="hidden" value="{{ redirect_to }}" />
6
+ <select name="language" onchange="this.form.submit()" aria-label="{% translate 'Language' %}">
7
+ {% for language in request|get_language_info_list_ex %}
8
+ <option value="{{ language.code }}"{% if language.is_current %} selected="selected"{% endif %}>
9
+ <span class="flag">{{ language.flag }}</span>
10
+ </option>
11
+ {% endfor %}
12
+ </select>
13
+ </form>
14
+ </div>
File without changes
@@ -0,0 +1,74 @@
1
+ import flag
2
+ from django import template
3
+ from django.conf import settings
4
+ from django.utils import translation
5
+
6
+ from lang.utils import build_language_flag_map, get_hreflang_code
7
+
8
+ register = template.Library()
9
+
10
+
11
+ @register.filter
12
+ def hreflang_bcp47(django_language_code: str) -> str:
13
+ """Map Django language code to hreflang (BCP 47); see :mod:`lang.conf`."""
14
+ return get_hreflang_code(django_language_code)
15
+
16
+
17
+ @register.filter
18
+ def get_language_info_list_ex(request):
19
+ """
20
+ Sample result:
21
+
22
+ [{'bidi': False,
23
+ 'code': 'en',
24
+ 'flag': '🇬🇧',
25
+ 'is_current': False,
26
+ 'name': 'English',
27
+ 'name_local': 'English',
28
+ 'name_translated': 'Inglese'},
29
+ {'bidi': False,
30
+ 'code': 'it',
31
+ 'flag': '🇮🇹',
32
+ 'is_current': True,
33
+ 'name': 'Italian',
34
+ 'name_local': 'italiano',
35
+ 'name_translated': 'Italiano'},
36
+ {'bidi': False,
37
+ 'code': 'es',
38
+ 'flag': '🇪🇸',
39
+ 'is_current': False,
40
+ 'name': 'Spanish',
41
+ 'name_local': 'español',
42
+ 'name_translated': 'Spagnolo'}]
43
+ """
44
+ data = []
45
+
46
+ # From django.templatetags.i18n.GetLanguageInfoListNode
47
+ def get_language_info(language):
48
+ # ``language`` is either a language code string or a sequence
49
+ # with the language code as its first item
50
+ if len(language[0]) > 1:
51
+ return translation.get_language_info(language[0])
52
+ else:
53
+ return translation.get_language_info(str(language))
54
+
55
+ flag_map = build_language_flag_map()
56
+
57
+ # Es: 'es'
58
+ current_language = translation.get_language()
59
+
60
+ # Es: [('en', 'Inglés'), ('it', 'Italiano'), ('es', 'Español')]
61
+ # languages = [(k, translation.gettext(v)) for k, v in settings.LANGUAGES]
62
+ for language in settings.LANGUAGES:
63
+ # Es: {'bidi': False, 'code': 'es', 'name': 'Spanish',
64
+ # 'name_local': 'español', 'name_translated': 'Español'}
65
+ info = get_language_info(language)
66
+
67
+ code = info["code"]
68
+ info["is_current"] = code == current_language
69
+
70
+ # This requires emoji-country-flag Python package
71
+ info["flag"] = flag.flag(flag_map.get(code, code))
72
+ data.append(info)
73
+
74
+ return data
@@ -0,0 +1,83 @@
1
+ import logging
2
+ from typing import Any, Dict, Optional
3
+ from urllib.parse import unquote, urlsplit, urlunsplit
4
+
5
+ from django import template, urls
6
+ from django.urls import Resolver404, resolve, reverse
7
+ from django.utils.translation import override
8
+
9
+ register = template.Library()
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ @register.simple_tag(takes_context=True)
15
+ def translate_url(context: Dict[str, Any], language: Optional[str]) -> str:
16
+ """
17
+ Given the context of the current page, try to get its translated version in
18
+ the `language` (either by i18n_patterns or by translated regex).
19
+ Return the original URL if no translated version is found.
20
+
21
+ Usage:
22
+ {% translate_url 'en' %}
23
+ """
24
+ request = context["request"]
25
+ iter_key = f"translated_iter_{language}"
26
+ url_key = f"translated_url_{language}"
27
+ session = request.session
28
+
29
+ # set session's iteration to avoid circular calls
30
+ try:
31
+ if session.get(iter_key, 0) > 0:
32
+ return session[url_key]
33
+ session[iter_key] = 1
34
+ except Exception as err:
35
+ raise Exception(f"Avoid circular execution! [{err}]")
36
+
37
+ if url_key in session:
38
+ logger.debug("session has %s: %s", url_key, session[url_key])
39
+ return session[url_key]
40
+ url = request.build_absolute_uri()
41
+ try:
42
+ parsed = urlsplit(url)
43
+ match = resolve(unquote(parsed.path))
44
+ except Resolver404:
45
+ pass
46
+ else:
47
+ with override(language):
48
+ try:
49
+ view = match.func(request, **match.kwargs)
50
+ if hasattr(view, "url"):
51
+ translated_url = urlunsplit(
52
+ (
53
+ parsed.scheme,
54
+ parsed.netloc,
55
+ view.url,
56
+ parsed.query,
57
+ parsed.fragment,
58
+ )
59
+ )
60
+ else:
61
+ view_url = urlunsplit(
62
+ (
63
+ parsed.scheme,
64
+ parsed.netloc,
65
+ reverse(
66
+ match.url_name,
67
+ args=match.args,
68
+ kwargs=match.kwargs,
69
+ ),
70
+ parsed.query,
71
+ parsed.fragment,
72
+ )
73
+ )
74
+ translated_url = urls.translate_url(view_url, language)
75
+ session[url_key] = translated_url
76
+ return translated_url
77
+ except Exception as err:
78
+ logger.debug(f"Translate url exception: [{err}]")
79
+ pass
80
+
81
+ translated_url = urls.translate_url(url, language)
82
+ session[url_key] = translated_url
83
+ return translated_url
lang/utils.py ADDED
@@ -0,0 +1,35 @@
1
+ """
2
+ Shared helpers for language display (hreflang BCP 47, flag region codes).
3
+
4
+ Resolved values come from :mod:`lang.conf` (settings + ``APP_CONFIG`` + defaults).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ # Reasonable defaults for common Django language codes (ISO 3166-1 for flag.emoji).
10
+ DEFAULT_LANGUAGE_FLAG_MAP: dict[str, str] = {
11
+ "ar": "sa",
12
+ "en": "gb",
13
+ "ja": "jp",
14
+ "ko": "kr",
15
+ "pt": "pt",
16
+ "ru": "ru",
17
+ "zh-hans": "cn",
18
+ "zh-hant": "tw",
19
+ }
20
+
21
+
22
+ def get_hreflang_code(django_language_code: str) -> str:
23
+ """Return the hreflang attribute value for a Django ``LANGUAGE_CODE``."""
24
+ from lang.conf import get_language_hreflang_map
25
+
26
+ return get_language_hreflang_map().get(
27
+ django_language_code, django_language_code
28
+ )
29
+
30
+
31
+ def build_language_flag_map() -> dict[str, str]:
32
+ """Merge built-in flag regions with project / ``APP_CONFIG`` overrides."""
33
+ from lang.conf import get_language_flag_map
34
+
35
+ return get_language_flag_map()