# i18nice [](https://github.com/solaluset/i18nice/actions/workflows/ci.yml) [](https://coveralls.io/github/solaluset/i18nice) [](https://pypi.org/project/i18nice/) [](https://anaconda.org/conda-forge/i18nice)
This library provides i18n functionality for Python 3 out of the box. The usage is mostly based on Rails i18n library.
[CHANGELOG](https://github.com/solaluset/i18nice/blob/master/CHANGELOG.md)
[CONTRIBUTING](https://github.com/solaluset/i18nice/blob/master/CONTRIBUTING.md)
## Installation
Just run
```shell
pip install i18nice
```
If you want to use YAML to store your translations, use
```shell
pip install i18nice[YAML]
```
## Usage
### Basic usage
The simplest, though not very useful usage would be
```python
import i18n
i18n.add_translation('foo', 'bar')
i18n.t('foo') # bar
```
### Using translation files
YAML and JSON formats are supported to store translations. With the default configuration, if you have the following `foo.en.yml` file
```yaml
en:
hi: Hello world !
```
Or a JSON file `foo.en.json`
```json
{
"en": {
"hi": "Hello world !"
}
}
```
in `/path/to/translations` folder, you simply need to add the folder to the translations path.
```python
import i18n
i18n.load_path.append('/path/to/translations')
i18n.t('foo.hi') # Hello world !
```
Please note that YAML format is used as default file format if you have `yaml` module installed.
If both `yaml` and `json` modules available and you want to use JSON to store translations, explicitly specify that: `i18n.set('file_format', 'json')`
**!WARNING!**
`yaml.FullLoader` is no longer used by default.
If you need full yaml functionalities, override it with a custom loader:
```python
class MyLoader(i18n.loaders.YamlLoader):
loader = yaml.FullLoader
i18n.register_loader(MyLoader, ["yml", "yaml"])
```
### Memoization
The configuration value `enable_memoization` (`True` by default) disables reloading of files every time when searching for missing translation.
When translations are loaded, they're always stored in memory, hence it does not affect how existing translations are accessed.
### Load everything
`i18n.load_everything()` will load every file in `load_path` and subdirectories that matches `filename_format` and `file_format`.
You can call it with locale argument to load only one locale.
`i18n.unload_everything()` will clear all caches.
`i18n.reload_everything()` is just a shortcut for `unload_everything()` followed by `load_everything()`.
For the best performance, you can pass `lock=True` to `load_everything()` to disable searching for missing translations completely.
It'll prevent slowdowns caused by missing translations, but you'll need to use `unload_everything()` to be able to load files again.
### Namespaces
#### File namespaces
In the above example, the translation key is `foo.hi` and not just `hi`. This is because the translation filename format is by default `{namespace}.{locale}.{format}`, so the {namespace} part of the file is used as translation.
To remove `{namespace}` from filename format please change the `filename_format` configuration.
```python
i18n.set('filename_format', '{locale}.{format}')
```
#### Directory namespaces
If your files are in subfolders, the foldernames are also used as namespaces, so for example if your translation root path is `/path/to/translations` and you have the file `/path/to/translations/my/app/name/foo.en.yml`, the translation namespace for the file will be `my.app.name` and the file keys will therefore be accessible from `my.app.name.foo.my_key`.
## Functionalities
### Placeholder
You can of course use placeholders in your translations. With the default configuration, the placeholders are used by inserting `%{placeholder_name}` in the translation string. Here is a sample usage.
```python
i18n.add_translation('hi', 'Hello %{name} !')
i18n.t('hi', name='Bob') # Hello Bob !
```
Braces are optional if the identifier is separated from following words.
To escape the delimiter you need to put it twice (like `%%`).
### Pluralization
Pluralization is based on Rail i18n module. By passing a `count` variable to your translation, it will be pluralized. The translation value should be a dictionary with at least the keys `one` and `many`. You can add a `zero` or `few` key when needed, if it is not present `many` will be used instead. Here is a sample usage.
```python
i18n.add_translation('mail_number', {
'zero': 'You do not have any mail.',
'one': 'You have a new mail.',
'few': 'You only have %{count} mails.',
'many': 'You have %{count} new mails.'
})
i18n.t('mail_number', count=0) # You do not have any mail.
i18n.t('mail_number', count=1) # You have a new mail.
i18n.t('mail_number', count=3) # You only have 3 new mails.
i18n.t('mail_number', count=12) # You have 12 new mails.
```
### Fallback
You can set a fallback which will be used when the key is not found in the default locale.
```python
i18n.set('locale', 'jp')
i18n.set('fallback', 'en')
i18n.add_translation('foo', 'bar', locale='en')
i18n.t('foo') # bar
```
Note that setting `locale` and `fallback` to the same value will result in `fallback` being `None`.
### Skip locale from root
Sometimes i18n structure file came from another project or not contains root element with locale eg. `en` name.
```json
{
"foo": "FooBar"
}
```
However, we would like to use this i18n .json file in our Python sub-project or micro service as base file for translations.
`i18nice` has special configuration that is skipping locale eg. `en` root data element from the file.
```python
i18n.set('skip_locale_root_data', True)
```
### Different directory structure
If your tree of translation files looks similar to this, you can enable `use_locale_dirs` to get the files properly loaded:
```
locales
├── en-US
│ ├── common.yml
│ └── gui
│ ├── page1.yml
│ └── page2.yml
└── fr-FR
├── common.yml
└── gui
├── page1.yml
└── page2.yml
```
Example code:
```python
import i18n
i18n.load_path.append("locales")
i18n.set("file_format", "yml")
i18n.set("filename_format", "{namespace}.{format}")
i18n.set("skip_locale_root_data", True)
i18n.set("use_locale_dirs", True)
print(i18n.t("common.text1", locale="en-US"))
print(i18n.t("gui.page1.title", locale="en-US"))
```
### Lists
It's possible to use lists of translations, for example:
```yaml
# translations.en.yml
en:
days:
- Monday
- Tuesday
- Wednesday
...
```
```python
# translate.py
from datetime import date
import i18n
i18n.load_path.append(".")
# will print current day
print(i18n.t("translations.days")[date.today().weekday()])
```
It's also possible to use pluralization in lists:
```yml
days:
- one: Monday
many: Mondays
...
```
Note 1:
The function actually returns a `LazyTranslationTuple` instead of `list`.
Note 2:
Because the tuple is lazy, it'll only process elements when they're requested.
If you need to get fully processed translation, you can force it with `[:]`:
```python
# ({'one': 'Monday', 'many': 'Mondays'}, {'one': 'Tuesday', 'many': 'Tuesdays'}, ...)
print(i18n.t("translations.days", count=3))
# ('Mondays', 'Tuesdays', 'Wednesdays')
print(i18n.t("translations.days", count=3)[:])
```
Note 3 (for type checking):
`t` declares its return type as `str` by default. To simplify type checking in situations where lists are used, you can pass `_list=True` to it, which should have less overhead than calling `cast` and be less intrusive than `type: ignore` comment.
**This will NOT affect actual return type and is purely for type checkers.**
### Static references
Static references allow you to refer to other translation values. This can be useful to avoid repetition. To create a static reference, simply put a key prefixed with namespace delimiter to a placeholder. For example:
```json
{
"en": {
"progname": "Program Name",
"welcome": "Welcome to %{.progname}!"
}
}
```
Note that you don't need to specify the absolute key:
```json
{
"en": {
"interface": {
"progname": "Program Name",
"ref": "%{.progname} and %{.interface.progname} refer to the same value"
}
}
}
```
To be exact, keys are searched from top to bottom. For example, if you referred to `.c.my_key` in `a.b.c.d`, the library will first check for `c.my_key`, then `a.c.my_key`, and finally find `a.b.c.my_key` if it's present. If not, it'll try to search `c.my_key` in other files and throw an exception if that also fails.
### Error handling
There are three config options for handling different situations.
Setting it to `None` disables handling (default), `"error"` enables error throwing.
You can also set your custom handlers:
`on_missing_translation(key, locale, **kwargs)`
`on_missing_plural(key, locale, translation, count)`
`on_missing_placeholder(key, locale, translation, placeholder)`
Example:
```python
import logging, i18n
def handler(key, locale, text, name):
logging.warning(f"Missing placeholder {name!r} while translating {key!r} to {locale!r} (in {text!r})")
return "undefined"
i18n.set("on_missing_placeholder", handler)
i18n.add_translation("am", "Amount is %{amount}")
print(i18n.t("am"))
# output:
# WARNING:root:Missing placeholder 'amount' while translating 'am' to 'en' (in 'Amount is %{amount}')
# Amount is undefined
```
### Custom functions
Add your custom functions and choose translation variants during runtime.
The function should accept as many positional arguments as there are values specified between brackets.
All keyword arguments given to `t` will be passed to the function.
The call in translation will be substituted with the return value.
This may be an alternative for pluralization, especially if a language has more than one plural form.
Example (correct plural form of days in Ukrainian):
```python
i18n.set("locale", "uk")
i18n.add_translation("days", "%{count} %{p(день|дні|днів)}")
def determine_plural_form(*args, count, **_):
count = abs(count)
if count % 10 >= 5 or count % 10 == 0 or (count % 100) in range(11, 20):
return args[2]
elif count % 10 == 1:
return args[0]
return args[1]
i18n.add_function("p", determine_plural_form, "uk")
i18n.t("days", count=1) # 1 день
i18n.t("days", count=2) # 2 дні
i18n.t("days", count=5) # 5 днів
```
## Testing
You can run tests with `python dev-helper.py run-tests`.
Raw data
{
"_id": null,
"home_page": "https://github.com/solaluset/i18nice",
"name": "i18nice",
"maintainer": "Sola Luset",
"docs_url": null,
"requires_python": null,
"maintainer_email": null,
"keywords": null,
"author": "Daniel Perez",
"author_email": "tuvistavie@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/e5/59/2fbe7fb5645328c77a0fa251db1ce358959538ebf582f69cfa653c983255/i18nice-0.15.5.tar.gz",
"platform": null,
"description": "# i18nice [](https://github.com/solaluset/i18nice/actions/workflows/ci.yml) [](https://coveralls.io/github/solaluset/i18nice) [](https://pypi.org/project/i18nice/) [](https://anaconda.org/conda-forge/i18nice)\n\nThis library provides i18n functionality for Python 3 out of the box. The usage is mostly based on Rails i18n library.\n\n[CHANGELOG](https://github.com/solaluset/i18nice/blob/master/CHANGELOG.md)\n\n[CONTRIBUTING](https://github.com/solaluset/i18nice/blob/master/CONTRIBUTING.md)\n\n## Installation\n\nJust run\n\n```shell\npip install i18nice\n```\n\nIf you want to use YAML to store your translations, use\n\n```shell\npip install i18nice[YAML]\n```\n\n## Usage\n### Basic usage\n\nThe simplest, though not very useful usage would be\n\n```python\nimport i18n\ni18n.add_translation('foo', 'bar')\ni18n.t('foo') # bar\n```\n\n### Using translation files\n\nYAML and JSON formats are supported to store translations. With the default configuration, if you have the following `foo.en.yml` file\n\n```yaml\nen:\n hi: Hello world !\n```\n\nOr a JSON file `foo.en.json`\n\n```json\n{\n \"en\": {\n \"hi\": \"Hello world !\"\n }\n}\n```\n\nin `/path/to/translations` folder, you simply need to add the folder to the translations path.\n\n```python\nimport i18n\ni18n.load_path.append('/path/to/translations')\ni18n.t('foo.hi') # Hello world !\n```\n\nPlease note that YAML format is used as default file format if you have `yaml` module installed.\nIf both `yaml` and `json` modules available and you want to use JSON to store translations, explicitly specify that: `i18n.set('file_format', 'json')`\n\n**!WARNING!**\n`yaml.FullLoader` is no longer used by default.\nIf you need full yaml functionalities, override it with a custom loader:\n\n```python\nclass MyLoader(i18n.loaders.YamlLoader):\n loader = yaml.FullLoader\n\ni18n.register_loader(MyLoader, [\"yml\", \"yaml\"])\n```\n\n### Memoization\n\nThe configuration value `enable_memoization` (`True` by default) disables reloading of files every time when searching for missing translation.\nWhen translations are loaded, they're always stored in memory, hence it does not affect how existing translations are accessed.\n\n### Load everything\n\n`i18n.load_everything()` will load every file in `load_path` and subdirectories that matches `filename_format` and `file_format`.\nYou can call it with locale argument to load only one locale.\n\n`i18n.unload_everything()` will clear all caches.\n\n`i18n.reload_everything()` is just a shortcut for `unload_everything()` followed by `load_everything()`.\n\nFor the best performance, you can pass `lock=True` to `load_everything()` to disable searching for missing translations completely.\nIt'll prevent slowdowns caused by missing translations, but you'll need to use `unload_everything()` to be able to load files again.\n\n### Namespaces\n\n#### File namespaces\nIn the above example, the translation key is `foo.hi` and not just `hi`. This is because the translation filename format is by default `{namespace}.{locale}.{format}`, so the {namespace} part of the file is used as translation.\n\nTo remove `{namespace}` from filename format please change the `filename_format` configuration.\n\n```python\ni18n.set('filename_format', '{locale}.{format}')\n```\n\n#### Directory namespaces\nIf your files are in subfolders, the foldernames are also used as namespaces, so for example if your translation root path is `/path/to/translations` and you have the file `/path/to/translations/my/app/name/foo.en.yml`, the translation namespace for the file will be `my.app.name` and the file keys will therefore be accessible from `my.app.name.foo.my_key`.\n\n## Functionalities\n### Placeholder\n\nYou can of course use placeholders in your translations. With the default configuration, the placeholders are used by inserting `%{placeholder_name}` in the translation string. Here is a sample usage.\n\n```python\ni18n.add_translation('hi', 'Hello %{name} !')\ni18n.t('hi', name='Bob') # Hello Bob !\n```\n\nBraces are optional if the identifier is separated from following words.\n\nTo escape the delimiter you need to put it twice (like `%%`).\n\n### Pluralization\n\nPluralization is based on Rail i18n module. By passing a `count` variable to your translation, it will be pluralized. The translation value should be a dictionary with at least the keys `one` and `many`. You can add a `zero` or `few` key when needed, if it is not present `many` will be used instead. Here is a sample usage.\n\n```python\ni18n.add_translation('mail_number', {\n 'zero': 'You do not have any mail.',\n 'one': 'You have a new mail.',\n 'few': 'You only have %{count} mails.',\n 'many': 'You have %{count} new mails.'\n})\ni18n.t('mail_number', count=0) # You do not have any mail.\ni18n.t('mail_number', count=1) # You have a new mail.\ni18n.t('mail_number', count=3) # You only have 3 new mails.\ni18n.t('mail_number', count=12) # You have 12 new mails.\n```\n\n### Fallback\n\nYou can set a fallback which will be used when the key is not found in the default locale.\n\n```python\ni18n.set('locale', 'jp')\ni18n.set('fallback', 'en')\ni18n.add_translation('foo', 'bar', locale='en')\ni18n.t('foo') # bar\n```\n\nNote that setting `locale` and `fallback` to the same value will result in `fallback` being `None`.\n\n### Skip locale from root\nSometimes i18n structure file came from another project or not contains root element with locale eg. `en` name.\n\n```json\n{\n \"foo\": \"FooBar\"\n}\n```\n\nHowever, we would like to use this i18n .json file in our Python sub-project or micro service as base file for translations.\n`i18nice` has special configuration that is skipping locale eg. `en` root data element from the file.\n\n```python\ni18n.set('skip_locale_root_data', True)\n```\n\n### Different directory structure\nIf your tree of translation files looks similar to this, you can enable `use_locale_dirs` to get the files properly loaded:\n\n```\nlocales\n\u251c\u2500\u2500 en-US\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 common.yml\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 gui\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 page1.yml\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 page2.yml\n\u2514\u2500\u2500 fr-FR\n \u251c\u2500\u2500 common.yml\n \u2514\u2500\u2500 gui\n \u251c\u2500\u2500 page1.yml\n \u2514\u2500\u2500 page2.yml\n```\n\nExample code:\n\n```python\nimport i18n\n\ni18n.load_path.append(\"locales\")\ni18n.set(\"file_format\", \"yml\")\ni18n.set(\"filename_format\", \"{namespace}.{format}\")\ni18n.set(\"skip_locale_root_data\", True)\ni18n.set(\"use_locale_dirs\", True)\n\nprint(i18n.t(\"common.text1\", locale=\"en-US\"))\nprint(i18n.t(\"gui.page1.title\", locale=\"en-US\"))\n```\n\n### Lists\n\nIt's possible to use lists of translations, for example:\n\n```yaml\n# translations.en.yml\nen:\n days:\n - Monday\n - Tuesday\n - Wednesday\n ...\n```\n\n```python\n# translate.py\nfrom datetime import date\nimport i18n\n\ni18n.load_path.append(\".\")\n# will print current day\nprint(i18n.t(\"translations.days\")[date.today().weekday()])\n```\n\nIt's also possible to use pluralization in lists:\n\n```yml\ndays:\n - one: Monday\n many: Mondays\n ...\n```\n\nNote 1:\nThe function actually returns a `LazyTranslationTuple` instead of `list`.\n\nNote 2:\nBecause the tuple is lazy, it'll only process elements when they're requested.\nIf you need to get fully processed translation, you can force it with `[:]`:\n\n```python\n# ({'one': 'Monday', 'many': 'Mondays'}, {'one': 'Tuesday', 'many': 'Tuesdays'}, ...)\nprint(i18n.t(\"translations.days\", count=3))\n# ('Mondays', 'Tuesdays', 'Wednesdays')\nprint(i18n.t(\"translations.days\", count=3)[:])\n```\n\nNote 3 (for type checking):\n`t` declares its return type as `str` by default. To simplify type checking in situations where lists are used, you can pass `_list=True` to it, which should have less overhead than calling `cast` and be less intrusive than `type: ignore` comment.\n**This will NOT affect actual return type and is purely for type checkers.**\n\n### Static references\n\nStatic references allow you to refer to other translation values. This can be useful to avoid repetition. To create a static reference, simply put a key prefixed with namespace delimiter to a placeholder. For example:\n\n```json\n{\n \"en\": {\n \"progname\": \"Program Name\",\n \"welcome\": \"Welcome to %{.progname}!\"\n }\n}\n```\n\nNote that you don't need to specify the absolute key:\n\n```json\n{\n \"en\": {\n \"interface\": {\n \"progname\": \"Program Name\",\n \"ref\": \"%{.progname} and %{.interface.progname} refer to the same value\"\n }\n }\n}\n```\n\nTo be exact, keys are searched from top to bottom. For example, if you referred to `.c.my_key` in `a.b.c.d`, the library will first check for `c.my_key`, then `a.c.my_key`, and finally find `a.b.c.my_key` if it's present. If not, it'll try to search `c.my_key` in other files and throw an exception if that also fails.\n\n### Error handling\n\nThere are three config options for handling different situations.\nSetting it to `None` disables handling (default), `\"error\"` enables error throwing.\nYou can also set your custom handlers:\n\n`on_missing_translation(key, locale, **kwargs)`\n\n`on_missing_plural(key, locale, translation, count)`\n\n`on_missing_placeholder(key, locale, translation, placeholder)`\n\nExample:\n\n```python\nimport logging, i18n\n\ndef handler(key, locale, text, name):\n logging.warning(f\"Missing placeholder {name!r} while translating {key!r} to {locale!r} (in {text!r})\")\n return \"undefined\"\n\ni18n.set(\"on_missing_placeholder\", handler)\ni18n.add_translation(\"am\", \"Amount is %{amount}\")\nprint(i18n.t(\"am\"))\n# output:\n# WARNING:root:Missing placeholder 'amount' while translating 'am' to 'en' (in 'Amount is %{amount}')\n# Amount is undefined\n```\n\n### Custom functions\n\nAdd your custom functions and choose translation variants during runtime.\n\nThe function should accept as many positional arguments as there are values specified between brackets.\nAll keyword arguments given to `t` will be passed to the function.\nThe call in translation will be substituted with the return value.\n\nThis may be an alternative for pluralization, especially if a language has more than one plural form.\n\nExample (correct plural form of days in Ukrainian):\n\n```python\ni18n.set(\"locale\", \"uk\")\ni18n.add_translation(\"days\", \"%{count} %{p(\u0434\u0435\u043d\u044c|\u0434\u043d\u0456|\u0434\u043d\u0456\u0432)}\")\n\ndef determine_plural_form(*args, count, **_):\n count = abs(count)\n if count % 10 >= 5 or count % 10 == 0 or (count % 100) in range(11, 20):\n return args[2]\n elif count % 10 == 1:\n return args[0]\n return args[1]\n\ni18n.add_function(\"p\", determine_plural_form, \"uk\")\ni18n.t(\"days\", count=1) # 1 \u0434\u0435\u043d\u044c\ni18n.t(\"days\", count=2) # 2 \u0434\u043d\u0456\ni18n.t(\"days\", count=5) # 5 \u0434\u043d\u0456\u0432\n```\n\n## Testing\nYou can run tests with `python dev-helper.py run-tests`.\n\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Translation library for Python",
"version": "0.15.5",
"project_urls": {
"Download": "https://github.com/solaluset/i18nice/archive/master.zip",
"Homepage": "https://github.com/solaluset/i18nice"
},
"split_keywords": [],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "10c1a0b3d912bd1f8dcc6176fccb91acc0279f54112a079505a35ef518e4d9ac",
"md5": "92a7c6d6780cf277d2a6bdb9aba4dfb8",
"sha256": "baec8d78aa3f8cf4a928586abe5ee369f93008bea59dd69c273ef827611847a5"
},
"downloads": -1,
"filename": "i18nice-0.15.5-py3-none-any.whl",
"has_sig": false,
"md5_digest": "92a7c6d6780cf277d2a6bdb9aba4dfb8",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": null,
"size": 19638,
"upload_time": "2024-06-12T21:25:22",
"upload_time_iso_8601": "2024-06-12T21:25:22.798398Z",
"url": "https://files.pythonhosted.org/packages/10/c1/a0b3d912bd1f8dcc6176fccb91acc0279f54112a079505a35ef518e4d9ac/i18nice-0.15.5-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "e5592fbe7fb5645328c77a0fa251db1ce358959538ebf582f69cfa653c983255",
"md5": "cf83075ec30a65509a4f45d880c1293d",
"sha256": "28fa4563390116dd6b16f638c400595d35ce55cd35486dd743daa8a3d31b7d2c"
},
"downloads": -1,
"filename": "i18nice-0.15.5.tar.gz",
"has_sig": false,
"md5_digest": "cf83075ec30a65509a4f45d880c1293d",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 22025,
"upload_time": "2024-06-12T21:25:24",
"upload_time_iso_8601": "2024-06-12T21:25:24.151669Z",
"url": "https://files.pythonhosted.org/packages/e5/59/2fbe7fb5645328c77a0fa251db1ce358959538ebf582f69cfa653c983255/i18nice-0.15.5.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-06-12 21:25:24",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "solaluset",
"github_project": "i18nice",
"travis_ci": false,
"coveralls": true,
"github_actions": true,
"lcname": "i18nice"
}