*discontinued* Advanced TranslationStrings

Custom Packages that plugins can require for common usages.
User avatar
iPlayer
Developer
Posts: 590
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

*discontinued* Advanced TranslationStrings

Postby iPlayer » Fri Jan 15, 2016 6:06 pm

DISCONTINUED
Features implemented by this package are now possible without it due to implementing .tokenized method directly into Source.Python: https://github.com/Source-Python-Dev-Team/Source.Python/commit/520cc3b8d282651cba9304020eb828776bb4b0ac

Note that this package used .tokenize method.

Source.Python Advanced TranslationStrings (custom package)
Download: https://github.com/KirillMysnik/sp-advanced-ts-package/releases/latest

The package includes 2 classes:
  • BaseLangStrings - basically LangStrings class but after initialization replaces all TranslationStrings instances with the given base:

    Syntax: Select all

    class BaseLangStrings(LangStrings):
    """
    This is a LangStrings class but after initialization replaces
    all TranslationStrings values with instances of the given
    dict-inherited base class.
    """
    def __init__(self, infile, encoding='utf_8', base=RecursiveTS):
    super().__init__(infile, encoding)

    for key, value in self.items():
    if isinstance(value, TranslationStrings):
    new_translation_strings = base()

    for key_, value_ in value.items():
    new_translation_strings[key_] = value_

    self[key] = new_translation_strings


    If base kwarg is implemented in SP's LangStrings, there's no need to keep this class in the package.


  • RecursiveTS - provides new .tokenize method and makes .get_string method recursive

    Syntax: Select all

    class RecursiveTS(TranslationStrings):
    """
    This class provides recursive get_string method.
    """
    def get_string(self, language=None, **tokens):
    """
    Exposes packed tokens (self.tokens) and additional tokens (**tokens).
    """

    # Deeply expose all TranslationStrings instances in self.tokens
    for token_name, token in self.tokens.items():
    if isinstance(token, TranslationStrings):
    new_tokens = self.tokens.copy()
    del new_tokens[token_name]

    # Pass additional tokens - these will NOT be used to
    # continue deep exposition but will be used to call
    # super().get_string.
    token = token.get_string(language, **tokens)
    self.tokens[token_name] = token

    # Then shallowly expose all TranslationsStrings instances in **tokens
    for token_name, token in tokens.items():
    if isinstance(token, TranslationStrings):

    # Don't pass any additional tokens.
    # The token should either be trivial (regular
    # TranslationStrings instance) or rely on itself (self.tokens).
    tokens[token_name] = token.get_string(language)

    # Finally with all of the tokens exposed, call the original get_string
    return super().get_string(language, **tokens)

    def tokenize(self, **tokens):
    """Return new TranslationStrings object with updated tokens."""
    rs = type(self)()
    for key, value in self.items():
    rs[key] = value

    rs.tokens = self.tokens.copy()
    rs.tokens.update(tokens)
    return rs

Now, why we would need all this. Here's an example plugin that utilizes the package:
cstrike/resource/source-python/translations/advanced_ts_test.ini

Code: Select all

[popup title]
en="My Popup"

[popup base]
en="{text}"

[popup base disabled]
en="(DISABLED) {text}"

[popup slay_team]
en="Slay {team} players"

[team_alpha]
en="Alpha"

[team_bravo]
en="Bravo"

[team_charlie]
en="Charlie"

[chat base]
en="{colorful_sign}{text}"

[chat base with_tag]
en="{colorful_sign}{tag} {text}"

[tag]
en="{color_special}[{color_default}My Plugin{color_special}]{color_default}"

[your_team_is]
en="Your team is team {team}"


cstrike/addons/source-python/plugins/advanced_ts_test/advanced_ts_test.py
Import/constants

Syntax: Select all

from colors import Color
from events import Event
from menus import PagedMenu
from menus import PagedOption
from messages import SayText2
from players.entity import Player
from players.helpers import index_from_userid

from advanced_ts import BaseLangStrings, RecursiveTS


COLORFUL_SIGN = '\x01'
COLOR_DEFAULT = Color(255, 255, 255)
COLOR_SPECIAL = Color(255, 0, 0)


strings = BaseLangStrings('advanced_ts_test', base=RecursiveTS)
color_tokens = {
'color_default': COLOR_DEFAULT,
'color_special': COLOR_SPECIAL,
}


Now let's create a simple popup but we will take advantage of .tokenize. The thing is that we want to show 'your_team_is' translation but one of its tokens, 'team', is a translation itself. What is more, we should add an ability to add a translated "(DISABLED)" phrase to options we want to disable. Here it is (for tokens explanation look at the advanced_ts.ini above):

Syntax: Select all

popup = PagedMenu(title=strings['popup title'])

# Regular option
popup.append(PagedOption(
text=strings['popup base'].tokenize(
text=strings['popup slay_team'].tokenize(
team=strings['team_alpha']
)
)
))

# "(DISABLED)" option
popup.append(PagedOption(
text=strings['popup base disabled'].tokenize(
text=strings['popup slay_team'].tokenize(
team=strings['team_bravo'],
)
),
highlight=False,
selectable=False
))


Then let's think about more complex structures. For example, chat message. It may include a tag - in this case tag will need colors, as you can see in strings file. So we will need to tokenize it with color_tokens (see import/constants section). Then there's a message itself. Not only should it accept various tokens which in turn may turn out to be even more TranslationStrings instanes, but it also should receive color_tokens.
Let's see create tell() function which takes care of all of that. I will actually firstly show you Python 3.5 version which won't work with current SP builds, but it's easier to read.

Syntax: Select all

def tell(*players, message, with_tag=True, **tokens):
"""Send a SayText2 message to players"""
player_indexes = [player.index for player in players]
base = strings['chat base with_tag' if with_tag else 'chat base']

tokenized_message = base.tokenize(
tag=strings['tag'].tokenize(
**color_tokens # Tag only needs color tokens
),
text=message.tokenize(
**color_tokens, # Message needs color tokens
**tokens # But it also needs tokens passed to tell()
),
colorful_sign=COLORFUL_SIGN, # The base needs this symbol
)

# Note how we don't pass any tokens in .send()
SayText2(message=tokenized_message).send(*player_indexes)


Finally, let's use all of that

Syntax: Select all

@Event('player_spawn')
def on_player_spawn(game_event):
player = Player(index_from_userid(game_event.get_int('userid')))

# Let tell() pack all additional tokens ('team' in our case) by itself
tell(player, message=strings['your_team_is'], team=strings['team_alpha'])

# Or pack such tokens by ourselves - the previous approach is shorter
tell(player, message=strings['your_team_is'].tokenize(team=strings['team_bravo']))

# Also send a popup
popup.send(player.index)


Result:
Image

The package can be found here
Sample plugin is all given above, the one thing is that here's working Python 3.4 version of tell():

Syntax: Select all

def tell(*players, message, with_tag=True, **tokens):
"""Send a SayText2 message to players"""
player_indexes = [player.index for player in players]
base = strings['chat base with_tag' if with_tag else 'chat base']

tokens.update(color_tokens)

tokenized_message = base.tokenize(
tag=strings['tag'].tokenize(
**color_tokens # Tag only needs color tokens
),
text=message.tokenize(
**tokens
),
colorful_sign=COLORFUL_SIGN, # The base needs this
)

SayText2(message=tokenized_message).send(*player_indexes)


I hope some ideas of this package can make their way into Source.Python. Thanks.
Last edited by iPlayer on Mon May 08, 2017 1:18 am, edited 2 times in total.
Image /id/its_iPlayer
My plugins: Map Cycle • Killstreaker • DeadChat • Infinite Jumping • TripMines • AdPurge • Bot Damage • PLRBots • Entity AntiSpam

Hail, Companion. [...] Hands to yourself, sneak thief. Image
User avatar
Ayuto
Project Leader
Posts: 2197
Joined: Sat Jul 07, 2012 8:17 am
Location: Germany

Postby Ayuto » Sat Jan 16, 2016 12:23 pm

User avatar
iPlayer
Developer
Posts: 590
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Postby iPlayer » Sat Jan 16, 2016 12:59 pm

No-no-no, you can see that I changed .get_string recursion. The code in that thread actually did randomly break (from my experience with SP Map Cycle). So I decided to give it all more testing and release it as a package and see what you guys think. Because I don't want to do pull requests that you guys may not want to merge.
Take it as "more consideration".

Example of how code in that thread broke:

Code: Select all

[token]
en="Here goes subtoken1: {subtoken1}"

[subtoken1]
en="and here's subtoken2: {subtoken2}"

[subtoken2]
en="finally, this is it!"


Syntax: Select all

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


Now what happens with the old code:

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



Code: Select all

0. **getting into strings['token'].get_string**
1.     tokens = {'subtoken1': strings['subtoken1'], 'subtoken2': strings['subtoken2']}
2.     token_name, token = 'subtoken2', strings['subtoken2']    // This is the key moment! If Python decided to pick subtoken1 first, everything would work!
3.     **getting into strings['subtoken2'].get_string**
4.         tokens = {'subtoken1': strings['subtoken1']}    // We've removed subtoken2 from tokens before we were getting into its .get_string
5.         token_name, token = 'subtoken1', strings['subtoken1']
6.         **getting into strings['subtoken1'].get_string**
7.             tokens = {}
8.             KeyError as strings['subtoken1'] requires 'subtoken2' but our tokens dict is empty


But if Python decided to iter over 'subtoken1' first, everything will work:

Code: Select all

0.  **getting into strings['token'].get_string**
1.      tokens = {'subtoken1': strings['subtoken1'], 'subtoken2': strings['subtoken2']}
2.      token_name, token = 'subtoken1', strings['subtoken1']
3.      **getting into strings['subtoken1'].get_string**
4.          tokens = {'subtoken2': strings['subtoken2']}    // We've removed subtoken1 from tokens before we were getting into its .get_string
5.          token_name, token = 'subtoken2', strings['subtoken2']
6.          **getting into strings['subtoken2'].get_string**
7.              tokens = {}
8.          **successfully formatting subtoken2 w/o tokens with empty tokens dict and getting out of strings['subtoken2'].get_string**
9.      **successfully formatting subtoken1 with 'subtoken2' and getting out of strings['subtoken1'].get_string**
// Now here are unwanted loops!!!
10.     token_name, token = 'subtoken2', strings['subtoken2']
11.     **getting into strings['subtoken1'].get_string**
12.         tokens = {'subtoken2': strings['subtoken2']}
13.         token_name, token = 'subtoken2', strings['subtoken2']
14.         **getting into strings['subtoken2'].get_string**
15.             tokens = {}
16.         **successfully formatting subtoken2 w/o tokens with empty tokens dict and getting out of strings['subtoken2'].get_string**
17.     **successfully formatting subtoken1 with 'subtoken2' and getting out of strings['subtoken1'].get_string**
18. **successfully formatting token with all the subtokens and getting out of strings['token'].get_string**


This kind of bugs is the worst. You can't predict the order Python will iter over dictionary items. So sometimes the bug happens, but if you reload the plugin, it won't happen.

But, even working code included extra loops. Why? Simply because

Syntax: Select all

.send(index, subtoken1=strings['subtoken1'], subtoken2=strings['subtoken2'])

provides absolutely no idea in what order we should expose the tokens.

The way with .tokenize'ing particular TranslationStrings works better. I have yet to receive any feedback on that though :(
Image /id/its_iPlayer
My plugins: Map Cycle • Killstreaker • DeadChat • Infinite Jumping • TripMines • AdPurge • Bot Damage • PLRBots • Entity AntiSpam

Hail, Companion. [...] Hands to yourself, sneak thief. Image
User avatar
iPlayer
Developer
Posts: 590
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Postby iPlayer » Sat Jan 16, 2016 1:34 pm

So. Ideas to do in a pull request:

1. Remove .get_strings from LangStrings as it's not used and messes up original TranslationStrings that are stored inside of LangStrings
2. Add a 'base' kwarg to LangString which defines the class to use instead of TranslationStrings. Defaulting to TranslationStrings, of course.
3. Add .tokenize method to TranslationStrings
4. Make .get_string method of TranslationStrings recursive - I haven't found the best way to it yet. Any suggestions? The best I could do is in this package.
Image /id/its_iPlayer
My plugins: Map Cycle • Killstreaker • DeadChat • Infinite Jumping • TripMines • AdPurge • Bot Damage • PLRBots • Entity AntiSpam

Hail, Companion. [...] Hands to yourself, sneak thief. Image
User avatar
iPlayer
Developer
Posts: 590
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Re: *discontinued* Advanced TranslationStrings

Postby iPlayer » Mon May 08, 2017 1:20 am

This package is now discontinued (details in the first post)
Image /id/its_iPlayer
My plugins: Map Cycle • Killstreaker • DeadChat • Infinite Jumping • TripMines • AdPurge • Bot Damage • PLRBots • Entity AntiSpam

Hail, Companion. [...] Hands to yourself, sneak thief. Image

Return to “Custom Packages”

Who is online

Users browsing this forum: No registered users and 1 guest