"""Language related functionality.""" from __future__ import annotations import json import os from typing import TYPE_CHECKING import _ba if TYPE_CHECKING: from typing import Any, Dict, List, Optional class Lstr: """Used to specify strings in a language-independent way. category: General Utility Classes These should be used whenever possible in place of hard-coded strings so that in-game or UI elements show up correctly on all clients in their currently-active language. To see available resource keys, look at any of the bs_language_*.py files in the game or the translations pages at bombsquadgame.com/translate. # EXAMPLE 1: specify a string from a resource path mynode.text = ba.Lstr(resource='audioSettingsWindow.titleText') # EXAMPLE 2: specify a translated string via a category and english value; # if a translated value is available, it will be used; otherwise the # english value will be. To see available translation categories, look # under the 'translations' resource section. mynode.text = ba.Lstr(translate=('gameDescriptions', 'Defeat all enemies')) # EXAMPLE 3: specify a raw value and some substitutions. Substitutions can # be used with resource and translate modes as well. mynode.text = ba.Lstr(value='${A} / ${B}', subs=[('${A}', str(score)), ('${B}', str(total))]) # EXAMPLE 4: Lstrs can be nested. This example would display the resource # at res_a but replace ${NAME} with the value of the resource at res_b mytextnode.text = ba.Lstr(resource='res_a', subs=[('${NAME}', ba.Lstr(resource='res_b'))]) """ def __init__(self, *args: Any, **keywds: Any) -> None: """Instantiate a Lstr. Pass a value for either 'resource', 'translate', or 'value'. (see Lstr help for examples). 'subs' can be a sequence of 2-member sequences consisting of values and replacements. 'fallback_resource' can be a resource key that will be used if the main one is not present for the current language in place of falling back to the english value ('resource' mode only). 'fallback_value' can be a literal string that will be used if neither the resource nor the fallback resource is found ('resource' mode only). """ # pylint: disable=too-many-branches if args: raise Exception('Lstr accepts only keyword arguments') # Basically just store the exact args they passed. # However if they passed any Lstr values for subs, # replace them with that Lstr's dict. self.args = keywds our_type = type(self) if isinstance(self.args.get('value'), our_type): raise Exception("'value' must be a regular string; not an Lstr") if 'subs' in self.args: subs_new = [] for key, value in keywds['subs']: if isinstance(value, our_type): subs_new.append((key, value.args)) else: subs_new.append((key, value)) self.args['subs'] = subs_new # As of protocol 31 we support compact key names # ('t' instead of 'translate', etc). Convert as needed. if 'translate' in keywds: keywds['t'] = keywds['translate'] del keywds['translate'] if 'resource' in keywds: keywds['r'] = keywds['resource'] del keywds['resource'] if 'value' in keywds: keywds['v'] = keywds['value'] del keywds['value'] if 'fallback' in keywds: from ba import _error _error.print_error( 'deprecated "fallback" arg passed to Lstr(); use ' 'either "fallback_resource" or "fallback_value"', once=True) keywds['f'] = keywds['fallback'] del keywds['fallback'] if 'fallback_resource' in keywds: keywds['f'] = keywds['fallback_resource'] del keywds['fallback_resource'] if 'subs' in keywds: keywds['s'] = keywds['subs'] del keywds['subs'] if 'fallback_value' in keywds: keywds['fv'] = keywds['fallback_value'] del keywds['fallback_value'] def evaluate(self) -> str: """Evaluate the Lstr and returns a flat string in the current language. You should avoid doing this as much as possible and instead pass and store Lstr values. """ return _ba.evaluate_lstr(self._get_json()) def is_flat_value(self) -> bool: """Return whether the Lstr is a 'flat' value. This is defined as a simple string value incorporating no translations, resources, or substitutions. In this case it may be reasonable to replace it with a raw string value, perform string manipulation on it, etc. """ return bool('v' in self.args and not self.args.get('s', [])) def _get_json(self) -> str: try: return json.dumps(self.args, separators=(',', ':')) except Exception: from ba import _error _error.print_exception('_get_json failed for', self.args) return 'JSON_ERR' def __str__(self) -> str: return '' def __repr__(self) -> str: return '' def setlanguage(language: Optional[str], print_change: bool = True, store_to_config: bool = True) -> None: """Set the active language used for the game. category: General Utility Functions Pass None to use OS default language. """ # pylint: disable=too-many-locals # pylint: disable=too-many-branches cfg = _ba.app.config cur_language = cfg.get('Lang', None) # Store this in the config if its changing. if language != cur_language and store_to_config: if language is None: if 'Lang' in cfg: del cfg['Lang'] # Clear it out for default. else: cfg['Lang'] = language cfg.commit() switched = True else: switched = False with open('data/data/languages/english.json') as infile: lenglishvalues = json.loads(infile.read()) # None implies default. if language is None: language = _ba.app.default_language try: if language == 'English': lmodvalues = None else: lmodfile = 'data/data/languages/' + language.lower() + '.json' with open(lmodfile) as infile: lmodvalues = json.loads(infile.read()) except Exception: from ba import _error _error.print_exception('Exception importing language:', language) _ba.screenmessage("Error setting language to '" + language + "'; see log for details", color=(1, 0, 0)) switched = False lmodvalues = None # Create an attrdict of *just* our target language. _ba.app.language_target = AttrDict() langtarget = _ba.app.language_target assert langtarget is not None _add_to_attr_dict(langtarget, lmodvalues if lmodvalues is not None else lenglishvalues) # Create an attrdict of our target language overlaid on our base (english). languages = [lenglishvalues] if lmodvalues is not None: languages.append(lmodvalues) lfull = AttrDict() for lmod in languages: _add_to_attr_dict(lfull, lmod) _ba.app.language_merged = lfull # Pass some keys/values in for low level code to use; # start with everything in their 'internal' section. internal_vals = [ v for v in list(lfull['internal'].items()) if isinstance(v[1], str) ] # Cherry-pick various other values to include. # (should probably get rid of the 'internal' section # and do everything this way) for value in [ 'replayNameDefaultText', 'replayWriteErrorText', 'replayVersionErrorText', 'replayReadErrorText' ]: internal_vals.append((value, lfull[value])) internal_vals.append( ('axisText', lfull['configGamepadWindow']['axisText'])) lmerged = _ba.app.language_merged assert lmerged is not None random_names = [ n.strip() for n in lmerged['randomPlayerNamesText'].split(',') ] random_names = [n for n in random_names if n != ''] _ba.set_internal_language_keys(internal_vals, random_names) if switched and print_change: _ba.screenmessage(Lstr(resource='languageSetText', subs=[('${LANGUAGE}', Lstr(translate=('languages', language))) ]), color=(0, 1, 0)) def _add_to_attr_dict(dst: AttrDict, src: Dict) -> None: for key, value in list(src.items()): if isinstance(value, dict): try: dst_dict = dst[key] except Exception: dst_dict = dst[key] = AttrDict() if not isinstance(dst_dict, AttrDict): raise Exception("language key '" + key + "' is defined both as a dict and value") _add_to_attr_dict(dst_dict, value) else: if not isinstance(value, (float, int, bool, str, str, type(None))): raise Exception("invalid value type for res '" + key + "': " + str(type(value))) dst[key] = value class AttrDict(dict): """A dict that can be accessed with dot notation. (so foo.bar is equivalent to foo['bar']) """ def __getattr__(self, attr: str) -> Any: val = self[attr] assert not isinstance(val, bytes) return val def __setattr__(self, attr: str, value: Any) -> None: raise Exception() def get_resource(resource: str, fallback_resource: str = None, fallback_value: Any = None) -> Any: """Return a translation resource by name.""" try: # If we have no language set, go ahead and set it. if _ba.app.language_merged is None: language = _ba.app.language try: setlanguage(language, print_change=False, store_to_config=False) except Exception: from ba import _error _error.print_exception('exception setting language to', language) # Try english as a fallback. if language != 'English': print('Resorting to fallback language (English)') try: setlanguage('English', print_change=False, store_to_config=False) except Exception: _error.print_exception( 'error setting language to english fallback') # If they provided a fallback_resource value, try the # target-language-only dict first and then fall back to trying the # fallback_resource value in the merged dict. if fallback_resource is not None: try: values = _ba.app.language_target splits = resource.split('.') dicts = splits[:-1] key = splits[-1] for dct in dicts: assert values is not None values = values[dct] assert values is not None val = values[key] return val except Exception: # FIXME: Shouldn't we try the fallback resource in the merged # dict AFTER we try the main resource in the merged dict? try: values = _ba.app.language_merged splits = fallback_resource.split('.') dicts = splits[:-1] key = splits[-1] for dct in dicts: assert values is not None values = values[dct] assert values is not None val = values[key] return val except Exception: # If we got nothing for fallback_resource, default to the # normal code which checks or primary value in the merge # dict; there's a chance we can get an english value for # it (which we weren't looking for the first time through). pass values = _ba.app.language_merged splits = resource.split('.') dicts = splits[:-1] key = splits[-1] for dct in dicts: assert values is not None values = values[dct] assert values is not None val = values[key] return val except Exception: # Ok, looks like we couldn't find our main or fallback resource # anywhere. Now if we've been given a fallback value, return it; # otherwise fail. if fallback_value is not None: return fallback_value raise Exception("resource not found: '" + resource + "'") def translate(category: str, strval: str, raise_exceptions: bool = False, print_errors: bool = False) -> str: """Translate a value (or return the value if no translation available) Generally you should use ba.Lstr which handles dynamic translation, as opposed to this which returns a flat string. """ try: translated = get_resource('translations')[category][strval] except Exception as exc: if raise_exceptions: raise if print_errors: print(('Translate error: category=\'' + category + '\' name=\'' + strval + '\' exc=' + str(exc) + '')) translated = None translated_out: str if translated is None: translated_out = strval else: translated_out = translated assert isinstance(translated_out, str) return translated_out def get_valid_languages() -> List[str]: """Return a list containing names of all available languages. category: General Utility Functions Languages that may be present but are not displayable on the running version of the game are ignored. """ langs = set() app = _ba.app try: names = os.listdir('data/data/languages') names = [n.replace('.json', '').capitalize() for n in names] except Exception: from ba import _error _error.print_exception() names = [] for name in names: if app.can_display_language(name): langs.add(name) return sorted(name for name in names if app.can_display_language(name)) def is_custom_unicode_char(char: str) -> bool: """Return whether a char is in the custom unicode range we use.""" if not isinstance(char, str) or len(char) != 1: raise Exception("Invalid Input; not unicode or not length 1") return 0xE000 <= ord(char) <= 0xF8FF