Source code for shuup.utils.i18n

from functools import lru_cache, wraps

import babel
import babel.numbers
from babel import UnknownLocaleError
from babel.dates import format_datetime
from babel.numbers import format_currency, format_decimal, parse_pattern
from django.apps import apps
from django.utils import translation
from django.utils.timezone import localtime
from django.utils.translation import get_language
from django.views.decorators.cache import cache_page


[docs] def lang_lru_cache(func): """Language aware least recently used cache decorator""" @lru_cache def cached(*args, __lang=None, **kwargs): return func(*args, **kwargs) @wraps(func) def wrapper(*args, **kwargs): return cached(*args, **kwargs, __lang=translation.get_language()) wrapper.cache_clear = cached.cache_clear return wrapper
[docs] @lru_cache def get_babel_locale(locale_string): """ Parse a Django-format (dash-separated) locale string and return a Babel locale. This function is decorated with lru_cache, so executions should be cheap even if `babel.Locale.parse()` most definitely is not. :param locale_string: A locale string ("en-US", "fi-FI", "fi") :type locale_string: str :return: Babel Locale :rtype: babel.Locale """ return babel.Locale.parse(locale_string, "-")
[docs] def get_current_babel_locale(fallback="en-US-POSIX"): """ Get a Babel locale based on the thread's locale context. :param fallback: Locale to fallback to; set to None to raise an exception instead. :return: Babel Locale :rtype: babel.Locale """ locale_string = translation.get_language() if not locale_string: # Handle None case from translation.override(None) locale_string = fallback locale = get_babel_locale(locale_string=locale_string) if not locale: if fallback: locale = get_babel_locale(fallback) if not locale: raise ValueError(f"Error! Failed to get the current babel locale (lang={locale_string}).") return locale
[docs] def format_number(value, digits=None): locale = get_current_babel_locale() if digits is None: return format_decimal(value, locale=locale) (min_digits, max_digits) = digits if isinstance(digits, tuple) else (digits, digits) format = locale.decimal_formats.get(None) pattern = parse_pattern(format) # type: babel.numbers.NumberPattern return pattern.apply(value, locale, force_frac=(min_digits, max_digits))
[docs] def format_percent(value, digits=0): locale = get_current_babel_locale() pattern = locale.percent_formats.get(None).pattern new_pattern = pattern.replace("0", "0." + (digits * "0")) return babel.numbers.format_percent(value, new_pattern, locale)
[docs] def get_locally_formatted_datetime(datetime): """ Return a formatted, localized version of datetime based on the current context. """ return format_datetime(localtime(datetime), locale=get_current_babel_locale())
[docs] def format_money(amount, digits=None, widen=0, locale=None): """ Format a Money object in the given locale. If neither digits or widen is passed, the preferred number of digits for the amount's currency is used. :param amount: The Money object to format :type amount: Money :param digits: How many digits to format the currency with. :type digits: int|None :param widen: How many digits to widen any existing decimal width with. :type widen: int|None :param locale: Locale object or locale identifier :type locale: Locale|str :return: Formatted string :rtype: str """ if not locale: loc = get_current_babel_locale() else: loc = get_babel_locale(locale) if widen == 0 and digits is None: # No special treatment required; format with the currency's digits. # Custom currency symbol overrides for certain currency-locale combinations currency_overrides = { ("SEK", "en"): "kr", # Swedish Krona in English should use "kr" not "SEK" } locale_key = str(loc).split("_")[0] # Get base locale (en from en_US) override_symbol = currency_overrides.get((amount.currency, locale_key)) if override_symbol: # Use custom formatting with override symbol formatted_number = format_decimal(amount.value, locale=loc, decimal_quantization=False) pattern = loc.currency_formats["standard"].pattern # Replace currency placeholder with custom symbol and number with formatted value # Handle common pattern formats like '¤#,##0.00', '#,##0.00\xa0¤', etc. if pattern.startswith("¤"): return override_symbol + formatted_number elif pattern.endswith("¤"): return formatted_number + override_symbol else: # Fallback: replace currency symbol placeholder import re return re.sub( r"[¤#,0.]+", lambda m: override_symbol + formatted_number if "¤" in m.group() else m.group(), pattern, ) return format_currency(amount.value, amount.currency, locale=loc, currency_digits=True) pattern = loc.currency_formats["standard"].pattern # pattern is a formatting string. Couple examples: # '¤#,##0.00', '#,##0.00\xa0¤', '\u200e¤#,##0.00', and '¤#0.00' if digits is not None: pattern = pattern.replace(".00", "." + (digits * "0")) if widen: pattern = pattern.replace(".00", ".00" + (widen * "0")) return format_currency(amount.value, amount.currency, pattern, loc, currency_digits=False)
[docs] @lang_lru_cache def get_language_name(language_code): """ Get a language's name in the currently active locale. :param language_code: Language code (possibly with an added script suffix (zh_Hans, zh-Hans)) :type language_code: str :return: The language name, or the code if the language couldn't be derived. :rtype: str """ try: lang_dict = get_current_babel_locale().languages except (AttributeError, ValueError): # The locale lookup failed, return language_code # so return the code as-is. for option in ( language_code, str(language_code).replace("-", "_"), ): if option in lang_dict: return lang_dict[option] return language_code
[docs] @cache_page(3600, key_prefix=f"js18n-{get_language()}") def javascript_catalog_all(request, domain="djangojs"): """ Get JavaScript message catalog for all apps in `INSTALLED_APPS`. """ all_apps = [x.name for x in apps.get_app_configs()] from django.views.i18n import JavaScriptCatalog js_catalog = JavaScriptCatalog(packages=all_apps, domain=domain) return js_catalog.get(request)
[docs] def get_currency_name(currency): locale = get_current_babel_locale() return babel.numbers.get_currency_name(currency, locale=locale)
[docs] def is_existing_language(language_code): """ Try to find out if the language actually exists. Calling `babel.Locale("en").languages.keys()` will contain extinct languages. :param language_code: A language code string ("fi", "en") :type language_code: str :return: True or False :rtype: bool """ try: get_babel_locale(language_code) except (UnknownLocaleError, ValueError): """ Catch errors with babel locale parsing. For example language `bew` raises `UnknownLocaleError` and `ValueError` is being raised if language_code is an empty string. """ return False return True
[docs] @lru_cache def remove_extinct_languages(language_codes): language_codes = set(language_codes) codes = language_codes.copy() for language_code in codes: if not is_existing_language(language_code): language_codes.remove(language_code) return language_codes