From 915191cb1863a20dca9dbdf88428d9ca2364a788 Mon Sep 17 00:00:00 2001 From: Julien Mougenot Date: Fri, 4 Feb 2022 10:07:14 +0000 Subject: [PATCH] [FIX] web: assign numbering system to luxon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this commit, the numbering system used by luxon was the default one set by the browser (in most cases: 'latn'). This caused issues when translating luxon dates with moment dates, as moment supports some locales differently (e.g.: ar_001 uses the 'arab' numbering system while Intl uses 'latn'). To have proper consistency, this commit assigns (as much as possible) the same numbering systems as luxon for each supported locale. Warning: since the numbering system is now set according to the user 'lang' parameter, it's now unsafe to use `DateTime.fromFormat(...)` with static date strings, as they might not be parsable with the current numbering system. closes odoo/odoo#84833 X-original-commit: 8bd8b299d57ea930b0549446900bd51d03800041 Signed-off-by: Géry Debongnie Signed-off-by: Julien Mougenot (jum) --- .../src/core/l10n/localization_service.js | 36 +++- .../search/filter_menu/custom_filter_item.js | 14 +- .../tests/core/l10n/translation_tests.js | 159 +++++++++++++----- 3 files changed, 154 insertions(+), 55 deletions(-) diff --git a/addons/web/static/src/core/l10n/localization_service.js b/addons/web/static/src/core/l10n/localization_service.js index 15c04bb904d..7ca45473704 100644 --- a/addons/web/static/src/core/l10n/localization_service.js +++ b/addons/web/static/src/core/l10n/localization_service.js @@ -1,11 +1,26 @@ /** @odoo-module **/ +import { session } from "@web/session"; import { browser } from "../browser/browser"; import { registry } from "../registry"; import { strftimeToLuxonFormat } from "./dates"; import { localization } from "./localization"; import { translatedTerms, _t } from "./translation"; -import { session } from "@web/session"; + +const { Settings } = luxon; + +/** @type {[RegExp, string][]} */ +const NUMBERING_SYSTEMS = [ + [/^ar-(sa|001)$/i, "arab"], + [/^bn/i, "beng"], + [/^bo/i, "tibt"], + // [/^fa/i, "Farsi (Persian)"], // No numberingSystem found in Intl + // [/^(hi|mr|ne)/i, "Hindi"], // No numberingSystem found in Intl + // [/^my/i, "Burmese"], // No numberingSystem found in Intl + [/^pa-in/i, "guru"], + [/^ta/i, "tamldec"], + [/.*/i, "latn"], +]; export const localizationService = { dependencies: ["user"], @@ -38,13 +53,18 @@ export const localizationService = { Object.setPrototypeOf(translatedTerms, terms); env._t = _t; - // Setup lang inside luxon. The locale codes received from the server contain "_", whereas - // the Intl codes use "-" (Unicode BCP 47). There's only one exception, which is locale - // "sr@latin", for which we manually fallback to the "sr-Latn-RS" locale. - if (lang === "sr@latin") { - luxon.Settings.defaultLocale = "sr-Latn-RS"; - } else if (lang) { - luxon.Settings.defaultLocale = lang.replace(/_/g, "-"); + if (lang) { + // Setup lang inside luxon. The locale codes received from the server contain "_", + // whereas the Intl codes use "-" (Unicode BCP 47). There's only one exception, which + // is locale "sr@latin", for which we manually fallback to the "sr-Latn-RS" locale. + const locale = lang === "sr@latin" ? "sr-Latn-RS" : lang.replace(/_/g, "-"); + Settings.defaultLocale = locale; + for (const [re, numberingSystem] of NUMBERING_SYSTEMS) { + if (re.test(locale)) { + Settings.defaultNumberingSystem = numberingSystem; + break; + } + } } const dateFormat = strftimeToLuxonFormat(userLocalization.date_format); diff --git a/addons/web/static/src/search/filter_menu/custom_filter_item.js b/addons/web/static/src/search/filter_menu/custom_filter_item.js index f62e0d0fd81..dca07157828 100644 --- a/addons/web/static/src/search/filter_menu/custom_filter_item.js +++ b/addons/web/static/src/search/filter_menu/custom_filter_item.js @@ -167,17 +167,17 @@ export class CustomFilterItem extends Component { condition.value = 0; break; } - case "date": { + case "date": + case "datetime": { condition.value = [DateTime.local()]; if (operator.symbol === "between") { condition.value.push(DateTime.local()); } - break; - } - case "datetime": { - condition.value = [DateTime.fromFormat("00:00:00", "hh:mm:ss")]; - if (operator.symbol === "between") { - condition.value.push(DateTime.fromFormat("23:59:59", "hh:mm:ss")); + if (genericType === "datetime") { + condition.value[0].set({ hour: 0, minute: 0, second: 0 }); + if (operator.symbol === "between") { + condition.value[1].set({ hour: 23, minute: 59, second: 59 }); + } } break; } diff --git a/addons/web/static/tests/core/l10n/translation_tests.js b/addons/web/static/tests/core/l10n/translation_tests.js index 8c03b1433a3..2ecaf0b9bab 100644 --- a/addons/web/static/tests/core/l10n/translation_tests.js +++ b/addons/web/static/tests/core/l10n/translation_tests.js @@ -1,22 +1,58 @@ /** @odoo-module **/ -import { browser } from "@web/core/browser/browser"; -import { translatedTerms, _lt } from "@web/core/l10n/translation"; -import { localizationService } from "@web/core/l10n/localization_service"; -import { registry } from "@web/core/registry"; -import { patch, unpatch } from "@web/core/utils/patch"; -import { session } from "@web/session"; +import { registerCleanup } from "@web/../tests/helpers/cleanup"; import { makeTestEnv } from "@web/../tests/helpers/mock_env"; import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services"; import { getFixture, mount, patchWithCleanup } from "@web/../tests/helpers/utils"; -import { registerCleanup } from "@web/../tests/helpers/cleanup"; +import { browser } from "@web/core/browser/browser"; +import { localizationService } from "@web/core/l10n/localization_service"; +import { translatedTerms, _lt } from "@web/core/l10n/translation"; +import { registry } from "@web/core/registry"; +import { patch, unpatch } from "@web/core/utils/patch"; +import { session } from "@web/session"; const { Component, xml } = owl; +const { DateTime, Settings } = luxon; + const terms = { Hello: "Bonjour" }; const serviceRegistry = registry.category("services"); class TestComponent extends Component {} +/** + * Patches the 'lang' of the user session and context. + * + * @param {string} lang + * @returns {Promise} + */ +const patchLang = async (lang) => { + const { defaultLocale, defaultNumberingSystem } = Settings; + registerCleanup(() => { + Settings.defaultLocale = defaultLocale; + Settings.defaultNumberingSystem = defaultNumberingSystem; + }); + patchWithCleanup(session.user_context, { lang }); + patchWithCleanup(browser, { + fetch: async () => ({ + ok: true, + json: async () => ({ + modules: {}, + lang_parameters: { + direction: "ltr", + date_format: "%d/%m/%Y", + time_format: "%H:%M:%S", + grouping: "[3,0]", + decimal_point: ",", + thousands_sep: ".", + week_start: 1, + }, + }), + }), + }); + serviceRegistry.add("localization", localizationService); + await makeTestEnv(); +}; + QUnit.module("Translations"); QUnit.test("can translate a text node", async (assert) => { @@ -61,37 +97,80 @@ QUnit.test("_t is in env", async (assert) => { }); QUnit.test("luxon is configured in the correct lang", async (assert) => { - const defaultLocale = luxon.Settings.defaultLocale; - registerCleanup(() => { - luxon.Settings.defaultLocale = defaultLocale; - }); - patchWithCleanup(session, { - user_context: { ...session.user_context, lang: "fr_BE" }, - }); - patchWithCleanup(browser, { - fetch() { - return { - ok: true, - json() { - return { - modules: {}, - lang_parameters: { - direction: "ltr", - date_format: "%d/%m/%Y", - time_format: "%H:%M:%S", - grouping: "[3,0]", - decimal_point: ",", - thousands_sep: ".", - week_start: 1, - }, - }; - }, - }; - }, - }); - serviceRegistry.add("localization", localizationService); - - await makeTestEnv(); - - assert.strictEqual(luxon.DateTime.utc(2021, 12, 10).toFormat("MMMM"), "décembre"); + await patchLang("fr_BE"); + assert.strictEqual(DateTime.utc(2021, 12, 10).toFormat("MMMM"), "décembre"); +}); + +QUnit.module("Numbering system"); + +QUnit.test("arabic has the correct numbering system (generic)", async (assert) => { + await patchLang("ar_001"); + assert.strictEqual( + DateTime.utc(2021, 12, 10).toFormat("dd MMM, yyyy hh:mm:ss"), + "١٠ ديسمبر, ٢٠٢١ ١٢:٠٠:٠٠" + ); +}); + +QUnit.test("arabic has the correct numbering system (Algeria)", async (assert) => { + await patchLang("ar_DZ"); + assert.strictEqual( + DateTime.utc(2021, 12, 10).toFormat("dd MMM, yyyy hh:mm:ss"), + "10 ديسمبر, 2021 12:00:00" + ); +}); + +QUnit.test("arabic has the correct numbering system (Lybia)", async (assert) => { + await patchLang("ar_LY"); + assert.strictEqual( + DateTime.utc(2021, 12, 10).toFormat("dd MMM, yyyy hh:mm:ss"), + "10 ديسمبر, 2021 12:00:00" + ); +}); + +QUnit.test("arabic has the correct numbering system (Morocco)", async (assert) => { + await patchLang("ar_MA"); + assert.strictEqual( + DateTime.utc(2021, 12, 10).toFormat("dd MMM, yyyy hh:mm:ss"), + "10 دجنبر, 2021 12:00:00" + ); +}); + +QUnit.test("arabic has the correct numbering system (Saudi Arabia)", async (assert) => { + await patchLang("ar_SA"); + assert.strictEqual( + DateTime.utc(2021, 12, 10).toFormat("dd MMM, yyyy hh:mm:ss"), + "١٠ جمادى الأولى, ٢٠٢١ ١٢:٠٠:٠٠" + ); +}); + +QUnit.test("arabic has the correct numbering system (Tunisia)", async (assert) => { + await patchLang("ar_TN"); + assert.strictEqual( + DateTime.utc(2021, 12, 10).toFormat("dd MMM, yyyy hh:mm:ss"), + "10 ديسمبر, 2021 12:00:00" + ); +}); + +QUnit.test("bengalese has the correct numbering system", async (assert) => { + await patchLang("bn"); + assert.strictEqual( + DateTime.utc(2021, 12, 10).toFormat("dd MMM, yyyy hh:mm:ss"), + "১০ ডিসেম্বর, ২০২১ ১২:০০:০০" + ); +}); + +QUnit.test("punjabi (gurmukhi) has the correct numbering system", async (assert) => { + await patchLang("pa_in"); + assert.strictEqual( + DateTime.utc(2021, 12, 10).toFormat("dd MMM, yyyy hh:mm:ss"), + "੧੦ M12, ੨੦੨੧ ੧੨:੦੦:੦੦" + ); +}); + +QUnit.test("tamil has the correct numbering system", async (assert) => { + await patchLang("ta"); + assert.strictEqual( + DateTime.utc(2021, 12, 10).toFormat("dd MMM, yyyy hh:mm:ss"), + "௧௦ டிச., ௨௦௨௧ ௧௨:௦௦:௦௦" + ); });