mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-07 09:52:30 -05:00
[i18n] Basic Implementation (#948)
* Initial commit * Beginning of working i18n * Add some translation files * Add more strings to translate * Update and add some more translations * Update spanish translation * Update french translation * Add alias translation templates * Add bank translations * Add economy translations * Add general translations * Add image translations * Add core translations
This commit is contained in:
@@ -9,6 +9,7 @@ from discord.ext import commands
|
||||
from core import checks
|
||||
from core.config import Config
|
||||
from core.utils.chat_formatting import box
|
||||
from core.i18n import CogI18n
|
||||
|
||||
__all__ = ["CogManager"]
|
||||
|
||||
@@ -178,6 +179,9 @@ class CogManager:
|
||||
invalidate_caches()
|
||||
|
||||
|
||||
_ = CogI18n("CogManagerUI", __file__)
|
||||
|
||||
|
||||
class CogManagerUI:
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
@@ -189,7 +193,7 @@ class CogManagerUI:
|
||||
cog_paths = ctx.bot.cog_mgr.paths
|
||||
cog_paths = [p for p in cog_paths if p != install_path]
|
||||
|
||||
msg = "Install Path: {}\n\n".format(install_path)
|
||||
msg = _("Install Path: {}\n\n").format(install_path)
|
||||
|
||||
partial = []
|
||||
for i, p in enumerate(cog_paths, start=1):
|
||||
@@ -205,8 +209,8 @@ class CogManagerUI:
|
||||
Add a path to the list of available cog paths.
|
||||
"""
|
||||
if not path.is_dir():
|
||||
await ctx.send("That path is does not exist or does not"
|
||||
" point to a valid directory.")
|
||||
await ctx.send(_("That path is does not exist or does not"
|
||||
" point to a valid directory."))
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -214,7 +218,7 @@ class CogManagerUI:
|
||||
except ValueError as e:
|
||||
await ctx.send(str(e))
|
||||
else:
|
||||
await ctx.send("Path successfully added.")
|
||||
await ctx.send(_("Path successfully added."))
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
@@ -227,11 +231,11 @@ class CogManagerUI:
|
||||
try:
|
||||
to_remove = cog_paths[path_number]
|
||||
except IndexError:
|
||||
await ctx.send("That is an invalid path number.")
|
||||
await ctx.send(_("That is an invalid path number."))
|
||||
return
|
||||
|
||||
await ctx.bot.cog_mgr.remove_path(to_remove)
|
||||
await ctx.send("Path successfully removed.")
|
||||
await ctx.send(_("Path successfully removed."))
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
@@ -247,17 +251,17 @@ class CogManagerUI:
|
||||
try:
|
||||
to_move = all_paths.pop(from_)
|
||||
except IndexError:
|
||||
await ctx.send("Invalid 'from' index.")
|
||||
await ctx.send(_("Invalid 'from' index."))
|
||||
return
|
||||
|
||||
try:
|
||||
all_paths.insert(to, to_move)
|
||||
except IndexError:
|
||||
await ctx.send("Invalid 'to' index.")
|
||||
await ctx.send(_("Invalid 'to' index."))
|
||||
return
|
||||
|
||||
await ctx.bot.cog_mgr.set_paths(all_paths)
|
||||
await ctx.send("Paths reordered.")
|
||||
await ctx.send(_("Paths reordered."))
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
@@ -275,9 +279,9 @@ class CogManagerUI:
|
||||
try:
|
||||
await ctx.bot.cog_mgr.set_install_path(path)
|
||||
except ValueError:
|
||||
await ctx.send("That path does not exist.")
|
||||
await ctx.send(_("That path does not exist."))
|
||||
return
|
||||
|
||||
install_path = await ctx.bot.cog_mgr.install_path()
|
||||
await ctx.send("The bot will install new cogs to the `{}`"
|
||||
" directory.".format(install_path))
|
||||
await ctx.send(_("The bot will install new cogs to the `{}`"
|
||||
" directory.").format(install_path))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import itertools
|
||||
from discord.ext import commands
|
||||
from core import checks
|
||||
from core import i18n
|
||||
from string import ascii_letters, digits
|
||||
from random import SystemRandom
|
||||
from collections import namedtuple
|
||||
@@ -18,6 +19,8 @@ OWNER_DISCLAIMER = ("⚠ **Only** the person who is hosting Red should be "
|
||||
"owner can access any data that is present on the host "
|
||||
"system.** ⚠")
|
||||
|
||||
_ = i18n.CogI18n("Core", __file__)
|
||||
|
||||
|
||||
class Core:
|
||||
"""Commands related to core functions"""
|
||||
@@ -29,19 +32,19 @@ class Core:
|
||||
try:
|
||||
spec = await ctx.bot.cog_mgr.find_cog(cog_name)
|
||||
except RuntimeError:
|
||||
await ctx.send("No module by that name was found in any"
|
||||
" cog path.")
|
||||
await ctx.send(_("No module by that name was found in any"
|
||||
" cog path."))
|
||||
return
|
||||
|
||||
try:
|
||||
ctx.bot.load_extension(spec)
|
||||
except Exception as e:
|
||||
log.exception("Package loading failed", exc_info=e)
|
||||
await ctx.send("Failed to load package. Check your console or "
|
||||
"logs for details.")
|
||||
await ctx.send(_("Failed to load package. Check your console or "
|
||||
"logs for details."))
|
||||
else:
|
||||
await ctx.bot.add_loaded_package(cog_name)
|
||||
await ctx.send("Done.")
|
||||
await ctx.send(_("Done."))
|
||||
|
||||
@commands.group()
|
||||
@checks.is_owner()
|
||||
@@ -50,9 +53,9 @@ class Core:
|
||||
if cog_name in ctx.bot.extensions:
|
||||
ctx.bot.unload_extension(cog_name)
|
||||
await ctx.bot.remove_loaded_package(cog_name)
|
||||
await ctx.send("Done.")
|
||||
await ctx.send(_("Done."))
|
||||
else:
|
||||
await ctx.send("That extension is not loaded.")
|
||||
await ctx.send(_("That extension is not loaded."))
|
||||
|
||||
@commands.command(name="reload")
|
||||
@checks.is_owner()
|
||||
@@ -65,12 +68,12 @@ class Core:
|
||||
ctx.bot.load_extension(spec)
|
||||
except Exception as e:
|
||||
log.exception("Package reloading failed", exc_info=e)
|
||||
await ctx.send("Failed to reload package. Check your console or "
|
||||
"logs for details.")
|
||||
await ctx.send(_("Failed to reload package. Check your console or "
|
||||
"logs for details."))
|
||||
else:
|
||||
curr_pkgs = await ctx.bot.db.packages()
|
||||
await ctx.bot.save_packages_status(curr_pkgs)
|
||||
await ctx.send("Done.")
|
||||
await ctx.send(_("Done."))
|
||||
|
||||
@commands.command(name="shutdown")
|
||||
@checks.is_owner()
|
||||
@@ -80,7 +83,7 @@ class Core:
|
||||
skin = "\N{EMOJI MODIFIER FITZPATRICK TYPE-3}"
|
||||
try: # We don't want missing perms to stop our shutdown
|
||||
if not silently:
|
||||
await ctx.send("Shutting down... " + wave + skin)
|
||||
await ctx.send(_("Shutting down... ") + wave + skin)
|
||||
except:
|
||||
pass
|
||||
await ctx.bot.shutdown()
|
||||
@@ -117,7 +120,7 @@ class Core:
|
||||
async def adminrole(self, ctx, *, role: discord.Role):
|
||||
"""Sets the admin role for this server"""
|
||||
await ctx.bot.db.guild(ctx.guild).admin_role.set(role.id)
|
||||
await ctx.send("The admin role for this server has been set.")
|
||||
await ctx.send(_("The admin role for this guild has been set."))
|
||||
|
||||
@_set.command()
|
||||
@checks.guildowner()
|
||||
@@ -125,7 +128,7 @@ class Core:
|
||||
async def modrole(self, ctx, *, role: discord.Role):
|
||||
"""Sets the mod role for this server"""
|
||||
await ctx.bot.db.guild(ctx.guild).mod_role.set(role.id)
|
||||
await ctx.send("The mod role for this server has been set.")
|
||||
await ctx.send(_("The mod role for this guild has been set."))
|
||||
|
||||
@_set.command()
|
||||
@checks.is_owner()
|
||||
@@ -139,13 +142,13 @@ class Core:
|
||||
try:
|
||||
await ctx.bot.user.edit(avatar=data)
|
||||
except discord.HTTPException:
|
||||
await ctx.send("Failed. Remember that you can edit my avatar "
|
||||
"up to two times a hour. The URL must be a "
|
||||
"direct link to a JPG / PNG.")
|
||||
await ctx.send(_("Failed. Remember that you can edit my avatar "
|
||||
"up to two times a hour. The URL must be a "
|
||||
"direct link to a JPG / PNG."))
|
||||
except discord.InvalidArgument:
|
||||
await ctx.send("JPG / PNG format only.")
|
||||
await ctx.send(_("JPG / PNG format only."))
|
||||
else:
|
||||
await ctx.send("Done.")
|
||||
await ctx.send(_("Done."))
|
||||
|
||||
@_set.command(name="game")
|
||||
@checks.is_owner()
|
||||
@@ -155,7 +158,7 @@ class Core:
|
||||
status = ctx.me.status
|
||||
game = discord.Game(name=game)
|
||||
await ctx.bot.change_presence(status=status, game=game)
|
||||
await ctx.send("Game set.")
|
||||
await ctx.send(_("Game set."))
|
||||
|
||||
@_set.command()
|
||||
@checks.is_owner()
|
||||
@@ -184,7 +187,7 @@ class Core:
|
||||
else:
|
||||
await ctx.bot.change_presence(status=status,
|
||||
game=game)
|
||||
await ctx.send("Status changed to %s." % status)
|
||||
await ctx.send(_("Status changed to %s.") % status)
|
||||
|
||||
@_set.command()
|
||||
@checks.is_owner()
|
||||
@@ -206,7 +209,7 @@ class Core:
|
||||
return
|
||||
else:
|
||||
await ctx.bot.change_presence(game=None, status=status)
|
||||
await ctx.send("Done.")
|
||||
await ctx.send(_("Done."))
|
||||
|
||||
@_set.command(name="username", aliases=["name"])
|
||||
@checks.is_owner()
|
||||
@@ -215,12 +218,12 @@ class Core:
|
||||
try:
|
||||
await ctx.bot.user.edit(username=username)
|
||||
except discord.HTTPException:
|
||||
await ctx.send("Failed to change name. Remember that you can "
|
||||
"only do it up to 2 times an hour. Use "
|
||||
"nicknames if you need frequent changes. "
|
||||
"`{}set nickname`".format(ctx.prefix))
|
||||
await ctx.send(_("Failed to change name. Remember that you can "
|
||||
"only do it up to 2 times an hour. Use "
|
||||
"nicknames if you need frequent changes. "
|
||||
"`{}set nickname`").format(ctx.prefix))
|
||||
else:
|
||||
await ctx.send("Done.")
|
||||
await ctx.send(_("Done."))
|
||||
|
||||
@_set.command(name="nickname")
|
||||
@checks.admin()
|
||||
@@ -230,8 +233,8 @@ class Core:
|
||||
try:
|
||||
await ctx.bot.user.edit(nick=nickname)
|
||||
except discord.Forbidden:
|
||||
await ctx.send("I lack the permissions to change my own "
|
||||
"nickname.")
|
||||
await ctx.send(_("I lack the permissions to change my own "
|
||||
"nickname."))
|
||||
else:
|
||||
await ctx.send("Done.")
|
||||
|
||||
@@ -244,7 +247,7 @@ class Core:
|
||||
return
|
||||
prefixes = sorted(prefixes, reverse=True)
|
||||
await ctx.bot.db.prefix.set(prefixes)
|
||||
await ctx.send("Prefix set.")
|
||||
await ctx.send(_("Prefix set."))
|
||||
|
||||
@_set.command(aliases=["serverprefixes"])
|
||||
@checks.admin()
|
||||
@@ -253,11 +256,11 @@ class Core:
|
||||
"""Sets Red's server prefix(es)"""
|
||||
if not prefixes:
|
||||
await ctx.bot.db.guild(ctx.guild).prefix.set([])
|
||||
await ctx.send("Server prefixes have been reset.")
|
||||
await ctx.send(_("Guild prefixes have been reset."))
|
||||
return
|
||||
prefixes = sorted(prefixes, reverse=True)
|
||||
await ctx.bot.db.guild(ctx.guild).prefix.set(prefixes)
|
||||
await ctx.send("Prefix set.")
|
||||
await ctx.send(_("Prefix set."))
|
||||
|
||||
@_set.command()
|
||||
@commands.cooldown(1, 60 * 10, commands.BucketType.default)
|
||||
@@ -276,29 +279,38 @@ class Core:
|
||||
token += random.choice(chars)
|
||||
log.info("{0} ({0.id}) requested to be set as owner."
|
||||
"".format(ctx.author))
|
||||
print("\nVerification token:")
|
||||
print(_("\nVerification token:"))
|
||||
print(token)
|
||||
|
||||
await ctx.send("Remember:\n" + OWNER_DISCLAIMER)
|
||||
await ctx.send(_("Remember:\n") + OWNER_DISCLAIMER)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
await ctx.send("I have printed a one-time token in the console. "
|
||||
"Copy and paste it here to confirm you are the owner.")
|
||||
await ctx.send(_("I have printed a one-time token in the console. "
|
||||
"Copy and paste it here to confirm you are the owner."))
|
||||
|
||||
try:
|
||||
message = await ctx.bot.wait_for("message", check=check,
|
||||
timeout=60)
|
||||
except asyncio.TimeoutError:
|
||||
self.owner.reset_cooldown(ctx)
|
||||
await ctx.send("The set owner request has timed out.")
|
||||
await ctx.send(_("The set owner request has timed out."))
|
||||
else:
|
||||
if message.content.strip() == token:
|
||||
self.owner.reset_cooldown(ctx)
|
||||
await ctx.bot.db.owner.set(ctx.author.id)
|
||||
ctx.bot.owner_id = ctx.author.id
|
||||
await ctx.send("You have been set as owner.")
|
||||
await ctx.send(_("You have been set as owner."))
|
||||
else:
|
||||
await ctx.send("Invalid token.")
|
||||
await ctx.send(_("Invalid token."))
|
||||
|
||||
@_set.command()
|
||||
@checks.is_owner()
|
||||
async def locale(self, ctx: commands.Context, locale_name: str):
|
||||
"""
|
||||
Changes bot locale.
|
||||
"""
|
||||
i18n.set_locale(locale_name)
|
||||
await ctx.send(_("Locale has been set."))
|
||||
|
||||
@commands.command()
|
||||
@commands.cooldown(1, 60, commands.BucketType.user)
|
||||
@@ -308,13 +320,13 @@ class Core:
|
||||
owner = discord.utils.get(ctx.bot.get_all_members(),
|
||||
id=ctx.bot.owner_id)
|
||||
author = ctx.message.author
|
||||
footer = "User ID: %s" % author.id
|
||||
footer = _("User ID: %s") % author.id
|
||||
|
||||
if ctx.guild is None:
|
||||
source = "through DM"
|
||||
source = _("through DM")
|
||||
else:
|
||||
source = "from {}".format(guild)
|
||||
footer += " | Server ID: %s" % guild.id
|
||||
source = _("from {}").format(guild)
|
||||
footer += _(" | Server ID: %s") % guild.id
|
||||
|
||||
# We need to grab the DM command prefix (global)
|
||||
# Since it can also be set through cli flags, bot.db is not a reliable
|
||||
@@ -322,15 +334,15 @@ class Core:
|
||||
fake_message = namedtuple('Message', 'guild')
|
||||
prefix = ctx.bot.command_prefix(ctx.bot, fake_message(guild=None))[0]
|
||||
|
||||
content = ("Use `{}dm {} <text>` to reply to this user"
|
||||
"".format(prefix, author.id))
|
||||
content = _("Use `{}dm {} <text>` to reply to this user"
|
||||
"").format(prefix, author.id)
|
||||
|
||||
if isinstance(author, discord.Member):
|
||||
colour = author.colour
|
||||
else:
|
||||
colour = discord.Colour.red()
|
||||
|
||||
description = "Sent by {} {}".format(author, source)
|
||||
description = _("Sent by {} {}").format(author, source)
|
||||
|
||||
e = discord.Embed(colour=colour, description=message)
|
||||
if author.avatar_url:
|
||||
@@ -342,12 +354,12 @@ class Core:
|
||||
try:
|
||||
await owner.send(content, embed=e)
|
||||
except discord.InvalidArgument:
|
||||
await ctx.send("I cannot send your message, I'm unable to find "
|
||||
"my owner... *sigh*")
|
||||
await ctx.send(_("I cannot send your message, I'm unable to find "
|
||||
"my owner... *sigh*"))
|
||||
except:
|
||||
await ctx.send("I'm unable to deliver your message. Sorry.")
|
||||
await ctx.send(_("I'm unable to deliver your message. Sorry."))
|
||||
else:
|
||||
await ctx.send("Your message has been sent.")
|
||||
await ctx.send(_("Your message has been sent."))
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
@@ -361,17 +373,17 @@ class Core:
|
||||
destination = discord.utils.get(ctx.bot.get_all_members(),
|
||||
id=user_id)
|
||||
if destination is None:
|
||||
await ctx.send("Invalid ID or user not found. You can only "
|
||||
"send messages to people I share a server "
|
||||
"with.")
|
||||
await ctx.send(_("Invalid ID or user not found. You can only "
|
||||
"send messages to people I share a server "
|
||||
"with."))
|
||||
return
|
||||
|
||||
e = discord.Embed(colour=discord.Colour.red(), description=message)
|
||||
description = "Owner of %s" % ctx.bot.user
|
||||
description = _("Owner of %s") % ctx.bot.user
|
||||
fake_message = namedtuple('Message', 'guild')
|
||||
prefix = ctx.bot.command_prefix(ctx.bot, fake_message(guild=None))[0]
|
||||
e.set_footer(text=("You can reply to this message with %scontact"
|
||||
"" % prefix))
|
||||
e.set_footer(text=_("You can reply to this message with %scontact"
|
||||
"") % prefix)
|
||||
if ctx.bot.user.avatar_url:
|
||||
e.set_author(name=description, icon_url=ctx.bot.user.avatar_url)
|
||||
else:
|
||||
@@ -380,7 +392,7 @@ class Core:
|
||||
try:
|
||||
await destination.send(embed=e)
|
||||
except:
|
||||
await ctx.send("Sorry, I couldn't deliver your message "
|
||||
"to %s" % destination)
|
||||
await ctx.send(_("Sorry, I couldn't deliver your message "
|
||||
"to %s") % destination)
|
||||
else:
|
||||
await ctx.send("Message delivered to %s" % destination)
|
||||
await ctx.send(_("Message delivered to %s") % destination)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from discord.ext import commands
|
||||
from core.utils.chat_formatting import box, pagify
|
||||
from core import checks
|
||||
from core.i18n import CogI18n
|
||||
import asyncio
|
||||
import discord
|
||||
import traceback
|
||||
@@ -18,6 +19,8 @@ Notice:
|
||||
https://github.com/Rapptz/RoboDanny/blob/master/cogs/repl.py
|
||||
"""
|
||||
|
||||
_ = CogI18n("Dev", __file__)
|
||||
|
||||
|
||||
class Dev:
|
||||
"""Various development focused utilities"""
|
||||
@@ -159,11 +162,11 @@ class Dev:
|
||||
}
|
||||
|
||||
if ctx.channel.id in self.sessions:
|
||||
await ctx.send('Already running a REPL session in this channel. Exit it with `quit`.')
|
||||
await ctx.send(_('Already running a REPL session in this channel. Exit it with `quit`.'))
|
||||
return
|
||||
|
||||
self.sessions.add(ctx.channel.id)
|
||||
await ctx.send('Enter code to execute or evaluate. `exit()` or `quit` to exit.')
|
||||
await ctx.send(_('Enter code to execute or evaluate. `exit()` or `quit` to exit.'))
|
||||
|
||||
def msg_check(m):
|
||||
return m.author == ctx.author and m.channel == ctx.channel and \
|
||||
@@ -228,7 +231,7 @@ class Dev:
|
||||
except discord.Forbidden:
|
||||
pass
|
||||
except discord.HTTPException as e:
|
||||
await ctx.send('Unexpected error: `{}`'.format(e))
|
||||
await ctx.send(_('Unexpected error: `{}`').format(e))
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
|
||||
203
core/i18n.py
Normal file
203
core/i18n.py
Normal file
@@ -0,0 +1,203 @@
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
__all__ = ['get_locale', 'set_locale', 'reload_locales', 'CogI18n']
|
||||
|
||||
_current_locale = 'en_us'
|
||||
|
||||
WAITING_FOR_MSGID = 1
|
||||
IN_MSGID = 2
|
||||
WAITING_FOR_MSGSTR = 3
|
||||
IN_MSGSTR = 4
|
||||
|
||||
MSGID = 'msgid "'
|
||||
MSGSTR = 'msgstr "'
|
||||
|
||||
_i18n_cogs = {}
|
||||
|
||||
|
||||
def get_locale():
|
||||
return _current_locale
|
||||
|
||||
|
||||
def set_locale(locale):
|
||||
global _current_locale
|
||||
_current_locale = locale
|
||||
reload_locales()
|
||||
|
||||
|
||||
def reload_locales():
|
||||
for cog_name, i18n in _i18n_cogs.items():
|
||||
i18n.load_translations()
|
||||
|
||||
|
||||
def _parse(translation_file):
|
||||
"""
|
||||
Custom gettext parsing of translation files. All credit for this code goes
|
||||
to ProgVal/Valentin Lorentz and the Limnoria project.
|
||||
|
||||
https://github.com/ProgVal/Limnoria/blob/master/src/i18n.py
|
||||
|
||||
:param translation_file:
|
||||
An open file-like object containing translations.
|
||||
:return:
|
||||
A set of 2-tuples containing the original string and the translated version.
|
||||
"""
|
||||
step = WAITING_FOR_MSGID
|
||||
translations = set()
|
||||
for line in translation_file:
|
||||
line = line[0:-1] # Remove the ending \n
|
||||
line = line
|
||||
|
||||
if line.startswith(MSGID):
|
||||
# Don't check if step is WAITING_FOR_MSGID
|
||||
untranslated = ''
|
||||
translated = ''
|
||||
data = line[len(MSGID):-1]
|
||||
if len(data) == 0: # Multiline mode
|
||||
step = IN_MSGID
|
||||
else:
|
||||
untranslated += data
|
||||
step = WAITING_FOR_MSGSTR
|
||||
|
||||
elif step is IN_MSGID and line.startswith('"') and \
|
||||
line.endswith('"'):
|
||||
untranslated += line[1:-1]
|
||||
elif step is IN_MSGID and untranslated == '': # Empty MSGID
|
||||
step = WAITING_FOR_MSGID
|
||||
elif step is IN_MSGID: # the MSGID is finished
|
||||
step = WAITING_FOR_MSGSTR
|
||||
|
||||
if step is WAITING_FOR_MSGSTR and line.startswith(MSGSTR):
|
||||
data = line[len(MSGSTR):-1]
|
||||
if len(data) == 0: # Multiline mode
|
||||
step = IN_MSGSTR
|
||||
else:
|
||||
translations |= {(untranslated, data)}
|
||||
step = WAITING_FOR_MSGID
|
||||
|
||||
elif step is IN_MSGSTR and line.startswith('"') and \
|
||||
line.endswith('"'):
|
||||
translated += line[1:-1]
|
||||
elif step is IN_MSGSTR: # the MSGSTR is finished
|
||||
step = WAITING_FOR_MSGID
|
||||
if translated == '':
|
||||
translated = untranslated
|
||||
translations |= {(untranslated, translated)}
|
||||
if step is IN_MSGSTR:
|
||||
if translated == '':
|
||||
translated = untranslated
|
||||
translations |= {(untranslated, translated)}
|
||||
return translations
|
||||
|
||||
|
||||
def _normalize(string, remove_newline=False):
|
||||
"""
|
||||
String normalization.
|
||||
|
||||
All credit for this code goes
|
||||
to ProgVal/Valentin Lorentz and the Limnoria project.
|
||||
|
||||
https://github.com/ProgVal/Limnoria/blob/master/src/i18n.py
|
||||
|
||||
:param string:
|
||||
:param remove_newline:
|
||||
:return:
|
||||
"""
|
||||
def normalize_whitespace(s):
|
||||
"""Normalizes the whitespace in a string; \s+ becomes one space."""
|
||||
if not s:
|
||||
return str(s) # not the same reference
|
||||
starts_with_space = (s[0] in ' \n\t\r')
|
||||
ends_with_space = (s[-1] in ' \n\t\r')
|
||||
if remove_newline:
|
||||
newline_re = re.compile('[\r\n]+')
|
||||
s = ' '.join(filter(bool, newline_re.split(s)))
|
||||
s = ' '.join(filter(bool, s.split('\t')))
|
||||
s = ' '.join(filter(bool, s.split(' ')))
|
||||
if starts_with_space:
|
||||
s = ' ' + s
|
||||
if ends_with_space:
|
||||
s += ' '
|
||||
return s
|
||||
|
||||
string = string.replace('\\n\\n', '\n\n')
|
||||
string = string.replace('\\n', ' ')
|
||||
string = string.replace('\\"', '"')
|
||||
string = string.replace("\'", "'")
|
||||
string = normalize_whitespace(string)
|
||||
string = string.strip('\n')
|
||||
string = string.strip('\t')
|
||||
return string
|
||||
|
||||
|
||||
def get_locale_path(cog_folder: Path, extension: str) -> Path:
|
||||
"""
|
||||
Gets the folder path containing localization files.
|
||||
|
||||
:param Path cog_folder:
|
||||
The cog folder that we want localizations for.
|
||||
:param str extension:
|
||||
Extension of localization files.
|
||||
:return:
|
||||
Path of possible localization file, it may not exist.
|
||||
"""
|
||||
return cog_folder / 'locales' / "{}.{}".format(get_locale(), extension)
|
||||
|
||||
|
||||
class CogI18n:
|
||||
def __init__(self, name, file_location):
|
||||
"""
|
||||
Initializes the internationalization object for a given cog.
|
||||
|
||||
:param name: Your cog name.
|
||||
:param file_location:
|
||||
This should always be ``__file__`` otherwise your localizations
|
||||
will not load.
|
||||
"""
|
||||
self.cog_folder = Path(file_location).resolve().parent
|
||||
self.cog_name = name
|
||||
self.translations = {}
|
||||
|
||||
_i18n_cogs.update({self.cog_name: self})
|
||||
|
||||
self.load_translations()
|
||||
|
||||
def __call__(self, untranslated: str):
|
||||
normalized_untranslated = _normalize(untranslated, True)
|
||||
try:
|
||||
return self.translations[normalized_untranslated]
|
||||
except KeyError:
|
||||
return untranslated
|
||||
|
||||
def load_translations(self):
|
||||
"""
|
||||
Loads the current translations for this cog.
|
||||
"""
|
||||
self.translations = {}
|
||||
translation_file = None
|
||||
locale_path = get_locale_path(self.cog_folder, 'po')
|
||||
try:
|
||||
|
||||
try:
|
||||
translation_file = locale_path.open('ru')
|
||||
except ValueError: # We are using Windows
|
||||
translation_file = locale_path.open('r')
|
||||
self._parse(translation_file)
|
||||
except (IOError, FileNotFoundError): # The translation is unavailable
|
||||
pass
|
||||
finally:
|
||||
if translation_file is not None:
|
||||
translation_file.close()
|
||||
|
||||
def _parse(self, translation_file):
|
||||
self.translations = {}
|
||||
for translation in _parse(translation_file):
|
||||
self._add_translation(*translation)
|
||||
|
||||
def _add_translation(self, untranslated, translated):
|
||||
untranslated = _normalize(untranslated, True)
|
||||
translated = _normalize(translated)
|
||||
if translated:
|
||||
self.translations.update({untranslated: translated})
|
||||
|
||||
223
core/locales/messages.pot
Normal file
223
core/locales/messages.pot
Normal file
@@ -0,0 +1,223 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR ORGANIZATION
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"POT-Creation-Date: 2017-08-26 18:11+EDT\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: ENCODING\n"
|
||||
"Generated-By: pygettext.py 1.5\n"
|
||||
|
||||
|
||||
#: ../cog_manager.py:196
|
||||
msgid ""
|
||||
"Install Path: {}\n"
|
||||
"\n"
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:212
|
||||
msgid "That path is does not exist or does not point to a valid directory."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:221
|
||||
msgid "Path successfully added."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:234
|
||||
msgid "That is an invalid path number."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:238
|
||||
msgid "Path successfully removed."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:254
|
||||
msgid "Invalid 'from' index."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:260
|
||||
msgid "Invalid 'to' index."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:264
|
||||
msgid "Paths reordered."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:282
|
||||
msgid "That path does not exist."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:286
|
||||
msgid "The bot will install new cogs to the `{}` directory."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:35
|
||||
msgid "No module by that name was found in any cog path."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:43
|
||||
msgid "Failed to load package. Check your console or logs for details."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:47 ../core_commands.py:56 ../core_commands.py:76
|
||||
#: ../core_commands.py:151 ../core_commands.py:212 ../core_commands.py:226
|
||||
msgid "Done."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:58
|
||||
msgid "That extension is not loaded."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:71
|
||||
msgid "Failed to reload package. Check your console or logs for details."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:86
|
||||
msgid "Shutting down... "
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:123
|
||||
msgid "The admin role for this guild has been set."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:131
|
||||
msgid "The mod role for this guild has been set."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:145
|
||||
msgid "Failed. Remember that you can edit my avatar up to two times a hour. The URL must be a direct link to a JPG / PNG."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:149
|
||||
msgid "JPG / PNG format only."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:161
|
||||
msgid "Game set."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:190
|
||||
msgid "Status changed to %s."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:221
|
||||
msgid "Failed to change name. Remember that you can only do it up to 2 times an hour. Use nicknames if you need frequent changes. `{}set nickname`"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:236
|
||||
msgid "I lack the permissions to change my own nickname."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:250 ../core_commands.py:263
|
||||
msgid "Prefix set."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:259
|
||||
msgid "Guild prefixes have been reset."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:282
|
||||
msgid ""
|
||||
"\n"
|
||||
"Verification token:"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:285
|
||||
msgid ""
|
||||
"Remember:\n"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:288
|
||||
msgid "I have printed a one-time token in the console. Copy and paste it here to confirm you are the owner."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:296
|
||||
msgid "The set owner request has timed out."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:302
|
||||
msgid "You have been set as owner."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:304
|
||||
msgid "Invalid token."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:313
|
||||
msgid "Locale has been set."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:323
|
||||
msgid "User ID: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:326
|
||||
msgid "through DM"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:328
|
||||
msgid "from {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:329
|
||||
msgid " | Server ID: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:337
|
||||
msgid "Use `{}dm {} <text>` to reply to this user"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:345
|
||||
msgid "Sent by {} {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:357
|
||||
msgid "I cannot send your message, I'm unable to find my owner... *sigh*"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:360
|
||||
msgid "I'm unable to deliver your message. Sorry."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:362
|
||||
msgid "Your message has been sent."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:376
|
||||
msgid "Invalid ID or user not found. You can only send messages to people I share a server with."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:382
|
||||
msgid "Owner of %s"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:385
|
||||
msgid "You can reply to this message with %scontact"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:395
|
||||
msgid "Sorry, I couldn't deliver your message to %s"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:398
|
||||
msgid "Message delivered to %s"
|
||||
msgstr ""
|
||||
|
||||
#: ../dev_commands.py:165
|
||||
msgid "Already running a REPL session in this channel. Exit it with `quit`."
|
||||
msgstr ""
|
||||
|
||||
#: ../dev_commands.py:169
|
||||
msgid "Enter code to execute or evaluate. `exit()` or `quit` to exit."
|
||||
msgstr ""
|
||||
|
||||
#: ../dev_commands.py:234
|
||||
msgid "Unexpected error: `{}`"
|
||||
msgstr ""
|
||||
|
||||
17
core/locales/regen_messages.py
Normal file
17
core/locales/regen_messages.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = [
|
||||
'../cog_manager.py',
|
||||
'../core_commands.py',
|
||||
'../dev_commands.py'
|
||||
]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(
|
||||
['pygettext', '-n'] + TO_TRANSLATE
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
Reference in New Issue
Block a user