Skip to content

Commit

Permalink
add support for using nested keys
Browse files Browse the repository at this point in the history
  • Loading branch information
rnxm committed Aug 8, 2024
1 parent 23b0e41 commit b51c033
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 103 deletions.
204 changes: 122 additions & 82 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,99 +2,139 @@

--------
## Setting up
Start by downloading the package via pip.
```
python3 -m pip install discord-localization
```

After that, you can just import it into your file as a discord.py extension.
Start by downloading the package via pip.
```
python3 -m pip install discord-localization
```
After that, you can just import it into your file as a discord.py extension.

--------
## Examples
```py
from discord.ext import localization
import random

_ = localization.Localization("main.i18n.json", "en") # the first option is the relative path to the localization file. the second is the fallback/default language that is used if the actual language is not found.
dice = random.randint(1, 6)
language = input("What's your native language? / Was ist Deine Muttersprache? (en/de) >>> ")

print(_("dice_result", language, dice=dice))
```
```json
{
"en": {
"dice_result": "Your dice rolled {dice}!"
},
"de": {
"dice_result": "Dein Würfel hat eine {dice} gewürfelt!"
}
}
```
```py
from discord.ext import localization
import random

_ = localization.Localization("main.i18n.json", "en")
# the first option is the relative path to the localization
# file. the second is the fallback/default language that is
# used if the actual language is not found.
dice = random.randint(1, 6)
language = input("What's your native language? / Was ist Deine Muttersprache? (en/de) >>> ")

print(_("dice_result", language, dice=dice))
```
```json
{
"en": {
"dice_result": "Your dice rolled {dice}!"
},
"de": {
"dice_result": "Dein Würfel hat eine {dice} gewürfelt!"
}
}
```

-------
## Integrating it into your Discord bot
```py
import discord
from discord.ext import commands, localization
```py
import discord
from discord.ext import commands, localization
bot = commands.Bot(command_prefix="!", intents=discord.Intents.all())
# hey guys, breaking the 4th wall here. please stop using Intents.default()
# just to set the message_contentintent to True right after. Intents.all() does
# it for you.
_ = localization.Localization("main_localization.json", "en")

@bot.event
async def on_ready():
print("Bot is ready!")

bot = commands.Bot(command_prefix="!", intents=discord.Intents.all()) # hey guys, breaking the 4th wall here. please stop using Intents.default() just to set the message_content intent to True right after. Intents.all() does it for you.
_ = localization.Localization("main_localization.json", "en")

@bot.event
async def on_ready():
print("Bot is ready!")
@bot.command()
async def ping(ctx):
await ctx.reply(_("ping", ctx, latency=bot.latency * 1000))
```
```json
{
"en": {
"ping": "Pong! {latency}ms"
},
"en-US": {
"ping": "Pong! {latency}ms, but American! (because Discord makes a difference between en-US and en-UK locales - you can circumvent this by setting the default_locale)"
},
"en-UK": {
"ping": "Ping is {latency}ms, innit, bruv? (i'm sorry)"
},
"fr": {
"ping": "Bonjour! Ping is {latency}ms!"
}
}
```
Explanation:
- By setting the default locale to "en", we won't have any issues with unfinished localizations. This way, we are also not required to define localizations for both "en-US" and"en-UK" (since Discord's API differentiates them, however, usually, bots don't.)
- We are passing a `latency` argument to the localization function (which is also available with `Localization._`! But calling the object is fine too.), which will be later usedwhenever we use `{latency}` in a localization.
- We are defining an `en` locale in the JSON file, even though both Discord's API and discord.py doesn't have such a locale. This is to make it more obvious that everything inthat locale should be in English - you could also name it `default_locale`, you just have to make sure to edit it in the `Localization` object.
- We pass `ctx` to the localization function, which will automatically try getting `ctx.guild.preferred_locale`. This works with `Interaction` (by using `Interaction.guildpreferred_locale` internally), `Guild` (by using `guild.preferred_locale` internally), or just passing the `Locale` itself, for example, `discord.Locale.american_english` (whichwill convert to the `en-US` locale).
------
# Working with plurals
```py
from discord.ext.localization import Localization
import locale

@bot.command()
async def ping(ctx):
await ctx.reply(_("ping", ctx, latency=bot.latency * 1000))
```
```json
{
_ = Localization("main_plurals.json") # this will look for main_plurals.json as the language JSON file
apples = int(input("How many apples do you have? >>> "))
language = locale.getlocale()[0][:2] # this will return the default OS language, such as en, hu, de, fr, es etc.

print(_.one("apples", apples, language, apples=apples))
```
```json
{
"en": {
"ping": "Pong! {latency}ms"
},
"en-US": {
"ping": "Pong! {latency}ms, but American! (because Discord makes a difference between en-US and en-UK locales - you can circumvent this by setting the default_locale)"
"apples": ["You only have one apple! What a loser...", "You have {apples} apples!"]
},
"en-UK": {
"ping": "Ping is {latency}ms, innit, bruv? (i'm sorry)"
},
"fr": {
"ping": "Bonjour! Ping is {latency}ms!"
"hu": {
"apples": ["{apples} almád van!"]
}
}
```
Explanation:
- By setting the default locale to "en", we won't have any issues with unfinished localizations. This way, we are also not required to define localizations for both "en-US" and "en-UK" (since Discord's API differentiates them, however, usually, bots don't.)
- We are passing a `latency` argument to the localization function (which is also available with `Localization._`! But calling the object is fine too.), which will be later used whenever we use `{latency}` in a localization.
- We are defining an `en` locale in the JSON file, even though both Discord's API and discord.py doesn't have such a locale. This is to make it more obvious that everything in that locale should be in English - you could also name it `default_locale`, you just have to make sure to edit it in the `Localization` object.
- We pass `ctx` to the localization function, which will automatically try getting `ctx.guild.preferred_locale`. This works with `Interaction` (by using `Interaction.guild.preferred_locale` internally), `Guild` (by using `guild.preferred_locale` internally), or just passing the `Locale` itself, for example, `discord.Locale.american_english` (which will convert to the `en-US` locale).
}
```
This example does a great job at presenting how the `.one()` function works. Here's the explanation for the code:
The code will look for the default OS language, and will tell you how many apples you have with plurals in mind. I always get angry when I see "you have 1 apples"-ish text on awebsite, and it's an easy fix, but for some reason, many developers don't pay attention to it.
Hungarian doesn't make a difference between plurals, so the list for "hu"/"apples" only has 1 item. This won't be a problem though - the library isn't hardcoded to only look forthe first and second elements in the returned list, it will look for the **first** and the **last**. Which means that if a list only has 1 item, it will only return that item.

-------
## Working with plurals
```py
from discord.ext.localization import Localization
import locale

_ = Localization("main_plurals.json") # this will look for main_plurals.json as the language JSON file
apples = int(input("How many apples do you have? >>> "))
language = locale.getlocale()[0][:2] # this will return the default OS language, such as en, hu, de, fr, es etc.

print(_.one("apples", apples, language, apples=apples))
```
```json
{
"en": {
"apples": ["You only have one apple! What a loser...", "You have {apples} apples!"]
},
"hu": {
"apples": ["{apples} almád van!"]
}
}
```

This example does a great job at presenting how the `.one()` function works. Here's the explanation for the code:
## Advanced usage with nested keys and nested objects
With v1.1.0, we now have the ability to do some **nesting** with the dictionaries.

The code will look for the default OS language, and will tell you how many apples you have with plurals in mind. I always get angry when I see "you have 1 apples"-ish text on a website, and it's an easy fix, but for some reason, many developers don't pay attention to it.
For example, now that we've learned how to work with plurals, we can also work with nested keys. Here's an example of a nested JSON file:
```json
{
"en": {
"apples": {
"one": "You only have one apple! What a loser...",
"three": "You have three apples! That's a lot!",
"many": "You have {apples} apples!"
}
},
"hu": {
"apples": {
"one": "{apples} almád van!",
"three": "Három almád van! Ez nagyon sok!",
"many": "Van {apples} almád!"
}
}
}
```
We may now use the `localize()` method to get the nested keys, with the `text` argument being a "path" to our string, seperated by dots. Here's an example:
```py
from discord.ext.localization import Localization

Hungarian doesn't make a difference between plurals, so the list for "hu"/"apples" only has 1 item. This won't be a problem though - the library isn't hardcoded to only look for the first and second elements in the returned list, it will look for the **first** and the **last**. Which means that if a list only has 1 item, it will only return that item.
_ = Localization("nested.json")
apples = int(input("How many apples do you have? >>> "))
locale = input("What's your locale? >>> ")
if apples == 1:
print(_.localize("apples.one", "one", apples=apples))
# you could use an f-string here, but I'm trying to keep it simple
elif apples == 3:
print(_.localize("apples.three", "three", apples=apples))
else:
print(_.localize("apples.many", "many", apples=apples))
```
21 changes: 18 additions & 3 deletions discord/ext/localization/localization.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ def localize(self, text: str, locale: Union[str, Locale, Guild, Interaction, Con
locale = str(locale)
else:
raise TypeError("Locale must be of type str, discord.Locale, discord.Guild, discord.Interaction, or discord.ext.commands.Context, received {}".format(type(locale)))



localizations = self.file.get(locale) or self.file.get(self.default_locale)
if not localizations:
Expand All @@ -108,15 +110,28 @@ def localize(self, text: str, locale: Union[str, Locale, Guild, Interaction, Con
logging.error(InvalidLocale(locale))
return text

localized_text = localizations.get(text)
if localized_text:
return self.format_strings(localized_text, **kwargs)
if "." in text:
keys = text.split(".")
value = localizations
for key in keys:
if isinstance(value, dict):
value = value.get(key)
if value is None:
break
else:
value = None
break
else:
value = localizations.get(text)

if value is None:
if self.error:
raise LocalizationNotFound(text, locale)
else:
logging.error(LocalizationNotFound(text, locale))
return text

return self.format_strings(value, **kwargs)

_ = t = translate = localise = localize

Expand Down
3 changes: 2 additions & 1 deletion examples/!discord_integration_as_cog.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
class LocalizationCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
self._ = localization.Localization("cogs/discord_integration_as_cog.i18n.json", "en") # ! Because a cog is run from the main file, the path is relative to the main file!!!
self._ = localization.Localization("cogs/discord_integration_as_cog.i18n.json", "en")
# ! Because a cog is run from the main file, the path is relative to the main file!!!

@commands.Cog.listener()
async def on_ready(self):
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from setuptools import setup
from pathlib import Path

VERSION = '1.0.1'
VERSION = '1.1.0'
DESCRIPTION = 'A discord.py extension for command localization.'

# Setting up
Expand All @@ -14,7 +14,7 @@
long_description_content_type="text/markdown",
long_description=Path("README.md").read_text(),
packages=["discord.ext.localization"],
install_requires=[],
install_requires=["discord.py"],
keywords=['discord.py', 'i18n', 'translation', 'localisation', 'localization'],
classifiers=[
"Programming Language :: Python :: 3",
Expand Down
14 changes: 10 additions & 4 deletions tests/test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from discord.ext import localization
import pprint
from discord.ext.localization import Localization

_ = localization.Localization("test_lang.json", error=True)
pprint.pprint(_("test", "hu", a="EXAMPLE"))
_ = Localization("test_lang.json")
apples = int(input("How many apples do you have? >>> "))
locale = input("What's your locale? >>> ")
if apples == 1:
print(_.localize("apples.one", locale, apples=apples))
elif apples == 3:
print(_.localize("apples.three", locale, apples=apples))
else:
print(_.localize("apples.many", locale, apples=apples))
20 changes: 9 additions & 11 deletions tests/test_lang.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
{
"en": {
"hello": "Hello",
"world": "World"
"apples": {
"one": "You only have one apple! What a loser...",
"three": "You have three apples! That's a lot!",
"many": "You have {apples} apples!"
}
},
"hu": {
"hello": ["Helló", "a"],
"world": "Világ",
"test": {
"nested": "Nested {a}",
"array": ["Array {a}", "of", "strings {a}"],
"object": {
"key": "Value {a}",
"list": ["List {a}", "of", "values {a}", { "nested": "Nested {a}", "this": "is {a}" }]
}
"apples": {
"one": "{apples} almád van!",
"three": "Három almád van! Ez nagyon sok!",
"many": "Van {apples} almád!"
}
}
}

0 comments on commit b51c033

Please sign in to comment.