feat!: enhance Language to become more of a Locale (#27178)
This commit is contained in:
parent
1feef4890c
commit
b91cacdd18
11 changed files with 224 additions and 51 deletions
|
|
@ -10,7 +10,12 @@
|
|||
"language_code",
|
||||
"language_name",
|
||||
"flag",
|
||||
"based_on"
|
||||
"based_on",
|
||||
"section_break_rtth",
|
||||
"date_format",
|
||||
"time_format",
|
||||
"number_format",
|
||||
"first_day_of_the_week"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -46,12 +51,40 @@
|
|||
"fieldname": "enabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_rtth",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "date_format",
|
||||
"fieldtype": "Select",
|
||||
"label": "Date Format",
|
||||
"options": "\nyyyy-mm-dd\ndd-mm-yyyy\ndd/mm/yyyy\ndd.mm.yyyy\nmm/dd/yyyy\nmm-dd-yyyy"
|
||||
},
|
||||
{
|
||||
"fieldname": "time_format",
|
||||
"fieldtype": "Select",
|
||||
"label": "Time Format",
|
||||
"options": "\nHH:mm:ss\nHH:mm"
|
||||
},
|
||||
{
|
||||
"fieldname": "number_format",
|
||||
"fieldtype": "Select",
|
||||
"label": "Number Format",
|
||||
"options": "\n#,###.##\n#.###,##\n# ###.##\n# ###,##\n#'###.##\n#, ###.##\n#,##,###.##\n#,###.###\n#.###\n#,###"
|
||||
},
|
||||
{
|
||||
"fieldname": "first_day_of_the_week",
|
||||
"fieldtype": "Select",
|
||||
"label": "First Day of the Week",
|
||||
"options": "\nSunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-globe",
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-06 18:25:01.010821",
|
||||
"modified": "2024-07-21 08:25:25.130166",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Language",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import re
|
|||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.defaults import clear_default, set_default
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
|
|
@ -18,10 +19,30 @@ class Language(Document):
|
|||
from frappe.types import DF
|
||||
|
||||
based_on: DF.Link | None
|
||||
date_format: DF.Literal[
|
||||
"", "yyyy-mm-dd", "dd-mm-yyyy", "dd/mm/yyyy", "dd.mm.yyyy", "mm/dd/yyyy", "mm-dd-yyyy"
|
||||
]
|
||||
enabled: DF.Check
|
||||
first_day_of_the_week: DF.Literal[
|
||||
"", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
|
||||
]
|
||||
flag: DF.Data | None
|
||||
language_code: DF.Data
|
||||
language_name: DF.Data
|
||||
number_format: DF.Literal[
|
||||
"",
|
||||
"#,###.##",
|
||||
"#.###,##",
|
||||
"# ###.##",
|
||||
"# ###,##",
|
||||
"#'###.##",
|
||||
"#, ###.##",
|
||||
"#,##,###.##",
|
||||
"#,###.###",
|
||||
"#.###",
|
||||
"#,###",
|
||||
]
|
||||
time_format: DF.Literal["", "HH:mm:ss", "HH:mm"]
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
|
|
@ -33,6 +54,22 @@ class Language(Document):
|
|||
def on_update(self):
|
||||
frappe.cache.delete_value("languages_with_name")
|
||||
frappe.cache.delete_value("languages")
|
||||
self.update_user_defaults()
|
||||
|
||||
def update_user_defaults(self):
|
||||
"""Update user defaults for date, time, number format and first day of the week.
|
||||
|
||||
When we change any settings of a language, the defaults for all users with that language
|
||||
should be updated.
|
||||
"""
|
||||
users = frappe.get_all("User", filters={"language": self.name}, pluck="name")
|
||||
for key in ("date_format", "time_format", "number_format", "first_day_of_the_week"):
|
||||
if self.has_value_changed(key):
|
||||
for user in users:
|
||||
if new_value := self.get(key):
|
||||
set_default(key, new_value, user)
|
||||
else:
|
||||
clear_default(key, parent=user)
|
||||
|
||||
|
||||
def validate_with_regex(name, label):
|
||||
|
|
|
|||
|
|
@ -268,6 +268,18 @@ class User(Document):
|
|||
if self.time_zone:
|
||||
frappe.defaults.set_default("time_zone", self.time_zone, self.name)
|
||||
|
||||
if self.has_value_changed("language"):
|
||||
locale_keys = ("date_format", "time_format", "number_format", "first_day_of_the_week")
|
||||
if self.language:
|
||||
language = frappe.get_doc("Language", self.language)
|
||||
for key in locale_keys:
|
||||
value = language.get(key)
|
||||
if value:
|
||||
frappe.defaults.set_default(key, value, self.name)
|
||||
else:
|
||||
for key in locale_keys:
|
||||
frappe.defaults.clear_default(key, parent=self.name)
|
||||
|
||||
if self.has_value_changed("enabled"):
|
||||
frappe.cache.delete_key("users_for_mentions")
|
||||
frappe.cache.delete_key("enabled_users")
|
||||
|
|
|
|||
|
|
@ -168,6 +168,9 @@ def set_default(key, value, parent, parenttype="__default"):
|
|||
else:
|
||||
_clear_cache(parent)
|
||||
|
||||
if parent:
|
||||
clear_defaults_cache(parent)
|
||||
|
||||
|
||||
def add_default(key, value, parent, parenttype=None):
|
||||
d = frappe.get_doc(
|
||||
|
|
|
|||
52
frappe/locale.py
Normal file
52
frappe/locale.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import frappe
|
||||
from frappe.utils.number_format import NumberFormat
|
||||
|
||||
|
||||
def get_number_format(language: str | None = None) -> NumberFormat:
|
||||
"""Return the number format for the given language.
|
||||
|
||||
:param language: The language code to get the value for. Defaults to the current user's language.
|
||||
:return: The number format. Defaults to "#,###.##" if not found.
|
||||
"""
|
||||
number_format = get_locale_value("number_format", language) or "#,###.##"
|
||||
return NumberFormat.from_string(number_format)
|
||||
|
||||
|
||||
def get_date_format(language: str | None = None) -> str:
|
||||
"""Return the date format for the given language.
|
||||
|
||||
:param language: The language code to get the value for. Defaults to the current user's language.
|
||||
:return: The date format string. Defaults to "yyyy-mm-dd" if not found.
|
||||
"""
|
||||
return get_locale_value("date_format", language) or "yyyy-mm-dd"
|
||||
|
||||
|
||||
def get_time_format(language: str | None = None) -> str:
|
||||
"""Return the time format for the given language.
|
||||
|
||||
:param language: The language code to get the value for. Defaults to the current user's language.
|
||||
:return: The time format string. Defaults to "HH:mm:ss" if not found.
|
||||
"""
|
||||
return get_locale_value("time_format", language) or "HH:mm:ss"
|
||||
|
||||
|
||||
def get_first_day_of_the_week(language: str | None = None) -> str:
|
||||
"""Return the first day of the week for the given language.
|
||||
|
||||
:param language: The language code to get the value for. Defaults to the current user's language.
|
||||
:return: The first day of the week. Defaults to "Sunday" if not found.
|
||||
"""
|
||||
return get_locale_value("first_day_of_the_week", language) or "Sunday"
|
||||
|
||||
|
||||
def get_locale_value(key: str, language: str | None = None) -> str | None:
|
||||
"""Return the value of the key from the Language record or System Settings.
|
||||
|
||||
:param key: The settings key to get the value for.
|
||||
:param language: The language code to get the value for. Defaults to the current user's language.
|
||||
"""
|
||||
lang = language or frappe.local.lang
|
||||
if lang:
|
||||
value = frappe.db.get_value("Language", lang, key)
|
||||
|
||||
return value or frappe.db.get_default(key)
|
||||
|
|
@ -811,7 +811,7 @@ def get_field_currency(df, doc=None):
|
|||
|
||||
def get_field_precision(df, doc=None, currency=None):
|
||||
"""get precision based on DocField options and fieldvalue in doc"""
|
||||
from frappe.utils import get_number_format_info
|
||||
from frappe.locale import get_number_format
|
||||
|
||||
if df.precision:
|
||||
precision = cint(df.precision)
|
||||
|
|
@ -819,8 +819,8 @@ def get_field_precision(df, doc=None, currency=None):
|
|||
elif df.fieldtype == "Currency":
|
||||
precision = cint(frappe.db.get_default("currency_precision"))
|
||||
if not precision:
|
||||
number_format = frappe.db.get_default("number_format") or "#,###.##"
|
||||
decimal_str, comma_str, precision = get_number_format_info(number_format)
|
||||
number_format = get_number_format()
|
||||
precision = number_format.precision
|
||||
else:
|
||||
precision = cint(frappe.db.get_default("float_precision")) or 3
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ from dateutil.relativedelta import relativedelta
|
|||
|
||||
import frappe
|
||||
from frappe.desk.utils import slug
|
||||
from frappe.locale import get_date_format, get_first_day_of_the_week, get_number_format, get_time_format
|
||||
from frappe.utils.deprecations import deprecated
|
||||
from frappe.utils.number_format import NUMBER_FORMAT_MAP, NumberFormat
|
||||
|
||||
DateTimeLikeObject = str | datetime.date | datetime.datetime
|
||||
NumericType = int | float
|
||||
|
|
@ -85,10 +88,6 @@ class Weekday(Enum):
|
|||
Saturday = 6
|
||||
|
||||
|
||||
def get_first_day_of_the_week() -> str:
|
||||
return frappe.get_system_settings("first_day_of_the_week") or "Sunday"
|
||||
|
||||
|
||||
def get_start_of_week_index() -> int:
|
||||
return Weekday[get_first_day_of_the_week()].value
|
||||
|
||||
|
|
@ -677,9 +676,9 @@ def get_time_str(timedelta_obj: datetime.timedelta | str) -> str:
|
|||
def get_user_date_format() -> str:
|
||||
"""Get the current user date format. The result will be cached."""
|
||||
if getattr(frappe.local, "user_date_format", None) is None:
|
||||
frappe.local.user_date_format = frappe.db.get_default("date_format")
|
||||
frappe.local.user_date_format = get_date_format()
|
||||
|
||||
return frappe.local.user_date_format or "yyyy-mm-dd"
|
||||
return frappe.local.user_date_format
|
||||
|
||||
|
||||
get_user_format = get_user_date_format # for backwards compatibility
|
||||
|
|
@ -688,9 +687,9 @@ get_user_format = get_user_date_format # for backwards compatibility
|
|||
def get_user_time_format() -> str:
|
||||
"""Get the current user time format. The result will be cached."""
|
||||
if getattr(frappe.local, "user_time_format", None) is None:
|
||||
frappe.local.user_time_format = frappe.db.get_default("time_format")
|
||||
frappe.local.user_time_format = get_time_format()
|
||||
|
||||
return frappe.local.user_time_format or "HH:mm:ss"
|
||||
return frappe.local.user_time_format
|
||||
|
||||
|
||||
def format_date(string_date=None, format_string: str | None = None, parse_day_first: bool = False) -> str:
|
||||
|
|
@ -1347,14 +1346,13 @@ def fmt_money(
|
|||
format: str | None = None,
|
||||
) -> str:
|
||||
"""Convert to string with commas for thousands, millions etc."""
|
||||
number_format = format or frappe.db.get_default("number_format") or "#,###.##"
|
||||
number_format = NumberFormat.from_string(format) if format else get_number_format()
|
||||
|
||||
if precision is None:
|
||||
precision = cint(frappe.db.get_default("currency_precision")) or None
|
||||
|
||||
decimal_str, comma_str, number_format_precision = get_number_format_info(number_format)
|
||||
|
||||
if precision is None:
|
||||
precision = number_format_precision
|
||||
precision = number_format.precision
|
||||
|
||||
# 40,000 -> 40,000.00
|
||||
# 40,000.00000 -> 40,000.00
|
||||
|
|
@ -1366,7 +1364,7 @@ def fmt_money(
|
|||
if amount is None:
|
||||
amount = 0
|
||||
|
||||
if decimal_str:
|
||||
if number_format.decimal_separator:
|
||||
decimals_after = str(round(amount % 1, precision))
|
||||
parts = decimals_after.split(".")
|
||||
parts = parts[1] if len(parts) > 1 else parts[0]
|
||||
|
|
@ -1377,7 +1375,7 @@ def fmt_money(
|
|||
fraction = frappe.db.get_value("Currency", currency, "fraction_units", cache=True) or 100
|
||||
precision = len(cstr(fraction)) - 1
|
||||
else:
|
||||
precision = number_format_precision
|
||||
precision = number_format.precision
|
||||
elif len(decimals) < precision:
|
||||
precision = len(decimals)
|
||||
|
||||
|
|
@ -1399,7 +1397,7 @@ def fmt_money(
|
|||
parts.append(amount[-3:])
|
||||
amount = amount[:-3]
|
||||
|
||||
val = number_format == "#,##,###.##" and 2 or 3
|
||||
val = 2 if number_format.string == "#,##,###.##" else 3
|
||||
|
||||
while len(amount) > val:
|
||||
parts.append(amount[-val:])
|
||||
|
|
@ -1409,7 +1407,9 @@ def fmt_money(
|
|||
|
||||
parts.reverse()
|
||||
|
||||
amount = comma_str.join(parts) + ((precision and decimal_str) and (decimal_str + decimals) or "")
|
||||
amount = number_format.thousands_separator.join(parts) + (
|
||||
(precision and number_format.decimal_separator) and (number_format.decimal_separator + decimals) or ""
|
||||
)
|
||||
if amount != "0":
|
||||
amount = minus + amount
|
||||
|
||||
|
|
@ -1425,29 +1425,21 @@ def fmt_money(
|
|||
return amount
|
||||
|
||||
|
||||
number_format_info = {
|
||||
"#,###.##": (".", ",", 2),
|
||||
"#.###,##": (",", ".", 2),
|
||||
"# ###.##": (".", " ", 2),
|
||||
"# ###,##": (",", " ", 2),
|
||||
"#'###.##": (".", "'", 2),
|
||||
"#, ###.##": (".", ", ", 2),
|
||||
"#,##,###.##": (".", ",", 2),
|
||||
"#,###.###": (".", ",", 3),
|
||||
"#.###": ("", ".", 0),
|
||||
"#,###": ("", ",", 0),
|
||||
"#.########": (".", "", 8),
|
||||
}
|
||||
# keep for backwards compatibility
|
||||
number_format_info = NUMBER_FORMAT_MAP
|
||||
|
||||
|
||||
@deprecated
|
||||
def get_number_format_info(format: str) -> tuple[str, str, int]:
|
||||
"""Return the decimal separator, thousands separator and precision for the given number `format` string.
|
||||
"""DEPRECATED: use `NumberFormat.from_string()` from `frappe.utils.number_format` instead.
|
||||
|
||||
e.g. get_number_format_info('1,00,000.50') -> ('.', ',', 2)
|
||||
Return the decimal separator, thousands separator and precision for the given number `format` string.
|
||||
|
||||
e.g. get_number_format_info('#,##,###.##') -> ('.', ',', 2)
|
||||
|
||||
Will return ('.', ',', 2) for format strings which can't be guessed.
|
||||
"""
|
||||
return number_format_info.get(format) or (".", ",", 2)
|
||||
return NUMBER_FORMAT_MAP.get(format) or (".", ",", 2)
|
||||
|
||||
|
||||
#
|
||||
|
|
@ -1481,13 +1473,13 @@ def money_in_words(
|
|||
"Cent"
|
||||
)
|
||||
|
||||
number_format = (
|
||||
frappe.db.get_value("Currency", main_currency, "number_format", cache=True)
|
||||
or frappe.db.get_default("number_format")
|
||||
or "#,###.##"
|
||||
)
|
||||
currency_format_str = frappe.db.get_value("Currency", main_currency, "number_format", cache=True)
|
||||
if currency_format_str:
|
||||
number_format = NumberFormat.from_string(currency_format_str)
|
||||
else:
|
||||
number_format = get_number_format()
|
||||
|
||||
fraction_length = get_number_format_info(number_format)[2]
|
||||
fraction_length = number_format.precision
|
||||
|
||||
n = f"%.{fraction_length}f" % number
|
||||
|
||||
|
|
@ -1499,7 +1491,7 @@ def money_in_words(
|
|||
fraction += zeros
|
||||
|
||||
in_million = True
|
||||
if number_format == "#,##,###.##":
|
||||
if number_format.string == "#,##,###.##":
|
||||
in_million = False
|
||||
|
||||
# 0.00
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import datetime
|
|||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
from frappe.locale import get_date_format
|
||||
from frappe.utils import add_to_date, get_datetime, getdate
|
||||
from frappe.utils.data import (
|
||||
get_first_day,
|
||||
|
|
@ -75,7 +76,7 @@ def parse_date(date):
|
|||
|
||||
def get_user_date_format():
|
||||
if getattr(frappe.local, "user_date_format", None) is None:
|
||||
frappe.local.user_date_format = frappe.defaults.get_global_default("date_format") or "yyyy-mm-dd"
|
||||
frappe.local.user_date_format = get_date_format()
|
||||
|
||||
return frappe.local.user_date_format
|
||||
|
||||
|
|
|
|||
36
frappe/utils/number_format.py
Normal file
36
frappe/utils/number_format.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
NUMBER_FORMAT_MAP = {
|
||||
"#,###.##": (".", ",", 2),
|
||||
"#.###,##": (",", ".", 2),
|
||||
"# ###.##": (".", " ", 2),
|
||||
"# ###,##": (",", " ", 2),
|
||||
"#'###.##": (".", "'", 2),
|
||||
"#, ###.##": (".", ", ", 2),
|
||||
"#,##,###.##": (".", ",", 2),
|
||||
"#,###.###": (".", ",", 3),
|
||||
"#.###": ("", ".", 0),
|
||||
"#,###": ("", ",", 0),
|
||||
"#.########": (".", "", 8),
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class NumberFormat:
|
||||
precision: int
|
||||
decimal_separator: str
|
||||
thousands_separator: str
|
||||
string: str
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, number_format: str) -> "NumberFormat":
|
||||
decimal_separator, thousands_separator, precision = NUMBER_FORMAT_MAP[number_format]
|
||||
return NumberFormat(
|
||||
precision=precision,
|
||||
decimal_separator=decimal_separator,
|
||||
thousands_separator=thousands_separator,
|
||||
string=number_format,
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.string
|
||||
|
|
@ -21,11 +21,13 @@ from frappe import _
|
|||
from frappe.core.utils import html2text
|
||||
from frappe.frappeclient import FrappeClient
|
||||
from frappe.handler import execute_cmd
|
||||
from frappe.locale import get_date_format, get_number_format, get_time_format
|
||||
from frappe.model.delete_doc import delete_doc
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.model.rename_doc import rename_doc
|
||||
from frappe.modules import scrub
|
||||
from frappe.utils.background_jobs import enqueue, get_jobs
|
||||
from frappe.utils.number_format import NumberFormat
|
||||
from frappe.website.utils import get_next_link, get_toc
|
||||
from frappe.www.printview import get_visible_columns
|
||||
|
||||
|
|
@ -164,11 +166,13 @@ def get_safe_globals():
|
|||
datautils = frappe._dict()
|
||||
|
||||
if frappe.db:
|
||||
date_format = frappe.db.get_default("date_format") or "yyyy-mm-dd"
|
||||
time_format = frappe.db.get_default("time_format") or "HH:mm:ss"
|
||||
date_format = get_date_format()
|
||||
time_format = get_time_format()
|
||||
number_format = get_number_format()
|
||||
else:
|
||||
date_format = "yyyy-mm-dd"
|
||||
time_format = "HH:mm:ss"
|
||||
number_format = NumberFormat.from_string("#,###.##")
|
||||
|
||||
add_data_utils(datautils)
|
||||
|
||||
|
|
@ -194,6 +198,7 @@ def get_safe_globals():
|
|||
format_value=frappe.format_value,
|
||||
date_format=date_format,
|
||||
time_format=time_format,
|
||||
number_format=number_format,
|
||||
format_date=frappe.utils.data.global_date_format,
|
||||
form_dict=form_dict,
|
||||
bold=frappe.bold,
|
||||
|
|
|
|||
|
|
@ -164,6 +164,8 @@ def get_home_page_via_hooks():
|
|||
|
||||
|
||||
def get_boot_data():
|
||||
from frappe.locale import get_date_format, get_first_day_of_the_week, get_number_format, get_time_format
|
||||
|
||||
return {
|
||||
"lang": frappe.local.lang or "en",
|
||||
"apps_data": {
|
||||
|
|
@ -173,10 +175,10 @@ def get_boot_data():
|
|||
},
|
||||
"sysdefaults": {
|
||||
"float_precision": cint(frappe.get_system_settings("float_precision")) or 3,
|
||||
"date_format": frappe.get_system_settings("date_format") or "yyyy-mm-dd",
|
||||
"time_format": frappe.get_system_settings("time_format") or "HH:mm:ss",
|
||||
"first_day_of_the_week": frappe.get_system_settings("first_day_of_the_week") or "Sunday",
|
||||
"number_format": frappe.get_system_settings("number_format") or "#,###.##",
|
||||
"date_format": get_date_format(),
|
||||
"time_format": get_time_format(),
|
||||
"first_day_of_the_week": get_first_day_of_the_week(),
|
||||
"number_format": get_number_format().string,
|
||||
},
|
||||
"time_zone": {
|
||||
"system": get_system_timezone(),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue