From 7792d456043f15e05c8d60037ad2a17ecef3debb Mon Sep 17 00:00:00 2001 From: "Louis Wicket (wil)" Date: Thu, 27 Apr 2023 13:57:56 +0000 Subject: [PATCH] [FIX] iap_mail, web: fix untranslatable text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some translations could not be exported, as reported in odoo/odoo#114711. This is due to Babel—the library we use to extract `_t` occurrences from the sources—ignoring javascript template literals, resulting in any gettext inside template literals not being parsed and exported as translatable resources. From version 2.12.0 of the library, support for javascript template literals can be enabled by providing the `parse_template_string` option to the Babel `extract` method. Unfortunately, the library cannot be updated for now, as our policy is to stick to the version of packages from the official Debian repository. This commit refactors some code so that no gettext is inlined within template literals. closes odoo/odoo#119996 Related: odoo/enterprise#40457 Signed-off-by: Louis Wicket (wil) --- .../js/services/iap_notification_service.js | 6 +++- .../control_panel_model_extension.js | 9 ++++-- .../src/legacy/js/fields/relational_fields.js | 7 ++++- addons/web/static/src/search/search_model.js | 20 ++++++++----- .../src/views/calendar/calendar_controller.js | 3 +- .../filter_panel/calendar_filter_panel.js | 2 +- .../view_dialogs/select_create_dialog.js | 6 +++- .../widgets/res_config_invite_users.js | 28 +++++++++++++++---- .../webclient/user_menu/user_menu_items.js | 6 +++- 9 files changed, 67 insertions(+), 20 deletions(-) diff --git a/addons/iap_mail/static/src/js/services/iap_notification_service.js b/addons/iap_mail/static/src/js/services/iap_notification_service.js index d4285940226..93e99a08157 100644 --- a/addons/iap_mail/static/src/js/services/iap_notification_service.js +++ b/addons/iap_mail/static/src/js/services/iap_notification_service.js @@ -1,6 +1,7 @@ /** @odoo-module **/ import { Markup } from 'web.utils'; +import { _t } from "@web/core/l10n/translation"; import { registry } from "@web/core/registry"; export const iapNotificationService = { @@ -33,7 +34,10 @@ export const iapNotificationService = { * Displays the IAP failure notification on user's screen */ function displayFailureIapNotification(notif) { - const message = Markup` ${env._t("Buy more credits")}`; + // ℹ️ `_t` can only be inlined directly inside JS template literals + // after Babel has been updated to version 2.12. + const translatedText = _t("Buy more credits"); + const message = Markup` ${translatedText}`; notification.add(message, { type: notif.error_type, title: notif.title diff --git a/addons/web/static/src/legacy/js/control_panel/control_panel_model_extension.js b/addons/web/static/src/legacy/js/control_panel/control_panel_model_extension.js index 32f8c654801..89f899b3082 100644 --- a/addons/web/static/src/legacy/js/control_panel/control_panel_model_extension.js +++ b/addons/web/static/src/legacy/js/control_panel/control_panel_model_extension.js @@ -1,5 +1,7 @@ /** @odoo-module alias=web/static/src/js/control_panel/control_panel_model_extension.js **/ - + + import { _t } from "@web/core/l10n/translation"; + import { sprintf } from "@web/core/utils/strings"; import ActionModel from "web.ActionModel"; import Domain from "web.Domain"; import pyUtils from "web.py_utils"; @@ -436,7 +438,10 @@ ); } catch (err) { throw new Error( - `${this.env._t("Control panel model extension failed to evaluate domain")}:/n${JSON.stringify(err)}` + sprintf( + _t("Control panel model extension failed to evaluate domain:\n%(error)s"), + { error: JSON.stringify(err) } + ) ); } } diff --git a/addons/web/static/src/legacy/js/fields/relational_fields.js b/addons/web/static/src/legacy/js/fields/relational_fields.js index 9d11189800f..9c94cab0850 100644 --- a/addons/web/static/src/legacy/js/fields/relational_fields.js +++ b/addons/web/static/src/legacy/js/fields/relational_fields.js @@ -663,9 +663,14 @@ var FieldMany2One = AbstractField.extend({ const [id, fullName] = result; const displayName = this._getDisplayName(fullName).trim(); result[1] = displayName; + // ℹ️ `_t` can only be inlined directly inside JS template literals + // after Babel has been updated to version 2.12. + const translatedText = _t("Unnamed"); return { id, - label: escape(displayName) || `${escape(_t("Unnamed"))}`, + label: + escape(displayName) || + `${escape(translatedText)}`, value: displayName, name: displayName, }; diff --git a/addons/web/static/src/search/search_model.js b/addons/web/static/src/search/search_model.js index f70db93be5e..843ad1505a9 100644 --- a/addons/web/static/src/search/search_model.js +++ b/addons/web/static/src/search/search_model.js @@ -5,6 +5,7 @@ import { Domain } from "@web/core/domain"; import { evaluateExpr } from "@web/core/py_js/py"; import { sortBy } from "@web/core/utils/arrays"; import { deepCopy } from "@web/core/utils/objects"; +import { sprintf } from "@web/core/utils/strings"; import { SearchArchParser } from "./search_arch_parser"; import { constructDateDomain, @@ -1590,7 +1591,10 @@ export class SearchModel extends EventBus { return context; } catch (error) { throw new Error( - `${this.env._t("Failed to evaluate the context")} ${context}.\n${error.message}` + sprintf(_t("Failed to evaluate the context: %(context)s.\n%(error)s"), { + context, + error: error.message, + }) ); } } @@ -1675,9 +1679,10 @@ export class SearchModel extends EventBus { return params.raw ? domain : domain.toList(this.domainEvalContext); } catch (error) { throw new Error( - `${this.env._t("Failed to evaluate the domain")} ${domain.toString()}.\n${ - error.message - }` + sprintf(_t("Failed to evaluate the domain: %(domain)s.\n%(error)s"), { + domain: domain.toString(), + error: error.message, + }) ); } } @@ -2075,9 +2080,10 @@ export class SearchModel extends EventBus { } } catch (error) { throw new Error( - `${this.env._t("Failed to evaluate the context")} "${ - searchItem.context - }".\n${error.message}` + sprintf(_t("Failed to evaluate the context: %(context)s.\n%(error)s"), { + context: searchItem.context, + error: error.message, + }) ); } } diff --git a/addons/web/static/src/views/calendar/calendar_controller.js b/addons/web/static/src/views/calendar/calendar_controller.js index 7944d6c62a3..1e34b5bc224 100644 --- a/addons/web/static/src/views/calendar/calendar_controller.js +++ b/addons/web/static/src/views/calendar/calendar_controller.js @@ -3,6 +3,7 @@ import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog"; import { _lt, _t } from "@web/core/l10n/translation"; import { useOwnedDialogs, useService } from "@web/core/utils/hooks"; +import { sprintf } from "@web/core/utils/strings"; import { Layout } from "@web/search/layout"; import { useModel } from "@web/views/model"; import { ViewScaleSelector } from "@web/views/view_components/view_scale_selector"; @@ -163,7 +164,7 @@ export class CalendarController extends Component { resModel: this.model.resModel, resId: record.id || false, context, - title: record.id ? `${_t("Open")}: ${record.title}` : _t("New Event"), + title: record.id ? sprintf(_t("Open: %s"), record.title) : _t("New Event"), viewId: this.model.formViewId, onRecordSaved: () => this.model.load(), }, diff --git a/addons/web/static/src/views/calendar/filter_panel/calendar_filter_panel.js b/addons/web/static/src/views/calendar/filter_panel/calendar_filter_panel.js index e38caed7dfd..c1ce925ec8a 100644 --- a/addons/web/static/src/views/calendar/filter_panel/calendar_filter_panel.js +++ b/addons/web/static/src/views/calendar/filter_panel/calendar_filter_panel.js @@ -29,7 +29,7 @@ export class CalendarFilterPanel extends Component { return { autoSelect: true, resetOnSelect: true, - placeholder: `+ ${_t("Add")} ${section.label}`, + placeholder: sprintf(_t("+ Add %s"), section.label), sources: [ { placeholder: _t("Loading..."), diff --git a/addons/web/static/src/views/view_dialogs/select_create_dialog.js b/addons/web/static/src/views/view_dialogs/select_create_dialog.js index 65ff76ae508..5642bc6679b 100644 --- a/addons/web/static/src/views/view_dialogs/select_create_dialog.js +++ b/addons/web/static/src/views/view_dialogs/select_create_dialog.js @@ -1,6 +1,7 @@ /** @odoo-module **/ import { Dialog } from "@web/core/dialog/dialog"; +import { _t } from "@web/core/l10n/translation"; import { useService } from "@web/core/utils/hooks"; import { View } from "@web/views/view"; import { escape } from "@web/core/utils/strings"; @@ -14,11 +15,14 @@ export class SelectCreateDialog extends Component { this.viewService = useService("view"); this.dialogService = useService("dialog"); this.state = useState({ resIds: [] }); + // ℹ️ `_t` can only be inlined directly inside JS template literals + // after Babel has been updated to version 2.12. + const translatedText = _t("No records found!"); this.baseViewProps = { display: { searchPanel: false }, editable: false, // readonly noBreadcrumbs: true, - noContentHelp: markup(`

${escape(this.env._t("No records found!"))}

`), + noContentHelp: markup(`

${escape(translatedText)}

`), showButtons: false, selectRecord: (resId) => this.select([resId]), onSelectionChanged: (resIds) => { diff --git a/addons/web/static/src/webclient/settings_form_view/widgets/res_config_invite_users.js b/addons/web/static/src/webclient/settings_form_view/widgets/res_config_invite_users.js index 0718acd8d1f..a9944c05d32 100644 --- a/addons/web/static/src/webclient/settings_form_view/widgets/res_config_invite_users.js +++ b/addons/web/static/src/webclient/settings_form_view/widgets/res_config_invite_users.js @@ -4,6 +4,7 @@ import { registry } from "@web/core/registry"; import { _t } from "@web/core/l10n/translation"; import { unique } from "@web/core/utils/arrays"; import { useService } from "@web/core/utils/hooks"; +import { sprintf } from "@web/core/utils/strings"; import { Component, useState, onWillStart } from "@odoo/owl"; import { standardWidgetProps } from "@web/views/widgets/standard_widget_props"; @@ -19,6 +20,7 @@ class ResConfigInviteUsers extends Component { this.invite = useService("user_invite"); this.action = useService("action"); this.notification = useService("notification"); + this.user = useService("user"); this.state = useState({ status: "idle", // idle, inviting @@ -61,11 +63,27 @@ class ResConfigInviteUsers extends Component { } } if (invalidEmails.length) { - throw new Error( - `${_t("Invalid email address")}${ - invalidEmails.length > 1 ? "es" : "" - }: ${invalidEmails.join(", ")}` - ); + const errorMessage = (() => { + const listFormatter = new Intl.ListFormat(this.user.lang.replace("_", "-"), { + type: "conjunction", + style: "long", + }); + switch (invalidEmails.length) { + case 1: + return sprintf(_t("Invalid email address: %(address)s"), { + address: invalidEmails[0], + }); + case 2: + return sprintf(_t("Invalid email addresses: %(2 addresses)s"), { + "2 addresses": listFormatter.format(invalidEmails), + }); + default: + return sprintf(_t("Invalid email addresses: %(addresses)s"), { + addresses: listFormatter.format(invalidEmails), + }); + } + })(); + throw new Error(errorMessage); } } diff --git a/addons/web/static/src/webclient/user_menu/user_menu_items.js b/addons/web/static/src/webclient/user_menu/user_menu_items.js index 11ddd5629cd..03c09995f38 100644 --- a/addons/web/static/src/webclient/user_menu/user_menu_items.js +++ b/addons/web/static/src/webclient/user_menu/user_menu_items.js @@ -2,6 +2,7 @@ import { Component, markup } from "@odoo/owl"; import { isMacOS } from "@web/core/browser/feature_detection"; +import { _t } from "@web/core/l10n/translation"; import { escape } from "@web/core/utils/strings"; import { session } from "@web/session"; import { browser } from "../../core/browser/browser"; @@ -43,13 +44,16 @@ class ShortcutsFooterComponent extends Component { ShortcutsFooterComponent.template = "web.UserMenu.ShortcutsFooterComponent"; function shortCutsItem(env) { + // ℹ️ `_t` can only be inlined directly inside JS template literals after + // Babel has been updated to version 2.12. + const translatedText = _t("Shortcuts"); return { type: "item", id: "shortcuts", hide: env.isSmall, description: markup( `
- ${escape(env._t("Shortcuts"))} + ${escape(translatedText)} ${isMacOS() ? "CMD" : "CTRL"}+K
` ),