Backed out 19 changesets (bug 1613705) for causing build bustages complaining about Document.cpp. CLOSED TREE

Backed out changeset 2ee1091dd20d (bug 1613705)
Backed out changeset d377afc0b09f (bug 1613705)
Backed out changeset de9d4378f0ac (bug 1613705)
Backed out changeset 9843372abb6e (bug 1613705)
Backed out changeset 5fc5918e5905 (bug 1613705)
Backed out changeset a7aeae7afd49 (bug 1613705)
Backed out changeset 5d61617a5402 (bug 1613705)
Backed out changeset 85bf98573899 (bug 1613705)
Backed out changeset 175af8a1b8c2 (bug 1613705)
Backed out changeset 93fcb23d7898 (bug 1613705)
Backed out changeset 595529cd906f (bug 1613705)
Backed out changeset 9f3e2963d925 (bug 1613705)
Backed out changeset 442289058933 (bug 1613705)
Backed out changeset fc3b9acb0e81 (bug 1613705)
Backed out changeset 408983c64f7f (bug 1613705)
Backed out changeset 08b637fc3fcd (bug 1613705)
Backed out changeset 6ef0aafd2db0 (bug 1613705)
Backed out changeset d88b294e0a5e (bug 1613705)
Backed out changeset e6bebff87544 (bug 1613705)
This commit is contained in:
Butkovits Atila 2021-08-03 12:36:01 +03:00
parent f73208bdf7
commit 949da905e7
81 changed files with 1596 additions and 1730 deletions

22
Cargo.lock generated
View File

@ -2042,7 +2042,6 @@ dependencies = [
"l10nregistry",
"l10nregistry-ffi",
"lmdb-rkv-sys",
"localization-ffi",
"log",
"mapped_hyph",
"mdns_service",
@ -2858,27 +2857,6 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "localization-ffi"
version = "0.1.0"
dependencies = [
"async-trait",
"cstr",
"fluent",
"fluent-fallback",
"fluent-ffi",
"futures 0.3.15",
"futures-channel",
"l10nregistry",
"l10nregistry-ffi",
"moz_task",
"nserror",
"nsstring",
"thin-vec",
"unic-langid",
"xpcom",
]
[[package]]
name = "lock_api"
version = "0.4.4"

View File

@ -88,7 +88,7 @@ add_task(async function test_nimbus_experiments() {
() =>
content.document.querySelector(
"#remote-experiments-tbody tr:first-child td"
)?.innerText
).innerText
);
return content.document.querySelector(
"#remote-experiments-tbody tr:first-child td"
@ -124,7 +124,7 @@ add_task(async function test_remote_configuration() {
() =>
content.document.querySelector(
"#remote-features-tbody tr:first-child td"
)?.innerText
).innerText
);
return content.document.querySelector(
"#remote-features-tbody tr:first-child td"

View File

@ -69,12 +69,10 @@ if (AppConstants.platform == "macosx") {
const type = "extension";
async function assertTelemetryMatches(events) {
function assertTelemetryMatches(events) {
events = events.map(([method, object, value, extra]) => {
return { method, object, value, extra };
});
// Wait a tick for telemetry
await Promise.resolve().then();
TelemetryTestUtils.assertEvents(events, {
category: "addonsManager",
method: /^(action|link|view)$/,
@ -369,7 +367,7 @@ add_task(async function browseraction_contextmenu_manage_extension() {
info("Run tests in normal mode");
await main(false);
await assertTelemetryMatches([
assertTelemetryMatches([
["action", "browserAction", null, { action: "manage", addonId: id }],
["view", "aboutAddons", "detail", { addonId: id, type }],
["action", "browserAction", null, { action: "manage", addonId: id }],
@ -378,7 +376,7 @@ add_task(async function browseraction_contextmenu_manage_extension() {
info("Run tests in customize mode");
await main(true);
await assertTelemetryMatches([
assertTelemetryMatches([
["action", "browserAction", null, { action: "manage", addonId: id }],
["view", "aboutAddons", "detail", { addonId: id, type }],
["action", "browserAction", null, { action: "manage", addonId: id }],
@ -472,14 +470,9 @@ add_task(async function browseraction_contextmenu_remove_extension() {
_response: 1,
QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]),
confirmEx: function(...args) {
promptService._resolveArgs(args);
promptService._confirmExArgs = args;
return promptService._response;
},
confirmArgs() {
return new Promise(resolve => {
promptService._resolveArgs = resolve;
});
},
};
Services.prompt = promptService;
registerCleanupFunction(() => {
@ -488,7 +481,6 @@ add_task(async function browseraction_contextmenu_remove_extension() {
async function testContextMenu(menuId, customizing) {
info(`Open browserAction context menu in ${menuId}`);
let confirmArgs = promptService.confirmArgs();
let menu = await openContextMenu(menuId, buttonId, win);
info(`Choosing 'Remove Extension' in ${menuId} should show confirm dialog`);
@ -496,12 +488,11 @@ add_task(async function browseraction_contextmenu_remove_extension() {
".customize-context-removeExtension"
);
await closeChromeContextMenu(menuId, removeExtension, win);
let args = await confirmArgs;
is(args[1], `Remove ${name}?`);
is(promptService._confirmExArgs[1], `Remove ${name}?`);
if (!Services.prefs.getBoolPref("prompts.windowPromptSubDialog", false)) {
is(args[2], `Remove ${name} from ${brand}?`);
is(promptService._confirmExArgs[2], `Remove ${name} from ${brand}?`);
}
is(args[4], "Remove");
is(promptService._confirmExArgs[4], "Remove");
return menu;
}
@ -515,7 +506,7 @@ add_task(async function browseraction_contextmenu_remove_extension() {
win,
});
await assertTelemetryMatches([
assertTelemetryMatches([
[
"action",
"browserAction",
@ -538,7 +529,7 @@ add_task(async function browseraction_contextmenu_remove_extension() {
win,
});
await assertTelemetryMatches([
assertTelemetryMatches([
[
"action",
"browserAction",
@ -569,7 +560,7 @@ add_task(async function browseraction_contextmenu_remove_extension() {
await testContextMenu("toolbar-context-menu", false);
await uninstalled;
await assertTelemetryMatches([
assertTelemetryMatches([
[
"action",
"browserAction",

View File

@ -184,27 +184,34 @@ class _RemoteL10n {
*/
_createDOML10n() {
/* istanbul ignore next */
let useRemoteL10n = Services.prefs.getBoolPref(USE_REMOTE_L10N_PREF, true);
if (useRemoteL10n && !L10nRegistry.getInstance().hasSource("cfr")) {
async function* generateBundles(resourceIds) {
const appLocale = Services.locale.appLocaleAsBCP47;
const appLocales = Services.locale.appLocalesAsBCP47;
const l10nFluentDir = OS.Path.join(
OS.Constants.Path.localProfileDir,
RS_DOWNLOADED_FILE_SUBDIR
);
let cfrIndexedFileSource = new L10nFileSource(
const fs = new L10nFileSource(
"cfr",
[appLocale],
`file://${l10nFluentDir}/`,
{
addResourceOptions: {
allowOverrides: true,
},
},
[`file://${l10nFluentDir}/browser/newtab/asrouter.ftl`]
`file://${l10nFluentDir}/`
);
L10nRegistry.getInstance().registerSources([cfrIndexedFileSource]);
} else if (!useRemoteL10n && L10nRegistry.getInstance().hasSource("cfr")) {
L10nRegistry.getInstance().removeSources(["cfr"]);
// In the case that the Fluent file has not been downloaded from Remote Settings,
// `fetchFile` will return `false` and fall back to the packaged Fluent file.
const resource = await fs.fetchFile(appLocale, "asrouter.ftl");
for await (let bundle of L10nRegistry.getInstance().generateBundles(
appLocales.slice(0, 1),
resourceIds
)) {
// Override built-in messages with the resource loaded from remote settings for
// the app locale, i.e. the first item of `appLocales`.
if (resource) {
bundle.addResource(resource, { allowOverrides: true });
}
yield bundle;
}
// Now generating bundles for the rest of locales of `appLocales`.
yield* L10nRegistry.generateBundles(appLocales.slice(1), resourceIds);
}
return new DOMLocalization(
@ -215,7 +222,10 @@ class _RemoteL10n {
"branding/brand.ftl",
"browser/defaultBrowserNotification.ftl",
],
false
false,
Services.prefs.getBoolPref(USE_REMOTE_L10N_PREF, true)
? { generateBundles }
: {}
);
}

View File

@ -17,8 +17,13 @@ test_newtab({
const contextMenuItems = await content.openContextMenuAndGetOptions(
siteSelector
);
const contextMenuItemsText = contextMenuItems.map(v => v.textContent);
Assert.equal(contextMenuItems.length, 5, "Number of options is correct");
Assert.equal(
contextMenuItemsText.length,
5,
"Number of options is correct"
);
const expectedItemsText = [
"Pin",
@ -28,9 +33,10 @@ test_newtab({
"Dismiss",
];
for (let i = 0; i < contextMenuItems.length; i++) {
await ContentTaskUtils.waitForCondition(
() => contextMenuItems[i].textContent === expectedItemsText[i],
for (let i = 0; i < contextMenuItemsText.length; i++) {
Assert.equal(
contextMenuItemsText[i],
expectedItemsText[i],
"Name option is correct"
);
}
@ -61,8 +67,9 @@ test_newtab({
const contextMenuItems = await content.openContextMenuAndGetOptions(
siteSelector
);
await ContentTaskUtils.waitForCondition(
() => contextMenuItems[4].textContent === "Dismiss",
Assert.equal(
contextMenuItems[4].textContent,
"Dismiss",
"'Dismiss' is the 5th item in the context menu list"
);

View File

@ -5,28 +5,11 @@ describe("RemoteL10n", () => {
let sandbox;
let globals;
let domL10nStub;
let l10nRegStub;
let l10nRegInstance;
let fileSourceStub;
beforeEach(() => {
sandbox = sinon.createSandbox();
globals = new GlobalOverrider();
domL10nStub = sandbox.stub();
l10nRegInstance = {
hasSource: sandbox.stub(),
registerSources: sandbox.stub(),
removeSources: sandbox.stub(),
};
fileSourceStub = sandbox.stub();
l10nRegStub = {
getInstance: () => {
return l10nRegInstance;
},
};
globals.set("DOMLocalization", domL10nStub);
globals.set("L10nRegistry", l10nRegStub);
globals.set("L10nFileSource", fileSourceStub);
});
afterEach(() => {
sandbox.restore();
@ -68,7 +51,6 @@ describe("RemoteL10n", () => {
describe("#_createDOML10n", () => {
it("should load the remote Fluent file if USE_REMOTE_L10N_PREF is true", async () => {
sandbox.stub(global.Services.prefs, "getBoolPref").returns(true);
l10nRegInstance.hasSource.returns(false);
RemoteL10n._createDOML10n();
assert.calledOnce(domL10nStub);
@ -76,7 +58,7 @@ describe("RemoteL10n", () => {
// The first arg is the resource array,
// the second one is false (use async),
// and the third one is the bundle generator.
assert.equal(args.length, 2);
assert.equal(args.length, 3);
assert.deepEqual(args[0], [
"browser/newtab/asrouter.ftl",
"browser/branding/brandings.ftl",
@ -85,20 +67,17 @@ describe("RemoteL10n", () => {
"browser/defaultBrowserNotification.ftl",
]);
assert.isFalse(args[1]);
assert.calledOnce(l10nRegInstance.hasSource);
assert.calledOnce(l10nRegInstance.registerSources);
assert.notCalled(l10nRegInstance.removeSources);
assert.isFunction(args[2].generateBundles);
});
it("should load the local Fluent file if USE_REMOTE_L10N_PREF is false", () => {
sandbox.stub(global.Services.prefs, "getBoolPref").returns(false);
l10nRegInstance.hasSource.returns(true);
RemoteL10n._createDOML10n();
const { args } = domL10nStub.firstCall;
// The first arg is the resource array,
// the second one is false (use async),
// and the third one is null.
assert.equal(args.length, 2);
assert.equal(args.length, 3);
assert.deepEqual(args[0], [
"browser/newtab/asrouter.ftl",
"browser/branding/brandings.ftl",
@ -107,9 +86,7 @@ describe("RemoteL10n", () => {
"browser/defaultBrowserNotification.ftl",
]);
assert.isFalse(args[1]);
assert.calledOnce(l10nRegInstance.hasSource);
assert.notCalled(l10nRegInstance.registerSources);
assert.calledOnce(l10nRegInstance.removeSources);
assert.isEmpty(args[2]);
});
});
describe("#createElement", () => {

View File

@ -259,11 +259,13 @@ function getBundleForLocales(newLocales) {
Services.locale.lastFallbackLocale,
])
);
function generateBundles(resourceIds) {
return L10nRegistry.getInstance().generateBundles(locales, resourceIds);
}
return new Localization(
["browser/preferences/preferences.ftl", "branding/brand.ftl"],
false,
undefined,
locales
{ generateBundles }
);
}

View File

@ -284,12 +284,6 @@ add_task(async function testDisabledBrowserLanguages() {
is(pl.version, "1.0", "pl is the old 1.0 version");
assertLocaleOrder(selected, "en-US,he");
// Wait for the children menu to be populated.
await BrowserTestUtils.waitForCondition(
() => !!available.children.length,
"Children list populated"
);
// Only fr is enabled and not selected, so it's the only locale available.
assertAvailableLocales(available, ["fr"]);
@ -419,7 +413,7 @@ add_task(async function testReorderingBrowserLanguages() {
ok(secondDialogId, "There was an id on the second dialog");
ok(firstDialogId != secondDialogId, "The dialog ids are different");
ok(
parseInt(firstDialogId) < parseInt(secondDialogId),
firstDialogId < secondDialogId,
"The second dialog id is larger than the first"
);

View File

@ -9,17 +9,18 @@ const SUGGEST_ALL_PREF = "browser.search.suggest.enabled";
const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
async function getResultText(element, expectedValue, description = "") {
async function getResultText(element) {
await initAccessibilityService();
await BrowserTestUtils.waitForCondition(
() => {
let accessible = accService.getAccessibleFor(element);
return accessible !== null && accessible.name === expectedValue;
},
description,
200
);
// Text localized with Fluent will only be available after the next refresh.
// requestAnimationFrame resolves at the beginning of the refresh driver tick
// at a time when accessible events haven't been processed yet.
// waitForTick puts us at the end of the event queue.
await new Promise(requestAnimationFrame);
await TestUtils.waitForTick();
let accessible = accService.getAccessibleFor(element);
return accessible.name;
}
let accService;
@ -74,8 +75,8 @@ add_task(async function switchToTab() {
window,
index
);
await getResultText(
element,
is(
await getResultText(element),
"about: robots— Switch to Tab",
"Result a11y label should be: <title>— Switch to Tab"
);
@ -136,15 +137,15 @@ add_task(async function searchSuggestions() {
element.toggleAttribute("selected", true);
}
if (result.searchParams.inPrivateWindow) {
await getResultText(
element,
Assert.equal(
await getResultText(element),
searchTerm + "— Search in a Private Window",
"Check result label"
);
} else {
let suggestion = expectedSearches.shift();
await getResultText(
element,
Assert.equal(
await getResultText(element),
suggestion +
"— Search with browser_searchSuggestionEngine searchSuggestionEngine.xml",
"Check result label"

View File

@ -4140,6 +4140,18 @@ bool Document::GetAllowPlugins() {
return true;
}
void Document::EnsureL10n() {
if (!mDocumentL10n) {
Element* elem = GetDocumentElement();
if (NS_WARN_IF(!elem)) {
return;
}
bool isSync = elem->HasAttr(kNameSpaceID_None, nsGkAtoms::datal10nsync);
mDocumentL10n = DocumentL10n::Create(this, isSync);
MOZ_ASSERT(mDocumentL10n);
}
}
bool Document::HasPendingInitialTranslation() {
return mDocumentL10n && mDocumentL10n->GetState() != DocumentL10nState::Ready;
}
@ -4162,20 +4174,15 @@ void Document::LocalizationLinkAdded(Element* aLinkElement) {
return;
}
EnsureL10n();
nsAutoString href;
aLinkElement->GetAttr(kNameSpaceID_None, nsGkAtoms::href, href);
if (!mDocumentL10n) {
Element* elem = GetDocumentElement();
MOZ_DIAGNOSTIC_ASSERT(elem);
bool isSync = elem->HasAttr(nsGkAtoms::datal10nsync);
mDocumentL10n = DocumentL10n::Create(this, isSync);
MOZ_ASSERT(mDocumentL10n);
}
mDocumentL10n->AddResourceId(NS_ConvertUTF16toUTF8(href));
mDocumentL10n->AddResourceId(href);
if (mReadyState >= READYSTATE_INTERACTIVE) {
mDocumentL10n->Activate(true);
mDocumentL10n->TriggerInitialTranslation();
} else {
if (!mDocumentL10n->mBlockingLayout) {
@ -4196,8 +4203,7 @@ void Document::LocalizationLinkRemoved(Element* aLinkElement) {
if (mDocumentL10n) {
nsAutoString href;
aLinkElement->GetAttr(kNameSpaceID_None, nsGkAtoms::href, href);
uint32_t remaining =
mDocumentL10n->RemoveResourceId(NS_ConvertUTF16toUTF8(href));
uint32_t remaining = mDocumentL10n->RemoveResourceId(href);
if (remaining == 0) {
if (mDocumentL10n->mBlockingLayout) {
mDocumentL10n->mBlockingLayout = false;
@ -4221,8 +4227,9 @@ void Document::LocalizationLinkRemoved(Element* aLinkElement) {
* collected.
*/
void Document::OnL10nResourceContainerParsed() {
// XXX: This is a scaffolding for where we might inject prefetch
// in bug 1717241.
if (mDocumentL10n) {
mDocumentL10n->Activate(false);
}
}
void Document::OnParsingCompleted() {

View File

@ -3854,6 +3854,8 @@ class Document : public nsINode,
private:
bool IsErrorPage() const;
void EnsureL10n();
// Takes the bits from mStyleUseCounters if appropriate, and sets them in
// mUseCounters.
void SetCssUseCounterBits();

View File

@ -465,6 +465,7 @@ DOMInterfaces = {
},
'Localization': {
'implicitJSContext': [ 'formatValue', 'formatValues', 'formatMessages', 'formatValueSync', 'formatValuesSync', 'formatMessagesSync' ],
'nativeType': 'mozilla::intl::Localization',
},

View File

@ -435,17 +435,6 @@ template <typename K, typename V>
return true;
}
template <typename T>
[[nodiscard]] bool ToJSValue(JSContext* aCx, const Nullable<T>& aArgument,
JS::MutableHandle<JS::Value> aValue) {
if (aArgument.IsNull()) {
aValue.setNull();
return true;
}
return ToJSValue(aCx, aArgument.Value(), aValue);
}
} // namespace dom
} // namespace mozilla

View File

@ -14,10 +14,6 @@ enum L10nFileSourceHasFileStatus {
"unknown"
};
dictionary FileSourceOptions {
FluentBundleAddResourceOptions addResourceOptions = {};
};
/**
* The interface represents a single source location for
* the registry.
@ -41,7 +37,7 @@ interface L10nFileSource {
* files not available in the source.
*/
[Throws]
constructor(UTF8String name, sequence<UTF8String> locales, UTF8String prePath, optional FileSourceOptions options = {}, optional sequence<UTF8String> index);
constructor(UTF8String name, sequence<UTF8String> locales, UTF8String prePath, optional sequence<UTF8String> index);
/**
* Tests may want to introduce custom file sources and
@ -109,13 +105,9 @@ interface FluentBundleAsyncIterator {
[Alias="@@asyncIterator"] FluentBundleAsyncIterator values();
};
dictionary L10nRegistryOptions {
FluentBundleOptions bundleOptions = {};
};
[ChromeOnly, Exposed=Window]
interface L10nRegistry {
constructor(optional L10nRegistryOptions aOptions = {});
constructor();
static L10nRegistry getInstance();

View File

@ -35,53 +35,44 @@ NS_IMPL_RELEASE_INHERITED(DOMLocalization, Localization)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMLocalization)
NS_INTERFACE_MAP_END_INHERITING(Localization)
DOMLocalization::DOMLocalization(nsIGlobalObject* aGlobal, bool aSync)
: Localization(aGlobal, aSync) {
mMutations = new L10nMutations(this);
/* static */
already_AddRefed<DOMLocalization> DOMLocalization::Create(
nsIGlobalObject* aGlobal, const bool aSync,
const BundleGenerator& aBundleGenerator) {
RefPtr<DOMLocalization> domLoc =
new DOMLocalization(aGlobal, aSync, aBundleGenerator);
domLoc->Init();
return domLoc.forget();
}
DOMLocalization::DOMLocalization(nsIGlobalObject* aGlobal, bool aIsSync,
const ffi::LocalizationRc* aRaw)
: Localization(aGlobal, aIsSync, aRaw) {
DOMLocalization::DOMLocalization(nsIGlobalObject* aGlobal, const bool aSync,
const BundleGenerator& aBundleGenerator)
: Localization(aGlobal, aSync, aBundleGenerator) {
mMutations = new L10nMutations(this);
}
already_AddRefed<DOMLocalization> DOMLocalization::Constructor(
const GlobalObject& aGlobal, const Sequence<nsCString>& aResourceIds,
bool aIsSync, const Optional<NonNull<L10nRegistry>>& aRegistry,
const Optional<Sequence<nsCString>>& aLocales, ErrorResult& aRv) {
nsTArray<nsCString> resIds = ToTArray<nsTArray<nsCString>>(aResourceIds);
Maybe<nsTArray<nsCString>> locales;
if (aLocales.WasPassed()) {
locales.emplace();
locales->SetCapacity(aLocales.Value().Length());
for (const auto& locale : aLocales.Value()) {
locales->AppendElement(locale);
}
const GlobalObject& aGlobal, const Sequence<nsString>& aResourceIds,
const bool aSync, const BundleGenerator& aBundleGenerator,
ErrorResult& aRv) {
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
if (!global) {
aRv.Throw(NS_ERROR_FAILURE);
return nullptr;
}
RefPtr<const ffi::LocalizationRc> raw;
bool result;
RefPtr<DOMLocalization> domLoc =
DOMLocalization::Create(global, aSync, aBundleGenerator);
if (aRegistry.WasPassed()) {
result = ffi::localization_new_with_locales(
&resIds, aIsSync, aRegistry.Value().Raw(), locales.ptrOr(nullptr),
getter_AddRefs(raw));
} else {
result = ffi::localization_new_with_locales(
&resIds, aIsSync, nullptr, locales.ptrOr(nullptr), getter_AddRefs(raw));
if (aResourceIds.Length()) {
domLoc->AddResourceIds(aResourceIds);
}
if (result) {
nsCOMPtr<nsIGlobalObject> global =
do_QueryInterface(aGlobal.GetAsSupports());
domLoc->Activate(true);
return do_AddRef(new DOMLocalization(global, aIsSync, raw));
}
aRv.ThrowInvalidStateError(
"Failed to create the Localization. Check the locales arguments.");
return nullptr;
return domLoc.forget();
}
JSObject* DOMLocalization::WrapObject(JSContext* aCx,
@ -311,6 +302,9 @@ already_AddRefed<Promise> DOMLocalization::TranslateElements(
return nullptr;
}
AutoEntryScript aes(mGlobal, "DOMLocalization TranslateElements");
JSContext* cx = aes.cx();
for (auto& domElement : aElements) {
if (!domElement->HasAttr(kNameSpaceID_None, nsGkAtoms::datal10nid)) {
continue;
@ -338,10 +332,10 @@ already_AddRefed<Promise> DOMLocalization::TranslateElements(
return nullptr;
}
if (IsSync()) {
if (mIsSync) {
nsTArray<Nullable<L10nMessage>> l10nMessages;
FormatMessagesSync(l10nKeys, l10nMessages, aRv);
FormatMessagesSync(cx, l10nKeys, l10nMessages, aRv);
bool allTranslated =
ApplyTranslations(domElements, l10nMessages, aProto, aRv);
@ -352,7 +346,7 @@ already_AddRefed<Promise> DOMLocalization::TranslateElements(
promise->MaybeResolveWithUndefined();
} else {
RefPtr<Promise> callbackResult = FormatMessages(l10nKeys, aRv);
RefPtr<Promise> callbackResult = FormatMessages(cx, l10nKeys, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
@ -534,7 +528,10 @@ bool DOMLocalization::ApplyTranslations(
void DOMLocalization::OnChange() {
Localization::OnChange();
RefPtr<Promise> promise = TranslateRoots(IgnoreErrors());
if (mLocalization && !mResourceIds.IsEmpty()) {
ErrorResult rv;
RefPtr<Promise> promise = TranslateRoots(rv);
}
}
void DOMLocalization::DisconnectMutations() {

View File

@ -14,8 +14,6 @@
#include "mozilla/dom/L10nMutations.h"
#include "mozilla/dom/L10nOverlaysBinding.h"
#include "mozilla/dom/LocalizationBinding.h"
#include "mozilla/dom/PromiseNativeHandler.h"
#include "mozilla/intl/L10nRegistry.h"
// XXX Avoid including this here by moving function bodies to the cpp file
#include "nsINode.h"
@ -31,13 +29,15 @@ class DOMLocalization : public intl::Localization {
NS_DECL_ISUPPORTS_INHERITED
NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DOMLocalization, Localization)
static already_AddRefed<DOMLocalization> Create(
nsIGlobalObject* aGlobal, const bool aSync,
const BundleGenerator& aBundleGenerator);
void Destroy();
static already_AddRefed<DOMLocalization> Constructor(
const dom::GlobalObject& aGlobal,
const dom::Sequence<nsCString>& aResourceIds, bool aIsSync,
const dom::Optional<dom::NonNull<intl::L10nRegistry>>& aRegistry,
const dom::Optional<dom::Sequence<nsCString>>& aLocales,
const GlobalObject& aGlobal, const Sequence<nsString>& aResourceIds,
const bool aSync, const BundleGenerator& aBundleGenerator,
ErrorResult& aRv);
virtual JSObject* WrapObject(JSContext* aCx,
@ -111,11 +111,9 @@ class DOMLocalization : public intl::Localization {
return false;
}
DOMLocalization(nsIGlobalObject* aGlobal, bool aSync);
DOMLocalization(nsIGlobalObject* aGlobal, bool aIsSync,
const intl::ffi::LocalizationRc* aRaw);
protected:
explicit DOMLocalization(nsIGlobalObject* aGlobal, const bool aSync,
const BundleGenerator& aBundleGenerator);
virtual ~DOMLocalization();
void OnChange() override;
void DisconnectMutations();

View File

@ -35,25 +35,33 @@ NS_INTERFACE_MAP_END_INHERITING(DOMLocalization)
bool DocumentL10n::mIsFirstBrowserWindow = true;
/* static */
RefPtr<DocumentL10n> DocumentL10n::Create(Document* aDocument, bool aSync) {
RefPtr<DocumentL10n> DocumentL10n::Create(Document* aDocument,
const bool aSync) {
RefPtr<DocumentL10n> l10n = new DocumentL10n(aDocument, aSync);
IgnoredErrorResult rv;
l10n->mReady = Promise::Create(l10n->mGlobal, rv);
if (NS_WARN_IF(rv.Failed())) {
if (!l10n->Init()) {
return nullptr;
}
return l10n.forget();
}
DocumentL10n::DocumentL10n(Document* aDocument, bool aSync)
: DOMLocalization(aDocument->GetScopeObject(), aSync),
DocumentL10n::DocumentL10n(Document* aDocument, const bool aSync)
: DOMLocalization(aDocument->GetScopeObject(), aSync, {}),
mDocument(aDocument),
mState(DocumentL10nState::Constructed) {
mContentSink = do_QueryInterface(aDocument->GetCurrentContentSink());
}
bool DocumentL10n::Init() {
DOMLocalization::Init();
ErrorResult rv;
mReady = Promise::Create(mGlobal, rv);
if (NS_WARN_IF(rv.Failed())) {
return false;
}
return true;
}
JSObject* DocumentL10n::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return DocumentL10n_Binding::Wrap(aCx, this, aGivenProto);
@ -296,7 +304,7 @@ void DocumentL10n::InitialTranslationCompleted(bool aL10nCached) {
// From now on, the state of Localization is unconditionally
// async.
SetAsync();
SetIsSync(false);
}
void DocumentL10n::ConnectRoot(nsINode& aNode, bool aTranslate,

View File

@ -47,10 +47,12 @@ class DocumentL10n final : public DOMLocalization {
NS_DECL_ISUPPORTS_INHERITED
NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DocumentL10n, DOMLocalization)
static RefPtr<DocumentL10n> Create(Document* aDocument, bool aSync);
static RefPtr<DocumentL10n> Create(Document* aDocument, const bool aSync);
protected:
explicit DocumentL10n(Document* aDocument, bool aSync);
explicit DocumentL10n(Document* aDocument, const bool aSync);
bool Init() override;
virtual ~DocumentL10n() = default;
RefPtr<Document> mDocument;

View File

@ -7,24 +7,22 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const l10nReg = new L10nRegistry();
const fs = [
{ path: "/localization/en-US/mock.ftl", source: `
async function* generateBundles(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource(`
key1 = Value for Key 1
key2 = Value for <a>Key 2<a/>.
` },
];
const source = L10nFileSource.createMock("test", ["en-US"], "/localization/{locale}", fs);
l10nReg.registerSources([source]);
`));
yield bundle;
}
SimpleTest.waitForExplicitFinish();
addLoadEvent(async () => {
const domLoc = new DOMLocalization(
["/mock.ftl"],
[],
false,
l10nReg,
["en-US"],
{ generateBundles },
);
await domLoc.translateFragment(document.body);

View File

@ -7,14 +7,13 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const l10nReg = new L10nRegistry();
const fs = [
{ path: "/localization/en-US/mock.ftl", source: `
async function* generateBundles(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource(`
key1 = Value for Key 1
` },
];
const source = L10nFileSource.createMock("test", ["en-US"], "/localization/{locale}", fs);
l10nReg.registerSources([source]);
`));
yield bundle;
}
window.onload = async function() {
SimpleTest.waitForExplicitFinish();
@ -22,10 +21,9 @@ key1 = Value for Key 1
const p1 = document.getElementById("p1");
const domLoc = new DOMLocalization(
["/mock.ftl"],
[],
false,
l10nReg,
["en-US"],
{ generateBundles },
);
await domLoc.translateRoots();

View File

@ -44,20 +44,18 @@
</script>
<script type="application/javascript">
"use strict";
const l10nReg = new L10nRegistry();
const fs = [
{ path: "/localization/en-US/mock.ftl", source: `
async function* generateBundles(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource(`
key1 = Value for Key 1
` },
];
const source = L10nFileSource.createMock("test", ["en-US"], "/localization/{locale}", fs);
l10nReg.registerSources([source]);
`));
yield bundle;
}
document.domLoc = new DOMLocalization(
["/mock.ftl"],
[],
false,
l10nReg,
["en-US"],
{ generateBundles }
);
</script>
</head>

View File

@ -7,15 +7,14 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const l10nReg = new L10nRegistry();
const fs = [
{ path: "/localization/en-US/mock.ftl", source: `
async function* generateBundles(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource(`
key1 = Value for Key 1
key2 = Value for Key 2
` },
];
const source = L10nFileSource.createMock("test", ["en-US"], "/localization/{locale}", fs);
l10nReg.registerSources([source]);
`));
yield bundle;
}
window.onload = async function() {
SimpleTest.waitForExplicitFinish();
@ -23,10 +22,9 @@ key2 = Value for Key 2
const p1 = document.getElementById("p1");
const domLoc = new DOMLocalization(
["/mock.ftl"],
[],
false,
l10nReg,
["en-US"],
{ generateBundles },
);
await domLoc.translateRoots();

View File

@ -10,9 +10,9 @@
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
<script type="application/javascript">
<![CDATA[
const l10nReg = new L10nRegistry();
const fs = [
{ path: "/localization/en-US/mock.ftl", source: `
async function* generateBundles(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource(`
file-menu =
.label = File
.accesskey = F
@ -20,10 +20,9 @@ new-tab =
.label = New Tab
.accesskey = N
container = Some text with an <image data-l10n-name="foo"> inside it.
` },
];
const source = L10nFileSource.createMock("test", ["en-US"], "/localization/{locale}", fs);
l10nReg.registerSources([source]);
`));
yield bundle;
}
SimpleTest.waitForExplicitFinish();
@ -31,12 +30,11 @@ container = Some text with an <image data-l10n-name="foo"> inside it.
const domLoc = new DOMLocalization(
[],
false,
l10nReg,
["en-US"],
{ generateBundles },
);
async function foo() {
domLoc.addResourceIds(["/mock.ftl"]);
domLoc.addResourceIds(["dummy.ftl"]);
domLoc.connectRoot(document.documentElement);
await domLoc.translateRoots();

View File

@ -8,12 +8,15 @@
<script type="application/javascript">
"use strict";
async function* generateBundles(resourceIds) {}
window.onload = function() {
SimpleTest.waitForExplicitFinish();
const domLoc = new DOMLocalization(
[],
false,
{ generateBundles },
);
const p1 = document.querySelectorAll("p")[0];

View File

@ -7,15 +7,12 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const l10nReg = new L10nRegistry();
const fs = [
{ path: "/localization/en-US/mock.ftl", source: `
title = Hello World
title2 = Hello Another World
` },
];
const source = L10nFileSource.createMock("test", ["en-US"], "/localization/{locale}", fs);
l10nReg.registerSources([source]);
async function* generateBundles(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource("title = Hello World"));
bundle.addResource(new FluentResource("title2 = Hello Another World"));
yield bundle;
}
window.onload = async function() {
SimpleTest.waitForExplicitFinish();
@ -23,13 +20,12 @@ title2 = Hello Another World
const domLoc = new DOMLocalization(
[],
false,
l10nReg,
["en-US"],
{ generateBundles },
);
const h1 = document.querySelectorAll("h1")[0];
domLoc.addResourceIds(["/mock.ftl"]);
domLoc.addResourceIds(["dummy.ftl"]);
domLoc.connectRoot(document.body);
await domLoc.translateRoots();

View File

@ -7,24 +7,20 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const l10nReg = new L10nRegistry();
const fs = [
{ path: "/localization/en-US/mock.ftl", source: `
title = <strong>Hello</strong> World
title2 = This is <a data-l10n-name="link">a link</a>!
` },
];
const source = L10nFileSource.createMock("test", ["en-US"], "/localization/{locale}", fs);
l10nReg.registerSources([source]);
async function* generateBundles(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource("title = <strong>Hello</strong> World"));
bundle.addResource(new FluentResource(`title2 = This is <a data-l10n-name="link">a link</a>!`));
yield bundle;
}
window.onload = async function() {
SimpleTest.waitForExplicitFinish();
const domLoc = new DOMLocalization(
["/mock.ftl"],
[],
false,
l10nReg,
["en-US"],
{ generateBundles },
);
const p1 = document.querySelectorAll("p")[0];

View File

@ -7,15 +7,18 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const l10nReg = new L10nRegistry();
async function* generateBundles(resourceIds) {
const bundle = new FluentBundle("en-US");
// No translations!
yield bundle;
}
SimpleTest.waitForExplicitFinish();
addLoadEvent(async () => {
const domLoc = new DOMLocalization(
[],
false,
l10nReg,
["en-US"],
{ generateBundles },
);
await domLoc.translateFragment(document.body).then(() => {

View File

@ -7,23 +7,19 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const l10nReg = new L10nRegistry();
const fs = [
{ path: "/localization/en-US/mock.ftl", source: `
title = Visit <a data-l10n-name="mozilla-link">Mozilla</a> or <a data-l10n-name="firefox-link">Firefox</a> website!
` },
];
const source = L10nFileSource.createMock("test", ["en-US"], "/localization/{locale}", fs);
l10nReg.registerSources([source]);
async function* generateBundles(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource(`title = Visit <a data-l10n-name="mozilla-link">Mozilla</a> or <a data-l10n-name="firefox-link">Firefox</a> website!`));
yield bundle;
}
window.onload = async function() {
SimpleTest.waitForExplicitFinish();
const domLoc = new DOMLocalization(
["/mock.ftl"],
[],
false,
l10nReg,
["en-US"],
{ generateBundles },
);
await domLoc.translateFragment(document.body);

View File

@ -7,23 +7,19 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const l10nReg = new L10nRegistry();
const fs = [
{ path: "/localization/en-US/mock.ftl", source: `
title = Visit <a data-l10n-name="mozilla-link">Mozilla</a> or <a data-l10n-name="firefox-link">Firefox</a> website!
` },
];
const source = L10nFileSource.createMock("test", ["en-US"], "/localization/{locale}", fs);
l10nReg.registerSources([source]);
async function* generateBundles(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource(`title = Visit <a data-l10n-name="mozilla-link">Mozilla</a> or <a data-l10n-name="firefox-link">Firefox</a> website!`));
yield bundle;
}
window.onload = async function() {
SimpleTest.waitForExplicitFinish();
const domLoc = new DOMLocalization(
["/mock.ftl"],
[],
false,
l10nReg,
["en-US"],
{ generateBundles },
);
await domLoc.translateFragment(document.body);

View File

@ -7,25 +7,23 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const l10nReg = new L10nRegistry();
const fs = [
{ path: "/localization/en-US/mock.ftl", source: `
async function* generateBundles(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource(`
key1 =
.href = https://www.hacked.com
key2 =
.href = https://pl.wikipedia.org
` },
];
const source = L10nFileSource.createMock("test", ["en-US"], "/localization/{locale}", fs);
l10nReg.registerSources([source]);
`));
yield bundle;
}
async function test() {
const domLoc = new DOMLocalization(
["/mock.ftl"],
[],
false,
l10nReg,
["en-US"],
{ generateBundles },
);
await domLoc.translateFragment(document.body);

View File

@ -7,24 +7,22 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const l10nReg = new L10nRegistry();
const fs = [
{ path: "/localization/en-US/mock.ftl", source: `
async function* generateBundles(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource(`
key1 = Translation For Key 1
key2 = Visit <a data-l10n-name="link">this link<a/>.
` },
];
const source = L10nFileSource.createMock("test", ["en-US"], "/localization/{locale}", fs);
l10nReg.registerSources([source]);
`));
yield bundle;
}
SimpleTest.waitForExplicitFinish();
addLoadEvent(async () => {
const domLoc = new DOMLocalization(
["/mock.ftl"],
[],
false,
l10nReg,
["en-US"],
{ generateBundles },
);
await domLoc.translateFragment(document.body);

View File

@ -7,7 +7,8 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const l10nReg = new L10nRegistry();
async function* generateBundles(resourceIds) {}
window.onload = function() {
SimpleTest.waitForExplicitFinish();
@ -15,7 +16,7 @@
const domLoc = new DOMLocalization(
[],
false,
l10nReg,
{ generateBundles },
);
const p1 = document.querySelectorAll("p")[0];

View File

@ -7,25 +7,20 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const l10nReg = new L10nRegistry();
const fs = [
{ path: "/localization/en-US/mock.ftl", source: `
title = Hello World
link =
.title = Click me
` },
];
const source = L10nFileSource.createMock("test", ["en-US"], "/localization/{locale}", fs);
l10nReg.registerSources([source]);
async function* generateBundles(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource("title = Hello World"));
bundle.addResource(new FluentResource("link =\n .title = Click me"));
yield bundle;
}
window.onload = async function() {
SimpleTest.waitForExplicitFinish();
const domLoc = new DOMLocalization(
["/mock.ftl"],
[],
false,
l10nReg,
["en-US"],
{ generateBundles },
);
const p1 = document.querySelectorAll("p")[0];

View File

@ -7,24 +7,20 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const l10nReg = new L10nRegistry();
const fs = [
{ path: "/localization/en-US/mock.ftl", source: `
title = Hello World
subtitle = Welcome to FluentBundle
` },
];
const source = L10nFileSource.createMock("test", ["en-US"], "/localization/{locale}", fs);
l10nReg.registerSources([source]);
async function* generateBundles(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource("title = Hello World"));
bundle.addResource(new FluentResource("subtitle = Welcome to FluentBundle"));
yield bundle;
}
window.onload = async function() {
SimpleTest.waitForExplicitFinish();
const domLoc = new DOMLocalization(
["/mock.ftl"],
[],
false,
l10nReg,
["en-US"],
{ generateBundles },
);
const frag = document.querySelectorAll("div")[0];

View File

@ -7,15 +7,12 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const l10nReg = new L10nRegistry();
const fs = [
{ path: "/localization/en-US/mock.ftl", source: `
title = Hello World
title2 = Hello Another World
` },
];
const source = L10nFileSource.createMock("test", ["en-US"], "/localization/{locale}", fs);
l10nReg.registerSources([source]);
async function* generateBundles(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource("title = Hello World"));
bundle.addResource(new FluentResource("title2 = Hello Another World"));
yield bundle;
}
window.onload = async function() {
SimpleTest.waitForExplicitFinish();
@ -23,8 +20,7 @@ title2 = Hello Another World
const domLoc = new DOMLocalization(
[],
false,
l10nReg,
["en-US"],
{ generateBundles },
);
const frag1 = document.querySelectorAll("div")[0];
@ -32,7 +28,7 @@ title2 = Hello Another World
const h1 = document.querySelectorAll("h1")[0];
const h2 = document.querySelectorAll("h2")[0];
domLoc.addResourceIds(["/mock.ftl"]);
domLoc.addResourceIds(["dummy.ftl"]);
domLoc.connectRoot(frag1);
domLoc.connectRoot(frag2);

View File

@ -7,23 +7,21 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const l10nReg = new L10nRegistry();
const fs = [
{ path: "/localization/en-US/mock.ftl", source: `
async function* generateBundles(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource(`
key1 = Key 1
key2 = Key 2
key3 = Key 3
key4 = Key 4
` },
];
const source = L10nFileSource.createMock("test", ["en-US"], "/localization/{locale}", fs);
l10nReg.registerSources([source]);
`));
yield bundle;
}
document.domLoc = new DOMLocalization(
["/mock.ftl"],
[],
false,
l10nReg,
["en-US"],
{ generateBundles }
);
document.domLoc.connectRoot(document.documentElement);
</script>

View File

@ -38,7 +38,7 @@
document.l10n.setAttributes(inputElem, "about-telemetry-filter-all-placeholder");
// Due to the async iteractions between nsINode.localize
// and DOMLocalization, we'll need to wait two frames
// and DOMLocalization.jsm, we'll need to wait two frames
// to verify that no mutations happened.
requestAnimationFrame(() => {
requestAnimationFrame(() => {

View File

@ -38,7 +38,7 @@
document.l10n.setAttributes(inputElem, "about-telemetry-filter-placeholder", {selectedTitle: "Test"});
// Due to the async iteractions between nsINode.localize
// and DOMLocalization, we'll need to wait two frames
// and DOMLocalization.jsm, we'll need to wait two frames
// to verify that no mutations happened.
requestAnimationFrame(() => {
requestAnimationFrame(() => {

View File

@ -114,15 +114,18 @@ void MediaControlService::Init() {
mControllerManager = MakeUnique<ControllerManager>(this);
// Initialize the fallback title
nsTArray<nsCString> resIds{
"branding/brand.ftl"_ns,
"dom/media.ftl"_ns,
};
RefPtr<Localization> l10n = Localization::Create(resIds, true);
nsCOMPtr<nsIGlobalObject> global =
xpc::NativeGlobal(xpc::PrivilegedJunkScope());
RefPtr<Localization> l10n = Localization::Create(global, true, {});
l10n->AddResourceId(u"branding/brand.ftl"_ns);
l10n->AddResourceId(u"dom/media.ftl"_ns);
{
AutoSafeJSContext cx;
nsAutoCString translation;
IgnoredErrorResult rv;
l10n->FormatValueSync("mediastatus-fallback-title"_ns, {}, translation, rv);
ErrorResult rv;
l10n->FormatValueSync(cx, "mediastatus-fallback-title"_ns, {}, translation,
rv);
if (!rv.Failed()) {
mFallbackTitle = NS_ConvertUTF8toUTF16(translation);
}

View File

@ -36,8 +36,7 @@ void DOMSecurityMonitor::AuditParsingOfHTMLXMLFragments(
uint32_t lineNum = 0;
uint32_t columnNum = 0;
JSContext* cx = nsContentUtils::GetCurrentJSContext();
if (!cx ||
!nsJSUtils::GetCallingLocation(cx, filename, &lineNum, &columnNum)) {
if (!nsJSUtils::GetCallingLocation(cx, filename, &lineNum, &columnNum)) {
return;
}

View File

@ -55,10 +55,7 @@ add_task(async function show_pointerlock_warning_escape() {
""
);
await BrowserTestUtils.waitForCondition(
() => warning.innerText == expectedWarningText,
"Warning text"
);
is(warning.innerText, expectedWarningText, "Warning text");
EventUtils.synthesizeKey("KEY_Escape");
await warningHiddenPromise;

View File

@ -36,14 +36,14 @@ interface DOMLocalization : Localization {
* This enables a number of synchronous methods on the
* Localization API and uses it for `TranslateElements`
* making the method return a synchronusly resolved promise.
* - aRegistry - optional custom L10nRegistry to be used by this Localization instance.
* - aLocales - custom set of locales to be used for this Localization.
* - aBundleGenerator - an object with two methods - `generateBundles` and
* `generateBundlesSync` allowing consumers to overload the
* default generators provided by Gecko.
*/
[Throws]
constructor(sequence<UTF8String> aResourceIds,
constructor(sequence<DOMString> aResourceIds,
optional boolean aSync = false,
optional L10nRegistry aRegistry,
optional sequence<UTF8String> aLocales);
optional BundleGenerator aBundleGenerator = {});
/**
* Adds a node to nodes observed for localization

View File

@ -43,6 +43,24 @@ dictionary L10nMessage {
sequence<AttributeNameValue>? attributes = null;
};
/**
* A callback function which takes a list of localization resources
* and produces an iterator over FluentBundle objects used for
* localization with fallbacks.
*/
callback GenerateBundles = Promise<any> (sequence<DOMString> aResourceIds);
callback GenerateBundlesSync = any (sequence<DOMString> aResourceIds);
/**
* The structure provides custom methods for the Localization API that
* will be used to generate the `FluentBundle` iterator.
* This allows the consumer to overload the default Gecko generator.
*/
dictionary BundleGenerator {
GenerateBundles generateBundles;
GenerateBundlesSync generateBundlesSync;
};
/**
* Localization is an implementation of the Fluent Localization API.
*
@ -69,26 +87,28 @@ interface Localization {
* - aSync - Specifies if the initial state of the Localization API is synchronous.
* This enables a number of synchronous methods on the
* Localization API.
* - aRegistry - optional custom L10nRegistry to be used by this Localization instance.
* - aLocales - custom set of locales to be used for this Localization.
* - aBundleGenerator - an object with two methods - `generateBundles` and
* `generateBundlesSync` allowing consumers to overload the
* default generators provided by Gecko.
*/
[Throws]
constructor(sequence<UTF8String> aResourceIds,
constructor(sequence<DOMString> aResourceIds,
optional boolean aSync = false,
optional L10nRegistry aRegistry,
optional sequence<UTF8String> aLocales);
optional BundleGenerator aBundleGenerator = {});
/**
* A method for adding resources to the localization context.
*
* Returns a new count of resources used by the context.
*/
void addResourceIds(sequence<UTF8String> aResourceIds);
unsigned long addResourceIds(sequence<DOMString> aResourceIds);
/**
* A method for removing resources from the localization context.
*
* Returns a new count of resources used by the context.
*/
unsigned long removeResourceIds(sequence<UTF8String> aResourceIds);
unsigned long removeResourceIds(sequence<DOMString> aResourceIds);
/**
* Formats a value of a localization message with a given id.
@ -139,7 +159,7 @@ interface Localization {
*/
[NewObject] Promise<sequence<L10nMessage?>> formatMessages(sequence<L10nKey> aKeys);
void setAsync();
void setIsSync(boolean aIsSync);
[NewObject, Throws]
UTF8String? formatValueSync(UTF8String aId, optional L10nArgs aArgs);

View File

@ -23,14 +23,11 @@ L10nFileSource::L10nFileSource(RefPtr<const ffi::FileSource> aRaw,
/* static */
already_AddRefed<L10nFileSource> L10nFileSource::Create(
const nsACString& aName, const nsTArray<nsCString>& aLocales,
const nsACString& aPrePath, const FileSourceOptions& aOptions,
ErrorResult& aRv) {
const nsACString& aPrePath, ErrorResult& aRv) {
ffi::L10nFileSourceStatus status;
bool allowOverrides = aOptions.mAddResourceOptions.mAllowOverrides;
RefPtr<const ffi::FileSource> raw(dont_AddRef(ffi::l10nfilesource_new(
&aName, &aLocales, &aPrePath, allowOverrides, &status)));
RefPtr<const ffi::FileSource> raw(dont_AddRef(
ffi::l10nfilesource_new(&aName, &aLocales, &aPrePath, &status)));
if (PopulateError(aRv, status)) {
return nullptr;
@ -43,22 +40,19 @@ already_AddRefed<L10nFileSource> L10nFileSource::Create(
already_AddRefed<L10nFileSource> L10nFileSource::Constructor(
const GlobalObject& aGlobal, const nsACString& aName,
const nsTArray<nsCString>& aLocales, const nsACString& aPrePath,
const dom::FileSourceOptions& aOptions,
const Optional<Sequence<nsCString>>& aIndex, ErrorResult& aRv) {
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
ffi::L10nFileSourceStatus status;
bool allowOverrides = aOptions.mAddResourceOptions.mAllowOverrides;
RefPtr<const ffi::FileSource> raw;
if (aIndex.WasPassed()) {
raw = dont_AddRef(ffi::l10nfilesource_new_with_index(
&aName, &aLocales, &aPrePath, aIndex.Value().Elements(),
aIndex.Value().Length(), allowOverrides, &status));
aIndex.Value().Length(), &status));
} else {
raw = dont_AddRef(ffi::l10nfilesource_new(&aName, &aLocales, &aPrePath,
allowOverrides, &status));
raw = dont_AddRef(
ffi::l10nfilesource_new(&aName, &aLocales, &aPrePath, &status));
}
if (PopulateError(aRv, status)) {

View File

@ -27,13 +27,11 @@ class L10nFileSource : public nsWrapperCache {
static already_AddRefed<L10nFileSource> Create(
const nsACString& aName, const nsTArray<nsCString>& aLocales,
const nsACString& aPrePath, const dom::FileSourceOptions& aOptions,
ErrorResult& aRv);
const nsACString& aPrePath, ErrorResult& aRv);
static already_AddRefed<L10nFileSource> Constructor(
const dom::GlobalObject& aGlobal, const nsACString& aName,
const nsTArray<nsCString>& aLocales, const nsACString& aPrePath,
const dom::FileSourceOptions& aOptions,
const dom::Optional<dom::Sequence<nsCString>>& aIndex, ErrorResult& aRv);
static already_AddRefed<L10nFileSource> CreateMock(

View File

@ -188,41 +188,35 @@ bool extendJSArrayWithErrors(JSContext* aCx, JS::Handle<JSObject*> aErrors,
return true;
}
/* static */
void FluentBundle::ConvertArgs(const L10nArgs& aArgs,
nsTArray<ffi::L10nArg>& aRetVal) {
aRetVal.SetCapacity(aArgs.Entries().Length());
for (const auto& entry : aArgs.Entries()) {
if (!entry.mValue.IsNull()) {
const auto& value = entry.mValue.Value();
if (value.IsUTF8String()) {
aRetVal.AppendElement(ffi::L10nArg{
&entry.mKey,
ffi::FluentArgument::String(&value.GetAsUTF8String())});
} else {
aRetVal.AppendElement(ffi::L10nArg{
&entry.mKey, ffi::FluentArgument::Double_(value.GetAsDouble())});
}
}
}
}
void FluentBundle::FormatPattern(JSContext* aCx, const FluentPattern& aPattern,
const Nullable<L10nArgs>& aArgs,
const Optional<JS::Handle<JSObject*>>& aErrors,
nsACString& aRetVal, ErrorResult& aRv) {
nsTArray<ffi::L10nArg> l10nArgs;
nsTArray<nsCString> argIds;
nsTArray<ffi::FluentArgument> argValues;
if (!aArgs.IsNull()) {
const L10nArgs& args = aArgs.Value();
ConvertArgs(args, l10nArgs);
for (auto& entry : args.Entries()) {
if (!entry.mValue.IsNull()) {
argIds.AppendElement(entry.mKey);
auto& value = entry.mValue.Value();
if (value.IsUTF8String()) {
argValues.AppendElement(
ffi::FluentArgument::String(&value.GetAsUTF8String()));
} else {
argValues.AppendElement(
ffi::FluentArgument::Double_(value.GetAsDouble()));
}
}
}
}
nsTArray<nsCString> errors;
bool succeeded = fluent_bundle_format_pattern(mRaw.get(), &aPattern.mId,
&aPattern.mAttrName, &l10nArgs,
&aRetVal, &errors);
&aPattern.mAttrName, &argIds,
&argValues, &aRetVal, &errors);
if (!succeeded) {
return aRv.ThrowInvalidStateError(

View File

@ -81,9 +81,6 @@ class FluentBundle final : public nsWrapperCache {
const dom::Optional<JS::Handle<JSObject*>>& aErrors,
nsACString& aRetVal, ErrorResult& aRv);
static void ConvertArgs(const L10nArgs& aArgs,
nsTArray<ffi::L10nArg>& aRetVal);
protected:
virtual ~FluentBundle();

View File

@ -114,9 +114,8 @@ NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(L10nRegistry, mGlobal)
NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(L10nRegistry, AddRef)
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(L10nRegistry, Release)
L10nRegistry::L10nRegistry(nsIGlobalObject* aGlobal, bool aUseIsolating)
: mGlobal(aGlobal),
mRaw(dont_AddRef(ffi::l10nregistry_new(aUseIsolating))) {}
L10nRegistry::L10nRegistry(nsIGlobalObject* aGlobal)
: mGlobal(aGlobal), mRaw(dont_AddRef(ffi::l10nregistry_new())) {}
L10nRegistry::L10nRegistry(nsIGlobalObject* aGlobal,
RefPtr<const ffi::GeckoL10nRegistry> aRaw)
@ -124,10 +123,9 @@ L10nRegistry::L10nRegistry(nsIGlobalObject* aGlobal,
/* static */
already_AddRefed<L10nRegistry> L10nRegistry::Constructor(
const GlobalObject& aGlobal, const L10nRegistryOptions& aOptions) {
const GlobalObject& aGlobal) {
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
return MakeAndAddRef<L10nRegistry>(global,
aOptions.mBundleOptions.mUseIsolating);
return MakeAndAddRef<L10nRegistry>(global);
}
/* static */

View File

@ -73,14 +73,12 @@ class L10nRegistry final : public nsWrapperCache {
NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(L10nRegistry)
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(L10nRegistry)
L10nRegistry(nsIGlobalObject* aGlobal, bool aUseIsolating);
explicit L10nRegistry(nsIGlobalObject* aGlobal);
L10nRegistry(nsIGlobalObject* aGlobal,
RefPtr<const ffi::GeckoL10nRegistry> aRaw);
static already_AddRefed<L10nRegistry> Constructor(
const dom::GlobalObject& aGlobal,
const dom::L10nRegistryOptions& aOptions);
const dom::GlobalObject& aGlobal);
static already_AddRefed<L10nRegistry> GetInstance(
const dom::GlobalObject& aGlobal);
@ -121,8 +119,6 @@ class L10nRegistry final : public nsWrapperCache {
JSObject* WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) override;
const ffi::GeckoL10nRegistry* Raw() const { return mRaw; }
protected:
virtual ~L10nRegistry() = default;
nsCOMPtr<nsIGlobalObject> mGlobal;

View File

@ -5,208 +5,162 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "Localization.h"
#include "nsImportModule.h"
#include "nsIObserverService.h"
#include "nsContentUtils.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/HoldDropJSObjects.h"
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"
#include "js/PropertyAndElement.h" // JS_GetProperty
#define INTL_APP_LOCALES_CHANGED "intl:app-locales-changed"
#define L10N_PSEUDO_PREF "intl.l10n.pseudo"
using namespace mozilla;
using namespace mozilla::dom;
using namespace mozilla::intl;
static const char* kObservedPrefs[] = {L10N_PSEUDO_PREF, nullptr};
// The state where the application contains incomplete localization resources
// is much more common than for other types of core resources.
//
// In result, we our localization is designed to handle missing resources
// gracefully, and we need a more fine-tuned way to communicate those problems
// to developers.
//
// In particular, we want developers and early adopters to be able to reason
// about missing translations, without bothering end user in production, where
// the user cannot react to that.
//
// We currently differentiate between nightly/dev-edition builds or automation
// where we report the errors, and beta/release, where we silence them.
static bool MaybeReportErrorsToGecko(const nsTArray<nsCString>& aErrors,
ErrorResult& aRv,
nsIGlobalObject* global) {
if (!aErrors.IsEmpty()) {
if (xpc::IsInAutomation()) {
aRv.ThrowInvalidStateError(aErrors.ElementAt(0));
return true;
}
using namespace mozilla::intl;
using namespace mozilla::dom;
#if defined(NIGHTLY_BUILD) || defined(MOZ_DEV_EDITION) || defined(DEBUG)
Document* doc = nullptr;
if (global) {
nsPIDOMWindowInner* innerWindow = global->AsInnerWindow();
if (innerWindow) {
doc = innerWindow->GetExtantDoc();
}
}
NS_IMPL_CYCLE_COLLECTION_MULTI_ZONE_JSHOLDER_CLASS(Localization)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Localization)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mLocalization)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal)
NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE
tmp->Destroy();
mozilla::DropJSObjects(tmp);
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Localization)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLocalization)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
for (const auto& error : aErrors) {
nsContentUtils::ReportToConsoleNonLocalized(NS_ConvertUTF8toUTF16(error),
nsIScriptError::warningFlag,
"l10n"_ns, doc);
}
#endif
}
return false;
}
static nsTArray<ffi::L10nKey> ConvertFromL10nKeys(
const Sequence<OwningUTF8StringOrL10nIdArgs>& aKeys) {
nsTArray<ffi::L10nKey> l10nKeys(aKeys.Length());
for (const auto& entry : aKeys) {
if (entry.IsUTF8String()) {
const auto& id = entry.GetAsUTF8String();
ffi::L10nKey* key = l10nKeys.AppendElement();
key->id = &id;
} else {
const auto& e = entry.GetAsL10nIdArgs();
ffi::L10nKey* key = l10nKeys.AppendElement();
key->id = &e.mId;
if (!e.mArgs.IsNull()) {
FluentBundle::ConvertArgs(e.mArgs.Value(), key->args);
}
}
}
return l10nKeys;
}
static FallibleTArray<AttributeNameValue> ConvertToAttributeNameValue(
const nsTArray<ffi::L10nAttribute>& aAttributes, OOMReporter& aError) {
FallibleTArray<AttributeNameValue> result(aAttributes.Length());
for (const auto& attr : aAttributes) {
auto cvtAttr = AttributeNameValue();
cvtAttr.mName = attr.name;
cvtAttr.mValue = attr.value;
if (!result.AppendElement(std::move(cvtAttr), fallible)) {
result.Clear();
aError.ReportOOM();
return result;
}
}
return result;
}
static FallibleTArray<Nullable<L10nMessage>> ConvertToL10nMessages(
const nsTArray<ffi::OptionalL10nMessage>& aMessages, ErrorResult& aError) {
FallibleTArray<Nullable<L10nMessage>> l10nMessages(aMessages.Length());
for (const auto& entry : aMessages) {
Nullable<L10nMessage> msg;
if (entry.is_present) {
L10nMessage& m = msg.SetValue();
if (!entry.message.value.IsVoid()) {
m.mValue = entry.message.value;
}
if (!entry.message.attributes.IsEmpty()) {
m.mAttributes.SetValue(
ConvertToAttributeNameValue(entry.message.attributes, aError));
}
}
if (!l10nMessages.AppendElement(std::move(msg), fallible)) {
l10nMessages.Clear();
aError.Throw(NS_ERROR_OUT_OF_MEMORY);
return l10nMessages;
}
}
return l10nMessages;
}
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Localization)
NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mGenerateBundles)
NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mGenerateBundlesSync)
NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mBundles)
NS_IMPL_CYCLE_COLLECTION_TRACE_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(Localization)
NS_IMPL_CYCLE_COLLECTING_RELEASE(Localization)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Localization)
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
NS_INTERFACE_MAP_ENTRY(nsIObserver)
NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_WEAK(Localization, mGlobal)
/* static */
already_AddRefed<Localization> Localization::Create(
const nsTArray<nsCString>& aResourceIds, bool aIsSync) {
return MakeAndAddRef<Localization>(aResourceIds, aIsSync);
nsIGlobalObject* aGlobal, const bool aSync,
const BundleGenerator& aBundleGenerator) {
RefPtr<Localization> loc = new Localization(aGlobal, aSync, aBundleGenerator);
loc->Init();
return loc.forget();
}
Localization::Localization(const nsTArray<nsCString>& aResIds, bool aIsSync) {
ffi::localization_new(&aResIds, aIsSync, nullptr, getter_AddRefs(mRaw));
RegisterObservers();
Localization::Localization(nsIGlobalObject* aGlobal, const bool aSync,
const BundleGenerator& aBundleGenerator)
: mGlobal(aGlobal), mIsSync(aSync) {
if (aBundleGenerator.mGenerateBundles.WasPassed()) {
GenerateBundles& generateBundles =
aBundleGenerator.mGenerateBundles.Value();
mGenerateBundles.setObject(*generateBundles.CallbackOrNull());
}
if (aBundleGenerator.mGenerateBundlesSync.WasPassed()) {
GenerateBundlesSync& generateBundlesSync =
aBundleGenerator.mGenerateBundlesSync.Value();
mGenerateBundlesSync.setObject(*generateBundlesSync.CallbackOrNull());
}
mIsSync = aSync;
}
Localization::Localization(nsIGlobalObject* aGlobal,
const nsTArray<nsCString>& aResIds, bool aIsSync)
: mGlobal(aGlobal) {
ffi::localization_new(&aResIds, aIsSync, nullptr, getter_AddRefs(mRaw));
bool Localization::Init() {
RegisterObservers();
return true;
}
Localization::Localization(nsIGlobalObject* aGlobal, bool aIsSync)
: mGlobal(aGlobal) {
nsTArray<nsCString> resIds;
ffi::localization_new(&resIds, aIsSync, nullptr, getter_AddRefs(mRaw));
void Localization::Activate(const bool aEager) {
mLocalization = do_ImportModule("resource://gre/modules/Localization.jsm",
"Localization");
RegisterObservers();
}
AutoJSContext cx;
Localization::Localization(nsIGlobalObject* aGlobal, bool aIsSync,
const ffi::LocalizationRc* aRaw)
: mGlobal(aGlobal), mRaw(aRaw) {
RegisterObservers();
JS::Rooted<JS::Value> generateBundlesJS(cx, mGenerateBundles);
JS::Rooted<JS::Value> generateBundlesSyncJS(cx, mGenerateBundlesSync);
JS::Rooted<JS::Value> bundlesJS(cx);
mLocalization->GenerateBundles(mResourceIds, mIsSync, aEager,
generateBundlesJS, generateBundlesSyncJS,
&bundlesJS);
mBundles.set(bundlesJS);
mozilla::HoldJSObjects(this);
}
already_AddRefed<Localization> Localization::Constructor(
const GlobalObject& aGlobal, const Sequence<nsCString>& aResourceIds,
bool aIsSync, const Optional<NonNull<L10nRegistry>>& aRegistry,
const Optional<Sequence<nsCString>>& aLocales, ErrorResult& aRv) {
nsTArray<nsCString> resIds = ToTArray<nsTArray<nsCString>>(aResourceIds);
Maybe<nsTArray<nsCString>> locales;
if (aLocales.WasPassed()) {
locales.emplace();
locales->SetCapacity(aLocales.Value().Length());
for (const auto& locale : aLocales.Value()) {
locales->AppendElement(locale);
}
}
RefPtr<const ffi::LocalizationRc> raw;
bool result = ffi::localization_new_with_locales(
&resIds, aIsSync,
aRegistry.WasPassed() ? aRegistry.Value().Raw() : nullptr,
locales.ptrOr(nullptr), getter_AddRefs(raw));
if (!result) {
aRv.ThrowInvalidStateError(
"Failed to create the Localization. Check the locales arguments.");
const GlobalObject& aGlobal, const Sequence<nsString>& aResourceIds,
const bool aSync, const BundleGenerator& aBundleGenerator,
ErrorResult& aRv) {
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
if (!global) {
aRv.Throw(NS_ERROR_FAILURE);
return nullptr;
}
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
RefPtr<Localization> loc =
Localization::Create(global, aSync, aBundleGenerator);
return do_AddRef(new Localization(global, aIsSync, raw));
if (aResourceIds.Length()) {
loc->AddResourceIds(aResourceIds);
}
loc->Activate(true);
return loc.forget();
}
nsIGlobalObject* Localization::GetParentObject() const { return mGlobal; }
JSObject* Localization::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return Localization_Binding::Wrap(aCx, this, aGivenProto);
}
Localization::~Localization() = default;
Localization::~Localization() {
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
if (obs) {
obs->RemoveObserver(this, INTL_APP_LOCALES_CHANGED);
}
Preferences::RemoveObservers(this, kObservedPrefs);
Destroy();
mozilla::DropJSObjects(this);
}
void Localization::Destroy() {
mGenerateBundles.setUndefined();
mGenerateBundlesSync.setUndefined();
mBundles.setUndefined();
}
/* Protected */
void Localization::RegisterObservers() {
DebugOnly<nsresult> rv = Preferences::AddWeakObservers(this, kObservedPrefs);
MOZ_ASSERT(NS_SUCCEEDED(rv), "Adding observers failed.");
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
if (obs) {
obs->AddObserver(this, INTL_APP_LOCALES_CHANGED, true);
}
}
NS_IMETHODIMP
Localization::Observe(nsISupports* aSubject, const char* aTopic,
@ -224,232 +178,261 @@ Localization::Observe(nsISupports* aSubject, const char* aTopic,
return NS_OK;
}
void Localization::RegisterObservers() {
DebugOnly<nsresult> rv = Preferences::AddWeakObservers(this, kObservedPrefs);
MOZ_ASSERT(NS_SUCCEEDED(rv), "Adding observers failed.");
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
if (obs) {
obs->AddObserver(this, INTL_APP_LOCALES_CHANGED, true);
void Localization::OnChange() {
if (mLocalization) {
AutoJSContext cx;
JS::Rooted<JS::Value> generateBundlesJS(cx, mGenerateBundles);
JS::Rooted<JS::Value> generateBundlesSyncJS(cx, mGenerateBundlesSync);
JS::Rooted<JS::Value> bundlesJS(cx);
mLocalization->GenerateBundles(mResourceIds, mIsSync, false,
generateBundlesJS, generateBundlesSyncJS,
&bundlesJS);
mBundles.set(bundlesJS);
}
}
void Localization::OnChange() { ffi::localization_on_change(mRaw.get()); }
void Localization::AddResourceId(const nsACString& aResourceId) {
ffi::localization_add_res_id(mRaw.get(), &aResourceId);
uint32_t Localization::AddResourceId(const nsAString& aResourceId) {
if (!mResourceIds.Contains(aResourceId)) {
mResourceIds.AppendElement(aResourceId);
Localization::OnChange();
}
return mResourceIds.Length();
}
uint32_t Localization::RemoveResourceId(const nsACString& aResourceId) {
return ffi::localization_remove_res_id(mRaw.get(), &aResourceId);
uint32_t Localization::RemoveResourceId(const nsAString& aResourceId) {
if (mResourceIds.RemoveElement(aResourceId)) {
Localization::OnChange();
}
return mResourceIds.Length();
}
void Localization::AddResourceIds(const nsTArray<nsCString>& aResourceIds) {
ffi::localization_add_res_ids(mRaw.get(), &aResourceIds);
/**
* Localization API
*/
uint32_t Localization::AddResourceIds(const nsTArray<nsString>& aResourceIds) {
bool added = false;
for (const auto& resId : aResourceIds) {
if (!mResourceIds.Contains(resId)) {
mResourceIds.AppendElement(resId);
added = true;
}
}
if (added) {
Localization::OnChange();
}
return mResourceIds.Length();
}
uint32_t Localization::RemoveResourceIds(
const nsTArray<nsCString>& aResourceIds) {
return ffi::localization_remove_res_ids(mRaw.get(), &aResourceIds);
const nsTArray<nsString>& aResourceIds) {
bool removed = false;
for (const auto& resId : aResourceIds) {
if (mResourceIds.RemoveElement(resId)) {
removed = true;
}
}
if (removed) {
Localization::OnChange();
}
return mResourceIds.Length();
}
already_AddRefed<Promise> Localization::FormatValue(
const nsACString& aId, const Optional<L10nArgs>& aArgs, ErrorResult& aRv) {
nsTArray<ffi::L10nArg> l10nArgs;
nsTArray<nsCString> errors;
JSContext* aCx, const nsACString& aId, const Optional<L10nArgs>& aArgs,
ErrorResult& aRv) {
if (!mLocalization) {
Activate(false);
}
JS::Rooted<JS::Value> args(aCx);
if (aArgs.WasPassed()) {
const L10nArgs& args = aArgs.Value();
FluentBundle::ConvertArgs(args, l10nArgs);
ConvertL10nArgsToJSValue(aCx, aArgs.Value(), &args, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
} else {
args = JS::UndefinedValue();
}
RefPtr<Promise> promise = Promise::Create(mGlobal, aRv);
ffi::localization_format_value(
mRaw.get(), &aId, &l10nArgs, promise,
[](const Promise* aPromise, const nsACString* aValue,
const nsTArray<nsCString>* aErrors) {
Promise* promise = const_cast<Promise*>(aPromise);
ErrorResult rv;
if (MaybeReportErrorsToGecko(*aErrors, rv,
promise->GetParentObject())) {
promise->MaybeReject(std::move(rv));
} else {
promise->MaybeResolve(aValue);
}
});
RefPtr<Promise> promise;
JS::Rooted<JS::Value> bundlesJS(aCx, mBundles);
nsresult rv = mLocalization->FormatValue(mResourceIds, bundlesJS, aId, args,
getter_AddRefs(promise));
if (NS_WARN_IF(NS_FAILED(rv))) {
aRv.Throw(rv);
return nullptr;
}
return MaybeWrapPromise(promise);
}
already_AddRefed<Promise> Localization::FormatValues(
const Sequence<OwningUTF8StringOrL10nIdArgs>& aKeys, ErrorResult& aRv) {
nsTArray<ffi::L10nKey> l10nKeys = ConvertFromL10nKeys(aKeys);
void Localization::SetIsSync(const bool aIsSync) { mIsSync = aIsSync; }
RefPtr<Promise> promise = Promise::Create(mGlobal, aRv);
if (aRv.Failed()) {
return nullptr;
already_AddRefed<Promise> Localization::FormatValues(
JSContext* aCx, const Sequence<OwningUTF8StringOrL10nIdArgs>& aKeys,
ErrorResult& aRv) {
if (!mLocalization) {
Activate(false);
}
nsTArray<JS::Value> jsKeys;
SequenceRooter<JS::Value> rooter(aCx, &jsKeys);
for (auto& key : aKeys) {
JS::RootedValue jsKey(aCx);
if (!ToJSValue(aCx, key, &jsKey)) {
aRv.NoteJSContextException(aCx);
return nullptr;
}
jsKeys.AppendElement(jsKey);
}
ffi::localization_format_values(
mRaw.get(), &l10nKeys, promise,
// callback function which will be invoked by the rust code, passing the
// promise back in.
[](const Promise* aPromise, const nsTArray<nsCString>* aValues,
const nsTArray<nsCString>* aErrors) {
Promise* promise = const_cast<Promise*>(aPromise);
ErrorResult rv;
if (MaybeReportErrorsToGecko(*aErrors, rv,
promise->GetParentObject())) {
promise->MaybeReject(std::move(rv));
} else {
promise->MaybeResolve(*aValues);
}
});
RefPtr<Promise> promise;
JS::Rooted<JS::Value> bundlesJS(aCx, mBundles);
aRv = mLocalization->FormatValues(mResourceIds, bundlesJS, jsKeys,
getter_AddRefs(promise));
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
return MaybeWrapPromise(promise);
}
already_AddRefed<Promise> Localization::FormatMessages(
const Sequence<OwningUTF8StringOrL10nIdArgs>& aKeys, ErrorResult& aRv) {
auto l10nKeys = ConvertFromL10nKeys(aKeys);
RefPtr<Promise> promise = Promise::Create(mGlobal, aRv);
if (aRv.Failed()) {
return nullptr;
JSContext* aCx, const Sequence<OwningUTF8StringOrL10nIdArgs>& aKeys,
ErrorResult& aRv) {
if (!mLocalization) {
Activate(false);
}
nsTArray<JS::Value> jsKeys;
SequenceRooter<JS::Value> rooter(aCx, &jsKeys);
for (auto& key : aKeys) {
JS::RootedValue jsKey(aCx);
if (!ToJSValue(aCx, key, &jsKey)) {
aRv.NoteJSContextException(aCx);
return nullptr;
}
jsKeys.AppendElement(jsKey);
}
ffi::localization_format_messages(
mRaw.get(), &l10nKeys, promise,
// callback function which will be invoked by the rust code, passing the
// promise back in.
[](const Promise* aPromise,
const nsTArray<ffi::OptionalL10nMessage>* aRaw,
const nsTArray<nsCString>* aErrors) {
Promise* promise = const_cast<Promise*>(aPromise);
ErrorResult rv;
if (MaybeReportErrorsToGecko(*aErrors, rv,
promise->GetParentObject())) {
promise->MaybeReject(std::move(rv));
} else {
ErrorResult rv;
FallibleTArray<Nullable<L10nMessage>> messages;
messages = ConvertToL10nMessages(*aRaw, rv);
if (rv.Failed()) {
promise->MaybeReject(std::move(rv));
} else {
promise->MaybeResolve(messages);
}
}
});
RefPtr<Promise> promise;
JS::Rooted<JS::Value> bundlesJS(aCx, mBundles);
aRv = mLocalization->FormatMessages(mResourceIds, bundlesJS, jsKeys,
getter_AddRefs(promise));
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
return MaybeWrapPromise(promise);
}
void Localization::FormatValueSync(const nsACString& aId,
void Localization::FormatValueSync(JSContext* aCx, const nsACString& aId,
const Optional<L10nArgs>& aArgs,
nsACString& aRetVal, ErrorResult& aRv) {
nsTArray<ffi::L10nArg> l10nArgs;
nsTArray<nsCString> errors;
if (aArgs.WasPassed()) {
const L10nArgs& args = aArgs.Value();
FluentBundle::ConvertArgs(args, l10nArgs);
}
bool rv = ffi::localization_format_value_sync(mRaw.get(), &aId, &l10nArgs,
&aRetVal, &errors);
if (rv) {
MaybeReportErrorsToGecko(errors, aRv, GetParentObject());
} else {
if (!mIsSync) {
aRv.ThrowInvalidStateError(
"Can't use formatValueSync when state is async.");
return;
}
if (!mLocalization) {
Activate(false);
}
JS::Rooted<JS::Value> args(aCx);
if (aArgs.WasPassed()) {
ConvertL10nArgsToJSValue(aCx, aArgs.Value(), &args, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return;
}
} else {
args = JS::UndefinedValue();
}
JS::Rooted<JS::Value> bundlesJS(aCx, mBundles);
aRv = mLocalization->FormatValueSync(mResourceIds, bundlesJS, aId, args,
aRetVal);
}
void Localization::FormatValuesSync(
const Sequence<OwningUTF8StringOrL10nIdArgs>& aKeys,
JSContext* aCx, const Sequence<OwningUTF8StringOrL10nIdArgs>& aKeys,
nsTArray<nsCString>& aRetVal, ErrorResult& aRv) {
nsTArray<ffi::L10nKey> l10nKeys(aKeys.Length());
nsTArray<nsCString> errors;
for (const auto& entry : aKeys) {
if (entry.IsUTF8String()) {
const auto& id = entry.GetAsUTF8String();
nsTArray<ffi::L10nArg> l10nArgs;
ffi::L10nKey* key = l10nKeys.AppendElement();
key->id = &id;
} else {
const auto& e = entry.GetAsL10nIdArgs();
nsTArray<ffi::L10nArg> l10nArgs;
ffi::L10nKey* key = l10nKeys.AppendElement();
key->id = &e.mId;
if (!e.mArgs.IsNull()) {
FluentBundle::ConvertArgs(e.mArgs.Value(), key->args);
}
}
}
bool rv = ffi::localization_format_values_sync(mRaw.get(), &l10nKeys,
&aRetVal, &errors);
if (rv) {
MaybeReportErrorsToGecko(errors, aRv, GetParentObject());
} else {
if (!mIsSync) {
aRv.ThrowInvalidStateError(
"Can't use formatValuesSync when state is async.");
return;
}
if (!mLocalization) {
Activate(false);
}
nsTArray<JS::Value> jsKeys;
SequenceRooter<JS::Value> rooter(aCx, &jsKeys);
for (auto& key : aKeys) {
JS::RootedValue jsKey(aCx);
if (!ToJSValue(aCx, key, &jsKey)) {
aRv.NoteJSContextException(aCx);
return;
}
jsKeys.AppendElement(jsKey);
}
JS::Rooted<JS::Value> bundlesJS(aCx, mBundles);
aRv =
mLocalization->FormatValuesSync(mResourceIds, bundlesJS, jsKeys, aRetVal);
}
void Localization::FormatMessagesSync(
const Sequence<OwningUTF8StringOrL10nIdArgs>& aKeys,
JSContext* aCx, const Sequence<OwningUTF8StringOrL10nIdArgs>& aKeys,
nsTArray<Nullable<L10nMessage>>& aRetVal, ErrorResult& aRv) {
nsTArray<ffi::L10nKey> l10nKeys(aKeys.Length());
nsTArray<nsCString> errors;
for (const auto& entry : aKeys) {
if (entry.IsUTF8String()) {
const auto& id = entry.GetAsUTF8String();
nsTArray<ffi::L10nArg> l10nArgs;
ffi::L10nKey* key = l10nKeys.AppendElement();
key->id = &id;
} else {
const auto& e = entry.GetAsL10nIdArgs();
nsTArray<ffi::L10nArg> l10nArgs;
ffi::L10nKey* key = l10nKeys.AppendElement();
key->id = &e.mId;
if (!e.mArgs.IsNull()) {
FluentBundle::ConvertArgs(e.mArgs.Value(), key->args);
}
}
}
nsTArray<ffi::OptionalL10nMessage> result(l10nKeys.Length());
bool rv = ffi::localization_format_messages_sync(mRaw.get(), &l10nKeys,
&result, &errors);
if (rv) {
MaybeReportErrorsToGecko(errors, aRv, GetParentObject());
if (!aRv.Failed()) {
aRetVal = ConvertToL10nMessages(result, aRv);
}
} else {
if (!mIsSync) {
aRv.ThrowInvalidStateError(
"Can't use formatMessagesSync when state is async.");
return;
}
if (!mLocalization) {
Activate(false);
}
nsTArray<JS::Value> jsKeys;
SequenceRooter<JS::Value> rooter(aCx, &jsKeys);
for (auto& key : aKeys) {
JS::RootedValue jsKey(aCx);
if (!ToJSValue(aCx, key, &jsKey)) {
aRv.NoteJSContextException(aCx);
return;
}
jsKeys.AppendElement(jsKey);
}
aRetVal = ConvertToL10nMessages(result, aRv);
nsTArray<JS::Value> messages;
SequenceRooter<JS::Value> messagesRooter(aCx, &messages);
JS::Rooted<JS::Value> bundlesJS(aCx, mBundles);
aRv = mLocalization->FormatMessagesSync(mResourceIds, bundlesJS, jsKeys,
messages);
if (NS_WARN_IF(aRv.Failed())) {
return;
}
JS::Rooted<JS::Value> rootedMsg(aCx);
for (auto& msg : messages) {
rootedMsg.set(msg);
Nullable<L10nMessage>* slotPtr = aRetVal.AppendElement(mozilla::fallible);
if (!slotPtr) {
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
return;
}
if (rootedMsg.isNull()) {
slotPtr->SetNull();
} else {
JS_WrapValue(aCx, &rootedMsg);
if (!slotPtr->SetValue().Init(aCx, rootedMsg)) {
aRv.NoteJSContextException(aCx);
return;
}
}
}
}
void Localization::SetAsync() { ffi::localization_set_async(mRaw.get()); }
bool Localization::IsSync() { return ffi::localization_is_sync(mRaw.get()); }
/**
* PromiseResolver is a PromiseNativeHandler used
* by MaybeWrapPromise method.
@ -518,3 +501,33 @@ already_AddRefed<Promise> Localization::MaybeWrapPromise(
aInnerPromise->AppendNativeHandler(resolver);
return docPromise.forget();
}
void Localization::ConvertL10nArgsToJSValue(
JSContext* aCx, const L10nArgs& aArgs, JS::MutableHandle<JS::Value> aRetVal,
ErrorResult& aRv) {
// This method uses a temporary dictionary to automate
// converting an IDL Record to a JS Value via a dictionary.
//
// Once we get ToJSValue for Record, we'll switch to that.
L10nArgsHelperDict helperDict;
for (auto& entry : aArgs.Entries()) {
L10nArgs::EntryType* newEntry =
helperDict.mArgs.Entries().AppendElement(fallible);
if (!newEntry) {
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
return;
}
newEntry->mKey = entry.mKey;
newEntry->mValue = entry.mValue;
}
JS::Rooted<JS::Value> jsVal(aCx);
if (!ToJSValue(aCx, helperDict, &jsVal)) {
aRv.Throw(NS_ERROR_UNEXPECTED);
return;
}
JS::Rooted<JSObject*> jsObj(aCx, &jsVal.toObject());
if (!JS_GetProperty(aCx, jsObj, "args", aRetVal)) {
aRv.Throw(NS_ERROR_UNEXPECTED);
return;
}
}

View File

@ -7,96 +7,109 @@
#ifndef mozilla_intl_l10n_Localization_h
#define mozilla_intl_l10n_Localization_h
#include "nsCycleCollectionParticipant.h"
#include "nsWeakReference.h"
#include "nsIObserver.h"
#include "nsWeakReference.h"
#include "nsWrapperCache.h"
#include "nsWeakReference.h"
#include "mozilla/ErrorResult.h"
#include "mozILocalization.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/LocalizationBinding.h"
#include "mozilla/intl/LocalizationBindings.h"
#include "mozilla/intl/L10nRegistry.h"
#include "mozilla/dom/PromiseNativeHandler.h"
class nsIGlobalObject;
using namespace mozilla::dom;
namespace mozilla {
class ErrorResult;
namespace intl {
class Localization : public nsIObserver,
public nsWrapperCache,
public nsSupportsWeakReference {
template <typename T, typename... Args>
friend already_AddRefed<T> mozilla::MakeAndAddRef(Args&&... aArgs);
typedef Record<nsCString, Nullable<OwningUTF8StringOrDouble>> L10nArgs;
public:
class Localization : public nsIObserver,
public nsSupportsWeakReference,
public nsWrapperCache {
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_AMBIGUOUS(Localization,
nsIObserver)
NS_DECL_NSIOBSERVER
static already_AddRefed<Localization> Constructor(
const dom::GlobalObject& aGlobal,
const dom::Sequence<nsCString>& aResourceIds, bool aIsSync,
const dom::Optional<dom::NonNull<L10nRegistry>>& aRegistry,
const dom::Optional<dom::Sequence<nsCString>>& aLocales,
ErrorResult& aRv);
static already_AddRefed<Localization> Create(
const nsTArray<nsCString>& aResourceIds, bool aIsSync);
nsIGlobalObject* aGlobal, const bool aSync,
const BundleGenerator& aBundleGenerator);
JSObject* WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) override;
nsIGlobalObject* GetParentObject() const { return mGlobal; }
void Activate(const bool aEager);
void SetIsSync(bool aIsSync);
void Destroy();
already_AddRefed<dom::Promise> FormatValue(
const nsACString& aId, const dom::Optional<L10nArgs>& aArgs,
static already_AddRefed<Localization> Constructor(
const GlobalObject& aGlobal, const Sequence<nsString>& aResourceIds,
const bool aSync, const BundleGenerator& aBundleGenerator,
ErrorResult& aRv);
already_AddRefed<dom::Promise> FormatValues(
const dom::Sequence<dom::OwningUTF8StringOrL10nIdArgs>& aKeys,
nsIGlobalObject* GetParentObject() const;
virtual JSObject* WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) override;
uint32_t AddResourceId(const nsAString& aResourceId);
uint32_t RemoveResourceId(const nsAString& aResourceId);
/**
* Localization API
*
* Methods documentation in Localization.webidl
*/
uint32_t AddResourceIds(const nsTArray<nsString>& aResourceIds);
uint32_t RemoveResourceIds(const nsTArray<nsString>& aResourceIds);
already_AddRefed<Promise> FormatValue(JSContext* aCx, const nsACString& aId,
const Optional<L10nArgs>& aArgs,
ErrorResult& aRv);
already_AddRefed<Promise> FormatValues(
JSContext* aCx, const Sequence<OwningUTF8StringOrL10nIdArgs>& aKeys,
ErrorResult& aRv);
already_AddRefed<dom::Promise> FormatMessages(
const dom::Sequence<dom::OwningUTF8StringOrL10nIdArgs>& aKeys,
already_AddRefed<Promise> FormatMessages(
JSContext* aCx, const Sequence<OwningUTF8StringOrL10nIdArgs>& aKeys,
ErrorResult& aRv);
void FormatValueSync(const nsACString& aId,
const dom::Optional<L10nArgs>& aArgs,
nsACString& aRetVal, ErrorResult& aRv);
void FormatValuesSync(
const dom::Sequence<dom::OwningUTF8StringOrL10nIdArgs>& aKeys,
nsTArray<nsCString>& aRetVal, ErrorResult& aRv);
void FormatMessagesSync(
const dom::Sequence<dom::OwningUTF8StringOrL10nIdArgs>& aKeys,
nsTArray<dom::Nullable<dom::L10nMessage>>& aRetVal, ErrorResult& aRv);
void SetIsSync(const bool aIsSync);
void AddResourceId(const nsACString& aResourceId);
uint32_t RemoveResourceId(const nsACString& aResourceId);
void AddResourceIds(const nsTArray<nsCString>& aResourceIds);
uint32_t RemoveResourceIds(const nsTArray<nsCString>& aResourceIds);
void SetAsync();
bool IsSync();
void FormatValueSync(JSContext* aCx, const nsACString& aId,
const Optional<L10nArgs>& aArgs, nsACString& aRetVal,
ErrorResult& aRv);
void FormatValuesSync(JSContext* aCx,
const Sequence<OwningUTF8StringOrL10nIdArgs>& aKeys,
nsTArray<nsCString>& aRetVal, ErrorResult& aRv);
void FormatMessagesSync(JSContext* aCx,
const Sequence<OwningUTF8StringOrL10nIdArgs>& aKeys,
nsTArray<Nullable<L10nMessage>>& aRetVal,
ErrorResult& aRv);
protected:
Localization(const nsTArray<nsCString>& aResIds, bool aIsSync);
Localization(nsIGlobalObject* aGlobal, bool aIsSync);
Localization(nsIGlobalObject* aGlobal, const nsTArray<nsCString>& aResIds,
bool aIsSync);
Localization(nsIGlobalObject* aGlobal, bool aIsSync,
const ffi::LocalizationRc* aRaw);
Localization(nsIGlobalObject* aGlobal, const bool aSync,
const BundleGenerator& aBundleGenerator);
virtual bool Init();
virtual ~Localization();
void RegisterObservers();
virtual void OnChange();
already_AddRefed<dom::Promise> MaybeWrapPromise(dom::Promise* aInnerPromise);
already_AddRefed<Promise> MaybeWrapPromise(Promise* aInnerPromise);
void ConvertL10nArgsToJSValue(JSContext* aCx, const L10nArgs& aArgs,
JS::MutableHandle<JS::Value> aRetVal,
ErrorResult& aRv);
nsCOMPtr<nsIGlobalObject> mGlobal;
RefPtr<const ffi::LocalizationRc> mRaw;
nsCOMPtr<mozILocalization> mLocalization;
bool mIsSync;
nsTArray<nsString> mResourceIds;
JS::Heap<JS::Value> mBundles;
JS::Heap<JS::Value> mGenerateBundles;
JS::Heap<JS::Value> mGenerateBundlesSync;
};
} // namespace intl

607
intl/l10n/Localization.jsm Normal file
View File

@ -0,0 +1,607 @@
/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
/* Copyright 2017 Mozilla Foundation and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* fluent-dom@fa25466f (October 12, 2018) */
/* eslint no-console: ["error", { allow: ["warn", "error"] }] */
/* global console */
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { AppConstants } = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
/*
* Base CachedIterable class.
*/
class CachedIterable extends Array {
/**
* Create a `CachedIterable` instance from an iterable or, if another
* instance of `CachedIterable` is passed, return it without any
* modifications.
*
* @param {Iterable} iterable
* @returns {CachedIterable}
*/
static from(iterable) {
if (iterable instanceof this) {
return iterable;
}
return new this(iterable);
}
}
/*
* CachedAsyncIterable caches the elements yielded by an async iterable.
*
* It can be used to iterate over an iterable many times without depleting the
* iterable.
*/
class CachedAsyncIterable extends CachedIterable {
/**
* Create an `CachedAsyncIterable` instance.
*
* @param {Iterable} iterable
* @returns {CachedAsyncIterable}
*/
constructor(iterable) {
super();
if (Symbol.asyncIterator in Object(iterable)) {
this.iterator = iterable[Symbol.asyncIterator]();
} else if (Symbol.iterator in Object(iterable)) {
this.iterator = iterable[Symbol.iterator]();
} else {
throw new TypeError("Argument must implement the iteration protocol.");
}
}
/**
* Asynchronous iterator caching the yielded elements.
*
* Elements yielded by the original iterable will be cached and available
* synchronously. Returns an async generator object implementing the
* iterator protocol over the elements of the original (async or sync)
* iterable.
*/
[Symbol.asyncIterator]() {
const cached = this;
let cur = 0;
return {
async next() {
if (cached.length <= cur) {
cached.push(cached.iterator.next());
}
return cached[cur++];
},
};
}
/**
* This method allows user to consume the next element from the iterator
* into the cache.
*
* @param {number} count - number of elements to consume
*/
async touchNext(count = 1) {
let idx = 0;
while (idx++ < count) {
const last = this[this.length - 1];
if (last && (await last).done) {
break;
}
this.push(this.iterator.next());
}
// Return the last cached {value, done} object to allow the calling
// code to decide if it needs to call touchNext again.
return this[this.length - 1];
}
}
/*
* CachedSyncIterable caches the elements yielded by an iterable.
*
* It can be used to iterate over an iterable many times without depleting the
* iterable.
*/
class CachedSyncIterable extends CachedIterable {
/**
* Create an `CachedSyncIterable` instance.
*
* @param {Iterable} iterable
* @returns {CachedSyncIterable}
*/
constructor(iterable) {
super();
if (Symbol.iterator in Object(iterable)) {
this.iterator = iterable[Symbol.iterator]();
} else {
throw new TypeError("Argument must implement the iteration protocol.");
}
}
[Symbol.iterator]() {
const cached = this;
let cur = 0;
return {
next() {
if (cached.length <= cur) {
cached.push(cached.iterator.next());
}
return cached[cur++];
},
};
}
/**
* This method allows user to consume the next element from the iterator
* into the cache.
*
* @param {number} count - number of elements to consume
*/
touchNext(count = 1) {
let idx = 0;
while (idx++ < count) {
const last = this[this.length - 1];
if (last && last.done) {
break;
}
this.push(this.iterator.next());
}
// Return the last cached {value, done} object to allow the calling
// code to decide if it needs to call touchNext again.
return this[this.length - 1];
}
}
/**
* The default localization strategy for Gecko. It comabines locales
* available in L10nRegistry, with locales requested by the user to
* generate the iterator over FluentBundles.
*
* In the future, we may want to allow certain modules to override this
* with a different negotitation strategy to allow for the module to
* be localized into a different language - for example DevTools.
*/
function defaultGenerateBundles(resourceIds) {
const appLocales = Services.locale.appLocalesAsBCP47;
return L10nRegistry.getInstance().generateBundles(appLocales, resourceIds);
}
function defaultGenerateBundlesSync(resourceIds) {
const appLocales = Services.locale.appLocalesAsBCP47;
return L10nRegistry.getInstance().generateBundlesSync(appLocales, resourceIds);
}
function maybeReportErrorToGecko(error) {
if (AppConstants.NIGHTLY_BUILD || Cu.isInAutomation) {
if (Cu.isInAutomation) {
// We throw a string, rather than Error
// to allow the C++ Promise handler
// to clone it
throw error;
}
console.warn(error);
}
}
/**
* The `Localization` class is a central high-level API for vanilla
* JavaScript use of Fluent.
* It combines language negotiation, FluentBundle and I/O to
* provide a scriptable API to format translations.
*/
const Localization = {
cached(iterable, isSync) {
if (isSync) {
return CachedSyncIterable.from(iterable);
} else {
return CachedAsyncIterable.from(iterable);
}
},
/**
* Format translations and handle fallback if needed.
*
* Format translations for `keys` from `FluentBundle` instances on this
* Localization. In case of errors, fetch the next context in the
* fallback chain.
*
* @param {Array<String>} resourceIds - List of resource ids used by this
* localization.
* @param {Iter<FluentBundle>} bundles - Iterator over bundles.
* @param {Array<string|Object>} keys - Translation keys to format.
* @param {Function} method - Formatting function.
* @returns {Promise<Array<string?|Object?>>}
* @private
*/
async formatWithFallback(resourceIds, bundles, keys, method) {
if (!bundles) {
throw new Error("Attempt to format on an uninitialized instance.");
}
const translations = new Array(keys.length).fill(null);
let hasAtLeastOneBundle = false;
for await (const bundle of bundles) {
hasAtLeastOneBundle = true;
const missingIds = keysFromBundle(method, bundle, keys, translations);
if (missingIds.size === 0) {
break;
}
const locale = bundle.locales[0];
const ids = Array.from(missingIds).join(", ");
maybeReportErrorToGecko(`[fluent] Missing translations in ${locale}: ${ids}.`);
}
if (!hasAtLeastOneBundle) {
maybeReportErrorToGecko(`[fluent] Request for keys failed because no resource bundles got generated.\n keys: ${JSON.stringify(keys)}.\n resourceIds: ${JSON.stringify(resourceIds)}.`);
}
return translations;
},
/**
* Format translations and handle fallback if needed.
*
* Format translations for `keys` from `FluentBundle` instances on this
* Localization. In case of errors, fetch the next context in the
* fallback chain.
*
* @param {Array<String>} resourceIds - List of resource ids used by this
* localization.
* @param {Iter<FluentBundle>} bundles - Iterator over bundles.
* @param {Array<string|Object>} keys - Translation keys to format.
* @param {Function} method - Formatting function.
* @returns {Array<string|Object>}
* @private
*/
formatWithFallbackSync(resourceIds, bundles, keys, method) {
if (!bundles) {
throw new Error("Attempt to format on an uninitialized instance.");
}
const translations = new Array(keys.length).fill(null);
let hasAtLeastOneBundle = false;
for (const bundle of bundles) {
hasAtLeastOneBundle = true;
const missingIds = keysFromBundle(method, bundle, keys, translations);
if (missingIds.size === 0) {
break;
}
const locale = bundle.locales[0];
const ids = Array.from(missingIds).join(", ");
maybeReportErrorToGecko(`[fluent] Missing translations in ${locale}: ${ids}.`);
}
if (!hasAtLeastOneBundle) {
maybeReportErrorToGecko(`[fluent] Request for keys failed because no resource bundles got generated.\n keys: ${JSON.stringify(keys)}.\n resourceIds: ${JSON.stringify(resourceIds)}.`);
}
return translations;
},
/**
* Format translations into {value, attributes} objects.
*
* The fallback logic is the same as in `formatValues` but it returns {value,
* attributes} objects which are suitable for the translation of DOM
* elements.
*
* docL10n.formatMessages([
* {id: 'hello', args: { who: 'Mary' }},
* {id: 'welcome'}
* ]).then(console.log);
*
* // [
* // { value: 'Hello, Mary!', attributes: null },
* // {
* // value: 'Welcome!',
* // attributes: [ { name: "title", value: 'Hello' } ]
* // }
* // ]
*
* Returns a Promise resolving to an array of the translation messages.
*
* @param {Array<String>} resourceIds - List of resource ids used by this
* localization.
* @param {Iter<FluentBundle>} bundles - Iterator over bundles.
* @param {Array<string|Object>} keys - Translation keys to format.
* @returns {Promise<Array<{value: string, attributes: Object}?>>}
* @private
*/
formatMessages(resourceIds, bundles, keys) {
return this.formatWithFallback(resourceIds, bundles, keys, messageFromBundle);
},
/**
* Sync version of `formatMessages`.
*
* Returns an array of the translation messages.
*
* @param {Array<String>} resourceIds - List of resource ids used by this
* localization.
* @param {Iter<FluentBundle>} bundles - Iterator over bundles.
* @param {Array<string|Object>} keys - Translation keys to format.
* @returns {Array<{value: string, attributes: Object}?>}
* @private
*/
formatMessagesSync(resourceIds, bundles, keys) {
return this.formatWithFallbackSync(resourceIds, bundles, keys, messageFromBundle);
},
/**
* Retrieve translations corresponding to the passed keys.
*
* A generalized version of `Localization.formatValue`. Keys must
* be `{id, args}` objects.
*
* docL10n.formatValues([
* {id: 'hello', args: { who: 'Mary' }},
* {id: 'hello', args: { who: 'John' }},
* {id: 'welcome'}
* ]).then(console.log);
*
* // ['Hello, Mary!', 'Hello, John!', 'Welcome!']
*
* Returns a Promise resolving to an array of the translation strings.
*
* @param {Array<String>} resourceIds - List of resource ids used by this
* localization.
* @param {Iter<FluentBundle>} bundles - Iterator over bundles.
* @param {Array<string|Object>} keys - Translation keys to format.
* @returns {Promise<Array<string?>>}
*/
formatValues(resourceIds, bundles, keys) {
return this.formatWithFallback(resourceIds, bundles, keys, valueFromBundle);
},
/**
* Sync version of `formatValues`.
*
* Returns an array of the translation strings.
*
* @param {Array<String>} resourceIds - List of resource ids used by this
* localization.
* @param {Iter<FluentBundle>} bundles - Iterator over bundles.
* @param {Array<string|Object>} keys - Translation keys to format.
* @returns {Array<string?>}
* @private
*/
formatValuesSync(resourceIds, bundles, keys) {
return this.formatWithFallbackSync(resourceIds, bundles, keys, valueFromBundle);
},
/**
* Retrieve the translation corresponding to the `id` identifier.
*
* If passed, `args` is a simple hash object with a list of variables that
* will be interpolated in the value of the translation.
*
* docL10n.formatValue(
* 'hello', { who: 'world' }
* ).then(console.log);
*
* // 'Hello, world!'
*
* Returns a Promise resolving to a translation string.
*
* Use this sparingly for one-off messages which don't need to be
* retranslated when the user changes their language preferences, e.g. in
* notifications.
*
* @param {Array<String>} resourceIds - List of resource ids used by this
* localization.
* @param {Iter<FluentBundle>} bundles - Iterator over bundles.
* @param {string} id - Identifier of the translation to format
* @param {Object} [args] - Optional external arguments
* @returns {Promise<string?>}
*/
async formatValue(resourceIds, bundles, id, args) {
const [val] = await this.formatValues(resourceIds, bundles, [{id, args}]);
return val;
},
/**
* Sync version of `formatValue`.
*
* Returns a translation string.
*
* @param {Array<String>} resourceIds - List of resource ids used by this
* localization.
* @param {Iter<FluentBundle>} bundles - Iterator over bundles.
* @param {string} id - Identifier of the translation to format
* @param {Object} [args] - Optional external arguments
* @returns {string?}
* @private
*/
formatValueSync(resourceIds, bundles, id, args) {
const [val] = this.formatValuesSync(resourceIds, bundles, [{id, args}]);
return val;
},
/**
* This method should be called when there's a reason to believe
* that language negotiation or available resources changed.
*
* @param {Array<String>} resourceIds - List of resource ids used by this
* localization.
* @param {bool} isSync - Whether the instance should be
* synchronous.
* @param {bool} eager - whether the I/O for new context should begin eagerly
* @param {Function} generateBundles - Custom FluentBundle asynchronous generator.
* @param {Function} generateBundlesSync - Custom FluentBundle generator.
* @returns {Iter<FluentBundle>}
*/
generateBundles(resourceIds, isSync, eager = false, generateBundles = defaultGenerateBundles, generateBundlesSync = defaultGenerateBundlesSync) {
// Store for error reporting from `formatWithFallback`.
let generateMessages = isSync ? generateBundlesSync : generateBundles;
let bundles = this.cached(generateMessages(resourceIds), isSync);
if (eager) {
// If the first app locale is the same as last fallback
// it means that we have all resources in this locale, and
// we want to eagerly fetch just that one.
// Otherwise, we're in a scenario where the first locale may
// be partial and we want to eagerly fetch a fallback as well.
const appLocale = Services.locale.appLocaleAsBCP47;
const lastFallback = Services.locale.lastFallbackLocale;
const prefetchCount = appLocale === lastFallback ? 1 : 2;
bundles.touchNext(prefetchCount);
}
return bundles;
},
}
/**
* Format the value of a message into a string or `null`.
*
* This function is passed as a method to `keysFromBundle` and resolve
* a value of a single L10n Entity using provided `FluentBundle`.
* If the message doesn't have a value, return `null`.
*
* @param {FluentBundle} bundle
* @param {Array<Error>} errors
* @param {Object} message
* @param {Object} args
* @returns {string?}
* @private
*/
function valueFromBundle(bundle, errors, message, args) {
if (message.value) {
return bundle.formatPattern(message.value, args, errors);
}
return null;
}
/**
* Format all public values of a message into a {value, attributes} object.
*
* This function is passed as a method to `keysFromBundle` and resolve
* a single L10n Entity using provided `FluentBundle`.
*
* The function will return an object with a value and attributes of the
* entity.
*
* @param {FluentBundle} bundle
* @param {Array<Error>} errors
* @param {Object} message
* @param {Object} args
* @returns {Object}
* @private
*/
function messageFromBundle(bundle, errors, message, args) {
const formatted = {
value: null,
attributes: null,
};
if (message.value) {
formatted.value = bundle.formatPattern(message.value, args, errors);
}
let attrNames = Object.keys(message.attributes);
if (attrNames.length > 0) {
formatted.attributes = new Array(attrNames.length);
for (let [i, name] of attrNames.entries()) {
let value = bundle.formatPattern(message.attributes[name], args, errors);
formatted.attributes[i] = {name, value};
}
}
return formatted;
}
/**
* This function is an inner function for `Localization.formatWithFallback`.
*
* It takes a `FluentBundle`, list of l10n-ids and a method to be used for
* key resolution (either `valueFromBundle` or `messageFromBundle`) and
* optionally a value returned from `keysFromBundle` executed against
* another `FluentBundle`.
*
* The idea here is that if the previous `FluentBundle` did not resolve
* all keys, we're calling this function with the next context to resolve
* the remaining ones.
*
* In the function, we loop over `keys` and check if we have the `prev`
* passed and if it has an error entry for the position we're in.
*
* If it doesn't, it means that we have a good translation for this key and
* we return it. If it does, we'll try to resolve the key using the passed
* `FluentBundle`.
*
* In the end, we fill the translations array, and return the Set with
* missing ids.
*
* See `Localization.formatWithFallback` for more info on how this is used.
*
* @param {Function} method
* @param {FluentBundle} bundle
* @param {Array<string|Object>} keys
* @param {{Array<{value: string, attributes: Object}>}} translations
*
* @returns {Set<string>}
* @private
*/
function keysFromBundle(method, bundle, keys, translations) {
const messageErrors = [];
const missingIds = new Set();
keys.forEach((key, i) => {
let id;
let args = undefined;
if (typeof key == "object" && "id" in key) {
id = String(key.id);
args = key.args;
} else {
id = String(key);
}
if (translations[i] !== null) {
return;
}
let message = bundle.getMessage(id);
if (message) {
messageErrors.length = 0;
translations[i] = method(bundle, messageErrors, message, args);
if (messageErrors.length > 0) {
const locale = bundle.locales[0];
const errors = messageErrors.join(", ");
maybeReportErrorToGecko(`[fluent][resolver] errors in ${locale}/${id}: ${errors}.`);
}
} else {
missingIds.add(id);
}
});
return missingIds;
}
this.Localization = Localization;
var EXPORTED_SYMBOLS = ["Localization"];

View File

@ -1,26 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_intl_l10n_LocalizationBindings_h
#define mozilla_intl_l10n_LocalizationBindings_h
#include "mozilla/intl/localization_ffi_generated.h"
#include "mozilla/RefPtr.h"
namespace mozilla {
template <>
struct RefPtrTraits<intl::ffi::LocalizationRc> {
static void AddRef(const intl::ffi::LocalizationRc* aPtr) {
intl::ffi::localization_addref(aPtr);
}
static void Release(const intl::ffi::LocalizationRc* aPtr) {
intl::ffi::localization_release(aPtr);
}
};
} // namespace mozilla
#endif

View File

@ -1,7 +1,7 @@
The content of this directory is partially sourced from the fluent.js project.
The following files are affected:
- FluentSyntax.jsm
- Localization.jsm
At the moment, the tool used to produce those files in fluent.js repository, doesn't
fully align with how the code is structured here, so we perform a manual adjustments

View File

@ -495,6 +495,10 @@ Localization object manually using the `Localization` class:
.. code-block:: javascript
const { Localization } =
ChromeUtils.import("resource://gre/modules/Localization.jsm", {});
const myL10n = new Localization([
"branding/brand.ftl",
"browser/preferences/preferences.ftl"
@ -524,6 +528,10 @@ on the class.
.. code-block:: javascript
const { Localization } =
ChromeUtils.import("resource://gre/modules/Localization.jsm", {});
const myL10n = new Localization([
"branding/brand.ftl",
"browser/preferences/preferences.ftl"

View File

@ -11,7 +11,6 @@ EXPORTS.mozilla.intl += [
"FluentResource.h",
"L10nRegistry.h",
"Localization.h",
"LocalizationBindings.h",
"RegistryBindings.h",
]
@ -23,6 +22,10 @@ UNIFIED_SOURCES += [
"Localization.cpp",
]
EXTRA_JS_MODULES += [
"Localization.jsm",
]
TESTING_JS_MODULES += [
"FluentSyntax.jsm",
]
@ -43,19 +46,13 @@ USE_LIBS += ["intlcomponents"]
if CONFIG["COMPILE_ENVIRONMENT"]:
CbindgenHeader("fluent_ffi_generated.h", inputs=["/intl/l10n/rust/fluent-ffi"])
CbindgenHeader(
"l10nregistry_ffi_generated.h", inputs=["/intl/l10n/rust/l10nregistry-ffi"]
)
CbindgenHeader(
"localization_ffi_generated.h", inputs=["/intl/l10n/rust/localization-ffi"]
)
EXPORTS.mozilla.intl += [
"!fluent_ffi_generated.h",
"!l10nregistry_ffi_generated.h",
"!localization_ffi_generated.h",
]
XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell.ini"]

View File

@ -20,16 +20,9 @@ pub type FluentBundleRc = FluentBundle<Rc<FluentResource>>;
#[derive(Debug)]
#[repr(C, u8)]
pub enum FluentArgument<'s> {
pub enum FluentArgument {
Double_(f64),
String(&'s nsACString),
}
#[derive(Debug)]
#[repr(C)]
pub struct L10nArg<'s> {
pub id: &'s nsACString,
pub value: FluentArgument<'s>,
String(*const nsCString),
}
fn transform_accented(s: &str) -> Cow<str> {
@ -156,12 +149,14 @@ pub fn adapt_bundle_for_gecko(bundle: &mut FluentBundleRc, pseudo_strategy: Opti
}
#[no_mangle]
pub extern "C" fn fluent_bundle_new_single(
pub unsafe extern "C" fn fluent_bundle_new_single(
locale: &nsACString,
use_isolating: bool,
pseudo_strategy: &nsACString,
) -> *mut FluentBundleRc {
let id = match locale.to_utf8().parse::<LanguageIdentifier>() {
// We can use as_str_unchecked because this string comes from WebIDL and is
// guaranteed utf-8.
let id = match locale.as_str_unchecked().parse::<LanguageIdentifier>() {
Ok(id) => id,
Err(..) => return std::ptr::null_mut(),
};
@ -183,7 +178,7 @@ pub unsafe extern "C" fn fluent_bundle_new(
let mut langids = Vec::with_capacity(locale_count);
let locales = std::slice::from_raw_parts(locales, locale_count);
for locale in locales {
let id = match locale.to_utf8().parse::<LanguageIdentifier>() {
let id = match locale.as_str_unchecked().parse::<LanguageIdentifier>() {
Ok(id) => id,
Err(..) => return std::ptr::null_mut(),
};
@ -233,13 +228,13 @@ pub extern "C" fn fluent_bundle_has_message(bundle: &FluentBundleRc, id: &nsACSt
}
#[no_mangle]
pub extern "C" fn fluent_bundle_get_message(
pub unsafe extern "C" fn fluent_bundle_get_message(
bundle: &FluentBundleRc,
id: &nsACString,
has_value: &mut bool,
attrs: &mut ThinVec<nsCString>,
) -> bool {
match bundle.get_message(&id.to_utf8()) {
match bundle.get_message(id.as_str_unchecked()) {
Some(message) => {
attrs.reserve(message.attributes().count());
*has_value = message.value().is_some();
@ -256,23 +251,24 @@ pub extern "C" fn fluent_bundle_get_message(
}
#[no_mangle]
pub extern "C" fn fluent_bundle_format_pattern(
pub unsafe extern "C" fn fluent_bundle_format_pattern(
bundle: &FluentBundleRc,
id: &nsACString,
attr: &nsACString,
args: &ThinVec<L10nArg>,
arg_ids: &ThinVec<nsCString>,
arg_vals: &ThinVec<FluentArgument>,
ret_val: &mut nsACString,
ret_errors: &mut ThinVec<nsCString>,
) -> bool {
let args = convert_args(&args);
let args = convert_args(arg_ids, arg_vals);
let message = match bundle.get_message(&id.to_utf8()) {
let message = match bundle.get_message(id.as_str_unchecked()) {
Some(message) => message,
None => return false,
};
let pattern = if !attr.is_empty() {
match message.get_attribute(&attr.to_utf8()) {
match message.get_attribute(attr.as_str_unchecked()) {
Some(attr) => attr.value(),
None => return false,
}
@ -308,20 +304,25 @@ pub unsafe extern "C" fn fluent_bundle_add_resource(
}
}
pub fn convert_args<'s>(args: &[L10nArg<'s>]) -> Option<FluentArgs<'s>> {
if args.is_empty() {
fn convert_args<'a>(
arg_ids: &'a [nsCString],
arg_vals: &'a [FluentArgument],
) -> Option<FluentArgs<'a>> {
debug_assert_eq!(arg_ids.len(), arg_vals.len());
if arg_ids.is_empty() {
return None;
}
let mut result = FluentArgs::with_capacity(args.len());
for arg in args {
let val = match arg.value {
let mut args = FluentArgs::with_capacity(arg_ids.len());
for (id, val) in arg_ids.iter().zip(arg_vals.iter()) {
let val = match val {
FluentArgument::Double_(d) => FluentValue::from(d),
FluentArgument::String(s) => FluentValue::from(s.to_utf8()),
FluentArgument::String(s) => FluentValue::from(unsafe { (**s).to_string() }),
};
result.set(arg.id.to_string(), val);
args.set(id.to_string(), val);
}
Some(result)
Some(args)
}
fn append_fluent_errors_to_ret_errors(ret_errors: &mut ThinVec<nsCString>, errors: &[FluentError]) {

View File

@ -2,9 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use crate::xpcom_utils::get_app_locales;
use cstr::cstr;
use fluent_fallback::env::LocalesProvider;
use l10nregistry::{
env::ErrorReporter,
errors::{L10nRegistryError, L10nRegistrySetupError},
@ -16,19 +14,12 @@ use std::{
ffi::CStr,
fmt::{self, Write},
};
use unic_langid::LanguageIdentifier;
use xpcom::interfaces;
#[derive(Clone)]
pub struct GeckoEnvironment {
custom_locales: Option<Vec<LanguageIdentifier>>,
}
pub struct GeckoEnvironment;
impl GeckoEnvironment {
pub fn new(custom_locales: Option<Vec<LanguageIdentifier>>) -> Self {
Self { custom_locales }
}
pub fn report_l10nregistry_setup_error(error: &L10nRegistrySetupError) {
warn!("L10nRegistry setup error: {}", error);
let result = log_simple_console_error(
@ -77,22 +68,6 @@ impl ErrorReporter for GeckoEnvironment {
}
}
impl LocalesProvider for GeckoEnvironment {
type Iter = std::vec::IntoIter<unic_langid::LanguageIdentifier>;
fn locales(&self) -> Self::Iter {
if let Some(custom_locales) = &self.custom_locales {
custom_locales.clone().into_iter()
} else {
let result = get_app_locales()
.expect("Failed to retrieve app locales")
.into_iter()
.map(|s| LanguageIdentifier::from_bytes(&s).expect("Failed to parse a locale"))
.collect::<Vec<_>>();
result.into_iter()
}
}
}
fn log_simple_console_error(
error: &impl fmt::Display,
category: &CStr,

View File

@ -2,9 +2,9 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
pub mod env;
mod env;
mod fetcher;
pub mod load;
pub mod registry;
mod registry;
mod source;
mod xpcom_utils;

View File

@ -105,7 +105,7 @@ fn get_packaged_locales() -> Vec<LanguageIdentifier> {
}
fn create_l10n_registry(sources: Option<Vec<FileSource>>) -> Rc<GeckoL10nRegistry> {
let env = GeckoEnvironment::new(None);
let env = GeckoEnvironment;
let mut reg = L10nRegistry::with_provider(env);
reg.set_adapt_bundle(GeckoBundleAdapter::default())
@ -165,11 +165,11 @@ pub enum L10nRegistryStatus {
}
#[no_mangle]
pub extern "C" fn l10nregistry_new(use_isolating: bool) -> *const GeckoL10nRegistry {
let env = GeckoEnvironment::new(None);
pub extern "C" fn l10nregistry_new() -> *const GeckoL10nRegistry {
let env = GeckoEnvironment;
let mut reg = L10nRegistry::with_provider(env);
let _ = reg
.set_adapt_bundle(GeckoBundleAdapter { use_isolating })
.set_adapt_bundle(GeckoBundleAdapter::default())
.report_error();
Rc::into_raw(Rc::new(reg))
}

View File

@ -6,7 +6,7 @@ use super::fetcher::{GeckoFileFetcher, MockFileFetcher};
use crate::env::GeckoEnvironment;
use fluent::FluentResource;
use l10nregistry::source::{FileSource, FileSourceOptions, ResourceStatus};
use l10nregistry::source::{FileSource, ResourceStatus};
use nsstring::{nsACString, nsCString};
use thin_vec::ThinVec;
@ -29,7 +29,6 @@ pub extern "C" fn l10nfilesource_new(
name: &nsACString,
locales: &ThinVec<nsCString>,
pre_path: &nsACString,
allow_override: bool,
status: &mut L10nFileSourceStatus,
) -> *const FileSource {
if name.is_empty() {
@ -56,10 +55,10 @@ pub extern "C" fn l10nfilesource_new(
name.to_string(),
locales,
pre_path.to_string(),
FileSourceOptions { allow_override },
Default::default(),
GeckoFileFetcher,
);
source.set_reporter(GeckoEnvironment::new(None));
source.set_reporter(GeckoEnvironment);
*status = L10nFileSourceStatus::None;
Rc::into_raw(Rc::new(source))
@ -72,7 +71,6 @@ pub unsafe extern "C" fn l10nfilesource_new_with_index(
pre_path: &nsACString,
index_elements: *const nsCString,
index_length: usize,
allow_override: bool,
status: &mut L10nFileSourceStatus,
) -> *const FileSource {
if name.is_empty() {
@ -109,11 +107,11 @@ pub unsafe extern "C" fn l10nfilesource_new_with_index(
name.to_string(),
locales,
pre_path.to_string(),
FileSourceOptions { allow_override },
Default::default(),
GeckoFileFetcher,
index,
);
source.set_reporter(GeckoEnvironment::new(None));
source.set_reporter(GeckoEnvironment);
*status = L10nFileSourceStatus::None;
Rc::into_raw(Rc::new(source))
@ -165,7 +163,7 @@ pub extern "C" fn l10nfilesource_new_mock(
Default::default(),
fetcher,
);
source.set_reporter(GeckoEnvironment::new(None));
source.set_reporter(GeckoEnvironment);
*status = L10nFileSourceStatus::None;
Rc::into_raw(Rc::new(source))

View File

@ -76,19 +76,6 @@ pub fn get_packaged_locales() -> Option<ThinVec<nsCString>> {
Some(locales)
}
pub fn get_app_locales() -> Option<ThinVec<nsCString>> {
let locale_service =
get_service::<mozILocaleService>(cstr!("@mozilla.org/intl/localeservice;1"))?;
let mut locales = ThinVec::new();
unsafe {
locale_service
.GetAppLocalesAsBCP47(&mut locales)
.to_result()
.ok()?;
}
Some(locales)
}
pub fn set_available_locales(locales: &ThinVec<nsCString>) {
let locale_service =
get_service::<mozILocaleService>(cstr!("@mozilla.org/intl/localeservice;1"))

View File

@ -1,22 +0,0 @@
[package]
name = "localization-ffi"
version = "0.1.0"
authors = ["nobody@mozilla.org"]
edition = "2018"
[dependencies]
futures-channel = "0.3"
futures = "0.3"
nserror = { path = "../../../../xpcom/rust/nserror" }
nsstring = { path = "../../../../xpcom/rust/nsstring" }
l10nregistry = { git = "https://github.com/mozilla/l10nregistry-rs", rev = "55bf7f826d773303a67d8d7fdab099a04322d4fb" }
fluent = { version = "0.16", features = ["fluent-pseudo"] }
unic-langid = "0.9"
thin-vec = { version = "0.2.1", features = ["gecko-ffi"] }
async-trait = "0.1"
moz_task = { path = "../../../../xpcom/rust/moz_task" }
fluent-ffi = { path = "../fluent-ffi" }
fluent-fallback = "0.5"
l10nregistry-ffi = { path = "../l10nregistry-ffi" }
xpcom = { path = "../../../../xpcom/rust/xpcom" }
cstr = "0.2"

View File

@ -1,26 +0,0 @@
header = """/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */"""
autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. See RunCbindgen.py */
#ifndef mozilla_intl_l10n_LocalizationBindings_h
#error "Don't include this file directly, instead include LocalizationBindings.h"
#endif
"""
include_version = true
braces = "SameLine"
line_length = 100
tab_width = 2
language = "C++"
namespaces = ["mozilla", "intl", "ffi"]
includes = ["mozilla/intl/RegistryBindings.h"]
[parse]
parse_deps = true
include = ["fluent-fallback", "l10nregistry-ffi"]
[enum]
derive_helper_methods = true
[export.rename]
"ThinVec" = "nsTArray"
"Promise" = "dom::Promise"

View File

@ -1,569 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use fluent::FluentValue;
use fluent_fallback::{
types::{
L10nAttribute as FluentL10nAttribute, L10nKey as FluentL10nKey,
L10nMessage as FluentL10nMessage,
},
Localization,
};
use fluent_ffi::{convert_args, FluentArgs, FluentArgument, L10nArg};
use l10nregistry_ffi::{
env::GeckoEnvironment,
registry::{get_l10n_registry, GeckoL10nRegistry},
};
use nsstring::{nsACString, nsCString};
use std::os::raw::c_void;
use std::{borrow::Cow, cell::RefCell};
use thin_vec::ThinVec;
use unic_langid::LanguageIdentifier;
use xpcom::{interfaces::nsrefcnt, RefCounted, RefPtr, Refcnt};
#[derive(Debug)]
#[repr(C)]
pub struct L10nKey<'s> {
id: &'s nsACString,
args: ThinVec<L10nArg<'s>>,
}
impl<'s> From<&'s L10nKey<'s>> for FluentL10nKey<'static> {
fn from(input: &'s L10nKey<'s>) -> Self {
FluentL10nKey {
id: input.id.to_utf8().to_string().into(),
args: convert_args_to_owned(&input.args),
}
}
}
// This is a variant of `convert_args` from `fluent-ffi` with a 'static constrain
// put on the resulting `FluentArgs` to make it acceptable into `spqwn_current_thread`.
pub fn convert_args_to_owned(args: &[L10nArg]) -> Option<FluentArgs<'static>> {
if args.is_empty() {
return None;
}
let mut result = FluentArgs::with_capacity(args.len());
for arg in args {
let val = match arg.value {
FluentArgument::Double_(d) => FluentValue::from(d),
// We need this to be owned because we pass the result into `spawn_current_thread`.
FluentArgument::String(s) => FluentValue::from(Cow::Owned(s.to_utf8().to_string())),
};
result.set(arg.id.to_string(), val);
}
Some(result)
}
#[derive(Debug)]
#[repr(C)]
pub struct L10nAttribute {
name: nsCString,
value: nsCString,
}
impl From<FluentL10nAttribute<'_>> for L10nAttribute {
fn from(attr: FluentL10nAttribute<'_>) -> Self {
Self {
name: nsCString::from(&*attr.name),
value: nsCString::from(&*attr.value),
}
}
}
#[derive(Debug)]
#[repr(C)]
pub struct L10nMessage {
value: nsCString,
attributes: ThinVec<L10nAttribute>,
}
impl std::default::Default for L10nMessage {
fn default() -> Self {
Self {
value: nsCString::new(),
attributes: ThinVec::new(),
}
}
}
#[derive(Debug)]
#[repr(C)]
pub struct OptionalL10nMessage {
is_present: bool,
message: L10nMessage,
}
impl From<FluentL10nMessage<'_>> for L10nMessage {
fn from(input: FluentL10nMessage) -> Self {
let value = if let Some(value) = input.value {
value.to_string().into()
} else {
let mut s = nsCString::new();
s.set_is_void(true);
s
};
Self {
value,
attributes: input.attributes.into_iter().map(Into::into).collect(),
}
}
}
pub struct LocalizationRc {
inner: RefCell<Localization<GeckoL10nRegistry, GeckoEnvironment>>,
refcnt: Refcnt,
}
// xpcom::RefPtr support
unsafe impl RefCounted for LocalizationRc {
unsafe fn addref(&self) {
localization_addref(self);
}
unsafe fn release(&self) {
localization_release(self);
}
}
impl LocalizationRc {
pub fn new(
res_ids: Vec<String>,
is_sync: bool,
registry: Option<&GeckoL10nRegistry>,
locales: Option<Vec<LanguageIdentifier>>,
) -> RefPtr<Self> {
let env = GeckoEnvironment::new(locales);
let inner = if let Some(reg) = registry {
Localization::with_env(res_ids, is_sync, env, reg.clone())
} else {
let reg = (*get_l10n_registry()).clone();
Localization::with_env(res_ids, is_sync, env, reg)
};
let loc = Box::new(LocalizationRc {
inner: RefCell::new(inner),
refcnt: unsafe { Refcnt::new() },
});
unsafe {
RefPtr::from_raw(Box::into_raw(loc))
.expect("Failed to create RefPtr<LocalizationRc> from Box<LocalizationRc>")
}
}
pub fn add_resource_id(&self, res_id: String) {
self.inner.borrow_mut().add_resource_id(res_id);
}
pub fn add_resource_ids(&self, res_ids: Vec<String>) {
self.inner.borrow_mut().add_resource_ids(res_ids);
}
pub fn remove_resource_id(&self, res_id: String) -> usize {
self.inner.borrow_mut().remove_resource_id(res_id)
}
pub fn remove_resource_ids(&self, res_ids: Vec<String>) -> usize {
self.inner.borrow_mut().remove_resource_ids(res_ids)
}
pub fn set_async(&self) {
if self.is_sync() {
self.inner.borrow_mut().set_async();
}
}
pub fn is_sync(&self) -> bool {
self.inner.borrow().is_sync()
}
pub fn on_change(&self) {
self.inner.borrow_mut().on_change();
}
pub fn format_value_sync(
&self,
id: &nsACString,
args: &ThinVec<L10nArg>,
ret_val: &mut nsACString,
ret_err: &mut ThinVec<nsCString>,
) -> bool {
let mut errors = vec![];
let args = convert_args(&args);
if let Ok(value) = self.inner.borrow().bundles().format_value_sync(
&id.to_utf8(),
args.as_ref(),
&mut errors,
) {
if let Some(value) = value {
ret_val.assign(&value);
} else {
ret_val.set_is_void(true);
}
ret_err.extend(errors.into_iter().map(|err| err.to_string().into()));
true
} else {
false
}
}
pub fn format_values_sync(
&self,
keys: &ThinVec<L10nKey>,
ret_val: &mut ThinVec<nsCString>,
ret_err: &mut ThinVec<nsCString>,
) -> bool {
ret_val.reserve(keys.len());
let keys: Vec<FluentL10nKey> = keys.into_iter().map(|k| k.into()).collect();
let mut errors = vec![];
if let Ok(values) = self
.inner
.borrow()
.bundles()
.format_values_sync(&keys, &mut errors)
{
for value in values.iter() {
if let Some(value) = value {
ret_val.push(value.as_ref().into());
} else {
let mut void_string = nsCString::new();
void_string.set_is_void(true);
ret_val.push(void_string);
}
}
ret_err.extend(errors.into_iter().map(|err| err.to_string().into()));
true
} else {
false
}
}
pub fn format_messages_sync(
&self,
keys: &ThinVec<L10nKey>,
ret_val: &mut ThinVec<OptionalL10nMessage>,
ret_err: &mut ThinVec<nsCString>,
) -> bool {
ret_val.reserve(keys.len());
let mut errors = vec![];
let keys: Vec<FluentL10nKey> = keys.into_iter().map(|k| k.into()).collect();
if let Ok(messages) = self
.inner
.borrow()
.bundles()
.format_messages_sync(&keys, &mut errors)
{
for msg in messages {
ret_val.push(if let Some(msg) = msg {
OptionalL10nMessage {
is_present: true,
message: msg.into(),
}
} else {
OptionalL10nMessage {
is_present: false,
message: L10nMessage::default(),
}
});
}
assert_eq!(keys.len(), ret_val.len());
ret_err.extend(errors.into_iter().map(|err| err.to_string().into()));
true
} else {
false
}
}
pub fn format_value(
&self,
id: &nsACString,
args: &ThinVec<L10nArg>,
promise: &xpcom::Promise,
callback: extern "C" fn(&xpcom::Promise, &nsACString, &ThinVec<nsCString>),
) {
let bundles = self.inner.borrow().bundles().clone();
let args = convert_args_to_owned(&args);
let id = nsCString::from(id);
let strong_promise = RefPtr::new(promise);
moz_task::spawn_current_thread(async move {
let mut errors = vec![];
let value = if let Some(value) = bundles
.format_value(&id.to_utf8(), args.as_ref(), &mut errors)
.await
{
let v: nsCString = value.to_string().into();
v
} else {
let mut v = nsCString::new();
v.set_is_void(true);
v
};
let errors = errors
.into_iter()
.map(|err| err.to_string().into())
.collect();
callback(&strong_promise, &value, &errors);
})
.expect("Failed to spawn future");
}
pub fn format_values(
&self,
keys: &ThinVec<L10nKey>,
promise: &xpcom::Promise,
callback: extern "C" fn(&xpcom::Promise, &ThinVec<nsCString>, &ThinVec<nsCString>),
) {
let bundles = self.inner.borrow().bundles().clone();
let keys: Vec<FluentL10nKey> = keys.into_iter().map(|k| k.into()).collect();
let strong_promise = RefPtr::new(promise);
moz_task::spawn_current_thread(async move {
let mut errors = vec![];
let ret_val = bundles
.format_values(&keys, &mut errors)
.await
.into_iter()
.map(|value| {
if let Some(value) = value {
nsCString::from(value.as_ref())
} else {
let mut v = nsCString::new();
v.set_is_void(true);
v
}
})
.collect::<ThinVec<_>>();
assert_eq!(keys.len(), ret_val.len());
let errors = errors
.into_iter()
.map(|err| err.to_string().into())
.collect();
callback(&strong_promise, &ret_val, &errors);
})
.expect("Failed to spawn future");
}
pub fn format_messages(
&self,
keys: &ThinVec<L10nKey>,
promise: &xpcom::Promise,
callback: extern "C" fn(
&xpcom::Promise,
&ThinVec<OptionalL10nMessage>,
&ThinVec<nsCString>,
),
) {
let bundles = self.inner.borrow().bundles().clone();
let keys: Vec<FluentL10nKey> = keys.into_iter().map(|k| k.into()).collect();
let strong_promise = RefPtr::new(promise);
moz_task::spawn_current_thread(async move {
let mut errors = vec![];
let ret_val = bundles
.format_messages(&keys, &mut errors)
.await
.into_iter()
.map(|msg| {
if let Some(msg) = msg {
OptionalL10nMessage {
is_present: true,
message: msg.into(),
}
} else {
OptionalL10nMessage {
is_present: false,
message: L10nMessage::default(),
}
}
})
.collect::<ThinVec<_>>();
assert_eq!(keys.len(), ret_val.len());
let errors = errors
.into_iter()
.map(|err| err.to_string().into())
.collect();
callback(&strong_promise, &ret_val, &errors);
})
.expect("Failed to spawn future");
}
}
#[no_mangle]
pub extern "C" fn localization_parse_locale(input: &nsCString) -> *const c_void {
let l: LanguageIdentifier = input.to_utf8().parse().unwrap();
Box::into_raw(Box::new(l)) as *const c_void
}
#[no_mangle]
pub extern "C" fn localization_new(
res_ids: &ThinVec<nsCString>,
is_sync: bool,
reg: Option<&GeckoL10nRegistry>,
result: &mut *const LocalizationRc,
) {
*result = std::ptr::null();
let res_ids: Vec<String> = res_ids.iter().map(|res| res.to_string()).collect();
*result = RefPtr::forget_into_raw(LocalizationRc::new(res_ids, is_sync, reg, None));
}
#[no_mangle]
pub extern "C" fn localization_new_with_locales(
res_ids: &ThinVec<nsCString>,
is_sync: bool,
reg: Option<&GeckoL10nRegistry>,
locales: Option<&ThinVec<nsCString>>,
result: &mut *const LocalizationRc,
) -> bool {
*result = std::ptr::null();
let res_ids: Vec<String> = res_ids.iter().map(|res| res.to_string()).collect();
let locales: Result<Option<Vec<LanguageIdentifier>>, _> = locales
.map(|locales| {
locales
.iter()
.map(|s| LanguageIdentifier::from_bytes(&s))
.collect()
})
.transpose();
if let Ok(locales) = locales {
*result = RefPtr::forget_into_raw(LocalizationRc::new(res_ids, is_sync, reg, locales));
true
} else {
false
}
}
#[no_mangle]
pub unsafe extern "C" fn localization_addref(loc: &LocalizationRc) -> nsrefcnt {
loc.refcnt.inc()
}
#[no_mangle]
pub unsafe extern "C" fn localization_release(loc: *const LocalizationRc) -> nsrefcnt {
let rc = (*loc).refcnt.dec();
if rc == 0 {
Box::from_raw(loc as *const _ as *mut LocalizationRc);
}
rc
}
#[no_mangle]
pub extern "C" fn localization_add_res_id(loc: &LocalizationRc, res_id: &nsACString) {
let res_id = res_id.to_string();
loc.add_resource_id(res_id);
}
#[no_mangle]
pub extern "C" fn localization_add_res_ids(loc: &LocalizationRc, res_ids: &ThinVec<nsCString>) {
let res_ids = res_ids.iter().map(|s| s.to_string()).collect();
loc.add_resource_ids(res_ids);
}
#[no_mangle]
pub extern "C" fn localization_remove_res_id(loc: &LocalizationRc, res_id: &nsACString) -> usize {
let res_id = res_id.to_string();
loc.remove_resource_id(res_id)
}
#[no_mangle]
pub extern "C" fn localization_remove_res_ids(
loc: &LocalizationRc,
res_ids: &ThinVec<nsCString>,
) -> usize {
let res_ids = res_ids.iter().map(|s| s.to_string()).collect();
loc.remove_resource_ids(res_ids)
}
#[no_mangle]
pub extern "C" fn localization_format_value_sync(
loc: &LocalizationRc,
id: &nsACString,
args: &ThinVec<L10nArg>,
ret_val: &mut nsACString,
ret_err: &mut ThinVec<nsCString>,
) -> bool {
loc.format_value_sync(id, args, ret_val, ret_err)
}
#[no_mangle]
pub extern "C" fn localization_format_values_sync(
loc: &LocalizationRc,
keys: &ThinVec<L10nKey>,
ret_val: &mut ThinVec<nsCString>,
ret_err: &mut ThinVec<nsCString>,
) -> bool {
loc.format_values_sync(keys, ret_val, ret_err)
}
#[no_mangle]
pub extern "C" fn localization_format_messages_sync(
loc: &LocalizationRc,
keys: &ThinVec<L10nKey>,
ret_val: &mut ThinVec<OptionalL10nMessage>,
ret_err: &mut ThinVec<nsCString>,
) -> bool {
loc.format_messages_sync(keys, ret_val, ret_err)
}
#[no_mangle]
pub extern "C" fn localization_format_value(
loc: &LocalizationRc,
id: &nsACString,
args: &ThinVec<L10nArg>,
promise: &xpcom::Promise,
callback: extern "C" fn(&xpcom::Promise, &nsACString, &ThinVec<nsCString>),
) {
loc.format_value(id, args, promise, callback);
}
#[no_mangle]
pub extern "C" fn localization_format_values(
loc: &LocalizationRc,
keys: &ThinVec<L10nKey>,
promise: &xpcom::Promise,
callback: extern "C" fn(&xpcom::Promise, &ThinVec<nsCString>, &ThinVec<nsCString>),
) {
loc.format_values(keys, promise, callback);
}
#[no_mangle]
pub extern "C" fn localization_format_messages(
loc: &LocalizationRc,
keys: &ThinVec<L10nKey>,
promise: &xpcom::Promise,
callback: extern "C" fn(&xpcom::Promise, &ThinVec<OptionalL10nMessage>, &ThinVec<nsCString>),
) {
loc.format_messages(keys, promise, callback);
}
#[no_mangle]
pub extern "C" fn localization_set_async(loc: &LocalizationRc) {
loc.set_async();
}
#[no_mangle]
pub extern "C" fn localization_is_sync(loc: &LocalizationRc) -> bool {
loc.is_sync()
}
#[no_mangle]
pub extern "C" fn localization_on_change(loc: &LocalizationRc) {
loc.on_change();
}

View File

@ -7,24 +7,20 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const mockSource = L10nFileSource.createMock("test", ["en-US"], "/localization/{locale}/", [
{
path: "/localization/en-US/mock.ftl",
source: `
async function* generateBundles(resourceIds) {
const bundle = new FluentBundle("en-US", {
useIsolating: false,
});
bundle.addResource(new FluentResource(`
key1 = Value
.title = Title 1
.accesskey = K
key2 =
.label = This is a label for { $user }
`
}
]);
let registry = new L10nRegistry({
bundleOptions: {
useIsolating: false
}
});
registry.registerSources([mockSource]);
`));
yield bundle;
}
function getAttributeByName(attributes, name) {
return attributes.find(attr => attr.name === name);
@ -36,7 +32,7 @@ key2 =
const loc = new Localization(
['mock.ftl'],
false,
registry,
{ generateBundles },
);
{
@ -72,8 +68,8 @@ key2 =
]);
ok(false, "Missing argument didn't cause an exception.");
} catch (e) {
is(e.message,
"[fluent][resolver] errors in en-US/key2: Resolver error: Unknown variable: $user",
is(e,
"[fluent][resolver] errors in en-US/key2: Resolver error: Unknown variable: $user.",
"Missing key causes an exception.");
}
}
@ -87,8 +83,8 @@ key2 =
]);
ok(false, "Missing key didn't cause an exception.");
} catch (e) {
is(e.message,
"[fluent] Missing message in locale en-US: key4",
is(e,
"[fluent] Missing translations in en-US: key4.",
"Missing key causes an exception.");
}
}

View File

@ -7,22 +7,17 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const mockSource = L10nFileSource.createMock("test", ["en-US"], "/localization/{locale}/", [
{
path: "/localization/en-US/mock.ftl",
source: `
async function* generateBundles(resourceIds) {
const bundle = new FluentBundle("en-US", {
useIsolating: false,
});
bundle.addResource(new FluentResource(`
key1 = Value
key2 = Value { $user }
key3 = Value { $count }
`
}
]);
let registry = new L10nRegistry({
bundleOptions: {
useIsolating: false
}
});
registry.registerSources([mockSource]);
`));
yield bundle;
}
(async () => {
SimpleTest.waitForExplicitFinish();
@ -30,7 +25,7 @@ key3 = Value { $count }
const loc = new Localization(
['mock.ftl'],
false,
registry,
{ generateBundles },
);
{
@ -58,8 +53,8 @@ key3 = Value { $count }
let val = await loc.formatValue("key3");
ok(false, "Missing argument didn't cause an exception.");
} catch (e) {
is(e.message,
"[fluent][resolver] errors in en-US/key3: Resolver error: Unknown variable: $count",
is(e,
"[fluent][resolver] errors in en-US/key3: Resolver error: Unknown variable: $count.",
"Missing key causes an exception.");
}
}

View File

@ -7,22 +7,18 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const mockSource = L10nFileSource.createMock("test", ["en-US"], "/localization/{locale}/", [
async function* generateBundles(resourceIds) {
const bundle = new FluentBundle("en-US",
{
path: "/localization/en-US/mock.ftl",
source: `
useIsolating: false,
});
bundle.addResource(new FluentResource(`
key1 = Value
key2 = Value { $user }
key3 = Value { $count }
`
}
]);
let registry = new L10nRegistry({
bundleOptions: {
useIsolating: false
}
});
registry.registerSources([mockSource]);
`));
yield bundle;
}
(async () => {
SimpleTest.waitForExplicitFinish();
@ -30,7 +26,7 @@ key3 = Value { $count }
const loc = new Localization(
['mock.ftl'],
false,
registry,
{ generateBundles },
);
{
@ -55,8 +51,8 @@ key3 = Value { $count }
]);
ok(false, "Missing argument didn't cause an exception.");
} catch (e) {
is(e.message,
"[fluent][resolver] errors in en-US/key2: Resolver error: Unknown variable: $user",
is(e,
"[fluent][resolver] errors in en-US/key2: Resolver error: Unknown variable: $user.",
"Missing key causes an exception.");
}
}
@ -70,8 +66,8 @@ key3 = Value { $count }
]);
ok(false, "Missing key didn't cause an exception.");
} catch (e) {
is(e.message,
"[fluent] Missing message in locale en-US: key4",
is(e,
"[fluent] Missing translations in en-US: key4.",
"Missing key causes an exception.");
}
}

View File

@ -166,7 +166,7 @@ add_task(async function test_has_two_sources() {
* missing files as `false` instead of `undefined`.
*/
add_task(function test_indexed() {
let oneSource = new L10nFileSource("langpack-pl", ["pl"], "/data/locales/{locale}/", {}, [
let oneSource = new L10nFileSource("langpack-pl", ["pl"], "/data/locales/{locale}/", [
"/data/locales/pl/test.ftl",
]);
equal(oneSource.hasFile("pl", "test.ftl"), "present");

View File

@ -28,9 +28,13 @@ key-attr =
const source = L10nFileSource.createMock("test", ["de", "en-US"], "/localization/{locale}", fs);
l10nReg.registerSources([source]);
async function* generateBundles(resIds) {
yield * l10nReg.generateBundles(["de", "en-US"], resIds);
}
const l10n = new Localization([
"/browser/menu.ftl",
], false, l10nReg, ["de", "en-US"]);
], false, { generateBundles });
{
let values = await l10n.formatValues([
@ -80,6 +84,9 @@ key-attr =
strictEqual(messages[2].value, "[en] Value3");
strictEqual(messages[3].value, null);
}
l10nReg.clearSources();
Services.locale.requestedLocales = originalRequested;
});
add_task(async function test_builtins() {
@ -103,14 +110,20 @@ key = { PLATFORM() ->
const source = L10nFileSource.createMock("test", ["en-US"], "/localization/{locale}", fs);
l10nReg.registerSources([source]);
async function* generateBundles(resIds) {
yield * await l10nReg.generateBundles(["en-US"], resIds);
}
const l10n = new Localization([
"/test.ftl",
], false, l10nReg, ["en-US"]);
], false, { generateBundles });
let values = await l10n.formatValues([{id: "key"}]);
ok(values[0].includes(
`${ known_platforms[AppConstants.platform].toUpperCase() } Value`));
l10nReg.clearSources();
});
add_task(async function test_add_remove_resourceIds() {
@ -120,11 +133,16 @@ add_task(async function test_add_remove_resourceIds() {
{ path: "/localization/en-US/toolkit/menu.ftl", source: "key2 = Value2" },
];
const originalRequested = Services.locale.requestedLocales;
const source = L10nFileSource.createMock("test", ["en-US"], "/localization/{locale}", fs);
l10nReg.registerSources([source]);
const l10n = new Localization(["/browser/menu.ftl"], false, l10nReg, ["en-US"]);
async function* generateBundles(resIds) {
yield * await l10nReg.generateBundles(["en-US"], resIds);
}
const l10n = new Localization(["/browser/menu.ftl"], false, { generateBundles });
let values = await l10n.formatValues([{id: "key1"}, {id: "key2"}]);
@ -154,35 +172,42 @@ add_task(async function test_add_remove_resourceIds() {
strictEqual(values[0], null);
strictEqual(values[1], "Value2");
l10nReg.clearSources();
Services.locale.requestedLocales = originalRequested;
});
add_task(async function test_switch_to_async() {
const l10nReg = new L10nRegistry();
const fs = [
{ path: "/localization/en-US/browser/menu.ftl", source: "key1 = Value1" },
{ path: "/localization/en-US/toolkit/menu.ftl", source: "key2 = Value2" },
];
const originalRequested = Services.locale.requestedLocales;
const source = L10nFileSource.createMock("test", ["en-US"], "/localization/{locale}", fs);
l10nReg.registerSources([source]);
const l10n = new Localization(["/browser/menu.ftl"], true, l10nReg, ["en-US"]);
async function* generateBundles(resIds) {
yield * await l10nReg.generateBundles(["en-US"], resIds);
}
let values = l10n.formatValuesSync([{id: "key1"}, {id: "key2"}]);
function* generateBundlesSync(resIds) {
yield * l10nReg.generateBundlesSync(["en-US"], resIds);
}
const l10n = new Localization(["/browser/menu.ftl"], false, { generateBundles, generateBundlesSync });
let values = await l10n.formatValues([{id: "key1"}, {id: "key2"}]);
strictEqual(values[0], "Value1");
strictEqual(values[1], null);
l10n.setAsync();
Assert.throws(() => {
l10n.formatValuesSync([{ id: "key1" }, { id: "key2" }]);
}, /Can't use formatValuesSync when state is async./);
l10n.setIsSync(true);
l10n.addResourceIds(["/toolkit/menu.ftl"]);
values = await l10n.formatValues([{id: "key1"}, {id: "key2"}]);
values = l10n.formatValuesSync([{id: "key1"}, {id: "key2"}]);
let values2 = await l10n.formatValues([{id: "key1"}, {id: "key2"}]);
deepEqual(values, values2);
@ -195,4 +220,7 @@ add_task(async function test_switch_to_async() {
strictEqual(values[0], null);
strictEqual(values[1], "Value2");
l10nReg.clearSources();
Services.locale.requestedLocales = originalRequested;
});

View File

@ -18,13 +18,18 @@ key-attr =
.label = [en] Label 3
` },
];
const originalRequested = Services.locale.requestedLocales;
const source = L10nFileSource.createMock("test", ["de", "en-US"], "/localization/{locale}", fs);
l10nReg.registerSources([source]);
function* generateBundlesSync(resIds) {
yield * l10nReg.generateBundlesSync(["de", "en-US"], resIds);
}
const l10n = new Localization([
"/browser/menu.ftl",
], true, l10nReg, ["de", "en-US"]);
], true, { generateBundlesSync });
{
@ -75,6 +80,8 @@ key-attr =
strictEqual(messages[2].value, "[en] Value3");
strictEqual(messages[3].value, null);
}
Services.locale.requestedLocales = originalRequested;
});
add_task(function test_builtins() {
@ -98,14 +105,19 @@ key = { PLATFORM() ->
const l10nReg = new L10nRegistry();
l10nReg.registerSources([source]);
function* generateBundlesSync(resIds) {
yield * l10nReg.generateBundlesSync(["en-US"], resIds);
}
const l10n = new Localization([
"/test.ftl",
], true, l10nReg, ["en-US"]);
], true, { generateBundlesSync });
let values = l10n.formatValuesSync([{id: "key"}]);
ok(values[0].includes(
`${ known_platforms[AppConstants.platform].toUpperCase() } Value`));
});
add_task(function test_add_remove_resourceIds() {
@ -119,7 +131,11 @@ add_task(function test_add_remove_resourceIds() {
const l10nReg = new L10nRegistry();
l10nReg.registerSources([source]);
const l10n = new Localization(["/browser/menu.ftl"], true, l10nReg, ["en-US"]);
function* generateBundlesSync(resIds) {
yield * l10nReg.generateBundlesSync(["en-US"], resIds);
}
const l10n = new Localization(["/browser/menu.ftl"], true, { generateBundlesSync });
let values = l10n.formatValuesSync([{id: "key1"}, {id: "key2"}]);
@ -149,6 +165,8 @@ add_task(function test_add_remove_resourceIds() {
strictEqual(values[0], null);
strictEqual(values[1], "Value2");
Services.locale.requestedLocales = originalRequested;
});
add_task(function test_calling_sync_methods_in_async_mode_fails() {

View File

@ -3,26 +3,25 @@
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const originalValues = {
requested: Services.locale.requestedLocales,
};
const originalValues = {};
const l10nReg = new L10nRegistry();
function getMockRegistry() {
const mockSource = L10nFileSource.createMock("test", ["en-US"], "/localization/{locale}/", [
{
path: "/localization/en-US/mock.ftl",
source: `
function addMockFileSource() {
const fs = [
{ path: "/localization/de/browser/menu.ftl", source: `
key = This is a single message
.tooltip = This is a tooltip
.accesskey = f
`
}
]);
let registry = new L10nRegistry();
registry.registerSources([mockSource]);
return registry;
.tooltip = This is a tooltip
.accesskey = f` },
];
originalValues.requested = Services.locale.requestedLocales;
const source = L10nFileSource.createMock("test", ["de"], "/localization/{locale}", fs);
l10nReg.registerSources([source]);
return async function* generateMessages(resIds) {
yield * await l10nReg.generateBundles(["de"], resIds);
};
}
function getAttributeByName(attributes, name) {
@ -41,11 +40,11 @@ function getAttributeByName(attributes, name) {
add_task(async function test_pseudo_works() {
Services.prefs.setStringPref("intl.l10n.pseudo", "");
let mockRegistry = getMockRegistry();
let generateBundles = addMockFileSource();
const l10n = new Localization([
"mock.ftl",
], false, mockRegistry);
"/browser/menu.ftl",
], false, { generateBundles });
{
// 1. Start with no pseudo
@ -98,6 +97,7 @@ add_task(async function test_pseudo_works() {
equal(attr1.value, "f");
}
l10nReg.clearSources();
Services.locale.requestedLocales = originalValues.requested;
});
@ -108,11 +108,11 @@ add_task(async function test_pseudo_works() {
add_task(async function test_unavailable_strategy_works() {
Services.prefs.setStringPref("intl.l10n.pseudo", "");
let mockRegistry = getMockRegistry();
let generateBundles = addMockFileSource();
const l10n = new Localization([
"mock.ftl",
], false, mockRegistry);
"/browser/menu.ftl",
], false, { generateBundles });
{
// 1. Set unavailable pseudo strategy
@ -128,5 +128,6 @@ add_task(async function test_unavailable_strategy_works() {
}
Services.prefs.setStringPref("intl.l10n.pseudo", "");
l10nReg.clearSources();
Services.locale.requestedLocales = originalValues.requested;
});

View File

@ -13,21 +13,14 @@ async function checkServerCertificates(win, expectedValues = []) {
expectedValues.length
);
}, `Expected to have ${expectedValues.length} but got ${win.document.getElementById("serverList").itemChildren.length}`);
// The strings we will get from the DOM are localized with Fluent,
// so they will only be available after the next refresh driver tick.
await new Promise(win.requestAnimationFrame);
let labels = win.document
.getElementById("serverList")
.querySelectorAll("label");
// The strings we will get from the DOM are localized with Fluent.
// This will wait until the translation is applied.
if (expectedValues.length > 0) {
await BrowserTestUtils.waitForCondition(
() => labels[1].value || labels[1].textContent.length > 0,
"At least one label is populated"
);
}
expectedValues.forEach((item, i) => {
let hostPort = labels[i * 3].value;
let certString = labels[i * 3 + 1].value || labels[i * 3 + 1].textContent;

View File

@ -153,14 +153,10 @@ function getTimeMultiplier(unit) {
}
throw new Error("Invalid time unit: " + unit);
}
async function testCpu(element, total, slope, assumptions) {
function testCpu(element, total, slope, assumptions) {
info(
`Testing CPU display ${element.textContent} - ${element.title} vs total ${total}, slope ${slope}`
);
await BrowserTestUtils.waitForCondition(
() => !!element.textContent.length,
"waiting for l10n to populate"
);
if (element.textContent == "(measuring)") {
info("Still measuring");
return;
@ -232,14 +228,10 @@ async function testCpu(element, total, slope, assumptions) {
);
}
async function testMemory(element, total, delta, assumptions) {
function testMemory(element, total, delta, assumptions) {
info(
`Testing memory display ${element.textContent} - ${element.title} vs total ${total}, delta ${delta}`
);
await BrowserTestUtils.waitForCondition(
() => !!element.textContent.length,
"waiting for l10n to populate"
);
const MEMORY_TEXT_CONTENT_REGEXP = /([0-9.,]+)(TB|GB|MB|KB|B)/;
// Example: "383.55MB"
let extracted = MEMORY_TEXT_CONTENT_REGEXP.exec(element.textContent);
@ -574,7 +566,7 @@ async function testAboutProcessesWithConfig({ showAllFrames, showThreads }) {
Assert.equal(pid, row.process.pid);
info("Sanity checks: memory resident");
await testMemory(
testMemory(
memory,
row.process.totalRamSize,
row.process.deltaRamSize,
@ -582,7 +574,7 @@ async function testAboutProcessesWithConfig({ showAllFrames, showThreads }) {
);
info("Sanity checks: CPU (Total)");
await testCpu(
testCpu(
cpu,
row.process.totalCpu,
row.process.slopeCpu,
@ -647,6 +639,10 @@ async function testAboutProcessesWithConfig({ showAllFrames, showThreads }) {
let twisty = threads.getElementsByClassName("twisty")[0];
twisty.click();
// `twisty.click()` is sync, but Fluent will update the visible
// table content during the next refresh driver tick, wait for it.
await new Promise(doc.defaultView.requestAnimationFrame);
let numberOfThreadsFound = 0;
for (
let threadRow = threads.nextSibling;
@ -666,11 +662,6 @@ async function testAboutProcessesWithConfig({ showAllFrames, showThreads }) {
threadRow && threadRow.classList.contains("thread");
threadRow = threadRow.nextSibling
) {
// Wait for l10n to populate.
await BrowserTestUtils.waitForCondition(
() => !!threadRow.children[1].textContent.length,
"waiting for l10n to populate"
);
Assert.ok(
threadRow.children.length >= 3 && threadRow.children[1].textContent,
"The thread row should be populated"
@ -695,7 +686,7 @@ async function testAboutProcessesWithConfig({ showAllFrames, showThreads }) {
Assert.equal(tid, threadRow.thread.tid, "Displayed tid is correct");
// Sanity checks: CPU (per thread)
await testCpu(
testCpu(
cpu,
threadRow.thread.totalCpu,
threadRow.thread.slopeCpu,

View File

@ -6,7 +6,6 @@
async function checkNonEmptyFields(url) {
await BrowserTestUtils.withNewTab(url, async function(browser) {
await SpecialPowers.spawn(browser, [], async function() {
await content.document.l10n.ready;
let certificateSection = await ContentTaskUtils.waitForCondition(() => {
return content.document.querySelector("certificate-section");
}, "Certificate section found");
@ -56,12 +55,8 @@ async function checkNonEmptyFields(url) {
for (let infoItem of infoItems) {
let item = infoItem.shadowRoot.querySelector(".info");
if (item.textContent.length === 0) {
await ContentTaskUtils.waitForCondition(
() => parseInt(item.textContent.length) > 0,
"info-item has not been localized."
);
}
let info = item.textContent;
Assert.notEqual(info, "", "Empty strings shouldn't be rendered");
}
}
}
@ -79,5 +74,7 @@ add_task(async function test() {
let urls = [url1, url2, url3];
await Promise.all(urls.map(checkNonEmptyFields));
for (let url of urls) {
await checkNonEmptyFields(url);
}
});

View File

@ -92,13 +92,6 @@ add_task(async function testPrintDoesNotWaitForPreview() {
helper.mockFilePicker("print_does_not_wait_for_preview.pdf");
await helper.setupMockPrint();
let systemPrint = helper.get("system-print");
await BrowserTestUtils.waitForCondition(
() => BrowserTestUtils.is_visible(systemPrint),
"Wait for the system-print to be visible"
);
helper.click(helper.get("open-dialog-link"));
helper.assertDialogHidden();

View File

@ -67,7 +67,6 @@ fluent-ffi = { path = "../../../../intl/l10n/rust/fluent-ffi" }
l10nregistry-ffi = { path = "../../../../intl/l10n/rust/l10nregistry-ffi" }
l10nregistry = { git = "https://github.com/mozilla/l10nregistry-rs", rev = "55bf7f826d773303a67d8d7fdab099a04322d4fb" }
fluent-fallback = "0.5"
localization-ffi = { path = "../../../../intl/l10n/rust/localization-ffi" }
processtools = { path = "../../../components/processtools" }
qcms = { path = "../../../../gfx/qcms", features = ["c_bindings", "neon"], default-features = false }

View File

@ -29,6 +29,7 @@ extern crate gkrust_utils;
extern crate http_sfv;
extern crate jsrust_shared;
extern crate kvstore;
extern crate l10nregistry_ffi;
extern crate mapped_hyph;
extern crate mozurl;
extern crate mp4parse_capi;
@ -74,8 +75,7 @@ extern crate fluent;
extern crate fluent_ffi;
extern crate fluent_fallback;
extern crate l10nregistry_ffi;
extern crate localization_ffi;
extern crate l10nregistry;
#[cfg(not(target_os = "android"))]
extern crate viaduct;

View File

@ -133,10 +133,9 @@ void nsPrinterListBase::EnsureCommonPaperInfo(JSContext* aCx) {
// available (otherwise leave them as the internal keys, which are at least
// somewhat recognizable).
IgnoredErrorResult rv;
nsTArray<nsCString> resIds = {
"toolkit/printing/printUI.ftl"_ns,
};
RefPtr<Localization> l10n = Localization::Create(resIds, true);
nsCOMPtr<nsIGlobalObject> global = xpc::CurrentNativeGlobal(aCx);
RefPtr<Localization> l10n = Localization::Create(global, true, {});
l10n->AddResourceId(u"toolkit/printing/printUI.ftl"_ns);
for (auto i : IntegerRange(nsPaper::kNumCommonPaperSizes)) {
const CommonPaperSize& size = nsPaper::kCommonPaperSizes[i];
@ -145,7 +144,7 @@ void nsPrinterListBase::EnsureCommonPaperInfo(JSContext* aCx) {
nsAutoCString key{"printui-paper-"};
key.Append(size.mLocalizableNameKey);
nsAutoCString name;
l10n->FormatValueSync(key, {}, name, rv);
l10n->FormatValueSync(aCx, key, {}, name, rv);
// Fill out the info with our PWG size and the localized name.
info.mId = size.mPWGName;

View File

@ -65,13 +65,9 @@ where
// Mutex ensures that future is polled serially.
self.enter(|cx| {
// The only way to have this `LocalTask` dispatched to the named
// event target is for it to be dispatched by the `Waker`, which will
// put the state into `POLL` before dispatching the runnable.
// Another waker may have put the state into `REPOLL` in the
// meantime, however we can clear that back to `POLL` now as we're
// about to begin polling.
self.state.start_poll();
// event target is for it to be dispatched by the Waker, which will
// put the state into POLL before dispatching the runnable.
assert!(self.state.is(POLL));
loop {
// # Safety
//
@ -132,6 +128,10 @@ impl Default for TaskState {
}
impl TaskState {
fn is(&self, state: usize) -> bool {
self.state.load(SeqCst) == state
}
/// Attempt to "wake up" the task and poll the future.
///
/// A `true` result indicates that the `POLL` state has been entered, and
@ -199,14 +199,6 @@ impl TaskState {
debug_assert!(matches!(self.state.load(SeqCst), POLL | REPOLL));
self.state.store(COMPLETE, SeqCst);
}
/// We're about to begin polling, clear any accumulated re-poll requests.
///
/// Should only be called from the `POLL`/`REPOLL` states immediately before polling.
fn start_poll(&self) {
assert!(matches!(self.state.load(SeqCst), POLL | REPOLL));
self.state.store(POLL, SeqCst);
}
}
struct LocalWakeHandle<F: Future<Output = ()> + 'static> {

View File

@ -113,11 +113,9 @@ using dom::AutoNoJSAPI;
using dom::BrowserHost;
using dom::BrowsingContext;
using dom::Document;
using dom::DocumentL10n;
using dom::Element;
using dom::EventTarget;
using dom::LoadURIOptions;
using dom::Promise;
AppWindow::AppWindow(uint32_t aChromeFlags)
: mChromeTreeOwner(nullptr),