translation.strings questions

Discuss API design here.
User avatar
iPlayer
Developer
Posts: 533
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

translation.strings questions

Postby iPlayer » Fri Jan 08, 2016 11:47 pm

Hey there

I've been working with translations and menus packages and I want to share my thoughts.

1. ../translations/strings.py/LangStrings.get_strings

Here's this method

Syntax: Select all

def get_strings(self, key, **tokens):
"""Return a TranslationStrings object with updated tokens."""
strings = self[key]
strings.tokens.update(tokens)
return strings


I don't quite understand its purpose. It updates TranslationStrings's tokens, but does it in place.

Say we have the following plugin

popuptest/my_strings.ini

Code: Select all

[title]
en="Popup Title :) "

[option]
en="Option ${num}!"


popuptest.py

Syntax: Select all

from filters.players import PlayerIter
from menus import PagedMenu, PagedOption
from translations.strings import LangStrings


my_strings = LangStrings("popuptest/my_strings")


def popup_callback(popup, player_index, option):
pass


popup = PagedMenu(select_callback=popup_callback,
title=my_strings['title'])

for i in range(15):
option = PagedOption(text=my_strings.get_strings('option', num=i))
popup.append(option)


def load():
popup.send(*[player.index for player in PlayerIter('human')])


Here I'm playing with popup (note that there's issue #107 is up, but it's doesn't fit this particular case) and I'm using the same exact translation string for each option, but want to pass to them different tokens. If .get_string() created a new TranslationStrings instance and modified its tokens instead of updating them in the original instance, we would get something beautiful:
1. Option 0!
2. Option 1!
3. Option 2!
...


Instead we're getting
1. Option 14!
2. Option 14!
3. Option 14!
...

I don't understand why we touch original TranslationStrings instance (the one that is stored in LangStrings)

If we instead created a shallow copy of the original TranslationStrings instance this way:

Syntax: Select all

def get_strings(self, key, **tokens):
"""Return a TranslationStrings object with updated tokens."""
strings = TranslationStrings(self[key])
strings.tokens.update(tokens)
return strings


and made TranslationStrings act like a real dict (currently it does not accept no args no kwargs - I want it to accept everything and pass to an internal dict):

Syntax: Select all

class TranslationStrings(dict):
"""Stores and get language strings for a particular string."""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.tokens = {}


then we would get the wanted result, the beautiful one.

2. ../translations/strings.py/TranslationStrings.get_string
I want it to accept other TranslationStrings instances as tokens values. Example:

Code: Select all

[team alpha]
en="Alpha"
ru="?????"

[your_team_is]
en="Your current team is $team"

Syntax: Select all

SayText2(message=my_strings['your_team_is']).send(player_index, team=my_strings['team alpha'])


This can be achieved by adding a few lines to get_string and making it recursive:

Syntax: Select all

def get_string(self, language=None, **tokens):
for token_name, token in tokens.items():
if isinstance(token, TranslationStrings):
new_tokens = tokens.copy()
del new_tokens[token_name] # To avoid infinite recursion

token = token.get_string(language, **new_tokens)
tokens[token_name] = token

# rest of the code...



What do you guys think?

Edit: after a second thought... We could possibly do a shallow copy with .copy method, but I'm unsure if that method would return an instance of the same class or a regular Python dict.
User avatar
Ayuto
Project Leader
Posts: 1619
Joined: Sat Jul 07, 2012 8:17 am
Location: Germany

Postby Ayuto » Sat Jan 09, 2016 1:03 pm

Sounds like two nice additions! Would you mind creating a pull request?

Note: We have changed the token format. E.g. $team is now {team}. Currently, you can still use $team, but that will be removed soon.
User avatar
iPlayer
Developer
Posts: 533
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Postby iPlayer » Sat Jan 09, 2016 4:12 pm

I'm trying to figure out the implementation for recursive .get_string.

What if a child TranslationStrings contains the same tokens names as a parent?

Code: Select all

[token]
en="Subtoken value: {subtoken}"

[subtoken]
en="Subtoken's subtoken value: {subtoken}"

[subsubtoken]
en="SubSubToken"


Syntax: Select all

SayText2(message=strings['token']).send(index, subtoken=...)

Token collision. We can't define 2 different token groups (1 for each token that requires them) in one kwarg dict.

Say, if we did this

Syntax: Select all

SayText2(message=strings['token']).send(index, subtoken=strings['subtoken'])

We would end up with an unresolved {subtoken} in the [subtoken] (if we cut infinite loops like I did in the first post) or we would end up with an infinite recursion (if not).

Plus I don't like the fact that 1 token goes directly to SayText2 constructor and the rest are messed together in the .send() method with other tokens.

Now look at this

Syntax: Select all

SayText2(
message=my_strings.get_strings(
'token', subtoken=my_strings.get_strings(
'subtoken', subtoken=my_strings['subsubtoken']
)
)
).send(player_index)


Much clearer, isn't it? Of course we can beautify it more:

Syntax: Select all

subsubtoken = my_strings['subsubtoken']
tokenized_subtoken = my_strings.get_strings('subtoken', subtoken=subsubtoken)
tokenized_token = my_strings.get_strings('token', subtoken=tokenized_subtoken)

SayText2(message=tokenized_token).send(player.index)


or example from my previous post

Syntax: Select all

SayText2(message=my_strings.get_strings('your_team_is', team=my_strings['team alpha'])).send(player_index)


I define some kind of a "Tokenized TranslationStrings" which is in fact is a usual TranslationStrings instance but comes with filled tokens via LangStrings.get_strings()

This in turn solves the issue #107 that I mentioned - because we don't need to send tokens in popup at all, we can fill it with tokenized translation strings.

Should we distinguish tokenized TranslationStrings in a separate class or not? If so, I guess we can:
1. Get rid of self.tokens in common (untokenized) TranslationStrings
2. Get rid of **tokens in TokenizedTranslationStrings

What do you think of that?

Return to “API Design”

Who is online

Users browsing this forum: No registered users and 1 guest