[IMP] *: unify sprintf and gettext

Improve gettext to directly handle value injection within translations,
removing the need for sprintf.

closes odoo/odoo#123932

Related: odoo/enterprise#45370
Signed-off-by: Samuel Degueldre (sad) <sad@odoo.com>
This commit is contained in:
Louis Wicket (wil)
2023-08-10 18:14:04 +02:00
parent 62fba62067
commit f1722c7334
123 changed files with 353 additions and 495 deletions
@@ -7,7 +7,6 @@ import { formatAST, toPyValue } from "@web/core/py_js/py_utils";
import { registry } from "@web/core/registry";
import { TagsList } from "@web/core/tags_list/tags_list";
import { useOwnedDialogs, useService } from "@web/core/utils/hooks";
import { sprintf } from "@web/core/utils/strings";
import { Expression } from "@web/core/domain_tree";
const SEARCH_LIMIT = 7;
@@ -89,7 +88,7 @@ class AutoCompleteWithSources extends Component {
const dynamicFilters = ids.length
? [
{
description: sprintf(_t("Quick search: %s"), name),
description: _t("Quick search: %s", name),
domain: [["id", operator, ids]],
},
]
@@ -97,7 +96,7 @@ class AutoCompleteWithSources extends Component {
// fine for now but we don't like this kind of dependence of core to views
const SelectCreateDialog = registry.category("dialogs").get("select_create");
this.addDialog(SelectCreateDialog, {
title: sprintf(_t("Search: %s"), fieldString),
title: _t("Search: %s", fieldString),
dynamicFilters,
resModel,
noCreate: true,
@@ -140,13 +139,13 @@ const getFormat = (val, displayNames) => {
text =
typeof displayNames[val] === "string"
? displayNames[val]
: sprintf(_t(`Inaccessible/missing record ID: %s`), val);
: _t("Inaccessible/missing record ID: %s", val);
colorIndex = typeof displayNames[val] === "string" ? 0 : 2; // 0 = grey, 2 = orange
} else {
text =
val instanceof Expression
? String(val)
: sprintf(_t(`Invalid record ID: %s`), formatAST(toPyValue(val)));
: _t("Invalid record ID: %s", formatAST(toPyValue(val)));
colorIndex = val instanceof Expression ? 2 : 1; // 1 = red
}
return { text, colorIndex };
@@ -5,7 +5,6 @@ import { getDefaultValue, getDefaultOperator } from "./domain_selector_fields";
import { useService } from "@web/core/utils/hooks";
import { _t } from "@web/core/l10n/translation";
import { unique, zip } from "@web/core/utils/arrays";
import { sprintf } from "@web/core/utils/strings";
import {
Expression,
toValue,
@@ -183,7 +182,7 @@ function formatValue(val, disambiguate, fieldDef, displayNames) {
if (typeof displayNames[val] === "string") {
val = displayNames[val];
} else {
return sprintf(_t(`Inaccessible/missing record ID: %s`), val);
return _t("Inaccessible/missing record ID: %s", val);
}
}
if (fieldDef?.type === "selection") {
@@ -1,7 +1,6 @@
/** @odoo-module **/
import { useService } from "../utils/hooks";
import { sprintf } from "../utils/strings";
import { ConfirmationDialog } from "../confirmation_dialog/confirmation_dialog";
import { Component } from "@odoo/owl";
@@ -16,7 +15,10 @@ export class FileUploadProgressBar extends Component {
return;
}
this.dialogService.add(ConfirmationDialog, {
body: sprintf(this.env._t("Do you really want to cancel the upload of %s?"), this.props.fileUpload.title),
body: this.env._t(
"Do you really want to cancel the upload of %s?",
this.props.fileUpload.title
),
confirm: () => {
this.props.fileUpload.xhr.abort();
},
@@ -1,6 +1,5 @@
/** @odoo-module **/
import { sprintf } from "../utils/strings";
import { FileUploadProgressBar } from "./file_upload_progress_bar";
import { Component } from "@odoo/owl";
@@ -18,8 +17,8 @@ export class FileUploadProgressRecord extends Component {
const mbLoaded = Math.round(fileUpload.loaded / 1000000);
const mbTotal = Math.round(fileUpload.total / 1000000);
return {
left: sprintf(this.env._t("Uploading... (%s%)"), percent),
right: sprintf(this.env._t("(%s/%sMB)"), mbLoaded, mbTotal),
left: this.env._t("Uploading... (%s%)", percent),
right: this.env._t("(%s/%sMB)", mbLoaded, mbTotal),
};
}
}
@@ -1,7 +1,6 @@
/** @odoo-module **/
import { registry } from "../registry";
import { sprintf } from "../utils/strings";
import { EventBus, reactive } from "@odoo/owl";
@@ -56,7 +55,7 @@ export const fileUploadService = {
loaded: 0,
total: 0,
state: "pending",
title: files.length === 1 ? files[0].name : sprintf(env._t("%s Files"), files.length),
title: files.length === 1 ? files[0].name : env._t("%s Files", files.length),
type: files.length === 1 ? files[0].type : undefined,
});
uploads[upload.id] = upload;
+1 -2
View File
@@ -3,7 +3,6 @@
import { localization } from "@web/core/l10n/localization";
import { _t } from "@web/core/l10n/translation";
import { memoize } from "@web/core/utils/functions";
import { sprintf } from "@web/core/utils/strings";
import { ensureArray } from "../utils/arrays";
const { DateTime, Settings } = luxon;
@@ -436,7 +435,7 @@ export function parseDateTime(value, options = {}) {
// No working parsing methods: throw an error
if (!isValidDate(result)) {
throw new ConversionError(sprintf(_t("'%s' is not a correct date or datetime"), value));
throw new ConversionError(_t("'%s' is not a correct date or datetime", value));
}
// Revert to original numbering system
+19 -5
View File
@@ -1,6 +1,8 @@
/** @odoo-module **/
import { Deferred } from "@web/core/utils/concurrency";
import { sprintf } from "@web/core/utils/strings";
export const translationLoaded = Symbol("translationLoaded");
export const translatedTerms = {
[translationLoaded]: false,
@@ -17,11 +19,15 @@ export const translationIsReady = new Deferred();
* @param {string} term
* @returns {string}
*/
export function _t(term) {
export function _t(term, ...values) {
if (translatedTerms[translationLoaded]) {
return translatedTerms[term] || term;
const translation = translatedTerms[term] ?? term;
if (values.length === 0) {
return translation;
}
return sprintf(translation, ...values);
} else {
return new LazyTranslatedString(term);
return new LazyTranslatedString(term, ...values);
}
}
@@ -35,13 +41,21 @@ export function _t(term) {
* @param {string} term
* @returns {LazyTranslatedString}
*/
export const _lt = (term) => _t(term);
export const _lt = (term, ...values) => _t(term, ...values);
class LazyTranslatedString extends String {
constructor(term, ...values) {
super(term);
this.values = values;
}
valueOf() {
const term = super.valueOf();
if (translatedTerms[translationLoaded]) {
return translatedTerms[term] || term;
const translation = translatedTerms[term] ?? term;
if (this.values.length === 0) {
return translation;
}
return sprintf(translation, ...this.values);
} else {
throw new Error(`translation error`);
}
@@ -1,6 +1,5 @@
/** @odoo-module **/
import { sprintf } from "@web/core/utils/strings";
import core from "@web/legacy/js/services/core";
import session from "web.session";
import time from "@web/legacy/js/core/time";
@@ -133,7 +132,7 @@ export function parseDate(value, field, options) {
return date;
}
}
throw new Error(sprintf(core._t("'%s' is not a correct date"), value));
throw new Error(core._t("'%s' is not a correct date", value));
}
/**
@@ -186,5 +185,5 @@ export function parseDateTime(value, field, options) {
return datetime;
}
}
throw new Error(sprintf(core._t("'%s' is not a correct datetime"), value));
throw new Error(core._t("'%s' is not a correct datetime", value));
}
+2 -3
View File
@@ -1,6 +1,5 @@
/** @odoo-module **/
import { sprintf } from "@web/core/utils/strings";
import translation from "@web/legacy/js/core/translation";
import utils from "@web/legacy/js/core/utils";
@@ -190,7 +189,7 @@ export function auto_str_to_date (value) {
try {
return str_to_time(value);
} catch {}
throw new Error(sprintf(_t("'%s' is not a correct date, datetime nor time"), value));
throw new Error(_t("'%s' is not a correct date, datetime nor time", value));
}
export function auto_date_to_str (value, type) {
@@ -202,7 +201,7 @@ export function auto_date_to_str (value, type) {
case 'time':
return time_to_str(value);
default:
throw new Error(sprintf(_t("'%s' is not convertible to date, datetime nor time"), type));
throw new Error(_t("'%s' is not convertible to date, datetime nor time", type));
}
}
@@ -1,6 +1,7 @@
/** @odoo-module **/
import { sprintf } from "@web/core/utils/strings";
import Class from "@web/legacy/js/core/class";
var TranslationDataBase = Class.extend(/** @lends instance.TranslationDataBase# */{
@@ -39,9 +40,12 @@ var TranslationDataBase = Class.extend(/** @lends instance.TranslationDataBase#
},
build_translation_function: function() {
var self = this;
var fcnt = function(str) {
var tmp = self.get(str);
return tmp === undefined ? str : tmp;
var fcnt = function(term, ...values) {
const translation = self.get(term) ?? term;
if (values.length === 0) {
return translation;
}
return sprintf(translation, ...values);
};
fcnt.database = this;
return fcnt;
@@ -2,7 +2,6 @@
import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { _t } from "@web/core/l10n/translation";
import { sprintf } from "@web/core/utils/strings";
import { DataPoint } from "./datapoint";
import { Record } from "./record";
@@ -200,8 +199,8 @@ export class DynamicList extends DataPoint {
resIds.length === this.model.activeIdsLimit &&
resIds.length < this.count
) {
const msg = sprintf(
_t(`Only the first %s records have been deleted (out of %s selected)`),
const msg = _t(
`Only the first %s records have been deleted (out of %s selected)`,
resIds.length,
this.count
);
@@ -366,8 +365,8 @@ export class DynamicList extends DataPoint {
resIds.length === this.model.activeIdsLimit &&
resIds.length < this.count
) {
const msg = sprintf(
_t("Of the %s records selected, only the first %s have been archived/unarchived."),
const msg = _t(
"Of the %s records selected, only the first %s have been archived/unarchived.",
resIds.length,
this.count
);
@@ -7,7 +7,6 @@ import { useService } from "@web/core/utils/hooks";
import { SearchBar } from "../search_bar/search_bar";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { useCommand } from "@web/core/commands/command_hook";
import { sprintf } from "@web/core/utils/strings";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { Component, useState, onMounted, useExternalListener, useRef, useEffect } from "@odoo/owl";
@@ -35,7 +34,7 @@ export class ControlPanel extends Component {
const { viewSwitcherEntries, viewType } = this.env.config;
for (const view of viewSwitcherEntries || []) {
useCommand(
sprintf(this.env._t("Show %s view"), view.name),
this.env._t("Show %s view", view.name),
() => this.onViewClicked(view.type),
{
category: "view_switcher",
+3 -4
View File
@@ -5,7 +5,6 @@ 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,
@@ -1501,7 +1500,7 @@ export class SearchModel extends EventBus {
return context;
} catch (error) {
throw new Error(
sprintf(_t("Failed to evaluate the context: %(context)s.\n%(error)s"), {
_t("Failed to evaluate the context: %(context)s.\n%(error)s", {
context,
error: error.message,
})
@@ -1589,7 +1588,7 @@ export class SearchModel extends EventBus {
return params.raw ? domain : domain.toList(this.domainEvalContext);
} catch (error) {
throw new Error(
sprintf(_t("Failed to evaluate the domain: %(domain)s.\n%(error)s"), {
_t("Failed to evaluate the domain: %(domain)s.\n%(error)s", {
domain: domain.toString(),
error: error.message,
})
@@ -1991,7 +1990,7 @@ export class SearchModel extends EventBus {
}
} catch (error) {
throw new Error(
sprintf(_t("Failed to evaluate the context: %(context)s.\n%(error)s"), {
_t("Failed to evaluate the context: %(context)s.\n%(error)s", {
context: searchItem.context,
error: error.message,
})
@@ -3,7 +3,6 @@
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { _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 { useModelWithSampleData } from "@web/model/model";
import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog";
@@ -175,7 +174,7 @@ export class CalendarController extends Component {
resModel: this.model.resModel,
resId: record.id || false,
context,
title: record.id ? sprintf(_t("Open: %s"), record.title) : _t("New Event"),
title: record.id ? _t("Open: %s", record.title) : _t("New Event"),
viewId: this.model.formViewId,
onRecordSaved: () => this.model.load(),
},
@@ -4,7 +4,6 @@ import { _t } from "@web/core/l10n/translation";
import { AutoComplete } from "@web/core/autocomplete/autocomplete";
import { Transition } from "@web/core/transition";
import { useOwnedDialogs, useService } from "@web/core/utils/hooks";
import { sprintf } from "@web/core/utils/strings";
import { SelectCreateDialog } from "@web/views/view_dialogs/select_create_dialog";
import { getColor } from "../colors";
import { Component, useState } from "@odoo/owl";
@@ -29,7 +28,7 @@ export class CalendarFilterPanel extends Component {
return {
autoSelect: true,
resetOnSelect: true,
placeholder: sprintf(_t("+ Add %s"), section.label),
placeholder: _t("+ Add %s", section.label),
sources: [
{
placeholder: _t("Loading..."),
@@ -93,11 +92,11 @@ export class CalendarFilterPanel extends Component {
context: {},
});
dynamicFilters.push({
description: sprintf(_t("Quick search: %s"), request),
description: _t("Quick search: %s", request),
domain: [["id", "in", nameGets.map((nameGet) => nameGet[0])]],
});
}
const title = sprintf(_t("Search: %s"), section.label);
const title = _t("Search: %s", section.label);
this.addDialog(SelectCreateDialog, {
title,
noCreate: true,
@@ -1,7 +1,6 @@
/** @odoo-module **/
import { useService } from "@web/core/utils/hooks";
import { sprintf } from "@web/core/utils/strings";
import { getDataURLFromFile } from "@web/core/utils/urls";
import { session } from "@web/session";
import { formatFloat } from "./formatters";
@@ -32,14 +31,11 @@ export class FileUploader extends Component {
for (const file of ev.target.files) {
if (file.size > this.maxUploadSize) {
this.notification.add(
sprintf(
this.env._t("The selected file exceed the maximum file size of %s."),
this.env._t(
"The selected file exceed the maximum file size of %s.",
formatFloat(this.maxUploadSize, { humanReadable: true })
),
{
title: this.env._t("File upload"),
type: "danger",
}
{ title: this.env._t("File upload"), type: "danger" }
);
}
this.state.isUploading = true;
@@ -4,7 +4,7 @@ import { formatDate, formatDateTime } from "@web/core/l10n/dates";
import { localization as l10n } from "@web/core/l10n/localization";
import { _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { escape, intersperse, nbsp, sprintf } from "@web/core/utils/strings";
import { escape, intersperse, nbsp } from "@web/core/utils/strings";
import { isBinarySize } from "@web/core/utils/binary";
import { markup } from "@odoo/owl";
@@ -364,7 +364,7 @@ export function formatX2many(value) {
} else if (count === 1) {
return _t("1 record");
} else {
return sprintf(_t("%s records"), count);
return _t("%s records", count);
}
}
@@ -8,7 +8,6 @@ import { _t } from "@web/core/l10n/translation";
import { evaluateExpr } from "@web/core/py_js/py";
import { registry } from "@web/core/registry";
import { useChildRef, useOwnedDialogs, useService } from "@web/core/utils/hooks";
import { sprintf } from "@web/core/utils/strings";
import { Many2XAutocomplete, useOpenMany2XRecord } from "@web/views/fields/relational_utils";
import * as BarcodeScanner from "@web/webclient/barcode/barcode_scanner";
import { standardFieldProps } from "../standard_field_props";
@@ -20,7 +19,7 @@ class CreateConfirmationDialog extends Component {
static components = { Dialog };
get title() {
return sprintf(this.env._t("New: %s"), this.props.name);
return this.env._t("New: %s", this.props.name);
}
async onCreate() {
@@ -10,7 +10,6 @@ import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { PropertyValue } from "./property_value";
import { useService } from "@web/core/utils/hooks";
import { usePopover } from "@web/core/popover/popover_hook";
import { sprintf } from "@web/core/utils/strings";
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { reposition } from "@web/core/position_hook";
import { archParseBoolean } from "@web/views/utils";
@@ -278,10 +277,8 @@ export class PropertiesField extends Component {
this.popover.close();
const dialogProps = {
title: _t("Delete Property Field"),
body: sprintf(
_t(
'Are you sure you want to delete this property field? It will be removed for everyone using the "%s" %s.'
),
body: _t(
'Are you sure you want to delete this property field? It will be removed for everyone using the "%s" %s.',
this.parentName,
this.parentString
),
@@ -325,7 +322,7 @@ export class PropertiesField extends Component {
propertiesDefinitions.push({
name: uuid(),
string: sprintf(_t("Property %s"), propertiesDefinitions.length + 1),
string: _t("Property %s", propertiesDefinitions.length + 1),
type: "char",
definition_changed: true,
});
@@ -583,7 +580,7 @@ async function actionAddProperty(env) {
if (addProperty) {
addProperty.click();
} else {
const message = sprintf(env._t("You can not create a new property."));
const message = env._t("You can not create a new property.");
env.services.notification.add(message, { type: "danger" });
}
}
@@ -12,7 +12,6 @@ import { Many2XAutocomplete } from "@web/views/fields/relational_utils";
import { useService, useOwnedDialogs } from "@web/core/utils/hooks";
import { PropertyDefinitionSelection } from "./property_definition_selection";
import { PropertyTags } from "./property_tags";
import { sprintf } from "@web/core/utils/strings";
import { SelectCreateDialog } from "@web/views/view_dialogs/select_create_dialog";
import { uuid } from "../../utils";
@@ -341,8 +340,8 @@ export class PropertyDefinition extends Component {
this.state.resModelDescription = result[0].display_name;
} catch {
// can not read the ir.model
this.state.resModelDescription = sprintf(
_t('You do not have access to the model "%s".'),
this.state.resModelDescription = _t(
'You do not have access to the model "%s".',
newModel
);
}
@@ -6,7 +6,6 @@ import { _t } from "@web/core/l10n/translation";
import { usePopover } from "@web/core/popover/popover_hook";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { sprintf } from "@web/core/utils/strings";
import { TagsList } from "@web/core/tags_list/tags_list";
import { Component } from "@odoo/owl";
@@ -139,7 +138,7 @@ export class PropertyTags extends Component {
return [
{
value: { toCreate: true, value: request },
label: sprintf(_t('Create "%s"'), request),
label: _t('Create "%s"', request),
classList: "o_field_property_dropdown_add",
},
];
@@ -13,7 +13,6 @@ import {
useOwnedDialogs,
useService,
} from "@web/core/utils/hooks";
import { sprintf } from "@web/core/utils/strings";
import { createElement } from "@web/core/utils/xml";
import { FormArchParser } from "@web/views/form/form_arch_parser";
import { loadSubViews } from "@web/views/form/form_controller";
@@ -283,7 +282,7 @@ export class Many2XAutocomplete extends Component {
if (this.props.quickCreate && request.length) {
options.push({
label: sprintf(this.env._t(`Create "%s"`), request),
label: this.env._t('Create "%s"', request),
classList: "o_m2o_dropdown_option o_m2o_dropdown_option_create",
action: async (params) => {
try {
@@ -363,13 +362,13 @@ export class Many2XAutocomplete extends Component {
dynamicFilters = [
{
description: sprintf(this.env._t("Quick search: %s"), request),
description: this.env._t("Quick search: %s", request),
domain: [["id", "in", nameGets.map((nameGet) => nameGet[0])]],
},
];
}
const title = sprintf(this.env._t("Search: %s"), fieldString);
const title = this.env._t("Search: %s", fieldString);
this.selectCreate({
domain,
context,
@@ -461,8 +460,7 @@ export function useOpenMany2XRecord({
let resolve = () => {};
if (!title) {
title = resId ? env._t("Open: %s") : env._t("Create %s");
title = sprintf(title, fieldString);
title = resId ? env._t("Open: %s", fieldString) : env._t("Create %s", fieldString);
}
const { create: canCreate, write: canWrite } = activeActions;
@@ -708,8 +706,9 @@ export function useOpenX2ManyRecord({
async function openRecord({ record, mode, context, title, onClose }) {
if (!title) {
title = record ? env._t("Open: %s") : env._t("Create %s");
title = sprintf(title, activeField.string);
title = record
? env._t("Open: %s", activeField.string)
: env._t("Create %s", activeField.string);
}
const list = getList();
const { archInfo, fields: _fields } = await getFormViewInfo({
@@ -5,7 +5,6 @@ import { formatDate, formatDateTime } from "@web/core/l10n/dates";
import { localization } from "@web/core/l10n/localization";
import { _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { sprintf } from "@web/core/utils/strings";
import { DateTimeField } from "../datetime/datetime_field";
import { standardFieldProps } from "../standard_field_props";
@@ -45,9 +44,9 @@ export class RemainingDaysField extends Component {
return this.formattedValue;
}
if (this.diffDays < 0) {
return sprintf(_t("%s days ago"), -this.diffDays);
return _t("%s days ago", -this.diffDays);
}
return sprintf(_t("In %s days"), this.diffDays);
return _t("In %s days", this.diffDays);
}
get formattedValue() {
@@ -6,7 +6,6 @@ import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { sprintf } from "@web/core/utils/strings";
import { formatSelection } from "../formatters";
import { standardFieldProps } from "../standard_field_props";
@@ -36,7 +35,7 @@ export class StateSelectionField extends Component {
const hotkeys = ["D", "F", "G"];
for (const [index, [value, label]] of this.options.entries()) {
useCommand(
sprintf(this.env._t("Set kanban state as %s"), label),
this.env._t("Set kanban state as %s", label),
() => {
this.updateRecord(value);
},
@@ -8,7 +8,7 @@ import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { groupBy } from "@web/core/utils/arrays";
import { escape, sprintf } from "@web/core/utils/strings";
import { escape } from "@web/core/utils/strings";
import { useSpecialData } from "@web/views/fields/relational_utils";
import { standardFieldProps } from "../standard_field_props";
@@ -35,7 +35,7 @@ export class StatusBarField extends Component {
setup() {
if (this.props.withCommand) {
const commandName = sprintf(this.env._t(`Move to %s...`), escape(this.displayName));
const commandName = this.env._t("Move to %s...", escape(this.displayName));
useCommand(
commandName,
() => {
@@ -61,7 +61,7 @@ export class StatusBarField extends Component {
}
);
useCommand(
sprintf(this.env._t(`Move to next %s`), this.displayName),
this.env._t("Move to next %s", this.displayName),
() => {
const options = this.computeItems(false);
const nextOption =
@@ -2,7 +2,6 @@
import { Dialog } from "@web/core/dialog/dialog";
import { useService } from "@web/core/utils/hooks";
import { sprintf } from "@web/core/utils/strings";
import { loadLanguages } from "@web/core/l10n/translation";
import { Component, onWillStart } from "@odoo/owl";
@@ -10,7 +9,7 @@ import { Component, onWillStart } from "@odoo/owl";
export class TranslationDialog extends Component {
setup() {
super.setup();
this.title = sprintf(this.env._t("Translate: %s"), this.props.fieldName);
this.title = this.env._t("Translate: %s", this.props.fieldName);
this.orm = useService("orm");
this.user = useService("user");
@@ -5,7 +5,6 @@ import { evalDomain } from "@web/core/domain";
import { _t } from "@web/core/l10n/translation";
import { Pager } from "@web/core/pager/pager";
import { registry } from "@web/core/registry";
import { sprintf } from "@web/core/utils/strings";
import {
useActiveActions,
useAddInlineRecord,
@@ -258,7 +257,7 @@ export class X2ManyField extends Component {
context = makeContext([this.props.context, context]);
if (this.isMany2Many) {
const { string } = this.props;
const title = sprintf(this.env._t("Add: %s"), string);
const title = this.env._t("Add: %s", string);
return this.selectCreate({ domain, context, title });
}
if (editable) {
@@ -7,7 +7,6 @@ import { getGroupBy } from "@web/search/utils/group_by";
import { GROUPABLE_TYPES } from "@web/search/utils/misc";
import { Model } from "@web/model/model";
import { computeReportMeasures, processMeasure } from "@web/views/utils";
import { sprintf } from "@web/core/utils/strings";
import { Domain } from "@web/core/domain";
export const SEP = " / ";
@@ -306,7 +305,7 @@ export class GraphModel extends Model {
if (!description) {
return this.env._t("Sum");
} else {
return sprintf(this.env._t("Sum (%s)"), description);
return this.env._t("Sum (%s)", description);
}
}
@@ -7,7 +7,6 @@ import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { usePopover } from "@web/core/popover/popover_hook";
import { memoize } from "@web/core/utils/functions";
import { useService } from "@web/core/utils/hooks";
import { sprintf } from "@web/core/utils/strings";
import { useDebounced } from "@web/core/utils/timing";
import { isRelational } from "@web/model/relational_model/utils";
import { isNull } from "@web/views/utils";
@@ -168,7 +167,7 @@ export class KanbanHeader extends Component {
context,
resId: value,
resModel: groupByField.relation,
title: sprintf(this.env._t("Edit: %s"), displayName),
title: this.env._t("Edit: %s", displayName),
onRecordSaved: async () => {
await this.props.list.load();
this.props.list.model.notify();
@@ -7,7 +7,6 @@ import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { registry } from "@web/core/registry";
import { useTooltip } from "@web/core/tooltip/tooltip_hook";
import { useService } from "@web/core/utils/hooks";
import { sprintf } from "@web/core/utils/strings";
import { url } from "@web/core/utils/urls";
import { useRecordObserver } from "@web/model/relational_model/utils";
import { Field } from "@web/views/fields/field";
@@ -334,10 +333,8 @@ export class KanbanRecord extends Component {
) {
this.dialog.add(KanbanCoverImageDialog, { autoOpen, fieldName, record });
} else {
const warning = sprintf(
env._t(
`Could not set the cover image: incorrect field ("%s") is provided in the view.`
),
const warning = env._t(
`Could not set the cover image: incorrect field ("%s") is provided in the view.`,
fieldName
);
this.notification.add({ title: warning, type: "danger" });
@@ -6,7 +6,6 @@ import { useHotkey } from "@web/core/hotkeys/hotkey_hook";
import { registry } from "@web/core/registry";
import { useBus, useService } from "@web/core/utils/hooks";
import { useSortable } from "@web/core/utils/sortable";
import { sprintf } from "@web/core/utils/strings";
import { isNull } from "@web/views/utils";
import { ColumnProgress } from "@web/views/view_components/column_progress";
import { useBounceButton } from "@web/views/view_hook";
@@ -329,7 +328,7 @@ export class KanbanRenderer extends Component {
if (this.exampleData && this.exampleData.ghostColumns) {
colNames = this.exampleData.ghostColumns;
} else {
colNames = [1, 2, 3, 4].map((num) => sprintf(this.env._t("Column %s"), num));
colNames = [1, 2, 3, 4].map((num) => this.env._t("Column %s", num));
}
return colNames.map((colName) => ({
name: colName,
@@ -7,7 +7,6 @@ import { evaluateExpr } from "@web/core/py_js/py";
import { registry } from "@web/core/registry";
import { Deferred, KeepLast } from "@web/core/utils/concurrency";
import { useBus, useService } from "@web/core/utils/hooks";
import { sprintf } from "@web/core/utils/strings";
import { cleanDomFromBootstrap } from "@web/legacy/utils";
import { View, ViewNotFoundError } from "@web/views/view";
import { ActionDialog } from "./action_dialog";
@@ -1306,10 +1305,7 @@ function makeActionManager(env) {
const view = _getView(viewType);
if (!view) {
throw new ViewNotFoundError(
sprintf(
env._t("No view of type '%s' could be found in the current action."),
viewType
)
env._t("No view of type '%s' could be found in the current action.", viewType)
);
}
const newController = controller.action.controllers[viewType] || {
@@ -33,10 +33,7 @@ class InvalidAction extends Component {
}
onMounted() {
const message = sprintf(
this.env._t("No action with id '%s' could be found"),
this.props.actionId
);
const message = this.env._t("No action with id '%s' could be found", this.props.actionId);
this.notification.add(message, { type: "danger" });
}
}
@@ -4,7 +4,6 @@ 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";
@@ -70,15 +69,15 @@ class ResConfigInviteUsers extends Component {
});
switch (invalidEmails.length) {
case 1:
return sprintf(_t("Invalid email address: %(address)s"), {
return _t("Invalid email address: %(address)s", {
address: invalidEmails[0],
});
case 2:
return sprintf(_t("Invalid email addresses: %(2 addresses)s"), {
return _t("Invalid email addresses: %(2 addresses)s", {
"2 addresses": listFormatter.format(invalidEmails),
});
default:
return sprintf(_t("Invalid email addresses: %(addresses)s"), {
return _t("Invalid email addresses: %(addresses)s", {
addresses: listFormatter.format(invalidEmails),
});
}
@@ -171,3 +171,23 @@ QUnit.test("tamil has the correct numbering system", async (assert) => {
"௧௦/௧௨/௨௦௨௧ ௧௨:௦௦:௦௦"
);
});
QUnit.test(
"_t fills the format specifiers in translated terms with its extra arguments",
async (assert) => {
patchWithCleanup(translatedTerms, { "Due in %s days": "Échéance dans %s jours" });
const translatedStr = _t("Due in %s days", 513);
assert.strictEqual(translatedStr, "Échéance dans 513 jours");
}
);
QUnit.test(
"_t fills the format specifiers in lazy translated terms with its extra arguments",
async (assert) => {
translatedTerms[translationLoaded] = false;
const translatedStr = _t("Due in %s days", 513);
patchWithCleanup(translatedTerms, { "Due in %s days": "Échéance dans %s jours" });
translatedTerms[translationLoaded] = true;
assert.equal(translatedStr, "Échéance dans 513 jours");
}
);