mirror of
https://github.com/hypervortex/VH-Bombsquad-Modded-Server-Files
synced 2025-11-07 17:36:08 +00:00
629 lines
21 KiB
Python
629 lines
21 KiB
Python
|
|
# Released under the MIT License. See LICENSE for details.
|
||
|
|
#
|
||
|
|
"""Language related functionality."""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
from typing import TYPE_CHECKING, overload
|
||
|
|
|
||
|
|
import _ba
|
||
|
|
|
||
|
|
if TYPE_CHECKING:
|
||
|
|
import ba
|
||
|
|
from typing import Any, Sequence
|
||
|
|
|
||
|
|
|
||
|
|
class LanguageSubsystem:
|
||
|
|
"""Wraps up language related app functionality.
|
||
|
|
|
||
|
|
Category: **App Classes**
|
||
|
|
|
||
|
|
To use this class, access the single instance of it at 'ba.app.lang'.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self) -> None:
|
||
|
|
self.language_target: AttrDict | None = None
|
||
|
|
self.language_merged: AttrDict | None = None
|
||
|
|
self.default_language = self._get_default_language()
|
||
|
|
|
||
|
|
def _can_display_language(self, language: str) -> bool:
|
||
|
|
"""Tell whether we can display a particular language.
|
||
|
|
|
||
|
|
On some platforms we don't have unicode rendering yet
|
||
|
|
which limits the languages we can draw.
|
||
|
|
"""
|
||
|
|
|
||
|
|
# We don't yet support full unicode display on windows or linux :-(.
|
||
|
|
if (
|
||
|
|
language
|
||
|
|
in {
|
||
|
|
'Chinese',
|
||
|
|
'ChineseTraditional',
|
||
|
|
'Persian',
|
||
|
|
'Korean',
|
||
|
|
'Arabic',
|
||
|
|
'Hindi',
|
||
|
|
'Vietnamese',
|
||
|
|
'Thai',
|
||
|
|
'Tamil',
|
||
|
|
}
|
||
|
|
and not _ba.can_display_full_unicode()
|
||
|
|
):
|
||
|
|
return False
|
||
|
|
return True
|
||
|
|
|
||
|
|
@property
|
||
|
|
def locale(self) -> str:
|
||
|
|
"""Raw country/language code detected by the game (such as 'en_US').
|
||
|
|
|
||
|
|
Generally for language-specific code you should look at
|
||
|
|
ba.App.language, which is the language the game is using
|
||
|
|
(which may differ from locale if the user sets a language, etc.)
|
||
|
|
"""
|
||
|
|
env = _ba.env()
|
||
|
|
assert isinstance(env['locale'], str)
|
||
|
|
return env['locale']
|
||
|
|
|
||
|
|
def _get_default_language(self) -> str:
|
||
|
|
languages = {
|
||
|
|
'ar': 'Arabic',
|
||
|
|
'be': 'Belarussian',
|
||
|
|
'zh': 'Chinese',
|
||
|
|
'hr': 'Croatian',
|
||
|
|
'cs': 'Czech',
|
||
|
|
'da': 'Danish',
|
||
|
|
'nl': 'Dutch',
|
||
|
|
'eo': 'Esperanto',
|
||
|
|
'fil': 'Filipino',
|
||
|
|
'fr': 'French',
|
||
|
|
'de': 'German',
|
||
|
|
'el': 'Greek',
|
||
|
|
'hi': 'Hindi',
|
||
|
|
'hu': 'Hungarian',
|
||
|
|
'id': 'Indonesian',
|
||
|
|
'it': 'Italian',
|
||
|
|
'ko': 'Korean',
|
||
|
|
'ms': 'Malay',
|
||
|
|
'fa': 'Persian',
|
||
|
|
'pl': 'Polish',
|
||
|
|
'pt': 'Portuguese',
|
||
|
|
'ro': 'Romanian',
|
||
|
|
'ru': 'Russian',
|
||
|
|
'sr': 'Serbian',
|
||
|
|
'es': 'Spanish',
|
||
|
|
'sk': 'Slovak',
|
||
|
|
'sv': 'Swedish',
|
||
|
|
'ta': 'Tamil',
|
||
|
|
'th': 'Thai',
|
||
|
|
'tr': 'Turkish',
|
||
|
|
'uk': 'Ukrainian',
|
||
|
|
'vec': 'Venetian',
|
||
|
|
'vi': 'Vietnamese',
|
||
|
|
}
|
||
|
|
|
||
|
|
# Special case for Chinese: map specific variations to traditional.
|
||
|
|
# (otherwise will map to 'Chinese' which is simplified)
|
||
|
|
if self.locale in ('zh_HANT', 'zh_TW'):
|
||
|
|
language = 'ChineseTraditional'
|
||
|
|
else:
|
||
|
|
language = languages.get(self.locale[:2], 'English')
|
||
|
|
if not self._can_display_language(language):
|
||
|
|
language = 'English'
|
||
|
|
return language
|
||
|
|
|
||
|
|
@property
|
||
|
|
def language(self) -> str:
|
||
|
|
"""The name of the language the game is running in.
|
||
|
|
|
||
|
|
This can be selected explicitly by the user or may be set
|
||
|
|
automatically based on ba.App.locale or other factors.
|
||
|
|
"""
|
||
|
|
assert isinstance(_ba.app.config, dict)
|
||
|
|
return _ba.app.config.get('Lang', self.default_language)
|
||
|
|
|
||
|
|
@property
|
||
|
|
def available_languages(self) -> list[str]:
|
||
|
|
"""A list of all available languages.
|
||
|
|
|
||
|
|
Note that languages that may be present in game assets but which
|
||
|
|
are not displayable on the running version of the game are not
|
||
|
|
included here.
|
||
|
|
"""
|
||
|
|
langs = set()
|
||
|
|
try:
|
||
|
|
names = os.listdir('ba_data/data/languages')
|
||
|
|
names = [n.replace('.json', '').capitalize() for n in names]
|
||
|
|
|
||
|
|
# FIXME: our simple capitalization fails on multi-word names;
|
||
|
|
# should handle this in a better way...
|
||
|
|
for i, name in enumerate(names):
|
||
|
|
if name == 'Chinesetraditional':
|
||
|
|
names[i] = 'ChineseTraditional'
|
||
|
|
except Exception:
|
||
|
|
from ba import _error
|
||
|
|
|
||
|
|
_error.print_exception()
|
||
|
|
names = []
|
||
|
|
for name in names:
|
||
|
|
if self._can_display_language(name):
|
||
|
|
langs.add(name)
|
||
|
|
return sorted(
|
||
|
|
name for name in names if self._can_display_language(name)
|
||
|
|
)
|
||
|
|
|
||
|
|
def setlanguage(
|
||
|
|
self,
|
||
|
|
language: str | None,
|
||
|
|
print_change: bool = True,
|
||
|
|
store_to_config: bool = True,
|
||
|
|
) -> None:
|
||
|
|
"""Set the active language used for the game.
|
||
|
|
|
||
|
|
Pass None to use OS default language.
|
||
|
|
"""
|
||
|
|
# pylint: disable=too-many-locals
|
||
|
|
# pylint: disable=too-many-statements
|
||
|
|
# 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(
|
||
|
|
'ba_data/data/languages/english.json', encoding='utf-8'
|
||
|
|
) as infile:
|
||
|
|
lenglishvalues = json.loads(infile.read())
|
||
|
|
|
||
|
|
# None implies default.
|
||
|
|
if language is None:
|
||
|
|
language = self.default_language
|
||
|
|
try:
|
||
|
|
if language == 'English':
|
||
|
|
lmodvalues = None
|
||
|
|
else:
|
||
|
|
lmodfile = (
|
||
|
|
'ba_data/data/languages/' + language.lower() + '.json'
|
||
|
|
)
|
||
|
|
with open(lmodfile, encoding='utf-8') 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.
|
||
|
|
self.language_target = AttrDict()
|
||
|
|
langtarget = self.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)
|
||
|
|
self.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'])
|
||
|
|
)
|
||
|
|
internal_vals.append(('buttonText', lfull['buttonText']))
|
||
|
|
lmerged = self.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 get_resource(
|
||
|
|
self,
|
||
|
|
resource: str,
|
||
|
|
fallback_resource: str | None = None,
|
||
|
|
fallback_value: Any = None,
|
||
|
|
) -> Any:
|
||
|
|
"""Return a translation resource by name.
|
||
|
|
|
||
|
|
DEPRECATED; use ba.Lstr functionality for these purposes.
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
# If we have no language set, go ahead and set it.
|
||
|
|
if self.language_merged is None:
|
||
|
|
language = self.language
|
||
|
|
try:
|
||
|
|
self.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:
|
||
|
|
self.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 = self.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 = self.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 = self.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.
|
||
|
|
from ba import _error
|
||
|
|
|
||
|
|
if fallback_value is not None:
|
||
|
|
return fallback_value
|
||
|
|
raise _error.NotFoundError(
|
||
|
|
f"Resource not found: '{resource}'"
|
||
|
|
) from None
|
||
|
|
|
||
|
|
def translate(
|
||
|
|
self,
|
||
|
|
category: str,
|
||
|
|
strval: str,
|
||
|
|
raise_exceptions: bool = False,
|
||
|
|
print_errors: bool = False,
|
||
|
|
) -> str:
|
||
|
|
"""Translate a value (or return the value if no translation available)
|
||
|
|
|
||
|
|
DEPRECATED; use ba.Lstr functionality for these purposes.
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
translated = self.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 is_custom_unicode_char(self, char: str) -> bool:
|
||
|
|
"""Return whether a char is in the custom unicode range we use."""
|
||
|
|
assert isinstance(char, str)
|
||
|
|
if len(char) != 1:
|
||
|
|
raise ValueError('Invalid Input; must be length 1')
|
||
|
|
return 0xE000 <= ord(char) <= 0xF8FF
|
||
|
|
|
||
|
|
|
||
|
|
class Lstr:
|
||
|
|
"""Used to define 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 legacy.ballistica.net/translate.
|
||
|
|
|
||
|
|
##### Examples
|
||
|
|
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: ba.Lstr's 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'))])
|
||
|
|
"""
|
||
|
|
|
||
|
|
# pylint: disable=dangerous-default-value
|
||
|
|
# noinspection PyDefaultArgument
|
||
|
|
@overload
|
||
|
|
def __init__(
|
||
|
|
self,
|
||
|
|
*,
|
||
|
|
resource: str,
|
||
|
|
fallback_resource: str = '',
|
||
|
|
fallback_value: str = '',
|
||
|
|
subs: Sequence[tuple[str, str | Lstr]] = [],
|
||
|
|
) -> None:
|
||
|
|
"""Create an Lstr from a string resource."""
|
||
|
|
|
||
|
|
# noinspection PyShadowingNames,PyDefaultArgument
|
||
|
|
@overload
|
||
|
|
def __init__(
|
||
|
|
self,
|
||
|
|
*,
|
||
|
|
translate: tuple[str, str],
|
||
|
|
subs: Sequence[tuple[str, str | Lstr]] = [],
|
||
|
|
) -> None:
|
||
|
|
"""Create an Lstr by translating a string in a category."""
|
||
|
|
|
||
|
|
# noinspection PyDefaultArgument
|
||
|
|
@overload
|
||
|
|
def __init__(
|
||
|
|
self, *, value: str, subs: Sequence[tuple[str, str | Lstr]] = []
|
||
|
|
) -> None:
|
||
|
|
"""Create an Lstr from a raw string value."""
|
||
|
|
|
||
|
|
# pylint: enable=redefined-outer-name, dangerous-default-value
|
||
|
|
|
||
|
|
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 TypeError('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 TypeError("'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 '<ba.Lstr: ' + self._get_json() + '>'
|
||
|
|
|
||
|
|
def __repr__(self) -> str:
|
||
|
|
return '<ba.Lstr: ' + self._get_json() + '>'
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def from_json(json_string: str) -> ba.Lstr:
|
||
|
|
"""Given a json string, returns a ba.Lstr. Does no data validation."""
|
||
|
|
lstr = Lstr(value='')
|
||
|
|
lstr.args = json.loads(json_string)
|
||
|
|
return lstr
|
||
|
|
|
||
|
|
|
||
|
|
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 RuntimeError(
|
||
|
|
"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 TypeError(
|
||
|
|
"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()
|