Source code for apimodel.localization

"""Localization support for the API."""
from __future__ import annotations

import collections
import typing

from . import apimodel, fields, tutils

__all__ = ["LocalizedAPIModel", "LocalizedAPIModelMeta", "LocalizedFieldInfo"]


[docs]class LocalizedFieldInfo(fields.ModelFieldInfo): """Complete information about a localized field.""" __slots__ = ("i18n", "localizator")
[docs] i18n: typing.Optional[typing.Union[str, typing.Mapping[str, str]]]
"""Localized field names."""
[docs] localizator: typing.Optional[typing.Callable[[typing.Any, str], typing.Optional[object]]]
"""Getter for localized values of the field. Called in `LocalizedAPIModel.as_dict`.""" def __init__( self, default: object = ..., *, alias: typing.Optional[str] = None, private: typing.Optional[bool] = False, validators: tutils.MaybeSequence[tutils.AnyCallable] = ..., i18n: typing.Optional[typing.Union[str, typing.Mapping[str, str]]] = None, localizator: typing.Optional[typing.Callable[[typing.Any, str], typing.Optional[object]]] = None, **extra: typing.Any, ) -> None: """Initialize a LocalizedFieldInfo. Extra arguments may be repurposed for subclass attributes. """ self.i18n = i18n self.localizator = localizator super().__init__( default, alias=alias, private=private, validators=validators, **extra, )
[docs] def get_localized_name( self, provider: typing.Mapping[str, typing.Mapping[str, str]], locale: str, name: typing.Optional[str] = None, ) -> str: """Get the localized name of the field.""" i18n = self.i18n or name or self.alias if isinstance(i18n, str): return provider[locale].get(i18n, name or self.alias) return i18n[locale]
[docs] def get_localized_value( self, value: object, provider: typing.Mapping[str, typing.Mapping[str, str]], locale: str, ) -> object: """Get the localized value of the field..""" if self.localizator is not None: return self.localizator(value, locale) or value if isinstance(value, str): return provider[locale].get(value, value) return value
[docs]class LocalizedAPIModelMeta(apimodel.APIModelMeta): """Localized API model metaclass."""
[docs] __fields__: typing.Mapping[str, LocalizedFieldInfo]
[docs] i18n: typing.ClassVar[typing.Dict[str, typing.Dict[str, str]]] = collections.defaultdict(dict)
"""Internationalization mapping of ``{locale: {key: "localized string"}}``.""" def __new__( cls, name: str, bases: typing.Tuple[type], namespace: typing.Dict[str, object], *, field_cls: typing.Optional[typing.Type[LocalizedFieldInfo]] = None, **options: object, ) -> tutils.Self: """Create a new model class. Collects all fields and validators. """ self = super().__new__(cls, name, bases, namespace, field_cls=field_cls or LocalizedFieldInfo, **options) self = typing.cast("tutils.Self", self) return self
[docs] def set_i18n(self, locale: str, key: str, value: str) -> None: """Set a new i18n entry.""" self.i18n[locale][key] = value
[docs]class LocalizedAPIModel(apimodel.APIModel, metaclass=LocalizedAPIModelMeta): """Localized API model.""" __slots__ = ()
[docs] locale: typing.Optional[str] = fields.Extra(None)
[docs] def as_dict( self, *, private: bool = False, properties: bool = True, alias: typing.Optional[bool] = None, locale: typing.Optional[str] = None, **options: object, ) -> typing.Mapping[str, object]: """Create a mapping from the model instance. Args: private: Include private attributes (prefixed with an underscore `_`). properties: Include methods decorated with `@property`. alias: Rename fields to their declared name if they cannot be localized. If `False`, do not rename any field even if it can be localized. locale: Locale to use for localization. By default the locale of the model instance is used. """ obj: typing.Mapping[str, object] = {} locale = locale or self.locale for attr_name, field in self.__class__.__fields__.items(): if field.private and not private: continue field_name = field.alias if alias else attr_name if locale is not None and alias is not False: field_name = field.get_localized_name(self.__class__.i18n, locale, name=field_name) value = apimodel._serialize_attr(getattr(self, attr_name), private=private, alias=alias, locale=locale) if locale is not None: value = field.get_localized_value(value, self.__class__.i18n, locale) obj[field_name] = value if properties: obj.update({name: getattr(self, attr_name) for attr_name, name in self.__class__.__properties__.items()}) return obj