Backed out 9 changesets (bug 1560038) for causing build bustage

CLOSED TREE

Backed out changeset 12069dae9b8d (bug 1560038)
Backed out changeset a0845cf79487 (bug 1560038)
Backed out changeset dc2406d01a63 (bug 1560038)
Backed out changeset 1b11616a5ee5 (bug 1560038)
Backed out changeset 06cdc27a39a7 (bug 1560038)
Backed out changeset a390456d9d26 (bug 1560038)
Backed out changeset 7e3d839a3e9d (bug 1560038)
Backed out changeset 0678db762fed (bug 1560038)
Backed out changeset 3b9b16532bf3 (bug 1560038)
This commit is contained in:
Daniel Varga 2020-03-11 04:53:10 +02:00
parent 9638f7914f
commit a2cbf61a81
235 changed files with 1926 additions and 28003 deletions

84
Cargo.lock generated
View File

@ -1278,44 +1278,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "fluent"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ebe7532e1e5146a909de9e019e31835a84b5dee3eeb234561e525844f3cf3bf"
dependencies = [
"fluent-bundle",
"fluent-pseudo",
"unic-langid",
]
[[package]]
name = "fluent-bundle"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27ade33328521266c81cc0924523988f43ccd7359f64689a1b6e818afca3a646"
dependencies = [
"fluent-langneg",
"fluent-syntax",
"intl-memoizer",
"intl_pluralrules",
"rental",
"smallvec 1.2.0",
"unic-langid",
]
[[package]]
name = "fluent-ffi"
version = "0.1.0"
dependencies = [
"fluent",
"fluent-pseudo",
"intl-memoizer",
"nsstring",
"thin-vec",
"unic-langid",
]
[[package]]
name = "fluent-langneg"
version = "0.12.1"
@ -1338,21 +1300,6 @@ dependencies = [
"xpcom",
]
[[package]]
name = "fluent-pseudo"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3a870aefc42d175d11fb1ec089221ced8a160d66ca1e0c64a57b4ae90d2462"
dependencies = [
"regex",
]
[[package]]
name = "fluent-syntax"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fe937dbd784d0f085f05c15a06f0d5dd06ce31cc823f7ab12ebb3758d948b39"
[[package]]
name = "fnv"
version = "1.0.6"
@ -1671,8 +1618,6 @@ dependencies = [
"cubeb-sys",
"encoding_glue",
"env_logger",
"fluent",
"fluent-ffi",
"fluent-langneg",
"fluent-langneg-ffi",
"fog",
@ -1980,26 +1925,6 @@ dependencies = [
"adler32",
]
[[package]]
name = "intl-memoizer"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9867e2d65d82936ef34217ed0f87b639a94384e93a0676158142c861c705391f"
dependencies = [
"type-map",
"unic-langid",
]
[[package]]
name = "intl_pluralrules"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d82c14d8eece42c03353e0ce86a4d3f97b1f1cef401e4d962dca6c6214a85002"
dependencies = [
"tinystr",
"unic-langid",
]
[[package]]
name = "iovec"
version = "0.1.2"
@ -4516,15 +4441,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382"
[[package]]
name = "type-map"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d2741b1474c327d95c1f1e3b0a2c3977c8e128409c572a33af2914e7d636717"
dependencies = [
"fxhash",
]
[[package]]
name = "typenum"
version = "1.10.0"

View File

@ -289,20 +289,6 @@ DOMInterfaces = {
'concrete': True,
},
'FluentBundle': {
'nativeType': 'mozilla::intl::FluentBundle',
},
'FluentPattern': {
'headerFile': 'mozilla/intl/FluentBundle.h',
'nativeType': 'mozilla::intl::FluentPattern',
},
'FluentResource': {
'headerFile': 'mozilla/intl/FluentResource.h',
'nativeType': 'mozilla::intl::FluentResource',
},
'FontFaceSet': {
'implicitJSContext': [ 'load' ],
},

View File

@ -1,51 +0,0 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/. */
[ChromeOnly, Exposed=Window]
interface FluentResource {
constructor(UTF8String source);
};
[ChromeOnly, Exposed=Window]
interface FluentPattern {};
/**
* FluentMessage is a structure storing an unresolved L10nMessage,
* as returned by the Fluent Bundle.
*
* It stores a FluentPattern of the value and attributes, which
* can be then passed to bundle.formatPattern.
*/
dictionary FluentMessage {
FluentPattern? value = null;
required record<UTF8String, FluentPattern> attributes;
};
typedef record<UTF8String, (UTF8String or double)?> L10nArgs;
dictionary FluentBundleOptions {
boolean useIsolating = false;
UTF8String pseudoStrategy;
};
dictionary FluentBundleAddResourceOptions {
boolean allowOverrides = false;
};
[ChromeOnly, Exposed=Window]
interface FluentBundle {
[Throws]
constructor((UTF8String or sequence<UTF8String>) aLocales, optional FluentBundleOptions aOptions = {});
[Pure, Cached]
readonly attribute sequence<UTF8String> locales;
void addResource(FluentResource aResource, optional FluentBundleAddResourceOptions aOptions = {});
boolean hasMessage(UTF8String id);
FluentMessage? getMessage(UTF8String id);
[Throws]
UTF8String formatPattern(FluentPattern pattern, optional L10nArgs? aArgs = null, optional object aErrors);
};

View File

@ -13,14 +13,16 @@
* The argument will be converted to/from JSON, and the API
* will only handle strings and numbers.
*/
typedef record<DOMString, (DOMString or double)?> L10nArgs;
dictionary L10nKey {
UTF8String? id = null;
DOMString? id = null;
L10nArgs? args = null;
};
/**
* L10nMessage is a compound translation unit from Fluent which
* encodes the value and (optionally) a list of attributes used
* encodes the value and (optionall) a list of attributes used
* to translate a given widget.
*
* Most simple imperative translations will only use the `value`,
@ -28,12 +30,12 @@ dictionary L10nKey {
* of a value and attributes will be used.
*/
dictionary AttributeNameValue {
required UTF8String name;
required UTF8String value;
required DOMString name;
required DOMString value;
};
dictionary L10nMessage {
UTF8String? value = null;
DOMString? value = null;
sequence<AttributeNameValue>? attributes = null;
};
@ -98,7 +100,7 @@ interface Localization {
* let value = await document.l10n.formatValue("unread-emails", {count: 5});
* assert.equal(value, "You have 5 unread emails");
*/
[NewObject] Promise<UTF8String> formatValue(UTF8String aId, optional L10nArgs aArgs);
[NewObject] Promise<DOMString> formatValue(DOMString aId, optional L10nArgs aArgs);
/**
* Formats values of a list of messages with given ids.
@ -113,7 +115,7 @@ interface Localization {
* "You have 5 unread emails"
* ]);
*/
[NewObject] Promise<sequence<UTF8String>> formatValues(sequence<L10nKey> aKeys);
[NewObject] Promise<sequence<DOMString>> formatValues(sequence<L10nKey> aKeys);
/**
* Formats values and attributes of a list of messages with given ids.

View File

@ -44,7 +44,6 @@ WEBIDL_FILES = [
'DominatorTree.webidl',
'DOMLocalization.webidl',
'Flex.webidl',
'Fluent.webidl',
'HeapSnapshot.webidl',
'InspectorUtils.webidl',
'IteratorResult.webidl',

View File

@ -150,7 +150,7 @@ void DOMLocalization::GetAttributes(JSContext* aCx, Element& aElement,
nsAutoString l10nArgs;
if (aElement.GetAttr(kNameSpaceID_None, nsGkAtoms::datal10nid, l10nId)) {
aResult.mId = NS_ConvertUTF16toUTF8(l10nId);
aResult.mId = l10nId;
}
if (aElement.GetAttr(kNameSpaceID_None, nsGkAtoms::datal10nargs, l10nArgs)) {

View File

@ -120,7 +120,7 @@ class AttributeNameValueComparator {
public:
bool Equals(const AttributeNameValue& aAttribute,
const nsAttrName* aAttrName) const {
return aAttrName->Equals(NS_ConvertUTF8toUTF16(aAttribute.mName));
return aAttrName->Equals(aAttribute.mName);
}
};
@ -164,9 +164,10 @@ void L10nOverlays::OverlayAttributes(
}
for (auto& attribute : aTranslation.Value()) {
RefPtr<nsAtom> nameAtom = NS_Atomize(attribute.mName);
nsString attrName = attribute.mName;
RefPtr<nsAtom> nameAtom = NS_Atomize(attrName);
if (IsAttrNameLocalizable(nameAtom, aToElement, &explicitlyAllowed)) {
NS_ConvertUTF8toUTF16 value(attribute.mValue);
nsString value = attribute.mValue;
if (!aToElement->AttrValueIs(kNameSpaceID_None, nameAtom, value,
eCaseMatters)) {
aToElement->SetAttr(nameAtom, value, aRv);
@ -193,11 +194,8 @@ void L10nOverlays::OverlayAttributes(Element* aFromElement, Element* aToElement,
AttributeNameValue* attr = sequence.AppendElement(fallible);
MOZ_ASSERT(info.mName->NamespaceEquals(kNameSpaceID_None),
"No namespaced attributes allowed.");
info.mName->LocalName()->ToUTF8String(attr->mName);
nsAutoString value;
info.mValue->ToString(value);
attr->mValue.Assign(NS_ConvertUTF16toUTF8(value));
info.mName->LocalName()->ToString(attr->mName);
info.mValue->ToString(attr->mValue);
}
attributes.SetValue(sequence);
@ -420,25 +418,26 @@ void L10nOverlays::TranslateElement(
}
}
bool L10nOverlays::ContainsMarkup(const nsACString& aStr) {
bool L10nOverlays::ContainsMarkup(const nsAString& aStr) {
// We use our custom ContainsMarkup rather than the
// one from FragmentOrElement.cpp, because we don't
// want to trigger HTML parsing on every `Preferences & Options`
// type of string.
const char* start = aStr.BeginReading();
const char* end = aStr.EndReading();
const char16_t* start = aStr.BeginReading();
const char16_t* end = aStr.EndReading();
while (start != end) {
char c = *start;
if (c == '<') {
char16_t c = *start;
if (c == char16_t('<')) {
return true;
}
++start;
if (c == '&' && start != end) {
if (c == char16_t('&') && start != end) {
c = *start;
if (c == '#' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z')) {
if (c == char16_t('#') || (c >= char16_t('0') && c <= char16_t('9')) ||
(c >= char16_t('a') && c <= char16_t('z')) ||
(c >= char16_t('A') && c <= char16_t('Z'))) {
return true;
}
++start;
@ -457,13 +456,13 @@ void L10nOverlays::TranslateElement(Element& aElement,
if (nodeInfo->NameAtom() == nsGkAtoms::title &&
nodeInfo->NamespaceID() == kNameSpaceID_XHTML) {
// A special case for the HTML title element whose content must be text.
aElement.SetTextContent(NS_ConvertUTF8toUTF16(aTranslation.mValue), aRv);
aElement.SetTextContent(aTranslation.mValue, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return;
}
} else if (!ContainsMarkup(aTranslation.mValue)) {
// If the translation doesn't contain any markup skip the overlay logic.
aElement.SetTextContent(NS_ConvertUTF8toUTF16(aTranslation.mValue), aRv);
aElement.SetTextContent(aTranslation.mValue, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return;
}
@ -472,9 +471,9 @@ void L10nOverlays::TranslateElement(Element& aElement,
// sanitize it and replace the element's content.
RefPtr<DocumentFragment> fragment =
new DocumentFragment(aElement.OwnerDoc()->NodeInfoManager());
nsContentUtils::ParseFragmentHTML(
NS_ConvertUTF8toUTF16(aTranslation.mValue), fragment,
nsGkAtoms::_template, kNameSpaceID_XHTML, false, true);
nsContentUtils::ParseFragmentHTML(aTranslation.mValue, fragment,
nsGkAtoms::_template,
kNameSpaceID_XHTML, false, true);
if (NS_WARN_IF(aRv.Failed())) {
return;
}

View File

@ -113,7 +113,7 @@ class L10nOverlays {
/**
* A helper used to test if the string contains HTML markup.
*/
static bool ContainsMarkup(const nsACString& aStr);
static bool ContainsMarkup(const nsAString& aStr);
};
} // namespace dom

View File

@ -7,6 +7,9 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { FluentBundle, FluentResource } =
ChromeUtils.import("resource://gre/modules/Fluent.jsm");
async function* mockGenerateMessages(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource(`

View File

@ -7,6 +7,9 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { FluentBundle, FluentResource } =
ChromeUtils.import("resource://gre/modules/Fluent.jsm");
async function* mockGenerateMessages(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource(`

View File

@ -44,6 +44,9 @@
</script>
<script type="application/javascript">
"use strict";
const { FluentBundle, FluentResource } =
ChromeUtils.import("resource://gre/modules/Fluent.jsm");
async function* mockGenerateMessages(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource(`

View File

@ -7,6 +7,9 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { FluentBundle, FluentResource } =
ChromeUtils.import("resource://gre/modules/Fluent.jsm");
async function* mockGenerateMessages(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource(`

View File

@ -10,6 +10,9 @@
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
<script type="application/javascript">
<![CDATA[
const { FluentBundle, FluentResource } =
ChromeUtils.import("resource://gre/modules/Fluent.jsm");
async function * generateMessages(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource(`

View File

@ -7,6 +7,9 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { FluentBundle, FluentResource } =
ChromeUtils.import("resource://gre/modules/Fluent.jsm");
async function* mockGenerateMessages(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource("title = Hello World"));

View File

@ -7,6 +7,9 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { FluentBundle, FluentResource } =
ChromeUtils.import("resource://gre/modules/Fluent.jsm");
async function* mockGenerateMessages(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource("title = <strong>Hello</strong> World"));

View File

@ -7,6 +7,9 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { FluentBundle } =
ChromeUtils.import("resource://gre/modules/Fluent.jsm");
async function* mockGenerateMessages(resourceIds) {
const bundle = new FluentBundle("en-US");
// No translations!

View File

@ -7,6 +7,9 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { FluentBundle, FluentResource } =
ChromeUtils.import("resource://gre/modules/Fluent.jsm");
async function* mockGenerateMessages(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!`));

View File

@ -7,6 +7,9 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { FluentBundle, FluentResource } =
ChromeUtils.import("resource://gre/modules/Fluent.jsm");
async function* mockGenerateMessages(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!`));

View File

@ -7,6 +7,9 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { FluentBundle, FluentResource } =
ChromeUtils.import("resource://gre/modules/Fluent.jsm");
async function* mockGenerateMessages(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource(`

View File

@ -7,6 +7,9 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { FluentBundle, FluentResource } =
ChromeUtils.import("resource://gre/modules/Fluent.jsm");
async function* mockGenerateMessages(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource(`

View File

@ -7,6 +7,9 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { FluentBundle, FluentResource } =
ChromeUtils.import("resource://gre/modules/Fluent.jsm");
async function* mockGenerateMessages(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource("title = Hello World"));

View File

@ -7,6 +7,9 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { FluentBundle, FluentResource } =
ChromeUtils.import("resource://gre/modules/Fluent.jsm");
async function* mockGenerateMessages(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource("title = Hello World"));

View File

@ -7,6 +7,9 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { FluentBundle, FluentResource } =
ChromeUtils.import("resource://gre/modules/Fluent.jsm");
async function* mockGenerateMessages(resourceIds) {
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource("title = Hello World"));

1222
intl/l10n/Fluent.jsm Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,34 +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_FluentBindings_h
#define mozilla_intl_l10n_FluentBindings_h
#include "mozilla/intl/fluent_ffi_generated.h"
#include "mozilla/RefPtr.h"
namespace mozilla {
template <>
struct RefPtrTraits<intl::ffi::FluentResource> {
static void AddRef(const intl::ffi::FluentResource* aPtr) {
intl::ffi::fluent_resource_addref(aPtr);
}
static void Release(const intl::ffi::FluentResource* aPtr) {
intl::ffi::fluent_resource_release(aPtr);
}
};
template <>
class DefaultDelete<intl::ffi::FluentBundleRc> {
public:
void operator()(intl::ffi::FluentBundleRc* aPtr) const {
fluent_bundle_destroy(aPtr);
}
};
} // namespace mozilla
#endif

View File

@ -1,317 +0,0 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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/. */
#include "FluentBundle.h"
#include "mozilla/dom/UnionTypes.h"
#include "unicode/numberformatter.h"
#include "unicode/datefmt.h"
using namespace mozilla::dom;
namespace mozilla {
namespace intl {
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FluentPattern, mParent)
NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(FluentPattern, AddRef)
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(FluentPattern, Release)
FluentPattern::FluentPattern(nsISupports* aParent, const nsACString& aId)
: mId(aId), mParent(aParent) {
MOZ_COUNT_CTOR(FluentPattern);
}
FluentPattern::FluentPattern(nsISupports* aParent, const nsACString& aId,
const nsACString& aAttrName)
: mId(aId), mAttrName(aAttrName), mParent(aParent) {
MOZ_COUNT_CTOR(FluentPattern);
}
JSObject* FluentPattern::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return FluentPattern_Binding::Wrap(aCx, this, aGivenProto);
}
FluentPattern::~FluentPattern() { MOZ_COUNT_DTOR(FluentPattern); };
/* FluentBundle */
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FluentBundle, mParent)
NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(FluentBundle, AddRef)
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(FluentBundle, Release)
FluentBundle::FluentBundle(nsISupports* aParent,
UniquePtr<ffi::FluentBundleRc> aRaw)
: mParent(aParent), mRaw(std::move(aRaw)) {
MOZ_COUNT_CTOR(FluentBundle);
}
already_AddRefed<FluentBundle> FluentBundle::Constructor(
const dom::GlobalObject& aGlobal,
const UTF8StringOrUTF8StringSequence& aLocales,
const dom::FluentBundleOptions& aOptions, ErrorResult& aRv) {
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
if (!global) {
aRv.Throw(NS_ERROR_FAILURE);
return nullptr;
}
bool useIsolating = aOptions.mUseIsolating;
nsAutoCString pseudoStrategy;
if (aOptions.mPseudoStrategy.WasPassed()) {
pseudoStrategy = aOptions.mPseudoStrategy.Value();
}
UniquePtr<ffi::FluentBundleRc> raw;
if (aLocales.IsUTF8String()) {
nsTArray<nsCString> locales;
locales.AppendElement(aLocales.GetAsUTF8String());
raw.reset(ffi::fluent_bundle_new(&locales, useIsolating, &pseudoStrategy));
} else {
nsTArray<nsCString> locales(aLocales.GetAsUTF8StringSequence());
raw.reset(ffi::fluent_bundle_new(&locales, useIsolating, &pseudoStrategy));
}
if (!raw) {
aRv.ThrowInvalidStateError("Failed to create the FluentBundle. Check the "
"locales and pseudo strategy arguments.");
return nullptr;
}
return do_AddRef(new FluentBundle(global, std::move(raw)));
}
JSObject* FluentBundle::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return FluentBundle_Binding::Wrap(aCx, this, aGivenProto);
}
FluentBundle::~FluentBundle() { MOZ_COUNT_DTOR(FluentBundle); };
void FluentBundle::GetLocales(nsTArray<nsCString>& aLocales) {
fluent_bundle_get_locales(mRaw.get(), &aLocales);
}
void FluentBundle::AddResource(
FluentResource& aResource,
const dom::FluentBundleAddResourceOptions& aOptions) {
bool allowOverrides = aOptions.mAllowOverrides;
fluent_bundle_add_resource(mRaw.get(), aResource.Raw(), allowOverrides);
}
bool FluentBundle::HasMessage(const nsACString& aId) {
return fluent_bundle_has_message(mRaw.get(), &aId);
}
void FluentBundle::GetMessage(const nsACString& aId,
Nullable<FluentMessage>& aRetVal) {
bool hasValue = false;
nsTArray<nsCString> attributes;
bool exists =
fluent_bundle_get_message(mRaw.get(), &aId, &hasValue, &attributes);
if (exists) {
FluentMessage& msg = aRetVal.SetValue();
if (hasValue) {
msg.mValue = new FluentPattern(mParent, aId);
}
for (auto& name : attributes) {
auto newEntry = msg.mAttributes.Entries().AppendElement(fallible);
newEntry->mKey = name;
newEntry->mValue = new FluentPattern(mParent, aId, name);
}
}
}
bool extendJSArrayWithErrors(JSContext* aCx, JS::Handle<JSObject*> aErrors,
nsTArray<nsCString>& aInput) {
uint32_t length;
if (NS_WARN_IF(!JS::GetArrayLength(aCx, aErrors, &length))) {
return false;
}
for (auto& err : aInput) {
JS::Rooted<JS::Value> jsval(aCx);
if (!ToJSValue(aCx, NS_ConvertUTF8toUTF16(err), &jsval)) {
return false;
}
if (!JS_DefineElement(aCx, aErrors, length++, jsval, JSPROP_ENUMERATE)) {
return false;
}
}
return true;
}
void FluentBundle::FormatPattern(JSContext* aCx, const FluentPattern& aPattern,
const Nullable<L10nArgs>& aArgs,
const Optional<JS::Handle<JSObject*>>& aErrors,
nsACString& aRetVal, ErrorResult& aRv) {
nsTArray<nsCString> argIds;
nsTArray<ffi::FluentArgument> argValues;
if (!aArgs.IsNull()) {
const L10nArgs& args = aArgs.Value();
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, &argIds,
&argValues, &aRetVal, &errors);
if (!succeeded) {
return aRv.ThrowInvalidStateError("Failed to format the FluentPattern. Likely the "
"pattern could not be retrieved from the bundle.");
}
if (aErrors.WasPassed()) {
if (!extendJSArrayWithErrors(aCx, aErrors.Value(), errors)) {
aRv.ThrowUnknownError("Failed to add errors to an error array.");
}
}
}
// FFI
extern "C" {
ffi::RawNumberFormatter* FluentBuiltInNumberFormatterCreate(
const nsCString* aLocale, const ffi::FluentNumberOptionsRaw* aOptions) {
auto grouping = aOptions->use_grouping
? UNumberGroupingStrategy::UNUM_GROUPING_AUTO
: UNumberGroupingStrategy::UNUM_GROUPING_OFF;
auto formatter =
icu::number::NumberFormatter::with().grouping(grouping).integerWidth(
icu::number::IntegerWidth::zeroFillTo(
aOptions->minimum_integer_digits));
// This is a bug in ICU where UNUM_ROUND_HALFEVEN keeps
// `4.2500` -> `4.2` instead of `4.3`.
formatter = formatter.roundingMode(UNUM_ROUND_HALFUP);
if (aOptions->style == ffi::FluentNumberStyleRaw::Currency) {
UErrorCode ec = U_ZERO_ERROR;
formatter =
formatter.unit(icu::CurrencyUnit(aOptions->currency.get(), ec));
MOZ_ASSERT(U_SUCCESS(ec), "Failed to format the currency unit.");
}
if (aOptions->style == ffi::FluentNumberStyleRaw::Percent) {
formatter = formatter.unit(icu::NoUnit::percent());
}
if (aOptions->minimum_significant_digits >= 0 ||
aOptions->maximum_significant_digits >= 0) {
auto precision = icu::number::Precision::minMaxSignificantDigits(
aOptions->minimum_significant_digits,
aOptions->maximum_significant_digits)
.minMaxFraction(aOptions->minimum_fraction_digits,
aOptions->maximum_fraction_digits);
formatter = formatter.precision(precision);
} else {
auto precision = icu::number::Precision::minMaxFraction(
aOptions->minimum_fraction_digits, aOptions->maximum_fraction_digits);
formatter = formatter.precision(precision);
}
return reinterpret_cast<ffi::RawNumberFormatter*>(
formatter.locale(aLocale->get()).clone().orphan());
}
uint8_t* FluentBuiltInNumberFormatterFormat(const ffi::RawNumberFormatter* aFormatter,
double input, uint32_t* aOutCount) {
auto formatter =
reinterpret_cast<const icu::number::LocalizedNumberFormatter*>(aFormatter);
UErrorCode ec = U_ZERO_ERROR;
icu::number::FormattedNumber result = formatter->formatDouble(input, ec);
icu::UnicodeString str = result.toTempString(ec);
return reinterpret_cast<uint8_t*>(ToNewUTF8String(nsDependentSubstring(str.getBuffer(), str.length()), aOutCount));
}
void FluentBuiltInNumberFormatterDestroy(ffi::RawNumberFormatter* aFormatter) {
delete reinterpret_cast<icu::number::LocalizedNumberFormatter*>(aFormatter);
}
/* DateTime */
static icu::DateFormat::EStyle GetICUStyle(ffi::FluentDateTimeStyle aStyle) {
switch (aStyle) {
case ffi::FluentDateTimeStyle::Full:
return icu::DateFormat::FULL;
case ffi::FluentDateTimeStyle::Long:
return icu::DateFormat::LONG;
case ffi::FluentDateTimeStyle::Medium:
return icu::DateFormat::MEDIUM;
case ffi::FluentDateTimeStyle::Short:
return icu::DateFormat::SHORT;
case ffi::FluentDateTimeStyle::None:
return icu::DateFormat::NONE;
default:
MOZ_ASSERT_UNREACHABLE("Unsupported date time style.");
return icu::DateFormat::NONE;
}
}
ffi::RawDateTimeFormatter* FluentBuiltInDateTimeFormatterCreate(
const nsCString* aLocale, const ffi::FluentDateTimeOptionsRaw* aOptions) {
icu::DateFormat* dtmf = nullptr;
if (aOptions->date_style != ffi::FluentDateTimeStyle::None &&
aOptions->time_style != ffi::FluentDateTimeStyle::None) {
dtmf = icu::DateFormat::createDateTimeInstance(
GetICUStyle(aOptions->date_style), GetICUStyle(aOptions->time_style),
aLocale->get());
} else if (aOptions->date_style != ffi::FluentDateTimeStyle::None) {
dtmf = icu::DateFormat::createDateInstance(
GetICUStyle(aOptions->date_style), aLocale->get());
} else if (aOptions->time_style != ffi::FluentDateTimeStyle::None) {
dtmf = icu::DateFormat::createTimeInstance(
GetICUStyle(aOptions->time_style), aLocale->get());
} else {
if (aOptions->skeleton.IsEmpty()) {
dtmf = icu::DateFormat::createDateTimeInstance(
icu::DateFormat::DEFAULT, icu::DateFormat::DEFAULT, aLocale->get());
} else {
UErrorCode status = U_ZERO_ERROR;
dtmf = icu::DateFormat::createInstanceForSkeleton(
aOptions->skeleton.get(), aLocale->get(), status);
}
}
MOZ_RELEASE_ASSERT(dtmf, "Failed to create a format for the skeleton.");
return reinterpret_cast<ffi::RawDateTimeFormatter*>(dtmf);
}
uint8_t* FluentBuiltInDateTimeFormatterFormat(const ffi::RawDateTimeFormatter* aFormatter,
uintptr_t input, uint32_t* aOutCount) {
auto formatter = reinterpret_cast<const icu::DateFormat*>(aFormatter);
UDate myDate = input;
icu::UnicodeString str;
formatter->format(myDate, str);
return reinterpret_cast<uint8_t*>(ToNewUTF8String(nsDependentSubstring(str.getBuffer(), str.length()), aOutCount));
}
void FluentBuiltInDateTimeFormatterDestroy(
ffi::RawDateTimeFormatter* aFormatter) {
delete reinterpret_cast<icu::DateFormat*>(aFormatter);
}
}
} // namespace intl
} // namespace mozilla

View File

@ -1,94 +0,0 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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_FluentBundle_h
#define mozilla_intl_l10n_FluentBundle_h
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/ErrorResult.h"
#include "nsCycleCollectionParticipant.h"
#include "nsWrapperCache.h"
#include "mozilla/dom/FluentBinding.h"
#include "mozilla/dom/LocalizationBinding.h"
#include "mozilla/intl/FluentResource.h"
#include "mozilla/intl/FluentBindings.h"
class nsIGlobalObject;
namespace mozilla {
namespace dom {
struct FluentMessage;
struct L10nMessage;
class OwningUTF8StringOrDouble;
class UTF8StringOrUTF8StringSequence;
struct FluentBundleOptions;
struct FluentBundleAddResourceOptions;
} // namespace dom
namespace intl {
using L10nArgs =
dom::Record<nsCString, dom::Nullable<dom::OwningUTF8StringOrDouble>>;
class FluentPattern : public nsWrapperCache {
public:
NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(FluentPattern)
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(FluentPattern)
FluentPattern(nsISupports* aParent, const nsACString& aId);
FluentPattern(nsISupports* aParent, const nsACString& aId,
const nsACString& aAttrName);
JSObject* WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) override;
nsISupports* GetParentObject() const { return mParent; }
nsCString mId;
nsCString mAttrName;
protected:
virtual ~FluentPattern();
nsCOMPtr<nsISupports> mParent;
};
class FluentBundle final : public nsWrapperCache {
public:
NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(FluentBundle)
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(FluentBundle)
static already_AddRefed<FluentBundle> Constructor(
const dom::GlobalObject& aGlobal,
const dom::UTF8StringOrUTF8StringSequence& aLocales,
const dom::FluentBundleOptions& aOptions, ErrorResult& aRv);
JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) final;
nsISupports* GetParentObject() const { return mParent; }
void GetLocales(nsTArray<nsCString>& aLocales);
void AddResource(FluentResource& aResource,
const dom::FluentBundleAddResourceOptions& aOptions);
bool HasMessage(const nsACString& aId);
void GetMessage(const nsACString& aId,
dom::Nullable<dom::FluentMessage>& aRetVal);
void FormatPattern(JSContext* aCx, const FluentPattern& aPattern,
const dom::Nullable<L10nArgs>& aArgs,
const dom::Optional<JS::Handle<JSObject*>>& aErrors,
nsACString& aRetVal, ErrorResult& aRv);
protected:
explicit FluentBundle(nsISupports* aParent,
UniquePtr<ffi::FluentBundleRc> aRaw);
virtual ~FluentBundle();
nsCOMPtr<nsISupports> mParent;
UniquePtr<ffi::FluentBundleRc> mRaw;
};
} // namespace intl
} // namespace mozilla
#endif

View File

@ -1,47 +0,0 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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/. */
#include "nsContentUtils.h"
#include "FluentResource.h"
using namespace mozilla::dom;
namespace mozilla {
namespace intl {
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FluentResource, mParent)
NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(FluentResource, AddRef)
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(FluentResource, Release)
FluentResource::FluentResource(nsISupports* aParent, const nsACString& aSource)
: mParent(aParent),
mRaw(dont_AddRef(ffi::fluent_resource_new(&aSource, &mHasErrors))) {
MOZ_COUNT_CTOR(FluentResource);
}
already_AddRefed<FluentResource> FluentResource::Constructor(
const GlobalObject& aGlobal, const nsACString& aSource) {
RefPtr<FluentResource> res =
new FluentResource(aGlobal.GetAsSupports(), aSource);
if (res->mHasErrors) {
nsContentUtils::LogSimpleConsoleError(
NS_LITERAL_STRING("Errors encountered while parsing Fluent Resource."),
"chrome", false, true /* from chrome context*/);
}
return res.forget();
}
JSObject* FluentResource::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return FluentResource_Binding::Wrap(aCx, this, aGivenProto);
}
FluentResource::~FluentResource() { MOZ_COUNT_DTOR(FluentResource); };
} // namespace intl
} // namespace mozilla

View File

@ -1,47 +0,0 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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_FluentResource_h
#define mozilla_intl_l10n_FluentResource_h
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/ErrorResult.h"
#include "nsCycleCollectionParticipant.h"
#include "nsWrapperCache.h"
#include "mozilla/dom/FluentBinding.h"
#include "mozilla/intl/FluentBindings.h"
namespace mozilla {
namespace intl {
class FluentResource : public nsWrapperCache {
public:
NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(FluentResource)
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(FluentResource)
explicit FluentResource(nsISupports* aParent, const nsACString& aSource);
static already_AddRefed<FluentResource> Constructor(
const dom::GlobalObject& aGlobal, const nsACString& aSource);
JSObject* WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) override;
nsISupports* GetParentObject() const { return mParent; }
const ffi::FluentResource* Raw() const { return mRaw; }
protected:
virtual ~FluentResource();
nsCOMPtr<nsISupports> mParent;
const RefPtr<const ffi::FluentResource> mRaw;
bool mHasErrors;
};
} // namespace intl
} // namespace mozilla
#endif

View File

@ -2,6 +2,7 @@ const { AppConstants } = ChromeUtils.import("resource://gre/modules/AppConstants
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
// eslint-disable-next-line mozilla/use-services
const appinfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
const { FluentBundle, FluentResource } = ChromeUtils.import("resource://gre/modules/Fluent.jsm");
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
ChromeUtils.defineModuleGetter(
@ -135,12 +136,12 @@ class L10nRegistryService {
async* generateBundles(requestedLangs, resourceIds) {
const resourceIdsDedup = Array.from(new Set(resourceIds));
const sourcesOrder = Array.from(this.sources.keys()).reverse();
const pseudoStrategy = Services.prefs.getStringPref("intl.l10n.pseudo", "");
const pseudoNameFromPref = Services.prefs.getStringPref("intl.l10n.pseudo", "");
for (const locale of requestedLangs) {
for await (const dataSets of generateResourceSetsForLocale(locale, sourcesOrder, resourceIdsDedup)) {
const bundle = new FluentBundle(locale, {
...MSG_CONTEXT_OPTIONS,
pseudoStrategy,
transform: PSEUDO_STRATEGIES[pseudoNameFromPref],
});
for (const data of dataSets) {
if (data === null) {
@ -168,12 +169,12 @@ class L10nRegistryService {
* generateBundlesSync(requestedLangs, resourceIds) {
const resourceIdsDedup = Array.from(new Set(resourceIds));
const sourcesOrder = Array.from(this.sources.keys()).reverse();
const pseudoStrategy = Services.prefs.getStringPref("intl.l10n.pseudo", "");
const pseudoNameFromPref = Services.prefs.getStringPref("intl.l10n.pseudo", "");
for (const locale of requestedLangs) {
for (const dataSets of generateResourceSetsForLocaleSync(locale, sourcesOrder, resourceIdsDedup)) {
const bundle = new FluentBundle(locale, {
...MSG_CONTEXT_OPTIONS,
pseudoStrategy
transform: PSEUDO_STRATEGIES[pseudoNameFromPref],
});
for (const data of dataSets) {
if (data === null) {
@ -426,6 +427,112 @@ const MSG_CONTEXT_OPTIONS = {
// Temporarily disable bidi isolation due to Microsoft not supporting FSI/PDI.
// See bug 1439018 for details.
useIsolating: Services.prefs.getBoolPref("intl.l10n.enable-bidi-marks", false),
functions: {
/**
* PLATFORM is a built-in allowing localizers to differentiate message
* variants depending on the target platform.
*/
PLATFORM: () => {
switch (AppConstants.platform) {
case "linux":
case "android":
return AppConstants.platform;
case "win":
return "windows";
case "macosx":
return "macos";
default:
return "other";
}
},
},
};
/**
* Pseudolocalizations
*
* PSEUDO_STRATEGIES is a dict of strategies to be used to modify a
* context in order to create pseudolocalizations. These can be used by
* developers to test the localizability of their code without having to
* actually speak a foreign language.
*
* Currently, the following pseudolocales are supported:
*
* accented - Ȧȧƈƈḗḗƞŧḗḗḓ Ḗḗƞɠŀīīşħ
*
* In Accented English all Latin letters are replaced by accented
* Unicode counterparts which don't impair the readability of the content.
* This allows developers to quickly test if any given string is being
* correctly displayed in its 'translated' form. Additionally, simple
* heuristics are used to make certain words longer to better simulate the
* experience of international users.
*
* bidi - ɥsıʅƃuƎ ıpıԐ
*
* Bidi English is a fake RTL locale. All words are surrounded by
* Unicode formatting marks forcing the RTL directionality of characters.
* In addition, to make the reversed text easier to read, individual
* letters are flipped.
*
* Note: The name above is hardcoded to be RTL in case code editors have
* trouble with the RLO and PDF Unicode marks. In reality, it should be
* surrounded by those marks as well.
*
* See https://bugzil.la/1450781 for more information.
*
* In this implementation we use code points instead of inline unicode characters
* because the encoding of JSM files mangles them otherwise.
*/
const ACCENTED_MAP = {
// ȦƁƇḒḖƑƓĦĪĴĶĿḾȠǾƤɊŘŞŦŬṼẆẊẎẐ
"caps": [550, 385, 391, 7698, 7702, 401, 403, 294, 298, 308, 310, 319, 7742, 544, 510, 420, 586, 344, 350, 358, 364, 7804, 7814, 7818, 7822, 7824],
// ȧƀƈḓḗƒɠħīĵķŀḿƞǿƥɋřşŧŭṽẇẋẏẑ
"small": [551, 384, 392, 7699, 7703, 402, 608, 295, 299, 309, 311, 320, 7743, 414, 511, 421, 587, 345, 351, 359, 365, 7805, 7815, 7819, 7823, 7825],
};
const FLIPPED_MAP = {
// ∀ԐↃᗡƎℲ⅁HIſӼ⅂WNOԀÒᴚS⊥∩ɅMX⅄Z
"caps": [8704, 1296, 8579, 5601, 398, 8498, 8513, 72, 73, 383, 1276, 8514, 87, 78, 79, 1280, 210, 7450, 83, 8869, 8745, 581, 77, 88, 8516, 90],
// ɐqɔpǝɟƃɥıɾʞʅɯuodbɹsʇnʌʍxʎz
"small": [592, 113, 596, 112, 477, 607, 387, 613, 305, 638, 670, 645, 623, 117, 111, 100, 98, 633, 115, 647, 110, 652, 653, 120, 654, 122],
};
function transformString(map, elongate = false, prefix = "", postfix = "", msg) {
// Exclude access-keys and other single-char messages
if (msg.length === 1) {
return msg;
}
// XML entities (&#x202a;) and XML tags.
const reExcluded = /(&[#\w]+;|<\s*.+?\s*>)/;
const parts = msg.split(reExcluded);
const modified = parts.map((part) => {
if (reExcluded.test(part)) {
return part;
}
return prefix + part.replace(/[a-z]/ig, (ch) => {
let cc = ch.charCodeAt(0);
if (cc >= 97 && cc <= 122) {
const newChar = String.fromCodePoint(map.small[cc - 97]);
// duplicate "a", "e", "o" and "u" to emulate ~30% longer text
if (elongate && (cc === 97 || cc === 101 || cc === 111 || cc === 117)) {
return newChar + newChar;
}
return newChar;
}
if (cc >= 65 && cc <= 90) {
return String.fromCodePoint(map.caps[cc - 65]);
}
return ch;
}) + postfix;
});
return modified.join("");
}
const PSEUDO_STRATEGIES = {
"accented": transformString.bind(null, ACCENTED_MAP, true, "", ""),
"bidi": transformString.bind(null, FLIPPED_MAP, false, "\u202e", "\u202c"),
};
/**

View File

@ -181,7 +181,7 @@ uint32_t Localization::RemoveResourceIds(
}
already_AddRefed<Promise> Localization::FormatValue(
JSContext* aCx, const nsACString& aId, const Optional<L10nArgs>& aArgs,
JSContext* aCx, const nsAString& aId, const Optional<L10nArgs>& aArgs,
ErrorResult& aRv) {
JS::Rooted<JS::Value> args(aCx);

View File

@ -22,7 +22,7 @@ using namespace mozilla::dom;
namespace mozilla {
namespace intl {
typedef Record<nsCString, Nullable<OwningUTF8StringOrDouble>> L10nArgs;
typedef Record<nsString, Nullable<OwningStringOrDouble>> L10nArgs;
class Localization : public nsIObserver,
public nsSupportsWeakReference,
@ -57,7 +57,7 @@ class Localization : public nsIObserver,
uint32_t RemoveResourceIds(const nsTArray<nsString>& aResourceIds);
already_AddRefed<Promise> FormatValue(JSContext* aCx, const nsACString& aId,
already_AddRefed<Promise> FormatValue(JSContext* aCx, const nsAString& aId,
const Optional<L10nArgs>& aArgs,
ErrorResult& aRv);

View File

@ -1,8 +1,15 @@
The content of this directory is partially sourced from the fluent.js project.
The following files are affected:
- Fluent.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
mostly around header and footer.
The result difference is stored in `./fluent.js.patch` file which can be used to
approximate the changes needed to be applied on the output of the
fluent.js/fluent-gecko's make.
In b.m.o. bug 1434434 we will try to reduce this difference to zero.

475
intl/l10n/fluent.js.patch Normal file
View File

@ -0,0 +1,475 @@
diff --git a/intl/l10n/Fluent.jsm b/intl/l10n/Fluent.jsm
--- a/intl/l10n/Fluent.jsm
+++ b/intl/l10n/Fluent.jsm
@@ -16,7 +16,7 @@
*/
-/* fluent@0.10.0 */
+/* fluent-dom@0.4.0 */
/* global Intl */
@@ -139,53 +139,7 @@ function values(opts) {
return unwrapped;
}
-/**
- * @overview
- *
- * The role of the Fluent resolver is to format a translation object to an
- * instance of `FluentType` or an array of instances.
- *
- * Translations can contain references to other messages or variables,
- * conditional logic in form of select expressions, traits which describe their
- * grammatical features, and can use Fluent builtins which make use of the
- * `Intl` formatters to format numbers, dates, lists and more into the
- * bundle's language. See the documentation of the Fluent syntax for more
- * information.
- *
- * In case of errors the resolver will try to salvage as much of the
- * translation as possible. In rare situations where the resolver didn't know
- * how to recover from an error it will return an instance of `FluentNone`.
- *
- * `MessageReference`, `VariantExpression`, `AttributeExpression` and
- * `SelectExpression` resolve to raw Runtime Entries objects and the result of
- * the resolution needs to be passed into `Type` to get their real value.
- * This is useful for composing expressions. Consider:
- *
- * brand-name[nominative]
- *
- * which is a `VariantExpression` with properties `id: MessageReference` and
- * `key: Keyword`. If `MessageReference` was resolved eagerly, it would
- * instantly resolve to the value of the `brand-name` message. Instead, we
- * want to get the message object and look for its `nominative` variant.
- *
- * All other expressions (except for `FunctionReference` which is only used in
- * `CallExpression`) resolve to an instance of `FluentType`. The caller should
- * use the `toString` method to convert the instance to a native value.
- *
- *
- * All functions in this file pass around a special object called `env`.
- * This object stores a set of elements used by all resolve functions:
- *
- * * {FluentBundle} bundle
- * bundle for which the given resolution is happening
- * * {Object} args
- * list of developer provided arguments that can be used
- * * {Array} errors
- * list of errors collected while resolving
- * * {WeakSet} dirty
- * Set of patterns already encountered during this resolution.
- * This is used to prevent cyclic resolutions.
- */
+/* global Intl */
// Prevent expansion of too long placeables.
const MAX_PLACEABLE_LENGTH = 2500;
@@ -514,7 +468,7 @@ function Pattern(env, ptn) {
*/
function resolve(bundle, args, message, errors = []) {
const env = {
- bundle, args, errors, dirty: new WeakSet(),
+ bundle, args, errors, dirty: new WeakSet()
};
return Type(env, message).toString(bundle);
}
@@ -1064,7 +1018,7 @@ class FluentBundle {
constructor(locales, {
functions = {},
useIsolating = true,
- transform = v => v,
+ transform = v => v
} = {}) {
this.locales = Array.isArray(locales) ? locales : [locales];
@@ -1235,6 +1189,14 @@ class FluentBundle {
}
}
+/*
+ * @module fluent
+ * @overview
+ *
+ * `fluent` is a JavaScript implementation of Project Fluent, a localization
+ * framework designed to unleash the expressive power of the natural language.
+ *
+ */
+
this.FluentBundle = FluentBundle;
-this.FluentResource = FluentResource;
-var EXPORTED_SYMBOLS = ["FluentBundle", "FluentResource"];
+this.EXPORTED_SYMBOLS = ["FluentBundle"];
diff --git a/intl/l10n/Localization.jsm b/intl/l10n/Localization.jsm
--- a/intl/l10n/Localization.jsm
+++ b/intl/l10n/Localization.jsm
@@ -16,34 +16,27 @@
*/
-/* fluent-dom@fa25466f (October 12, 2018) */
-
-/* eslint no-console: ["error", { allow: ["warn", "error"] }] */
-/* global console */
-
-const { L10nRegistry } = ChromeUtils.import("resource://gre/modules/L10nRegistry.jsm", {});
-const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm", {});
-const { AppConstants } = ChromeUtils.import("resource://gre/modules/AppConstants.jsm", {});
+/* fluent-dom@0.4.0 */
/*
* 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;
+ /**
+ * 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);
}
-
- return new this(iterable);
- }
}
/*
@@ -53,80 +46,88 @@ class CachedIterable extends Array {
* iterable.
*/
class CachedAsyncIterable extends CachedIterable {
- /**
- * Create an `CachedAsyncIterable` instance.
- *
- * @param {Iterable} iterable
- * @returns {CachedAsyncIterable}
- */
- constructor(iterable) {
- super();
+ /**
+ * 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.");
+ }
+ }
- 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.");
- }
- }
+ /**
+ * Synchronous iterator over the cached elements.
+ *
+ * Return a generator object implementing the iterator protocol over the
+ * cached elements of the original (async or sync) iterable.
+ */
+ [Symbol.iterator]() {
+ const cached = this;
+ let cur = 0;
- /**
- * 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 {
+ next() {
+ if (cached.length === cur) {
+ return {value: undefined, done: true};
+ }
+ return cached[cur++];
+ }
+ };
+ }
- return {
- async next() {
- if (cached.length <= cur) {
- cached.push(cached.iterator.next());
- }
- return cached[cur++];
- },
- };
- }
+ /**
+ * 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;
- /**
- * 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 {
+ async next() {
+ if (cached.length <= cur) {
+ cached.push(await cached.iterator.next());
+ }
+ return cached[cur++];
+ }
+ };
}
- // 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];
- }
+
+ /**
+ * 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 && last.done) {
+ break;
+ }
+ this.push(await 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.generateBundles(appLocales, resourceIds);
-}
+/* eslint no-console: ["error", { allow: ["warn", "error"] }] */
/**
* The `Localization` class is a central high-level API for vanilla
@@ -142,21 +143,16 @@ class Localization {
*
* @returns {Localization}
*/
- constructor(resourceIds = [], generateBundles = defaultGenerateBundles) {
+ constructor(resourceIds = [], generateBundles) {
this.resourceIds = resourceIds;
this.generateBundles = generateBundles;
this.bundles = CachedAsyncIterable.from(
this.generateBundles(this.resourceIds));
}
- /**
- * @param {Array<String>} resourceIds - List of resource IDs
- * @param {bool} eager - whether the I/O for new context should
- * begin eagerly
- */
- addResourceIds(resourceIds, eager = false) {
+ addResourceIds(resourceIds) {
this.resourceIds.push(...resourceIds);
- this.onChange(eager);
+ this.onChange();
return this.resourceIds.length;
}
@@ -188,12 +184,9 @@ class Localization {
break;
}
- if (AppConstants.NIGHTLY_BUILD || Cu.isInAutomation) {
+ if (typeof console !== "undefined") {
const locale = bundle.locales[0];
const ids = Array.from(missingIds).join(", ");
- if (Cu.isInAutomation) {
- throw new Error(`Missing translations in ${locale}: ${ids}`);
- }
console.warn(`Missing translations in ${locale}: ${ids}`);
}
}
@@ -281,64 +274,21 @@ class Localization {
return val;
}
- /**
- * Register weak observers on events that will trigger cache invalidation
- */
- registerObservers() {
- Services.obs.addObserver(this, "intl:app-locales-changed", true);
- Services.prefs.addObserver("intl.l10n.pseudo", this, true);
- }
-
- /**
- * Default observer handler method.
- *
- * @param {String} subject
- * @param {String} topic
- * @param {Object} data
- */
- observe(subject, topic, data) {
- switch (topic) {
- case "intl:app-locales-changed":
- this.onChange();
- break;
- case "nsPref:changed":
- switch (data) {
- case "intl.l10n.pseudo":
- this.onChange();
- }
- break;
- default:
- break;
- }
+ handleEvent() {
+ this.onChange();
}
/**
* This method should be called when there's a reason to believe
* that language negotiation or available resources changed.
- *
- * @param {bool} eager - whether the I/O for new context should begin eagerly
*/
- onChange(eager = false) {
+ onChange() {
this.bundles = CachedAsyncIterable.from(
this.generateBundles(this.resourceIds));
- 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;
- this.bundles.touchNext(prefetchCount);
- }
+ this.bundles.touchNext(2);
}
}
-Localization.prototype.QueryInterface = ChromeUtils.generateQI([
- Ci.nsISupportsWeakReference,
-]);
-
/**
* Format the value of a message into a string.
*
@@ -430,7 +380,7 @@ function messageFromBundle(bundle, error
* See `Localization.formatWithFallback` for more info on how this is used.
*
* @param {Function} method
- * @param {FluentBundle} bundle
+ * @param {FluentBundle} bundle
* @param {Array<string>} keys
* @param {{Array<{value: string, attributes: Object}>}} translations
*
@@ -458,5 +408,44 @@ function keysFromBundle(method, bundle,
return missingIds;
}
-this.Localization = Localization;
-var EXPORTED_SYMBOLS = ["Localization"];
+/* global Components */
+/* eslint no-unused-vars: 0 */
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+const { L10nRegistry } =
+ Cu.import("resource://gre/modules/L10nRegistry.jsm", {});
+const ObserverService =
+ Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
+const { Services } =
+ Cu.import("resource://gre/modules/Services.jsm", {});
+
+/**
+ * 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 requestedLocales = Services.locale.getRequestedLocales();
+ const availableLocales = L10nRegistry.getAvailableLocales();
+ const defaultLocale = Services.locale.defaultLocale;
+ const locales = Services.locale.negotiateLanguages(
+ requestedLocales, availableLocales, defaultLocale,
+ );
+ return L10nRegistry.generateContexts(locales, resourceIds);
+}
+
+class GeckoLocalization extends Localization {
+ constructor(resourceIds, generateBundles = defaultGenerateBundles) {
+ super(resourceIds, generateBundles);
+ }
+}
+
+this.Localization = GeckoLocalization;
+this.EXPORTED_SYMBOLS = ["Localization"];

View File

@ -5,19 +5,15 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
EXPORTS.mozilla.intl += [
'FluentBindings.h',
'FluentBundle.h',
'FluentResource.h',
'Localization.h',
]
UNIFIED_SOURCES += [
'FluentBundle.cpp',
'FluentResource.cpp',
'Localization.cpp',
]
EXTRA_JS_MODULES += [
'Fluent.jsm',
'L10nRegistry.jsm',
'Localization.jsm',
]
@ -36,21 +32,6 @@ LOCAL_INCLUDES += [
'/dom/base',
]
if CONFIG['COMPILE_ENVIRONMENT']:
GENERATED_FILES += [
'fluent_ffi_generated.h',
]
EXPORTS.mozilla.intl += [
'!fluent_ffi_generated.h',
]
ffi_generated = GENERATED_FILES['fluent_ffi_generated.h']
ffi_generated.script = '/build/RunCbindgen.py:generate'
ffi_generated.inputs = [
'/intl/l10n/rust/fluent-ffi',
]
XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini']

View File

@ -20,7 +20,7 @@ interface mozILocalization : nsISupports
Promise formatMessages(in Array<jsval> aKeys);
Promise formatValues(in Array<jsval> aKeys);
Promise formatValue(in AUTF8String aId, [optional] in jsval aArgs);
Promise formatValue(in AString aId, [optional] in jsval aArgs);
Array<jsval> formatMessagesSync(in Array<jsval> aKeys);
void setIsSync(in boolean isSync);

View File

@ -1,13 +0,0 @@
[package]
name = "fluent-ffi"
version = "0.1.0"
authors = ["Zibi Braniecki <zibi@braniecki.net>"]
edition = "2018"
[dependencies]
fluent = { version = "0.11" , features = ["fluent-pseudo"] }
fluent-pseudo = "0.2"
intl-memoizer = "0.4"
unic-langid = "0.8"
nsstring = { path = "../../../../xpcom/rust/nsstring" }
thin-vec = { version = "0.1.0", features = ["gecko-ffi"] }

View File

@ -1,25 +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_FluentBindings_h
#error "Don't include this file directly, instead include FluentBindings.h"
#endif
"""
include_version = true
braces = "SameLine"
line_length = 100
tab_width = 2
language = "C++"
namespaces = ["mozilla", "intl", "ffi"]
[parse]
parse_deps = true
include = ["fluent", "fluent-bundle"]
exclude = ["fxhash"]
[enum]
derive_helper_methods = true
[export.rename]
"ThinVec" = "nsTArray"

View File

@ -1,467 +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/. */
use crate::ffi;
use fluent::types::{FluentNumberOptions, FluentType, FluentValue};
use fluent::FluentArgs;
use intl_memoizer::IntlLangMemoizer;
use intl_memoizer::Memoizable;
use nsstring::nsCString;
use std::borrow::Cow;
use std::ptr::NonNull;
use unic_langid::LanguageIdentifier;
pub struct NumberFormat {
raw: NonNull<ffi::RawNumberFormatter>,
}
/**
* According to http://userguide.icu-project.org/design, as long as we constrain
* ourselves to const APIs ICU is const-correct.
*/
unsafe impl Send for NumberFormat {}
unsafe impl Sync for NumberFormat {}
impl NumberFormat {
pub fn new(locale: LanguageIdentifier, options: &FluentNumberOptions) -> Self {
let loc: String = locale.to_string();
Self {
raw: unsafe {
NonNull::new_unchecked(ffi::FluentBuiltInNumberFormatterCreate(
&loc.into(),
&options.into(),
))
},
}
}
pub fn format(&self, input: f64) -> String {
unsafe {
let mut byte_count = 0;
let buffer =
ffi::FluentBuiltInNumberFormatterFormat(self.raw.as_ptr(), input, &mut byte_count);
if buffer.is_null() {
return String::new();
}
String::from_raw_parts(buffer, byte_count as usize, byte_count as usize)
}
}
}
impl Drop for NumberFormat {
fn drop(&mut self) {
unsafe { ffi::FluentBuiltInNumberFormatterDestroy(self.raw.as_ptr()) };
}
}
impl Memoizable for NumberFormat {
type Args = (FluentNumberOptions,);
type Error = &'static str;
fn construct(lang: LanguageIdentifier, args: Self::Args) -> Result<Self, Self::Error> {
Ok(NumberFormat::new(lang, &args.0))
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum FluentDateTimeStyle {
Full,
Long,
Medium,
Short,
None,
}
impl Default for FluentDateTimeStyle {
fn default() -> Self {
Self::None
}
}
impl From<&str> for FluentDateTimeStyle {
fn from(input: &str) -> Self {
match input {
"full" => Self::Full,
"long" => Self::Long,
"medium" => Self::Medium,
"short" => Self::Short,
_ => Self::None,
}
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum FluentDateTimeHourCycle {
H24,
H23,
H12,
H11,
None,
}
impl Default for FluentDateTimeHourCycle {
fn default() -> Self {
Self::None
}
}
impl From<&str> for FluentDateTimeHourCycle {
fn from(input: &str) -> Self {
match input {
"h24" => Self::H24,
"h23" => Self::H23,
"h12" => Self::H12,
"h11" => Self::H11,
_ => Self::None,
}
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum FluentDateTimeTextComponent {
Long,
Short,
Narrow,
None,
}
impl Default for FluentDateTimeTextComponent {
fn default() -> Self {
Self::None
}
}
impl From<&str> for FluentDateTimeTextComponent {
fn from(input: &str) -> Self {
match input {
"long" => Self::Long,
"short" => Self::Short,
"narrow" => Self::Narrow,
_ => Self::None,
}
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum FluentDateTimeNumericComponent {
Numeric,
TwoDigit,
None,
}
impl Default for FluentDateTimeNumericComponent {
fn default() -> Self {
Self::None
}
}
impl From<&str> for FluentDateTimeNumericComponent {
fn from(input: &str) -> Self {
match input {
"numeric" => Self::Numeric,
"2-digit" => Self::TwoDigit,
_ => Self::None,
}
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum FluentDateTimeMonthComponent {
Numeric,
TwoDigit,
Long,
Short,
Narrow,
None,
}
impl Default for FluentDateTimeMonthComponent {
fn default() -> Self {
Self::None
}
}
impl From<&str> for FluentDateTimeMonthComponent {
fn from(input: &str) -> Self {
match input {
"numeric" => Self::Numeric,
"2-digit" => Self::TwoDigit,
"long" => Self::Long,
"short" => Self::Short,
"narrow" => Self::Narrow,
_ => Self::None,
}
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum FluentDateTimeTimeZoneNameComponent {
Long,
Short,
None,
}
impl Default for FluentDateTimeTimeZoneNameComponent {
fn default() -> Self {
Self::None
}
}
impl From<&str> for FluentDateTimeTimeZoneNameComponent {
fn from(input: &str) -> Self {
match input {
"long" => Self::Long,
"short" => Self::Short,
_ => Self::None,
}
}
}
#[repr(C)]
#[derive(Default, Debug, Clone, Hash, PartialEq, Eq)]
pub struct FluentDateTimeOptions {
pub date_style: FluentDateTimeStyle,
pub time_style: FluentDateTimeStyle,
pub hour_cycle: FluentDateTimeHourCycle,
pub weekday: FluentDateTimeTextComponent,
pub era: FluentDateTimeTextComponent,
pub year: FluentDateTimeNumericComponent,
pub month: FluentDateTimeMonthComponent,
pub day: FluentDateTimeNumericComponent,
pub hour: FluentDateTimeNumericComponent,
pub minute: FluentDateTimeNumericComponent,
pub second: FluentDateTimeNumericComponent,
pub time_zone_name: FluentDateTimeTimeZoneNameComponent,
}
impl FluentDateTimeOptions {
pub fn merge(&mut self, opts: &FluentArgs) {
for (key, value) in opts {
match (*key, value) {
("dateStyle", FluentValue::String(n)) => {
self.date_style = n.as_ref().into();
}
("timeStyle", FluentValue::String(n)) => {
self.time_style = n.as_ref().into();
}
("hourCycle", FluentValue::String(n)) => {
self.hour_cycle = n.as_ref().into();
}
("weekday", FluentValue::String(n)) => {
self.weekday = n.as_ref().into();
}
("era", FluentValue::String(n)) => {
self.era = n.as_ref().into();
}
("year", FluentValue::String(n)) => {
self.year = n.as_ref().into();
}
("month", FluentValue::String(n)) => {
self.month = n.as_ref().into();
}
("day", FluentValue::String(n)) => {
self.day = n.as_ref().into();
}
("hour", FluentValue::String(n)) => {
self.hour = n.as_ref().into();
}
("minute", FluentValue::String(n)) => {
self.minute = n.as_ref().into();
}
("second", FluentValue::String(n)) => {
self.second = n.as_ref().into();
}
("timeZoneName", FluentValue::String(n)) => {
self.time_zone_name = n.as_ref().into();
}
_ => {}
}
}
}
}
#[repr(C)]
pub struct FluentDateTimeOptionsRaw {
pub date_style: FluentDateTimeStyle,
pub time_style: FluentDateTimeStyle,
pub skeleton: nsCString,
}
impl FluentDateTimeOptionsRaw {
fn convert_options_to_skeleton(input: &FluentDateTimeOptions) -> String {
let mut result = String::new();
match input.weekday {
FluentDateTimeTextComponent::Narrow => result.push_str("EEEEE"),
FluentDateTimeTextComponent::Short => result.push_str("E"),
FluentDateTimeTextComponent::Long => result.push_str("EEEE"),
FluentDateTimeTextComponent::None => {}
}
match input.era {
FluentDateTimeTextComponent::Narrow => result.push_str("GGGGG"),
FluentDateTimeTextComponent::Short => result.push_str("G"),
FluentDateTimeTextComponent::Long => result.push_str("GGGG"),
FluentDateTimeTextComponent::None => {}
}
match input.year {
FluentDateTimeNumericComponent::Numeric => result.push_str("yy"),
FluentDateTimeNumericComponent::TwoDigit => result.push_str("y"),
FluentDateTimeNumericComponent::None => {}
}
match input.month {
FluentDateTimeMonthComponent::TwoDigit => result.push_str("MM"),
FluentDateTimeMonthComponent::Numeric => result.push_str("M"),
FluentDateTimeMonthComponent::Narrow => result.push_str("MMMMM"),
FluentDateTimeMonthComponent::Short => result.push_str("MMM"),
FluentDateTimeMonthComponent::Long => result.push_str("MMMM"),
FluentDateTimeMonthComponent::None => {}
}
match input.day {
FluentDateTimeNumericComponent::Numeric => result.push_str("dd"),
FluentDateTimeNumericComponent::TwoDigit => result.push_str("d"),
FluentDateTimeNumericComponent::None => {}
}
let hour_skeleton_char = match input.hour_cycle {
FluentDateTimeHourCycle::H24 => 'H',
FluentDateTimeHourCycle::H23 => 'H',
FluentDateTimeHourCycle::H12 => 'h',
FluentDateTimeHourCycle::H11 => 'h',
FluentDateTimeHourCycle::None => 'j',
};
match input.hour {
FluentDateTimeNumericComponent::Numeric => result.push(hour_skeleton_char),
FluentDateTimeNumericComponent::TwoDigit => {
result.push(hour_skeleton_char);
result.push(hour_skeleton_char);
}
FluentDateTimeNumericComponent::None => {}
}
match input.minute {
FluentDateTimeNumericComponent::Numeric => result.push_str("mm"),
FluentDateTimeNumericComponent::TwoDigit => result.push_str("m"),
FluentDateTimeNumericComponent::None => {}
}
match input.second {
FluentDateTimeNumericComponent::Numeric => result.push_str("ss"),
FluentDateTimeNumericComponent::TwoDigit => result.push_str("s"),
FluentDateTimeNumericComponent::None => {}
}
match input.time_zone_name {
FluentDateTimeTimeZoneNameComponent::Short => result.push_str("z"),
FluentDateTimeTimeZoneNameComponent::Long => result.push_str("zzzz"),
FluentDateTimeTimeZoneNameComponent::None => {}
}
result
}
}
impl From<&FluentDateTimeOptions> for FluentDateTimeOptionsRaw {
fn from(input: &FluentDateTimeOptions) -> Self {
let skeleton = if input.date_style == FluentDateTimeStyle::None
&& input.time_style == FluentDateTimeStyle::None
{
Self::convert_options_to_skeleton(&input).into()
} else {
nsCString::new()
};
Self {
date_style: input.date_style,
time_style: input.time_style,
skeleton,
}
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct FluentDateTime {
epoch: usize,
options: FluentDateTimeOptions,
}
impl FluentType for FluentDateTime {
fn duplicate(&self) -> Box<dyn FluentType> {
Box::new(self.clone())
}
fn as_string(&self, intls: &IntlLangMemoizer) -> Cow<'static, str> {
let result = intls
.with_try_get::<DateTimeFormat, _, _>((self.options.clone(),), |dtf| {
dtf.format(self.epoch)
})
.expect("Failed to retrieve a DateTimeFormat instance.");
result.into()
}
fn as_string_threadsafe(
&self,
_: &intl_memoizer::concurrent::IntlLangMemoizer,
) -> Cow<'static, str> {
unimplemented!()
}
}
impl std::fmt::Display for FluentDateTime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "DATETIME: {}", self.epoch)
}
}
impl FluentDateTime {
pub fn new(epoch: usize, options: FluentDateTimeOptions) -> Self {
Self { epoch, options }
}
}
pub struct DateTimeFormat {
raw: NonNull<ffi::RawDateTimeFormatter>,
}
/**
* According to http://userguide.icu-project.org/design, as long as we constrain
* ourselves to const APIs ICU is const-correct.
*/
unsafe impl Send for DateTimeFormat {}
unsafe impl Sync for DateTimeFormat {}
impl DateTimeFormat {
pub fn new(locale: LanguageIdentifier, options: FluentDateTimeOptions) -> Self {
// ICU needs null-termination here, otherwise we could use nsCStr.
let loc: nsCString = locale.to_string().into();
Self {
raw: unsafe {
NonNull::new_unchecked(ffi::FluentBuiltInDateTimeFormatterCreate(
&loc,
&(&options).into(),
))
},
}
}
pub fn format(&self, input: usize) -> String {
unsafe {
let mut byte_count = 0;
let buffer = ffi::FluentBuiltInDateTimeFormatterFormat(
self.raw.as_ptr(),
input,
&mut byte_count,
);
if buffer.is_null() {
return String::new();
}
String::from_raw_parts(buffer, byte_count as usize, byte_count as usize)
}
}
}
impl Drop for DateTimeFormat {
fn drop(&mut self) {
unsafe { ffi::FluentBuiltInDateTimeFormatterDestroy(self.raw.as_ptr()) };
}
}
impl Memoizable for DateTimeFormat {
type Args = (FluentDateTimeOptions,);
type Error = &'static str;
fn construct(lang: LanguageIdentifier, args: Self::Args) -> Result<Self, Self::Error> {
Ok(DateTimeFormat::new(lang, args.0))
}
}

View File

@ -1,275 +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/. */
use crate::builtins::{FluentDateTime, FluentDateTimeOptions, NumberFormat};
use fluent::resolve::ResolverError;
use fluent::{FluentArgs, FluentBundle, FluentError, FluentResource, FluentValue};
use fluent_pseudo::transform_dom;
use intl_memoizer::IntlLangMemoizer;
use nsstring::{nsACString, nsCString};
use std::borrow::Cow;
use std::collections::HashMap;
use std::mem;
use std::ops::{Deref, DerefMut};
use std::rc::Rc;
use thin_vec::ThinVec;
use unic_langid::LanguageIdentifier;
// Workaround for cbindgen limitation with types.
// See: https://github.com/eqrion/cbindgen/issues/488
pub struct FluentBundleRc(FluentBundle<Rc<FluentResource>>);
impl Deref for FluentBundleRc {
type Target = FluentBundle<Rc<FluentResource>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for FluentBundleRc {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Into<FluentBundleRc> for FluentBundle<Rc<FluentResource>> {
fn into(self) -> FluentBundleRc {
FluentBundleRc(self)
}
}
#[derive(Debug)]
#[repr(C, u8)]
pub enum FluentArgument {
Double_(f64),
String(*const nsCString),
}
fn transform_accented(s: &str) -> Cow<str> {
transform_dom(s, false, true)
}
fn transform_bidi(s: &str) -> Cow<str> {
transform_dom(s, true, false)
}
fn format_numbers(num: &FluentValue, intls: &IntlLangMemoizer) -> Option<String> {
match num {
FluentValue::Number(n) => {
let result = intls
.with_try_get::<NumberFormat, _, _>((n.options.clone(),), |nf| nf.format(n.value))
.expect("Failed to retrieve a NumberFormat instance.");
Some(result)
}
_ => None,
}
}
#[no_mangle]
pub unsafe extern "C" fn fluent_bundle_new(
locales: &ThinVec<nsCString>,
use_isolating: bool,
pseudo_strategy: &nsACString,
) -> *mut FluentBundleRc {
let mut langids = Vec::with_capacity(locales.len());
for locale in locales {
let langid: Result<LanguageIdentifier, _> = locale.to_string().parse();
match langid {
Ok(langid) => langids.push(langid),
Err(_) => return std::ptr::null_mut(),
}
}
let mut bundle = FluentBundle::new(&langids);
bundle.set_use_isolating(use_isolating);
bundle.set_formatter(Some(format_numbers));
bundle
.add_function("PLATFORM", |_args, _named_args| {
if cfg!(target_os = "linux") {
"linux".into()
} else if cfg!(target_os = "windows") {
"windows".into()
} else if cfg!(target_os = "macos") {
"macos".into()
} else if cfg!(target_os = "android") {
"android".into()
} else {
"other".into()
}
})
.expect("Failed to add a function to the bundle.");
bundle
.add_function("NUMBER", |args, named| {
if let Some(FluentValue::Number(n)) = args.get(0) {
let mut num = n.clone();
num.options.merge(named);
FluentValue::Number(num)
} else {
FluentValue::None
}
})
.expect("Failed to add a function to the bundle.");
bundle
.add_function("DATETIME", |args, named| {
if let Some(FluentValue::Number(n)) = args.get(0) {
let mut options = FluentDateTimeOptions::default();
options.merge(&named);
let epoch = n.value as usize;
FluentValue::Custom(Box::new(FluentDateTime::new(epoch, options)))
} else {
FluentValue::None
}
})
.expect("Failed to add a function to the bundle.");
if !pseudo_strategy.is_empty() {
match &pseudo_strategy[..] {
b"accented" => bundle.set_transform(Some(transform_accented)),
b"bidi" => bundle.set_transform(Some(transform_bidi)),
_ => bundle.set_transform(None),
}
}
Box::into_raw(Box::new(bundle.into()))
}
#[no_mangle]
pub unsafe extern "C" fn fluent_bundle_get_locales(
bundle: &FluentBundleRc,
result: &mut ThinVec<nsCString>,
) {
for locale in &bundle.locales {
result.push(locale.to_string().as_str().into());
}
}
#[no_mangle]
pub unsafe extern "C" fn fluent_bundle_destroy(bundle: *mut FluentBundleRc) {
let _ = Box::from_raw(bundle);
}
#[no_mangle]
pub unsafe extern "C" fn fluent_bundle_has_message(
bundle: &FluentBundleRc,
id: &nsACString,
) -> bool {
bundle.has_message(id.to_string().as_str())
}
#[no_mangle]
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.as_str_unchecked()) {
Some(message) => {
*has_value = message.value.is_some();
for key in message.attributes.keys() {
attrs.push((*key).into());
}
true
}
None => {
*has_value = false;
false
}
}
}
#[no_mangle]
pub unsafe extern "C" fn fluent_bundle_format_pattern(
bundle: &FluentBundleRc,
id: &nsACString,
attr: &nsACString,
arg_ids: &ThinVec<nsCString>,
arg_vals: &ThinVec<FluentArgument>,
ret_val: &mut nsACString,
ret_errors: &mut ThinVec<nsCString>,
) -> bool {
let arg_ids = arg_ids.iter().map(|id| id.to_string()).collect::<Vec<_>>();
let args = convert_args(&arg_ids, arg_vals);
let message = match bundle.get_message(id.as_str_unchecked()) {
Some(message) => message,
None => return false,
};
let pattern = if !attr.is_empty() {
match message.attributes.get(attr.as_str_unchecked()) {
Some(attr) => attr,
None => return false,
}
} else {
match message.value {
Some(value) => value,
None => return false,
}
};
let mut errors = vec![];
let value = bundle.format_pattern(pattern, args.as_ref(), &mut errors);
ret_val.assign(value.as_bytes());
for error in errors {
match error {
FluentError::ResolverError(ref err) => match err {
ResolverError::Reference(ref s) => {
let error = format!("ReferenceError: {}", s);
ret_errors.push((&error).into());
}
ResolverError::MissingDefault => {
let error = "RangeError: No default";
ret_errors.push(error.into());
}
ResolverError::Cyclic => {
let error = "RangeError: Cyclic reference";
ret_errors.push(error.into());
}
ResolverError::TooManyPlaceables => {
let error = "RangeError: Too many placeables";
ret_errors.push(error.into());
}
},
_ => panic!("Unknown error!"),
}
}
true
}
#[no_mangle]
pub unsafe extern "C" fn fluent_bundle_add_resource(
bundle: &mut FluentBundleRc,
r: &FluentResource,
allow_overrides: bool,
) {
// we don't own the resource
let r = mem::ManuallyDrop::new(Rc::from_raw(r));
if allow_overrides {
bundle.add_resource_overriding(Rc::clone(&r));
} else if bundle.add_resource(Rc::clone(&r)).is_err() {
eprintln!("Error while adding a resource");
}
}
fn convert_args<'a>(ids: &'a [String], arg_vals: &'a [FluentArgument]) -> Option<FluentArgs<'a>> {
debug_assert_eq!(ids.len(), arg_vals.len());
if ids.is_empty() {
return None;
}
let mut args = HashMap::with_capacity(arg_vals.len());
for (id, val) in ids.iter().zip(arg_vals.iter()) {
let val = match val {
FluentArgument::Double_(d) => FluentValue::from(d),
FluentArgument::String(s) => FluentValue::from(unsafe { (**s).to_string() }),
};
args.insert(id.as_str(), val);
}
Some(args)
}

View File

@ -1,153 +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/. */
use crate::builtins::FluentDateTimeOptionsRaw;
use fluent::types::FluentNumberCurrencyDisplayStyle;
use fluent::types::FluentNumberOptions;
use fluent::types::FluentNumberStyle;
use nsstring::nsCString;
pub enum RawNumberFormatter {}
#[repr(C)]
pub enum FluentNumberStyleRaw {
Decimal,
Currency,
Percent,
}
impl From<FluentNumberStyle> for FluentNumberStyleRaw {
fn from(input: FluentNumberStyle) -> Self {
match input {
FluentNumberStyle::Decimal => Self::Decimal,
FluentNumberStyle::Currency => Self::Currency,
FluentNumberStyle::Percent => Self::Percent,
}
}
}
#[repr(C)]
#[derive(Clone, Copy)]
pub enum FluentNumberCurrencyDisplayStyleRaw {
Symbol,
Code,
Name,
}
impl From<FluentNumberCurrencyDisplayStyle> for FluentNumberCurrencyDisplayStyleRaw {
fn from(input: FluentNumberCurrencyDisplayStyle) -> Self {
match input {
FluentNumberCurrencyDisplayStyle::Symbol => Self::Symbol,
FluentNumberCurrencyDisplayStyle::Code => Self::Code,
FluentNumberCurrencyDisplayStyle::Name => Self::Name,
}
}
}
#[repr(C)]
pub struct FluentNumberOptionsRaw {
pub style: FluentNumberStyleRaw,
pub currency: nsCString,
pub currency_display: FluentNumberCurrencyDisplayStyleRaw,
pub use_grouping: bool,
pub minimum_integer_digits: usize,
pub minimum_fraction_digits: usize,
pub maximum_fraction_digits: usize,
pub minimum_significant_digits: isize,
pub maximum_significant_digits: isize,
}
fn get_number_option(val: Option<usize>, min: usize, max: usize, default: usize) -> usize {
if let Some(val) = val {
if val >= min && val <= max {
val
} else {
default
}
} else {
default
}
}
impl From<&FluentNumberOptions> for FluentNumberOptionsRaw {
fn from(input: &FluentNumberOptions) -> Self {
let currency: nsCString = if let Some(ref currency) = input.currency {
currency.into()
} else {
nsCString::new()
};
//XXX: This should be fetched from currency table.
let currency_digits = 2;
// Keep it aligned with ECMA402 NumberFormat logic.
let minfd_default = if input.style == FluentNumberStyle::Currency {
currency_digits
} else {
0
};
let maxfd_default = match input.style {
FluentNumberStyle::Decimal => 3,
FluentNumberStyle::Currency => currency_digits,
FluentNumberStyle::Percent => 0,
};
let minid = get_number_option(input.minimum_integer_digits, 1, 21, 1);
let minfd = get_number_option(input.minimum_fraction_digits, 0, 20, minfd_default);
let maxfd_actual_default = std::cmp::max(minfd, maxfd_default);
let maxfd = get_number_option(
input.maximum_fraction_digits,
minfd,
20,
maxfd_actual_default,
);
let (minsd, maxsd) = if input.minimum_significant_digits.is_some()
|| input.maximum_significant_digits.is_some()
{
let minsd = get_number_option(input.minimum_significant_digits, 1, 21, 1);
let maxsd = get_number_option(input.maximum_significant_digits, minsd, 21, 21);
(minsd as isize, maxsd as isize)
} else {
(-1, -1)
};
Self {
style: input.style.into(),
currency,
currency_display: input.currency_display.into(),
use_grouping: input.use_grouping,
minimum_integer_digits: minid,
minimum_fraction_digits: minfd,
maximum_fraction_digits: maxfd,
minimum_significant_digits: minsd,
maximum_significant_digits: maxsd,
}
}
}
pub enum RawDateTimeFormatter {}
extern "C" {
pub fn FluentBuiltInNumberFormatterCreate(
locale: &nsCString,
options: &FluentNumberOptionsRaw,
) -> *mut RawNumberFormatter;
pub fn FluentBuiltInNumberFormatterFormat(
formatter: *const RawNumberFormatter,
input: f64,
out_count: &mut u32,
) -> *mut u8;
pub fn FluentBuiltInNumberFormatterDestroy(formatter: *mut RawNumberFormatter);
pub fn FluentBuiltInDateTimeFormatterCreate(
locale: &nsCString,
options: &FluentDateTimeOptionsRaw,
) -> *mut RawDateTimeFormatter;
pub fn FluentBuiltInDateTimeFormatterFormat(
formatter: *const RawDateTimeFormatter,
input: usize,
out_count: &mut u32,
) -> *mut u8;
pub fn FluentBuiltInDateTimeFormatterDestroy(formatter: *mut RawDateTimeFormatter);
}

View File

@ -1,11 +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/. */
mod builtins;
mod bundle;
mod ffi;
mod resource;
pub use bundle::*;
pub use resource::*;

View File

@ -1,38 +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/. */
pub use fluent::FluentResource;
use nsstring::nsACString;
use std::mem;
use std::rc::Rc;
#[no_mangle]
pub unsafe extern "C" fn fluent_resource_new(
name: &nsACString,
has_errors: &mut bool,
) -> *const FluentResource {
let res = match FluentResource::try_new(name.to_string()) {
Ok(res) => {
*has_errors = false;
res
}
Err((res, _)) => {
*has_errors = true;
res
}
};
Rc::into_raw(Rc::new(res))
}
#[no_mangle]
pub unsafe extern "C" fn fluent_resource_addref(res: &FluentResource) {
let raw = Rc::from_raw(res);
mem::forget(Rc::clone(&raw));
mem::forget(raw);
}
#[no_mangle]
pub unsafe extern "C" fn fluent_resource_release(res: &FluentResource) {
let _ = Rc::from_raw(res);
}

View File

@ -7,6 +7,8 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { FluentBundle, FluentResource } =
ChromeUtils.import("resource://gre/modules/Fluent.jsm");
async function* mockGenerateMessages(resourceIds) {
const bundle = new FluentBundle("en-US", {
@ -22,10 +24,6 @@ key2 =
yield bundle;
}
function getAttributeByName(attributes, name) {
return attributes.find(attr => attr.name === name);
}
(async () => {
SimpleTest.waitForExplicitFinish();
@ -40,22 +38,15 @@ key2 =
{id: "key1"},
{id: "key2", args: { user: "Amy"}},
]);
{
is(msgs[0].value, "Value");
let attr0 = getAttributeByName(msgs[0].attributes, "title");
is(attr0.name, "title");
is(attr0.value, "Title 1");
let attr1 = getAttributeByName(msgs[0].attributes, "accesskey");
is(attr1.name, "accesskey");
is(attr1.value, "K");
}
is(msgs[0].value, "Value");
is(msgs[0].attributes[0].name, "title");
is(msgs[0].attributes[0].value, "Title 1");
is(msgs[0].attributes[1].name, "accesskey");
is(msgs[0].attributes[1].value, "K");
{
is(msgs[1].value, null);
let attr0 = getAttributeByName(msgs[1].attributes, "label");
is(attr0.name, "label");
is(attr0.value, "This is a label for Amy");
}
is(msgs[1].value, null);
is(msgs[1].attributes[0].name, "label");
is(msgs[1].attributes[0].value, "This is a label for Amy");
}
{

View File

@ -7,6 +7,9 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { FluentBundle, FluentResource } =
ChromeUtils.import("resource://gre/modules/Fluent.jsm");
async function* mockGenerateMessages(resourceIds) {
const bundle = new FluentBundle("en-US", {
useIsolating: false,

View File

@ -7,6 +7,9 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { FluentBundle, FluentResource } =
ChromeUtils.import("resource://gre/modules/Fluent.jsm");
async function* mockGenerateMessages(resourceIds) {
const bundle = new FluentBundle("en-US",
{

View File

@ -2,6 +2,9 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
function run_test() {
const { FluentBundle, FluentResource } =
ChromeUtils.import("resource://gre/modules/Fluent.jsm");
test_methods_presence(FluentBundle);
test_methods_calling(FluentBundle, FluentResource);

View File

@ -30,10 +30,6 @@ key = This is a single message
};
}
function getAttributeByName(attributes, name) {
return attributes.find(attr => attr.name === name);
}
/**
* This test verifies that as we switching between
* different pseudo strategies the Localization object
@ -59,10 +55,8 @@ add_task(async function test_accented_works() {
let message = (await l10n.formatMessages([{id: "key"}]))[0];
ok(message.value.includes("This is a single message"));
let attr0 = getAttributeByName(message.attributes, "tooltip");
ok(attr0.value.includes("This is a tooltip"));
let attr1 = getAttributeByName(message.attributes, "accesskey");
equal(attr1.value, "f");
ok(message.attributes[0].value.includes("This is a tooltip"));
equal(message.attributes[1].value, "f");
}
{
@ -72,10 +66,8 @@ add_task(async function test_accented_works() {
let message = (await l10n.formatMessages([{id: "key"}]))[0];
ok(message.value.includes("Ŧħīş īş ȧȧ şīƞɠŀḗḗ ḿḗḗşşȧȧɠḗḗ"));
let attr0 = getAttributeByName(message.attributes, "tooltip");
ok(attr0.value.includes("Ŧħīş īş ȧȧ ŧǿǿǿǿŀŧīƥ"));
let attr1 = getAttributeByName(message.attributes, "accesskey");
equal(attr1.value, "f");
ok(message.attributes[0].value.includes("Ŧħīş īş ȧȧ ŧǿǿǿǿŀŧīƥ"));
equal(message.attributes[1].value, "f");
}
{
@ -85,10 +77,8 @@ add_task(async function test_accented_works() {
let message = (await l10n.formatMessages([{id: "key"}]))[0];
ok(message.value.includes("ıs ɐ sıuƃʅǝ ɯǝssɐƃǝ"));
let attr0 = getAttributeByName(message.attributes, "tooltip");
ok(attr0.value.includes("⊥ɥıs ıs ɐ ʇooʅʇıd"));
let attr1 = getAttributeByName(message.attributes, "accesskey");
equal(attr1.value, "f");
ok(message.attributes[0].value.includes("⊥ɥıs ıs ɐ ʇooʅʇıd"));
equal(message.attributes[1].value, "f");
}
{
@ -98,10 +88,8 @@ add_task(async function test_accented_works() {
let message = (await l10n.formatMessages([{id: "key"}]))[0];
ok(message.value.includes("This is a single message"));
let attr0 = getAttributeByName(message.attributes, "tooltip");
ok(attr0.value.includes("This is a tooltip"));
let attr1 = getAttributeByName(message.attributes, "accesskey");
equal(attr1.value, "f");
ok(message.attributes[0].value.includes("This is a tooltip"));
equal(message.attributes[1].value, "f");
}
L10nRegistry.sources.clear();
@ -130,10 +118,8 @@ add_task(async function test_unavailable_strategy_works() {
let message = (await l10n.formatMessages([{id: "key"}]))[0];
ok(message.value.includes("This is a single message"));
let attr0 = getAttributeByName(message.attributes, "tooltip");
ok(attr0.value.includes("This is a tooltip"));
let attr1 = getAttributeByName(message.attributes, "accesskey");
equal(attr1.value, "f");
ok(message.attributes[0].value.includes("This is a tooltip"));
equal(message.attributes[1].value, "f");
}
Services.prefs.setStringPref("intl.l10n.pseudo", "");

File diff suppressed because one or more lines are too long

View File

@ -1,145 +0,0 @@
# Changelog
## Unreleased
- …
## fluent-bundle 0.11.0 (March 10, 2020)
- Separate out `concurrent` version of `FluentBundle`.
- Switch FluentBundle functions to use function pointers.
## fluent-bundle 0.10.2 (February 20, 2020)
- Update to `intl_memoizer` 0.3.0 to allow for Send+Sync on FluentBundle.
## fluent-bundle 0.10.1 (February 15, 2020)
- Switch RefCell in FluentBundle to Mutex.
## fluent-bundle 0.10.0 (February 13, 2020)
- Update `fluent-langneg` to 0.12.
- Update `intl_pluralrules` to 6.0.
- Update `unic-langid` to 0.8.
- Introduce `intl-memoizer`.
- Improve the ergonomics of FluentArgs.
- Add `add_resource_overriding`.
- Remove dependency on `failure`.
- Switch the strategy to mitigate bomb attack to limit the number of placeables.
- Introduce `FluentType` for custom types.
- Improve ergonomics of `FluentNumber` and bring its features closer to ECMA402 Intl.NumberFormat.
## fluent-bundle 0.9.0 (November 26, 2019)
- Update `unic-langid` to 0.7.
- Update `fluent-langneg` to 0.11.
- Update `intl_pluralrules` to 5.0.
## fluent-bundle 0.8.0 (October 3, 2019)
- Update `unic-langid` to 0.6.
- Update `fluent-locale` to 0.10.
## fluent-bundle 0.7.2 (October 1, 2019)
- Update `unic-langid` to 0.5.
- Update `fluent-locale` to 0.9.
- Stop using macros to cut on compilation time and dependencies.
## fluent-bundle 0.7.1 (August 1, 2019)
- Fix FluentBundle::default to use isolating by default.
## fluent-bundle 0.7.0 (August 1, 2019)
- Turn FluentBundle to be a generic over Borrow<FluentResource> (#114)
- Update FluentBundle to the latest API (0.14) (#120)
- Switch to unic_langid for Language Identifier Management
- Refactor FluentArgs (#130)
- Add transform to FluentBundle to enable pseudolocalization (#131)
- Refactor resolver errors to provide better fallbacking and return errors out of formatting (#93)
- Enable FSI/PDI direction isolation (#116)
- Add more convenience From impls for FluentValue (#108)
- Fix `bare_trait_objects` warnings (#110)
## fluent-bundle 0.6.0 (March 26, 2019)
- Update to fluent-syntax 0.9
- Unify benchmark testsuite with fluent.js
## fluent-bundle 0.5.0 (January 31, 2019)
- Update to fluent-syntax 0.8
- Add unicode escaping
- Align with zero-copy parser
## fluent 0.4.3 (October 13, 2018)
- Support Sync+Send in Entry (#70)
## fluent 0.4.2 (October 1, 2018)
- Separate lifetimes of `FluentBundle::new` and return values. (#68)
## fluent 0.4.1 (August 31, 2018)
- Update README to make the example match new API
## fluent 0.4.0 (August 31, 2018)
- Rename MessageContext to FluentBundle
- Update the FluentBundle API to match fluent.js 0.8
- Update intl-pluralrules to 1.0
- Add FluentBundle::format_message
- Add FluentResource for external resource caching
- Update fluent-syntax to 0.1.1
- Update the signature of FluentBundle::format and FluentBundle::format_message
## fluent 0.3.1 (August 6, 2018)
- Update `fluent-locale` to 0.4.1.
- Switch MessageContext::locales to be an owned Vec\<String>
- Switch FluentValue::From\<i8> to FluentValue::From\<isize>
## fluent 0.3.0 (August 3, 2018)
- Add support for custom functions in MessageContext. (#50)
- Switch error handling to `annotate-snippets crate`.
- Separate `fluent` and `fluent-syntax` crates.
- Handle cyclic references. (#55)
- Switch parser binary to use `clap`.
- Switch plural rules handling to `intl_pluralrules`. (#56)
- Add `FluentValue::as_number`
- Move `IntlPluralRules` initialization into `MessageContext::new`
- General cleanups in line with `cargo fmt` and `cargo clippy`
## fluent 0.2.0 (February 11, 2018)
- Support Rust 1.23 stable
- Support Fluent 0.5 syntax
- Dual-license Apache 2.0 and MIT
## fluent 0.1.2 (October 14, 2017)
- Add more complex PluralRules support
## fluent 0.1.0 (October 13, 2017)
- Support parsing Fluent Syntax 0.3.
- Support formatting Messages and Attributes alike.
- Support string- and Number-typed external arguments
- Select expressions:
- without a selector.
- with literal strings and numbers as selector,
- with external arguments as selector,
- with message reference as selector (using tags).
- Support matching numbers in select expression to plural categories.
- Only a single mock plural rule has been implemented for now.
- Support Attribute expressions.
- Support Variant expressions.
- `MessageContext::new` now takes a slice as the `locales` argument.
- Added integration with Travis CI and Coveralls.
- Expanded module documentation.
## fluent 0.0.1 (January 17, 2017)
- This is the first release to be listed in the CHANGELOG.
- Basic parser support for the FTL syntax.
- Message references.

View File

@ -1,855 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
[[package]]
name = "bitflags"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "bstr"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "502ae1441a0a5adb8fbd38a5955a6416b9493e92b465de5e4a9bde6a539c2c48"
dependencies = [
"lazy_static",
"memchr",
"regex-automata",
"serde",
]
[[package]]
name = "bumpalo"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f359dc14ff8911330a51ef78022d376f25ed00248912803b58f00cb1c27f742"
[[package]]
name = "byteorder"
version = "1.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
[[package]]
name = "c2-chacha"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb"
dependencies = [
"ppv-lite86",
]
[[package]]
name = "cast"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b9434b9a5aa1450faa3f9cb14ea0e8c53bb5d2b3c1bfd1ab4fc03e9f33fbfb0"
dependencies = [
"rustc_version",
]
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "clap"
version = "2.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9"
dependencies = [
"bitflags",
"textwrap",
"unicode-width",
]
[[package]]
name = "criterion"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc755679c12bda8e5523a71e4d654b6bf2e14bd838dfc48cde6559a05caf7d1"
dependencies = [
"atty",
"cast",
"clap",
"criterion-plot",
"csv",
"itertools",
"lazy_static",
"num-traits",
"oorandom",
"plotters",
"rayon",
"regex",
"serde",
"serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a01e15e0ea58e8234f96146b1f91fa9d0e4dd7a38da93ff7a75d42c0b9d3a545"
dependencies = [
"cast",
"itertools",
]
[[package]]
name = "crossbeam-deque"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
"maybe-uninit",
]
[[package]]
name = "crossbeam-epoch"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace"
dependencies = [
"autocfg",
"cfg-if",
"crossbeam-utils",
"lazy_static",
"maybe-uninit",
"memoffset",
"scopeguard",
]
[[package]]
name = "crossbeam-queue"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c695eeca1e7173472a32221542ae469b3e9aac3a4fc81f7696bcad82029493db"
dependencies = [
"cfg-if",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
dependencies = [
"autocfg",
"cfg-if",
"lazy_static",
]
[[package]]
name = "csv"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00affe7f6ab566df61b4be3ce8cf16bc2576bca0963ceb0955e45d514bf9a279"
dependencies = [
"bstr",
"csv-core",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
dependencies = [
"memchr",
]
[[package]]
name = "dtoa"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3"
[[package]]
name = "either"
version = "1.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
[[package]]
name = "fluent-bundle"
version = "0.11.0"
dependencies = [
"criterion",
"fluent-langneg",
"fluent-syntax",
"intl-memoizer",
"intl_pluralrules",
"rand",
"rental",
"serde",
"serde_yaml",
"smallvec",
"unic-langid",
]
[[package]]
name = "fluent-langneg"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe5815efd5542e40841cd34ef9003822352b04c67a70c595c6758597c72e1f56"
dependencies = [
"unic-langid",
]
[[package]]
name = "fluent-syntax"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac0f7e83d14cccbf26e165d8881dcac5891af0d85a88543c09dd72ebd31d91ba"
[[package]]
name = "fxhash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
dependencies = [
"byteorder",
]
[[package]]
name = "getrandom"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "hermit-abi"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1010591b26bbfe835e9faeabeb11866061cc7dcebffd56ad7d0942d0e61aefd8"
dependencies = [
"libc",
]
[[package]]
name = "intl-memoizer"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9867e2d65d82936ef34217ed0f87b639a94384e93a0676158142c861c705391f"
dependencies = [
"type-map",
"unic-langid",
]
[[package]]
name = "intl_pluralrules"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d82c14d8eece42c03353e0ce86a4d3f97b1f1cef401e4d962dca6c6214a85002"
dependencies = [
"tinystr",
"unic-langid",
]
[[package]]
name = "itertools"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e"
[[package]]
name = "js-sys"
version = "0.3.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cb931d43e71f560c81badb0191596562bafad2be06a3f9025b845c847c60df5"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018"
[[package]]
name = "linked-hash-map"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83"
[[package]]
name = "log"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
dependencies = [
"cfg-if",
]
[[package]]
name = "maybe-uninit"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
[[package]]
name = "memchr"
version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
[[package]]
name = "memoffset"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75189eb85871ea5c2e2c15abbdd541185f63b408415e5051f5cac122d8c774b9"
dependencies = [
"rustc_version",
]
[[package]]
name = "num-traits"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096"
dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46203554f085ff89c235cd12f7075f3233af9b11ed7c9e16dfe2560d03313ce6"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "oorandom"
version = "11.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebcec7c9c2a95cacc7cd0ecb89d8a8454eca13906f6deb55258ffff0adeb9405"
[[package]]
name = "plotters"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e3bb8da247d27ae212529352020f3e5ee16e83c0c258061d27b08ab92675eeb"
dependencies = [
"js-sys",
"num-traits",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "ppv-lite86"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b"
[[package]]
name = "proc-macro-hack"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecd45702f76d6d3c75a80564378ae228a85f0b59d2f3ed43c91b4a69eb2ebfc5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c09721c6781493a2a492a96b5a5bf19b65917fe6728884e7c44dd0c60ca3435"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
"getrandom",
"libc",
"rand_chacha",
"rand_core",
"rand_hc",
]
[[package]]
name = "rand_chacha"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853"
dependencies = [
"c2-chacha",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [
"getrandom",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
dependencies = [
"rand_core",
]
[[package]]
name = "rayon"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db6ce3297f9c85e16621bb8cca38a06779ffc31bb8184e1be4bed2be4678a098"
dependencies = [
"crossbeam-deque",
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08a89b46efaf957e52b18062fb2f4660f8b8a4dde1807ca002690868ef2c85a9"
dependencies = [
"crossbeam-deque",
"crossbeam-queue",
"crossbeam-utils",
"lazy_static",
"num_cpus",
]
[[package]]
name = "regex"
version = "1.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "322cf97724bea3ee221b78fe25ac9c46114ebb51747ad5babd51a2fc6a8235a8"
dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4"
dependencies = [
"byteorder",
]
[[package]]
name = "regex-syntax"
version = "0.6.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1132f845907680735a84409c3bebc64d1364a5683ffbce899550cd09d5eaefc1"
[[package]]
name = "rental"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8545debe98b2b139fb04cad8618b530e9b07c152d99a5de83c860b877d67847f"
dependencies = [
"rental-impl",
"stable_deref_trait",
]
[[package]]
name = "rental-impl"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "475e68978dc5b743f2f40d8e0a8fdc83f1c5e78cbf4b8fa5e74e73beebc340de"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "rustc_version"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
dependencies = [
"semver",
]
[[package]]
name = "ryu"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "semver"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
dependencies = [
"semver-parser",
]
[[package]]
name = "semver-parser"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9371ade75d4c2d6cb154141b9752cf3781ec9c05e0e5cf35060e1e70ee7b9c25"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "691b17f19fc1ec9d94ec0b5864859290dff279dbd7b03f017afda54eb36c3c35"
dependencies = [
"dtoa",
"linked-hash-map",
"serde",
"yaml-rust",
]
[[package]]
name = "smallvec"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c2fb2ec9bcd216a5b0d0ccf31ab17b5ed1d627960edff65bbe95d3ce221cefc"
[[package]]
name = "stable_deref_trait"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8"
[[package]]
name = "syn"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "123bd9499cfb380418d509322d7a6d52e5315f064fe4b3ad18a53d6b92c07859"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"unicode-width",
]
[[package]]
name = "tinystr"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bac79c4b51eda1b090b1edebfb667821bbb51f713855164dc7cec2cb8ac2ba3"
[[package]]
name = "tinytemplate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57a3c6667d3e65eb1bc3aed6fd14011c6cbc3a0665218ab7f5daf040b9ec371a"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "type-map"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d2741b1474c327d95c1f1e3b0a2c3977c8e128409c572a33af2914e7d636717"
dependencies = [
"fxhash",
]
[[package]]
name = "unic-langid"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d81136159f779c35b10655f45210c71cd5ca5a45aadfe9840a61c7071735ed"
dependencies = [
"unic-langid-impl",
"unic-langid-macros",
]
[[package]]
name = "unic-langid-impl"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c43c61e94492eb67f20facc7b025778a904de83d953d8fcb60dd9adfd6e2d0ea"
dependencies = [
"tinystr",
]
[[package]]
name = "unic-langid-macros"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49bd90791278634d57e3ed4a4073108e3f79bfb87ab6a7b8664ba097425703df"
dependencies = [
"proc-macro-hack",
"tinystr",
"unic-langid-impl",
"unic-langid-macros-impl",
]
[[package]]
name = "unic-langid-macros-impl"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0098f77bd754f8fb7850cdf4ab143aa821898c4ac6dc16bcb2aa3e62ce858d1"
dependencies = [
"proc-macro-hack",
"quote",
"syn",
"unic-langid-impl",
]
[[package]]
name = "unicode-width"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479"
[[package]]
name = "unicode-xid"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
[[package]]
name = "walkdir"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d"
dependencies = [
"same-file",
"winapi",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "wasm-bindgen"
version = "0.2.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3557c397ab5a8e347d434782bcd31fc1483d927a6826804cec05cc792ee2519d"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0da9c9a19850d3af6df1cb9574970b566d617ecfaf36eb0b706b6f3ef9bd2f8"
dependencies = [
"bumpalo",
"lazy_static",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f6fde1d36e75a714b5fe0cffbb78978f222ea6baebb726af13c78869fdb4205"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25bda4168030a6412ea8a047e27238cadf56f0e53516e1e83fec0a8b7c786f6d"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc9f36ad51f25b0219a3d4d13b90eb44cd075dff8b6280cca015775d7acaddd8"
[[package]]
name = "web-sys"
version = "0.3.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "721c6263e2c66fd44501cc5efbfa2b7dfa775d13e4ea38c46299646ed1f9c70a"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ccfbf554c6ad11084fb7517daca16cfdcaccbdadba4fc336f032a8b12c2ad80"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "yaml-rust"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65923dd1784f44da1d2c3dbbc5e822045628c590ba72123e1c73d3c230c4434d"
dependencies = [
"linked-hash-map",
]

View File

@ -1,64 +0,0 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies
#
# If you believe there's an error in this file please file an
# issue against the rust-lang/cargo repository. If you're
# editing this file be aware that the upstream Cargo.toml
# will likely look very different (and much more reasonable)
[package]
edition = "2018"
name = "fluent-bundle"
version = "0.11.0"
authors = ["Zibi Braniecki <gandalf@mozilla.com>", "Staś Małolepszy <stas@mozilla.com>"]
description = "A localization system designed to unleash the entire expressive power of\nnatural language translations.\n"
homepage = "http://www.projectfluent.org"
readme = "README.md"
keywords = ["localization", "l10n", "i18n", "intl", "internationalization"]
categories = ["localization", "internationalization"]
license = "Apache-2.0/MIT"
repository = "https://github.com/projectfluent/fluent-rs"
[[bench]]
name = "resolver"
harness = false
[dependencies.fluent-langneg]
version = "0.12"
[dependencies.fluent-syntax]
version = "0.9"
[dependencies.intl-memoizer]
version = "0.4"
[dependencies.intl_pluralrules]
version = "6.0"
[dependencies.rental]
version = "0.5"
[dependencies.smallvec]
version = "1.0"
[dependencies.unic-langid]
version = "0.8"
[dev-dependencies.criterion]
version = "0.3"
[dev-dependencies.rand]
version = "0.7"
[dev-dependencies.serde]
version = "1.0"
features = ["derive"]
[dev-dependencies.serde_yaml]
version = "0.8"
[dev-dependencies.unic-langid]
version = "0.8"
features = ["macros"]

View File

@ -1,111 +0,0 @@
# Fluent
`fluent-rs` is a Rust implementation of [Project Fluent][], a localization
framework designed to unleash the entire expressive power of natural language
translations.
[![crates.io](http://meritbadge.herokuapp.com/fluent)](https://crates.io/crates/fluent)
[![Build Status](https://travis-ci.org/projectfluent/fluent-rs.svg?branch=master)](https://travis-ci.org/projectfluent/fluent-rs)
[![Coverage Status](https://coveralls.io/repos/github/projectfluent/fluent-rs/badge.svg?branch=master)](https://coveralls.io/github/projectfluent/fluent-rs?branch=master)
Project Fluent keeps simple things simple and makes complex things possible.
The syntax used for describing translations is easy to read and understand. At
the same time it allows, when necessary, to represent complex concepts from
natural languages like gender, plurals, conjugations, and others.
[Documentation][]
[Project Fluent]: http://projectfluent.org
[Documentation]: https://docs.rs/fluent/
Usage
-----
```rust
use fluent_bundle::{FluentBundle, FluentResource};
use unic_langid::langid;
fn main() {
let ftl_string = "hello-world = Hello, world!".to_owned();
let res = FluentResource::try_new(ftl_string)
.expect("Could not parse an FTL string.");
let langid_en = langid!("en");
let mut bundle = FluentBundle::new(&[langid_en]);
bundle.add_resource(&res)
.expect("Failed to add FTL resources to the bundle.");
let msg = bundle.get_message("hello-world")
.expect("Failed to retrieve a message.");
let val = msg.value.expect("Message has no value.");
let mut errors = vec![];
let value = bundle.format_pattern(val, None, &mut errors);
assert_eq!(&value, "Hello, world!");
}
```
Status
------
The implementation is in its early stages and supports only some of the Project
Fluent's spec. Consult the [list of milestones][] for more information about
release planning and scope.
[list of milestones]: https://github.com/projectfluent/fluent-rs/milestones
Local Development
-----------------
cargo build
cargo test
cargo bench
cargo run --example simple-app
When submitting a PR please use [`cargo fmt`][] (nightly).
[`cargo fmt`]: https://github.com/rust-lang-nursery/rustfmt
Learn the FTL syntax
--------------------
FTL is a localization file format used for describing translation resources.
FTL stands for _Fluent Translation List_.
FTL is designed to be simple to read, but at the same time allows to represent
complex concepts from natural languages like gender, plurals, conjugations, and
others.
hello-user = Hello, { $username }!
[Read the Fluent Syntax Guide][] in order to learn more about the syntax. If
you're a tool author you may be interested in the formal [EBNF grammar][].
[Read the Fluent Syntax Guide]: http://projectfluent.org/fluent/guide/
[EBNF grammar]: https://github.com/projectfluent/fluent/tree/master/spec
Get Involved
------------
`fluent-rs` is open-source, licensed under the Apache License, Version 2.0. We
encourage everyone to take a look at our code and we'll listen to your
feedback.
Discuss
-------
We'd love to hear your thoughts on Project Fluent! Whether you're a localizer
looking for a better way to express yourself in your language, or a developer
trying to make your app localizable and multilingual, or a hacker looking for
a project to contribute to, please do get in touch on the mailing list and the
IRC channel.
- Discourse: https://discourse.mozilla.org/c/fluent
- IRC channel: [irc://irc.mozilla.org/l20n](irc://irc.mozilla.org/l20n)

View File

@ -1,318 +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/.
## browser/locales/en-US/browser/menubar.ftl
## File menu
file-menu =
.label = File
.accesskey = F
tab-cmd =
.label = New Tab
.accesskey = T
new-user-context =
.label = New Container Tab
.accesskey = B
new-navigator-cmd =
.label = New Window
.accesskey = N
new-private-window =
.label = New Private Window
.accesskey = W
# Only displayed on OS X, and only on windows that aren't main browser windows,
# or when there are no windows but Firefox is still running.
open-location-cmd =
.label = Open Location…
open-file-cmd =
.label = Open File…
.accesskey = O
close-cmd =
.label = Close
.accesskey = C
close-window =
.label = Close Window
.accesskey = d
save-page-cmd =
.label = Save Page As…
.accesskey = A
email-page-cmd =
.label = Email Link…
.accesskey = E
print-setup-cmd =
.label = Page Setup…
.accesskey = u
print-preview-cmd =
.label = Print Preview
.accesskey = v
print-cmd =
.label = Print…
.accesskey = P
go-offline-cmd =
.label = Work Offline
.accesskey = k
quit-application-cmd =
.label = Quit
.accesskey = Q
## Edit menu
edit-menu =
.label = Edit
.accesskey = E
undo-cmd =
.label = Undo
.accesskey = U
redo-cmd =
.label = Redo
.accesskey = R
cut-cmd =
.label = Cut
.accesskey = t
copy-cmd =
.label = Copy
.accesskey = C
paste-cmd =
.label = Paste
.accesskey = P
delete-cmd =
.label = Delete
.accesskey = D
select-all-cmd =
.label = Select All
.accesskey = A
find-on-cmd =
.label = Find in This Page…
.accesskey = F
find-again-cmd =
.label = Find Again
.accesskey = g
bidi-switch-text-direction-item =
.label = Switch Text Direction
.accesskey = w
preferences-cmd-unix =
.label = Preferences
.accesskey = n
## View menu
view-menu =
.label = View
.accesskey = V
view-toolbars-menu =
.label = Toolbars
.accesskey = T
view-customize-toolbar =
.label = Customize…
.accesskey = C
view-sidebar-menu =
.label = Sidebar
.accesskey = e
bookmarks-button =
.label = Bookmarks
history-button =
.label = History
synced-tabs =
.label = Synced Tabs
full-zoom =
.label = Zoom
.accesskey = Z
full-zoom-enlarge-cmd =
.label = Zoom In
.accesskey = I
full-zoom-reduce-cmd =
.label = Zoom Out
.accesskey = O
full-zoom-reset-cmd =
.label = Reset
.accesskey = R
full-zoom-toggle-cmd =
.label = Zoom Text Only
.accesskey = T
page-style-menu =
.label = Page Style
.accesskey = y
page-style-no-style =
.label = No Style
.accesskey = n
page-style-persistent-only =
.label = Basic Page Style
.accesskey = b
charset-menu2 =
.label = Text Encoding
.accesskey = c
## Full Screen controls
## Match what Safari and other Apple applications use on OS X Lion.
#
enter-full-screen-cmd =
.accesskey = F
.label = Enter Full Screen
exit-full-screen-cmd =
.accesskey = F
.label = Exit Full Screen
full-screen-cmd =
.accesskey = F
.label = Full Screen
show-all-tabs-cmd =
.accesskey = A
.label = Show All Tabs
bidi-switch-page-direction-item =
.label = Switch Page Direction
.accesskey = D
## History menu
history-menu =
.label = History
.accesskey = s
show-all-history-cmd2 =
.label = Show All History
clear-recent-history =
.label = Clear Recent History…
sync-tabs-menu3 =
.label = Synced Tabs
history-restore-last-session =
.label = Restore Previous Session
hidden-tabs =
.label = Hidden Tabs
history-undo-menu =
.label = Recently Closed Tabs
history-undo-window-menu =
.label = Recently Closed Windows
## Bookmarks menu
bookmarks-menu =
.label = Bookmarks
.accesskey = B
show-all-bookmarks2 =
.label = Show All Bookmarks
add-cur-pages-cmd =
.label = Bookmark All Tabs…
personalbar-cmd =
.label = Bookmarks Toolbar
other-bookmarks-cmd =
.label = Other Bookmarks
mobile-bookmarks-cmd =
.label = Mobile Bookmarks
## Tools menu
tools-menu =
.label = Tools
.accesskey = T
downloads =
.label = Downloads
.accesskey = D
addons =
.label = Add-ons
.accesskey = A
sync-sign-in =
.label = Sign In To { -sync-brand-short-name }…
.accesskey = Y
sync-sync-now-item =
.label = Sync Now
.accesskey = S
sync-re-auth-item =
.label = Reconnect to { -sync-brand-short-name }…
.accesskey = R
web-developer-menu =
.label = Web Developer
.accesskey = W
page-source-cmd =
.label = Page Source
.accesskey = o
page-info-cmd =
.accesskey = I
.label = Page Info
preferences-cmd2 =
.label = Options
.accesskey = O
preferences-ldb-cmd =
.label = Layout Debugger
.accesskey = L
preferences-cmd-mac =
.label = Preferences…
services-menu-mac =
.label = Services
hide-this-app-cmd-mac2 =
.label = Hide { -brand-shorter-name }
hide-other-apps-cmd-mac =
.label = Hide Others
show-all-apps-cmd-mac =
.label = Show All
window-menu =
.label = Window
bring-all-to-front =
.label = Bring All to Front
help-menu =
.label = Help
.accesskey = H
product-help2 =
.label = { -brand-shorter-name } Help
.accesskey = H
help-show-tour2 =
.label = { -brand-shorter-name } Tour
.accesskey = o
help-keyboard-shortcuts =
.label = Keyboard Shortcuts
.accesskey = K
help-troubleshooting-info =
.accesskey = T
.label = Troubleshooting Information
help-feedback-page =
.accesskey = S
.label = Submit Feedback…
help-safe-mode =
.accesskey = R
.label = Restart with Add-ons Disabled…
.stopaccesskey = R
.stoplabel = Restart with Add-ons Enabled
report-deceptive-site-menu =
.label = Report Deceptive Site…
.accesskey = D
safeb =
.label = This isnt a deceptive site…
.accesskey = d
about-product2 =
.accesskey = A
.label = About { -brand-shorter-name }
# browser/locales/en-US/browser/toolbar.ftl
urlbar-textbox =
.placeholder = Search or enter address
.accesskey = d
## Toolbar items
view-bookmarks-broadcaster =
.label = Bookmarks
view-bookmarks-key =
.key = b
view-bookmarks-key-win =
.key = i
view-history-broadcaster =
.label = History
view-history-key =
.key = h
view-tabs-broadcaster =
.label = Synced Tabs
# browser/branding/official/locales/en-US/brand.ftl
-brand-shorter-name = Firefox
-brand-short-name = Firefox
-brand-full-name = Mozilla Firefox
-vendor-short-name = Mozilla
trademark-info =
Firefox and the Firefox logos are trademarks of the Mozilla Foundation.
-sync-brand-short-name = Sync

File diff suppressed because it is too large Load Diff

View File

@ -1,113 +0,0 @@
use criterion::criterion_group;
use criterion::criterion_main;
use criterion::Criterion;
use std::collections::HashMap;
use std::fs::File;
use std::io;
use std::io::Read;
use fluent_bundle::{FluentBundle, FluentResource, FluentValue};
use fluent_syntax::ast;
use unic_langid::langid;
fn read_file(path: &str) -> Result<String, io::Error> {
let mut f = File::open(path)?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
fn get_strings(tests: &[&'static str]) -> HashMap<&'static str, String> {
let mut ftl_strings = HashMap::new();
for test in tests {
let path = format!("./benches/{}.ftl", test);
ftl_strings.insert(*test, read_file(&path).expect("Couldn't load file"));
}
return ftl_strings;
}
fn get_ids(res: &FluentResource) -> Vec<String> {
res.ast()
.body
.iter()
.filter_map(|entry| match entry {
ast::ResourceEntry::Entry(ast::Entry::Message(ast::Message { id, .. })) => {
Some(id.name.to_owned())
}
_ => None,
})
.collect()
}
fn get_args(name: &str) -> Option<HashMap<&str, FluentValue>> {
match name {
"preferences" => {
let mut prefs_args = HashMap::new();
prefs_args.insert("name", FluentValue::from("John"));
prefs_args.insert("tabCount", FluentValue::from(5));
prefs_args.insert("count", FluentValue::from(3));
prefs_args.insert("version", FluentValue::from("65.0"));
prefs_args.insert("path", FluentValue::from("/tmp"));
prefs_args.insert("num", FluentValue::from(4));
prefs_args.insert("email", FluentValue::from("john@doe.com"));
prefs_args.insert("value", FluentValue::from(4.5));
prefs_args.insert("unit", FluentValue::from("mb"));
prefs_args.insert("service-name", FluentValue::from("Mozilla Disk"));
Some(prefs_args)
}
_ => None,
}
}
fn add_functions<R>(name: &'static str, bundle: &mut FluentBundle<R>) {
match name {
"preferences" => {
bundle
.add_function("PLATFORM", |_args, _named_args| {
return "linux".into();
})
.expect("Failed to add a function to the bundle.");
}
_ => {}
}
}
fn resolver_bench(c: &mut Criterion) {
let tests = &["simple", "preferences", "menubar", "unescape"];
let ftl_strings = get_strings(tests);
c.bench_function_over_inputs(
"resolve",
move |b, &&name| {
let source = &ftl_strings[name];
let res =
FluentResource::try_new(source.to_owned()).expect("Couldn't parse an FTL source");
let ids = get_ids(&res);
let lids = &[langid!("en")];
let mut bundle = FluentBundle::new(lids);
bundle
.add_resource(res)
.expect("Couldn't add FluentResource to the FluentBundle");
add_functions(name, &mut bundle);
let args = get_args(name);
b.iter(|| {
for id in &ids {
let msg = bundle.get_message(id).expect("Message found");
let mut errors = vec![];
if let Some(value) = msg.value {
let _ = bundle.format_pattern(value, args.as_ref(), &mut errors);
}
for (_, value) in msg.attributes {
let _ = bundle.format_pattern(value, args.as_ref(), &mut errors);
}
assert!(errors.len() == 0, "Resolver errors: {:#?}", errors);
}
})
},
tests,
);
}
criterion_group!(benches, resolver_bench);
criterion_main!(benches);

View File

@ -1,102 +0,0 @@
# Artificial testcase with 100 simple Fluent Messages
key0 = Value 0
key1 = Value 1
key2 = Value 2
key3 = Value 3
key4 = Value 4
key5 = Value 5
key6 = Value 6
key7 = Value 7
key8 = Value 8
key9 = Value 9
key10 = Value 10
key11 = Value 11
key12 = Value 12
key13 = Value 13
key14 = Value 14
key15 = Value 15
key16 = Value 16
key17 = Value 17
key18 = Value 18
key19 = Value 19
key20 = Value 20
key21 = Value 21
key22 = Value 22
key23 = Value 23
key24 = Value 24
key25 = Value 25
key26 = Value 26
key27 = Value 27
key28 = Value 28
key29 = Value 29
key30 = Value 30
key31 = Value 31
key32 = Value 32
key33 = Value 33
key34 = Value 34
key35 = Value 35
key36 = Value 36
key37 = Value 37
key38 = Value 38
key39 = Value 39
key40 = Value 40
key41 = Value 41
key42 = Value 42
key43 = Value 43
key44 = Value 44
key45 = Value 45
key46 = Value 46
key47 = Value 47
key48 = Value 48
key49 = Value 49
key50 = Value 50
key51 = Value 51
key52 = Value 52
key53 = Value 53
key54 = Value 54
key55 = Value 55
key56 = Value 56
key57 = Value 57
key58 = Value 58
key59 = Value 59
key60 = Value 60
key61 = Value 61
key62 = Value 62
key63 = Value 63
key64 = Value 64
key65 = Value 65
key66 = Value 66
key67 = Value 67
key68 = Value 68
key69 = Value 69
key70 = Value 70
key71 = Value 71
key72 = Value 72
key73 = Value 73
key74 = Value 74
key75 = Value 75
key76 = Value 76
key77 = Value 77
key78 = Value 78
key79 = Value 79
key80 = Value 80
key81 = Value 81
key82 = Value 82
key83 = Value 83
key84 = Value 84
key85 = Value 85
key86 = Value 86
key87 = Value 87
key88 = Value 88
key89 = Value 89
key90 = Value 90
key91 = Value 91
key92 = Value 92
key93 = Value 93
key94 = Value 94
key95 = Value 95
key96 = Value 96
key97 = Value 97
key98 = Value 98
key99 = Value 99

View File

@ -1,9 +0,0 @@
face-with-tears-of-joy = 😂
tetragram-for-centre = 𝌆
surrogates-in-text = \uD83D\uDE02
surrogates-in-string = {"\uD83D\uDE02"}
surrogates-in-adjacent-strings = {"\uD83D"}{"\uDE02"}
emoji-in-text = A face 😂 with tears of joy.
emoji-in-string = {"A face 😂 with tears of joy."}

View File

@ -1,6 +0,0 @@
This directory contains a set of examples
of how to use Fluent.
Start with the `simple-app.rs` which is a very
trivial example of a command line application
with localization handled by Fluent.

View File

@ -1,145 +0,0 @@
// This is an example of an application which uses a custom formatter
// to format selected types of values.
//
// This allows users to plug their own number formatter to Fluent.
use unic_langid::LanguageIdentifier;
use fluent_bundle::memoizer::MemoizerKind;
use fluent_bundle::types::{FluentNumber, FluentNumberOptions};
use fluent_bundle::{FluentArgs, FluentBundle, FluentResource, FluentValue};
fn custom_formatter<M: MemoizerKind>(num: &FluentValue, _intls: &M) -> Option<String> {
match num {
FluentValue::Number(n) => Some(format!("CUSTOM({})", n.value).into()),
_ => None,
}
}
fn main() {
// 1. Bootstrap a FluentBundle with a number of messages which use
// number formatting in different forms.
let ftl_string = String::from(
"
key-implicit = Here is an implicitly encoded number: { 5 }.
key-explicit = Here is an explicitly encoded number: { NUMBER(5) }.
key-var-implicit = Here is an implicitly encoded variable: { $num }.
key-var-explicit = Here is an explicitly encoded variable: { NUMBER($num) }.
key-var-with-arg = Here is a variable formatted with an argument { NUMBER($num, minimumFractionDigits: 5) }.
",
);
let res = FluentResource::try_new(ftl_string).expect("Could not parse an FTL string.");
let lang: LanguageIdentifier = "en".parse().unwrap();
let mut bundle = FluentBundle::new(&[lang]);
bundle
.add_resource(res)
.expect("Failed to add FTL resources to the bundle.");
bundle
.add_function("NUMBER", |positional, named| {
match positional.get(0) {
Some(FluentValue::Number(n)) => {
let mut num = n.clone();
// This allows us to merge the arguments provided
// as arguments to the function into the new FluentNumber.
num.options.merge(named);
FluentValue::Number(num)
}
_ => FluentValue::None,
}
})
.expect("Failed to add a function.");
bundle.set_use_isolating(false);
let mut errors = vec![];
// 2. First, we're going to format the number using the implicit formatter.
// At the moment the number will be formatted in a very dummy way, since
// we do not have a locale aware number formatter available yet.
let msg = bundle
.get_message("key-implicit")
.expect("Message doesn't exist.");
let pattern = msg.value.expect("Message has no value.");
let value = bundle.format_pattern(&pattern, None, &mut errors);
assert_eq!(value, "Here is an implicitly encoded number: 5.");
println!("{}", value);
// 3. Next, we're going to plug our custom formatter.
bundle.set_formatter(Some(custom_formatter));
// 4. Now, when you attempt to format a number, the custom formatter
// will be used instead of the default one.
let msg = bundle
.get_message("key-implicit")
.expect("Message doesn't exist.");
let pattern = msg.value.expect("Message has no value.");
let value = bundle.format_pattern(&pattern, None, &mut errors);
assert_eq!(value, "Here is an implicitly encoded number: CUSTOM(5).");
println!("{}", value);
// 5. The same custom formatter will be used for explicitly formatter numbers,
// and variables of type number.
let msg = bundle
.get_message("key-explicit")
.expect("Message doesn't exist.");
let pattern = msg.value.expect("Message has no value.");
let value = bundle.format_pattern(&pattern, None, &mut errors);
assert_eq!(value, "Here is an explicitly encoded number: CUSTOM(5).");
println!("{}", value);
let msg = bundle
.get_message("key-var-implicit")
.expect("Message doesn't exist.");
let pattern = msg.value.expect("Message has no value.");
let mut args = FluentArgs::new();
args.insert("num", FluentValue::from(-15));
let value = bundle.format_pattern(&pattern, Some(&args), &mut errors);
assert_eq!(
value,
"Here is an implicitly encoded variable: CUSTOM(-15)."
);
println!("{}", value);
let msg = bundle
.get_message("key-var-explicit")
.expect("Message doesn't exist.");
let pattern = msg.value.expect("Message has no value.");
let mut args = FluentArgs::new();
args.insert("num", FluentValue::from(-15));
let value = bundle.format_pattern(&pattern, Some(&args), &mut errors);
assert_eq!(
value,
"Here is an explicitly encoded variable: CUSTOM(-15)."
);
println!("{}", value);
// 6. The merging operation on FluentNumber options allows the
// options provided from the localizer to be merged into the
// default ones and ones provided by the developer.
let msg = bundle
.get_message("key-var-explicit")
.expect("Message doesn't exist.");
let pattern = msg.value.expect("Message has no value.");
let mut args = FluentArgs::new();
let num = FluentNumber::new(
25.2,
FluentNumberOptions {
maximum_fraction_digits: Some(8),
minimum_fraction_digits: Some(1),
..Default::default()
},
);
args.insert("num", num.into());
let value = bundle.format_pattern(&pattern, Some(&args), &mut errors);
// Notice, that since we specificed minimum and maximum fraction digits options
// to be 1 and 8 when construction the argument, and then the minimum fraction
// digits option has been overridden in the localization the formatter
// will received options:
// - minimum_fraction_digits: Some(5)
// - maximum_fraction_digits: Some(8)
assert_eq!(
value,
"Here is an explicitly encoded variable: CUSTOM(25.2)."
);
println!("{}", value);
}

View File

@ -1,190 +0,0 @@
// This is an example of an application which adds a custom type of value,
// and a function to format it.
//
// In this example we're going to add a new type - DateTime.
//
// We're also going to add a built-in function DATETIME to produce that type
// out of a number argument (epoch).
//
// Lastly, we'll also create a new formatter which will be memoizable.
//
// The type and its options are modelled after ECMA402 Intl.DateTimeFormat.
use intl_memoizer::Memoizable;
use unic_langid::LanguageIdentifier;
use fluent_bundle::types::FluentType;
use fluent_bundle::{FluentArgs, FluentBundle, FluentResource, FluentValue};
// First we're going to define what options our new type is going to accept.
// For the sake of the example, we're only going to allow two options:
// - dateStyle
// - timeStyle
//
// with an enum of allowed values.
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
enum DateTimeStyleValue {
Full,
Long,
Medium,
Short,
None,
}
impl std::default::Default for DateTimeStyleValue {
fn default() -> Self {
Self::None
}
}
// This is just a helper to make it easier to convert
// a value provided to FluentArgs into an option value.
impl<'l> From<&FluentValue<'l>> for DateTimeStyleValue {
fn from(input: &FluentValue) -> Self {
if let FluentValue::String(s) = input {
match s.as_ref() {
"full" => Self::Full,
"long" => Self::Long,
"medium" => Self::Medium,
"short" => Self::Short,
_ => Self::None,
}
} else {
Self::None
}
}
}
#[derive(Debug, PartialEq, Eq, Default, Clone, Hash)]
struct DateTimeOptions {
pub date_style: DateTimeStyleValue,
pub time_style: DateTimeStyleValue,
}
impl DateTimeOptions {
// The merge function is going to be used by the Fluent Function
// to merge localizer provided options into defaults of values
// provided by the developer.
//
// If you want to limit which options the localizer can override,
// here's the right place to do it.
pub fn merge(&mut self, input: &FluentArgs) {
for (key, value) in input {
match *key {
"dateStyle" => self.date_style = value.into(),
"timeStyle" => self.time_style = value.into(),
_ => {}
}
}
}
}
impl<'l> From<&FluentArgs<'l>> for DateTimeOptions {
fn from(input: &FluentArgs) -> Self {
let mut opts = Self::default();
opts.merge(input);
opts
}
}
// Our new custom type will store a value as an epoch number,
// and the options.
#[derive(Debug, PartialEq, Clone)]
struct DateTime {
epoch: usize,
options: DateTimeOptions,
}
impl DateTime {
pub fn new(epoch: usize, options: DateTimeOptions) -> Self {
Self { epoch, options }
}
}
impl FluentType for DateTime {
fn duplicate(&self) -> Box<dyn FluentType> {
Box::new(DateTime::new(self.epoch, DateTimeOptions::default()))
}
fn as_string(&self, intls: &intl_memoizer::IntlLangMemoizer) -> std::borrow::Cow<'static, str> {
intls
.with_try_get::<DateTimeFormatter, _, _>((self.options.clone(),), |dtf| {
dtf.format(self.epoch).into()
})
.expect("Failed to format a date.")
}
fn as_string_threadsafe(
&self,
_: &intl_memoizer::concurrent::IntlLangMemoizer,
) -> std::borrow::Cow<'static, str> {
format!("2020-01-20 {}:00", self.epoch).into()
}
}
/// Formatter
struct DateTimeFormatter {
lang: LanguageIdentifier,
options: DateTimeOptions,
}
impl DateTimeFormatter {
pub fn new(lang: LanguageIdentifier, options: DateTimeOptions) -> Result<Self, ()> {
Ok(Self { lang, options })
}
pub fn format(&self, epoch: usize) -> String {
format!(
"My Custom Formatted Date from epoch: {}, in locale: {}, using options: {:#?}",
epoch, self.lang, self.options
)
}
}
impl Memoizable for DateTimeFormatter {
type Args = (DateTimeOptions,);
type Error = ();
fn construct(lang: LanguageIdentifier, args: Self::Args) -> Result<Self, Self::Error> {
Self::new(lang, args.0)
}
}
fn main() {
// 1. Bootstrap a FluentBundle with a number of messages which use
// number formatting in different forms.
let ftl_string = String::from(
r#"
key-date = Today is { DATETIME($epoch, dateStyle: "long", timeStyle: "short") }
"#,
);
let res = FluentResource::try_new(ftl_string).expect("Could not parse an FTL string.");
let lang: LanguageIdentifier = "en".parse().unwrap();
let mut bundle = FluentBundle::new(&[lang]);
bundle
.add_resource(res)
.expect("Failed to add FTL resources to the bundle.");
bundle
.add_function("DATETIME", |positional, named| match positional.get(0) {
Some(FluentValue::Number(n)) => {
let epoch = n.value as usize;
let options = named.into();
FluentValue::Custom(Box::new(DateTime::new(epoch, options)))
}
_ => FluentValue::None,
})
.expect("Failed to add a function.");
bundle.set_use_isolating(false);
let msg = bundle
.get_message("key-date")
.expect("Failed to retrieve the message.");
let pattern = msg.value.expect("Message has no value.");
let mut errors = vec![];
let mut args = FluentArgs::new();
let epoch: u64 = 1580127760093;
args.insert("epoch", epoch.into());
let value = bundle.format_pattern(pattern, Some(&args), &mut errors);
println!("{}", value);
}

View File

@ -1,50 +0,0 @@
use fluent_bundle::{FluentArgs, FluentBundle, FluentResource, FluentValue};
use unic_langid::langid;
fn main() {
let ftl_string = String::from(
"
hello-world = Hello { $name }
ref = The previous message says { hello-world }
unread-emails =
{ $emailCount ->
[one] You have { $emailCount } unread email
*[other] You have { $emailCount } unread emails
}
",
);
let res = FluentResource::try_new(ftl_string).expect("Could not parse an FTL string.");
let langid_en = langid!("en");
let mut bundle = FluentBundle::new(&[langid_en]);
bundle
.add_resource(res)
.expect("Failed to add FTL resources to the bundle.");
let mut args = FluentArgs::new();
args.insert("name", FluentValue::from("John"));
let msg = bundle
.get_message("hello-world")
.expect("Message doesn't exist.");
let mut errors = vec![];
let pattern = msg.value.expect("Message has no value.");
let value = bundle.format_pattern(&pattern, Some(&args), &mut errors);
println!("{}", value);
let msg = bundle.get_message("ref").expect("Message doesn't exist.");
let mut errors = vec![];
let pattern = msg.value.expect("Message has no value.");
let value = bundle.format_pattern(&pattern, Some(&args), &mut errors);
println!("{}", value);
let mut args = FluentArgs::new();
args.insert("emailCount", 1.into());
let msg = bundle
.get_message("unread-emails")
.expect("Message doesn't exist.");
let mut errors = vec![];
let pattern = msg.value.expect("Message has no value.");
let value = bundle.format_pattern(&pattern, Some(&args), &mut errors);
println!("{}", value);
}

View File

@ -1,82 +0,0 @@
use fluent_bundle::{FluentBundle, FluentResource, FluentValue};
use unic_langid::langid;
fn main() {
// We define the resources here so that they outlive
// the bundle.
let ftl_string1 = String::from("hello-world = Hey there! { HELLO() }");
let ftl_string2 = String::from("meaning-of-life = { MEANING_OF_LIFE(42) }");
let ftl_string3 = String::from("all-your-base = { BASE_OWNERSHIP(hello, ownership: \"us\") }");
let res1 = FluentResource::try_new(ftl_string1).expect("Could not parse an FTL string.");
let res2 = FluentResource::try_new(ftl_string2).expect("Could not parse an FTL string.");
let res3 = FluentResource::try_new(ftl_string3).expect("Could not parse an FTL string.");
let langid_en_us = langid!("en-US");
let mut bundle = FluentBundle::new(&[langid_en_us]);
// Test for a simple function that returns a string
bundle
.add_function("HELLO", |_args, _named_args| {
return "I'm a function!".into();
})
.expect("Failed to add a function to the bundle.");
// Test for a function that accepts unnamed positional arguments
bundle
.add_function("MEANING_OF_LIFE", |args, _named_args| {
if let Some(arg0) = args.get(0) {
if *arg0 == 42.into() {
return "The answer to life, the universe, and everything".into();
}
}
FluentValue::None
})
.expect("Failed to add a function to the bundle.");
// Test for a function that accepts named arguments
bundle
.add_function("BASE_OWNERSHIP", |_args, named_args| {
return match named_args.get("ownership") {
Some(FluentValue::String(ref string)) => {
format!("All your base belong to {}", string).into()
}
_ => FluentValue::None,
};
})
.expect("Failed to add a function to the bundle.");
bundle
.add_resource(res1)
.expect("Failed to add FTL resources to the bundle.");
bundle
.add_resource(res2)
.expect("Failed to add FTL resources to the bundle.");
bundle
.add_resource(res3)
.expect("Failed to add FTL resources to the bundle.");
let msg = bundle
.get_message("hello-world")
.expect("Message doesn't exist.");
let mut errors = vec![];
let pattern = msg.value.expect("Message has no value.");
let value = bundle.format_pattern(&pattern, None, &mut errors);
assert_eq!(&value, "Hey there! \u{2068}I'm a function!\u{2069}");
let msg = bundle
.get_message("meaning-of-life")
.expect("Message doesn't exist.");
let mut errors = vec![];
let pattern = msg.value.expect("Message has no value.");
let value = bundle.format_pattern(&pattern, None, &mut errors);
assert_eq!(&value, "The answer to life, the universe, and everything");
let msg = bundle
.get_message("all-your-base")
.expect("Message doesn't exist.");
let mut errors = vec![];
let pattern = msg.value.expect("Message has no value.");
let value = bundle.format_pattern(&pattern, None, &mut errors);
assert_eq!(&value, "All your base belong to us");
}

View File

@ -1,18 +0,0 @@
use fluent_bundle::{FluentBundle, FluentResource};
fn main() {
let ftl_string = String::from("hello-world = Hello, world!");
let res = FluentResource::try_new(ftl_string).expect("Could not parse an FTL string.");
let mut bundle = FluentBundle::default();
bundle
.add_resource(&res)
.expect("Failed to add FTL resources to the bundle.");
let msg = bundle
.get_message("hello-world")
.expect("Message doesn't exist.");
let mut errors = vec![];
let pattern = msg.value.expect("Message has no value.");
let value = bundle.format_pattern(&pattern, None, &mut errors);
assert_eq!(&value, "Hello, world!");
}

View File

@ -1,33 +0,0 @@
use fluent_bundle::{FluentBundle, FluentResource};
fn main() {
let ftl_string = String::from(
"
foo = Foo
foobar = { foo } Bar
bazbar = { baz } Bar
",
);
let res = FluentResource::try_new(ftl_string).expect("Could not parse an FTL string.");
let mut bundle = FluentBundle::default();
bundle
.add_resource(res)
.expect("Failed to add FTL resources to the bundle.");
let msg = bundle
.get_message("foobar")
.expect("Message doesn't exist.");
let mut errors = vec![];
let pattern = msg.value.expect("Message has no value.");
let value = bundle.format_pattern(&pattern, None, &mut errors);
println!("{}", value);
let msg = bundle
.get_message("bazbar")
.expect("Message doesn't exist.");
let mut errors = vec![];
let pattern = msg.value.expect("Message has no value.");
let value = bundle.format_pattern(&pattern, None, &mut errors);
println!("{}", value);
}

View File

@ -1,7 +0,0 @@
missing-arg-error = Error: Please provide a number as argument.
input-parse-error = Error: Could not parse input `{ $input }`. Reason: { $reason }
response-msg =
{ $value ->
[one] "{ $input }" has one Collatz step.
*[other] "{ $input }" has { $value } Collatz steps.
}

View File

@ -1,7 +0,0 @@
missing-arg-error = Erreur : veuillez saisir un nombre en paramètre.
input-parse-error = Erreur : impossible d'interpréter le paramètre `{ $input }`. Raison : { $reason }
response-msg =
{ $value ->
[one] La suite de Syracuse du nombre "{ $input }" comporte une valeur.
*[other] La suite de Syracuse du nombre "{ $input }" comporte { $value } valeurs.
}

View File

@ -1,8 +0,0 @@
missing-arg-error = Błąd: Proszę wprowadzić liczbę jako argument.
input-parse-error = Błąd: Nie udało się sparsować `{ $input }`. Powód: { $reason }
response-msg =
{ $value ->
[one] "{ $input }" ma jeden krok Collatza.
[few] "{ $input }" ma { $value } kroki Collatza.
*[many] "{ $input }" ma { $value } kroków Collatza.
}

View File

@ -1,41 +0,0 @@
use fluent_bundle::{FluentArgs, FluentBundle, FluentResource, FluentValue};
fn main() {
let ftl_string = String::from(
"
hello-world = Hello { $missing ->
*[one] World
[two] Moon
}
hello-world2 = Hello { $name ->
*[world] World
[moon] Moon
}
",
);
let res = FluentResource::try_new(ftl_string).expect("Could not parse an FTL string.");
let mut bundle = FluentBundle::default();
bundle
.add_resource(res)
.expect("Failed to add FTL resources to the bundle.");
let msg = bundle
.get_message("hello-world")
.expect("Message doesn't exist.");
let mut errors = vec![];
let pattern = msg.value.expect("Message has no value.");
let value = bundle.format_pattern(&pattern, None, &mut errors);
println!("{}", value);
let mut args = FluentArgs::new();
args.insert("name", FluentValue::from("moon"));
let msg = bundle
.get_message("hello-world2")
.expect("Message doesn't exist.");
let mut errors = vec![];
let pattern = msg.value.expect("Message has no value.");
let value = bundle.format_pattern(&pattern, Some(&args), &mut errors);
println!("{}", value);
}

View File

@ -1,179 +0,0 @@
//! This is an example of a simple application
//! which calculates the Collatz conjecture.
//!
//! The function itself is trivial on purpose,
//! so that we can focus on understanding how
//! the application can be made localizable
//! via Fluent.
//!
//! To try the app launch `cargo run --example simple-app NUM (LOCALES)`
//!
//! NUM is a number to be calculated, and LOCALES is an optional
//! parameter with a comma-separated list of locales requested by the user.
//!
//! Example:
//!
//! cargo run --example simple-app 123 de,pl
//!
//! If the second argument is omitted, `en-US` locale is used as the
//! default one.
use fluent_bundle::{FluentArgs, FluentBundle, FluentResource, FluentValue};
use fluent_langneg::{negotiate_languages, NegotiationStrategy};
use std::env;
use std::fs;
use std::fs::File;
use std::io;
use std::io::prelude::*;
use std::path::Path;
use std::str::FromStr;
use unic_langid::{langid, LanguageIdentifier};
/// We need a generic file read helper function to
/// read the localization resource file.
///
/// The resource files are stored in
/// `./examples/resources/{locale}` directory.
fn read_file(path: &Path) -> Result<String, io::Error> {
let mut f = File::open(path)?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
/// This helper function allows us to read the list
/// of available locales by reading the list of
/// directories in `./examples/resources`.
///
/// It is expected that every directory inside it
/// has a name that is a valid BCP47 language tag.
fn get_available_locales() -> Result<Vec<LanguageIdentifier>, io::Error> {
let mut locales = vec![];
let mut dir = env::current_dir()?;
if dir.to_string_lossy().ends_with("fluent-rs") {
dir.push("fluent-bundle");
}
dir.push("examples");
dir.push("resources");
let res_dir = fs::read_dir(dir)?;
for entry in res_dir {
if let Ok(entry) = entry {
let path = entry.path();
if path.is_dir() {
if let Some(name) = path.file_name() {
if let Some(name) = name.to_str() {
let langid = name.parse().expect("Parsing failed.");
locales.push(langid);
}
}
}
}
}
return Ok(locales);
}
static L10N_RESOURCES: &[&str] = &["simple.ftl"];
fn main() {
// 1. Get the command line arguments.
let args: Vec<String> = env::args().collect();
// 3. If the argument length is more than 1,
// take the second argument as a comma-separated
// list of requested locales.
let requested = args.get(2).map_or(vec![], |arg| {
arg.split(",")
.map(|s| -> LanguageIdentifier { s.parse().expect("Parsing locale failed.") })
.collect()
});
// 4. Negotiate it against the available ones
let default_locale = langid!("en-US");
let available = get_available_locales().expect("Retrieving available locales failed.");
let resolved_locales = negotiate_languages(
&requested,
&available,
Some(&default_locale),
NegotiationStrategy::Filtering,
);
let current_locale = resolved_locales
.get(0)
.expect("At least one locale should match.");
// 5. Create a new Fluent FluentBundle using the
// resolved locales.
let mut bundle = FluentBundle::new(resolved_locales.clone());
// 6. Load the localization resource
for path in L10N_RESOURCES {
let mut full_path = env::current_dir().expect("Failed to retireve current dir.");
if full_path.to_string_lossy().ends_with("fluent-rs") {
full_path.push("fluent-bundle");
}
full_path.push("examples");
full_path.push("resources");
full_path.push(current_locale.to_string());
full_path.push(path);
let source = read_file(&full_path).expect("Failed to read file.");
let resource = FluentResource::try_new(source).expect("Could not parse an FTL string.");
bundle
.add_resource(resource)
.expect("Failed to add FTL resources to the bundle.");
}
// 7. Check if the input is provided.
match args.get(1) {
Some(input) => {
// 7.1. Cast it to a number.
match isize::from_str(&input) {
Ok(i) => {
// 7.2. Construct a map of arguments
// to format the message.
let mut args = FluentArgs::new();
args.insert("input", FluentValue::from(i));
args.insert("value", FluentValue::from(collatz(i)));
// 7.3. Format the message.
let mut errors = vec![];
let msg = bundle
.get_message("response-msg")
.expect("Message doesn't exist.");
let pattern = msg.value.expect("Message has no value.");
let value = bundle.format_pattern(&pattern, Some(&args), &mut errors);
println!("{}", value);
}
Err(err) => {
let mut args = FluentArgs::new();
args.insert("input", FluentValue::from(input.as_str()));
args.insert("reason", FluentValue::from(err.to_string()));
let mut errors = vec![];
let msg = bundle
.get_message("input-parse-error")
.expect("Message doesn't exist.");
let pattern = msg.value.expect("Message has no value.");
let value = bundle.format_pattern(&pattern, Some(&args), &mut errors);
println!("{}", value);
}
}
}
None => {
let mut errors = vec![];
let msg = bundle
.get_message("missing-arg-error")
.expect("Message doesn't exist.");
let pattern = msg.value.expect("Message has no value.");
let value = bundle.format_pattern(&pattern, None, &mut errors);
println!("{}", value);
}
}
}
/// Collatz conjecture calculating function.
fn collatz(n: isize) -> isize {
match n {
1 => 0,
_ => match n % 2 {
0 => 1 + collatz(n / 2),
_ => 1 + collatz(n * 3 + 1),
},
}
}

View File

@ -1,524 +0,0 @@
//! `FluentBundle` is a collection of localization messages in Fluent.
//!
//! It stores a list of messages in a single locale which can reference one another, use the same
//! internationalization formatters, functions, scopeironmental variables and are expected to be used
//! together.
use std::borrow::Borrow;
use std::borrow::Cow;
use std::collections::hash_map::{Entry as HashEntry, HashMap};
use std::default::Default;
use fluent_syntax::ast;
use unic_langid::LanguageIdentifier;
use crate::entry::Entry;
use crate::entry::GetEntry;
use crate::errors::FluentError;
use crate::memoizer::MemoizerKind;
use crate::resolve::{ResolveValue, Scope};
use crate::resource::FluentResource;
use crate::types::FluentValue;
/// A single localization unit composed of an identifier,
/// value, and attributes.
#[derive(Debug, PartialEq)]
pub struct FluentMessage<'m> {
pub value: Option<&'m ast::Pattern<'m>>,
pub attributes: HashMap<&'m str, &'m ast::Pattern<'m>>,
}
/// A map of arguments passed from the code to
/// the localization to be used for message
/// formatting.
pub type FluentArgs<'args> = HashMap<&'args str, FluentValue<'args>>;
/// A collection of localization messages for a single locale, which are meant
/// to be used together in a single view, widget or any other UI abstraction.
///
/// # Examples
///
/// ```
/// use fluent_bundle::{FluentBundle, FluentResource, FluentValue};
/// use std::collections::HashMap;
/// use unic_langid::langid;
///
/// let ftl_string = String::from("intro = Welcome, { $name }.");
/// let resource = FluentResource::try_new(ftl_string)
/// .expect("Could not parse an FTL string.");
///
/// let langid_en = langid!("en-US");
/// let mut bundle = FluentBundle::new(&[langid_en]);
///
/// bundle.add_resource(&resource)
/// .expect("Failed to add FTL resources to the bundle.");
///
/// let mut args = HashMap::new();
/// args.insert("name", FluentValue::from("Rustacean"));
///
/// let msg = bundle.get_message("intro").expect("Message doesn't exist.");
/// let mut errors = vec![];
/// let pattern = msg.value.expect("Message has no value.");
/// let value = bundle.format_pattern(&pattern, Some(&args), &mut errors);
/// assert_eq!(&value, "Welcome, \u{2068}Rustacean\u{2069}.");
///
/// ```
///
/// # `FluentBundle` Life Cycle
///
/// ## Create a bundle
///
/// To create a bundle, call [`FluentBundle::new`] with a locale list that represents the best
/// possible fallback chain for a given locale. The simplest case is a one-locale list.
///
/// Fluent uses [`LanguageIdentifier`] which can be created using `langid!` macro.
///
/// ## Add Resources
///
/// Next, call [`add_resource`] one or more times, supplying translations in the FTL syntax.
///
/// Since [`FluentBundle`] is generic over anything that can borrow a [`FluentResource`],
/// one can use [`FluentBundle`] to own its resources, store references to them,
/// or even [`Rc<FluentResource>`] or [`Arc<FluentResource>`].
///
/// The [`FluentBundle`] instance is now ready to be used for localization.
///
/// ## Format
///
/// To format a translation, call [`get_message`] to retrieve a [`FluentMessage`],
/// and then call [`format_pattern`] on the message value or attribute in order to
/// retrieve the translated string.
///
/// The result of [`format_pattern`] is an [`Cow<str>`]. It is
/// recommended to treat the result as opaque from the perspective of the program and use it only
/// to display localized messages. Do not examine it or alter in any way before displaying. This
/// is a general good practice as far as all internationalization operations are concerned.
///
/// If errors were encountered during formatting, they will be
/// accumulated in the [`Vec<FluentError>`] passed as the third argument.
///
/// While they are not fatal, they usually indicate problems with the translation,
/// and should be logged or reported in a way that allows the developer to notice
/// and fix them.
///
///
/// # Locale Fallback Chain
///
/// [`FluentBundle`] stores messages in a single locale, but keeps a locale fallback chain for the
/// purpose of language negotiation with i18n formatters. For instance, if date and time formatting
/// are not available in the first locale, [`FluentBundle`] will use its `locales` fallback chain
/// to negotiate a sensible fallback for date and time formatting.
///
/// [`add_resource`]: ./struct.FluentBundle.html#method.add_resource
/// [`FluentBundle::new`]: ./struct.FluentBundle.html#method.new
/// [`FluentMessage`]: ./struct.FluentMessage.html
/// [`FluentBundle`]: ./struct.FluentBundle.html
/// [`FluentResource`]: ./struct.FluentResource.html
/// [`get_message`]: ./struct.FluentBundle.html#method.get_message
/// [`format_pattern`]: ./struct.FluentBundle.html#method.format_pattern
/// [`add_resource`]: ./struct.FluentBundle.html#method.add_resource
/// [`Cow<str>`]: http://doc.rust-lang.org/std/borrow/enum.Cow.html
/// [`Rc<FluentResource>`]: https://doc.rust-lang.org/std/rc/struct.Rc.html
/// [`Arc<FluentResource>`]: https://doc.rust-lang.org/std/sync/struct.Arc.html
/// [`LanguageIdentifier`]: https://crates.io/crates/unic-langid
/// [`Vec<FluentError>`]: ./enum.FluentError.html
pub struct FluentBundleBase<R, M> {
pub locales: Vec<LanguageIdentifier>,
pub(crate) resources: Vec<R>,
pub(crate) entries: HashMap<String, Entry>,
pub(crate) intls: M,
pub(crate) use_isolating: bool,
pub(crate) transform: Option<fn(&str) -> Cow<str>>,
pub(crate) formatter: Option<fn(&FluentValue, &M) -> Option<String>>,
}
impl<R, M: MemoizerKind> FluentBundleBase<R, M> {
/// Constructs a FluentBundle. `locales` is the fallback chain of locales
/// to use for formatters like date and time. `locales` does not influence
/// message selection.
///
/// # Examples
///
/// ```
/// use fluent_bundle::FluentBundle;
/// use fluent_bundle::FluentResource;
/// use unic_langid::langid;
///
/// let langid_en = langid!("en-US");
/// let mut bundle: FluentBundle<FluentResource> = FluentBundle::new(&[langid_en]);
/// ```
///
/// # Errors
///
/// This will panic if no formatters can be found for the locales.
pub fn new<'a, L: 'a + Into<LanguageIdentifier> + PartialEq + Clone>(
locales: impl IntoIterator<Item = &'a L>,
) -> Self {
let locales = locales
.into_iter()
.map(|s| s.clone().into())
.collect::<Vec<_>>();
let lang = locales.get(0).cloned().unwrap_or_default();
FluentBundleBase {
locales,
resources: vec![],
entries: HashMap::new(),
intls: M::new(lang),
use_isolating: true,
transform: None,
formatter: None,
}
}
/// Adds a resource to the bundle, returning an empty [`Result<T>`] on success.
///
/// If any entry in the resource uses the same identifier as an already
/// existing key in the bundle, the new entry will be ignored and a
/// `FluentError::Overriding` will be added to the result.
///
/// The method can take any type that can be borrowed to FluentResource:
/// - FluentResource
/// - &FluentResource
/// - Rc<FluentResource>
/// - Arc<FluentResurce>
///
/// This allows the user to introduce custom resource management and share
/// resources between instances of `FluentBundle`.
///
/// # Examples
///
/// ```
/// use fluent_bundle::{FluentBundle, FluentResource};
/// use unic_langid::langid;
///
/// let ftl_string = String::from("
/// hello = Hi!
/// goodbye = Bye!
/// ");
/// let resource = FluentResource::try_new(ftl_string)
/// .expect("Could not parse an FTL string.");
/// let langid_en = langid!("en-US");
/// let mut bundle = FluentBundle::new(&[langid_en]);
/// bundle.add_resource(resource)
/// .expect("Failed to add FTL resources to the bundle.");
/// assert_eq!(true, bundle.has_message("hello"));
/// ```
///
/// # Whitespace
///
/// Message ids must have no leading whitespace. Message values that span
/// multiple lines must have leading whitespace on all but the first line. These
/// are standard FTL syntax rules that may prove a bit troublesome in source
/// code formatting. The [`indoc!`] crate can help with stripping extra indentation
/// if you wish to indent your entire message.
///
/// [FTL syntax]: https://projectfluent.org/fluent/guide/
/// [`indoc!`]: https://github.com/dtolnay/indoc
/// [`Result<T>`]: https://doc.rust-lang.org/std/result/enum.Result.html
pub fn add_resource(&mut self, r: R) -> Result<(), Vec<FluentError>>
where
R: Borrow<FluentResource>,
{
let mut errors = vec![];
let res = r.borrow();
let res_pos = self.resources.len();
for (entry_pos, entry) in res.ast().body.iter().enumerate() {
let id = match entry {
ast::ResourceEntry::Entry(ast::Entry::Message(ast::Message { ref id, .. }))
| ast::ResourceEntry::Entry(ast::Entry::Term(ast::Term { ref id, .. })) => id.name,
_ => continue,
};
let (entry, kind) = match entry {
ast::ResourceEntry::Entry(ast::Entry::Message(..)) => {
(Entry::Message([res_pos, entry_pos]), "message")
}
ast::ResourceEntry::Entry(ast::Entry::Term(..)) => {
(Entry::Term([res_pos, entry_pos]), "term")
}
_ => continue,
};
match self.entries.entry(id.to_string()) {
HashEntry::Vacant(empty) => {
empty.insert(entry);
}
HashEntry::Occupied(_) => {
errors.push(FluentError::Overriding {
kind,
id: id.to_string(),
});
}
}
}
self.resources.push(r);
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
/// Adds a resource to the bundle, returning an empty [`Result<T>`] on success.
///
/// If any entry in the resource uses the same identifier as an already
/// existing key in the bundle, the entry will override the previous one.
///
/// The method can take any type that can be borrowed as FluentResource:
/// - FluentResource
/// - &FluentResource
/// - Rc<FluentResource>
/// - Arc<FluentResurce>
///
/// This allows the user to introduce custom resource management and share
/// resources between instances of `FluentBundle`.
///
/// # Examples
///
/// ```
/// use fluent_bundle::{FluentBundle, FluentResource};
/// use unic_langid::langid;
///
/// let ftl_string = String::from("
/// hello = Hi!
/// goodbye = Bye!
/// ");
/// let resource = FluentResource::try_new(ftl_string)
/// .expect("Could not parse an FTL string.");
///
/// let ftl_string = String::from("
/// hello = Another Hi!
/// ");
/// let resource2 = FluentResource::try_new(ftl_string)
/// .expect("Could not parse an FTL string.");
///
/// let langid_en = langid!("en-US");
///
/// let mut bundle = FluentBundle::new(&[langid_en]);
/// bundle.add_resource(resource)
/// .expect("Failed to add FTL resources to the bundle.");
///
/// bundle.add_resource_overriding(resource2);
///
/// let mut errors = vec![];
/// let msg = bundle.get_message("hello")
/// .expect("Failed to retrieve the message");
/// let value = msg.value.expect("Failed to retrieve the value of the message");
/// assert_eq!(bundle.format_pattern(value, None, &mut errors), "Another Hi!");
/// ```
///
/// # Whitespace
///
/// Message ids must have no leading whitespace. Message values that span
/// multiple lines must have leading whitespace on all but the first line. These
/// are standard FTL syntax rules that may prove a bit troublesome in source
/// code formatting. The [`indoc!`] crate can help with stripping extra indentation
/// if you wish to indent your entire message.
///
/// [FTL syntax]: https://projectfluent.org/fluent/guide/
/// [`indoc!`]: https://github.com/dtolnay/indoc
/// [`Result<T>`]: https://doc.rust-lang.org/std/result/enum.Result.html
pub fn add_resource_overriding(&mut self, r: R)
where
R: Borrow<FluentResource>,
{
let res = r.borrow();
let res_pos = self.resources.len();
for (entry_pos, entry) in res.ast().body.iter().enumerate() {
let id = match entry {
ast::ResourceEntry::Entry(ast::Entry::Message(ast::Message { ref id, .. }))
| ast::ResourceEntry::Entry(ast::Entry::Term(ast::Term { ref id, .. })) => id.name,
_ => continue,
};
let entry = match entry {
ast::ResourceEntry::Entry(ast::Entry::Message(..)) => {
Entry::Message([res_pos, entry_pos])
}
ast::ResourceEntry::Entry(ast::Entry::Term(..)) => {
Entry::Term([res_pos, entry_pos])
}
_ => continue,
};
self.entries.insert(id.to_string(), entry);
}
self.resources.push(r);
}
/// When formatting patterns, `FluentBundle` inserts
/// Unicode Directionality Isolation Marks to indicate
/// that the direction of a placeable may differ from
/// the surrounding message.
///
/// This is important for cases such as when a
/// right-to-left user name is presented in the
/// left-to-right message.
///
/// In some cases, such as testing, the user may want
/// to disable the isolating.
pub fn set_use_isolating(&mut self, value: bool) {
self.use_isolating = value;
}
/// This method allows to specify a function that will
/// be called on all textual fragments of the pattern
/// during formatting.
///
/// This is currently primarly used for pseudolocalization,
/// and `fluent-pseudo` crate provides a function
/// that can be passed here.
pub fn set_transform(&mut self, func: Option<fn(&str) -> Cow<str>>) {
if let Some(f) = func {
self.transform = Some(f);
} else {
self.transform = None;
}
}
/// This method allows to specify a function that will
/// be called before any `FluentValue` is formatted
/// allowing overrides.
///
/// It's particularly useful for plugging in an external
/// formatter for `FluentValue::Number`.
pub fn set_formatter(&mut self, func: Option<fn(&FluentValue, &M) -> Option<String>>) {
if let Some(f) = func {
self.formatter = Some(f);
} else {
self.formatter = None;
}
}
/// Returns true if this bundle contains a message with the given id.
///
/// # Examples
///
/// ```
/// use fluent_bundle::{FluentBundle, FluentResource};
/// use unic_langid::langid;
///
/// let ftl_string = String::from("hello = Hi!");
/// let resource = FluentResource::try_new(ftl_string)
/// .expect("Failed to parse an FTL string.");
/// let langid_en = langid!("en-US");
/// let mut bundle = FluentBundle::new(&[langid_en]);
/// bundle.add_resource(&resource)
/// .expect("Failed to add FTL resources to the bundle.");
/// assert_eq!(true, bundle.has_message("hello"));
///
/// ```
pub fn has_message(&self, id: &str) -> bool
where
R: Borrow<FluentResource>,
{
self.get_entry_message(id).is_some()
}
pub fn get_message(&self, id: &str) -> Option<FluentMessage>
where
R: Borrow<FluentResource>,
{
let message = self.get_entry_message(id)?;
let value = message.value.as_ref();
let mut attributes = if message.attributes.is_empty() {
HashMap::new()
} else {
HashMap::with_capacity(message.attributes.len())
};
for attr in message.attributes.iter() {
attributes.insert(attr.id.name, &attr.value);
}
Some(FluentMessage { value, attributes })
}
pub fn format_pattern<'bundle>(
&'bundle self,
pattern: &'bundle ast::Pattern,
args: Option<&'bundle FluentArgs>,
errors: &mut Vec<FluentError>,
) -> Cow<'bundle, str>
where
R: Borrow<FluentResource>,
{
let mut scope = Scope::new(self, args);
let result = pattern.resolve(&mut scope).as_string(&scope);
for err in scope.errors {
errors.push(err.into());
}
result
}
/// Makes the provided rust function available to messages with the name `id`. See
/// the [FTL syntax guide] to learn how these are used in messages.
///
/// FTL functions accept both positional and named args. The rust function you
/// provide therefore has two parameters: a slice of values for the positional
/// args, and a HashMap of values for named args.
///
/// # Examples
///
/// ```
/// use fluent_bundle::{FluentBundle, FluentResource, FluentValue};
/// use unic_langid::langid;
///
/// let ftl_string = String::from("length = { STRLEN(\"12345\") }");
/// let resource = FluentResource::try_new(ftl_string)
/// .expect("Could not parse an FTL string.");
/// let langid_en = langid!("en-US");
/// let mut bundle = FluentBundle::new(&[langid_en]);
/// bundle.add_resource(&resource)
/// .expect("Failed to add FTL resources to the bundle.");
///
/// // Register a fn that maps from string to string length
/// bundle.add_function("STRLEN", |positional, _named| match positional {
/// [FluentValue::String(str)] => str.len().into(),
/// _ => FluentValue::None,
/// }).expect("Failed to add a function to the bundle.");
///
/// let msg = bundle.get_message("length").expect("Message doesn't exist.");
/// let mut errors = vec![];
/// let pattern = msg.value.expect("Message has no value.");
/// let value = bundle.format_pattern(&pattern, None, &mut errors);
/// assert_eq!(&value, "5");
/// ```
///
/// [FTL syntax guide]: https://projectfluent.org/fluent/guide/functions.html
pub fn add_function<F>(&mut self, id: &str, func: F) -> Result<(), FluentError>
where
F: for<'a> Fn(&[FluentValue<'a>], &FluentArgs) -> FluentValue<'a> + Sync + Send + 'static,
{
match self.entries.entry(id.to_owned()) {
HashEntry::Vacant(entry) => {
entry.insert(Entry::Function(Box::new(func)));
Ok(())
}
HashEntry::Occupied(_) => Err(FluentError::Overriding {
kind: "function",
id: id.to_owned(),
}),
}
}
}
impl<R, M: MemoizerKind> Default for FluentBundleBase<R, M> {
fn default() -> Self {
let langid = LanguageIdentifier::default();
FluentBundleBase {
locales: vec![langid.clone()],
resources: vec![],
entries: Default::default(),
use_isolating: true,
intls: M::new(langid),
transform: None,
formatter: None,
}
}
}

View File

@ -1,31 +0,0 @@
use intl_memoizer::{concurrent::IntlLangMemoizer, Memoizable};
use unic_langid::LanguageIdentifier;
use crate::bundle::FluentBundleBase;
use crate::memoizer::MemoizerKind;
use crate::types::FluentType;
pub type FluentBundle<R> = FluentBundleBase<R, IntlLangMemoizer>;
impl MemoizerKind for IntlLangMemoizer {
fn new(lang: LanguageIdentifier) -> Self
where
Self: Sized,
{
IntlLangMemoizer::new(lang)
}
fn with_try_get_threadsafe<I, R, U>(&self, args: I::Args, cb: U) -> Result<R, I::Error>
where
Self: Sized,
I: Memoizable + Send + Sync + 'static,
I::Args: Send + Sync + 'static,
U: FnOnce(&I) -> R,
{
self.with_try_get(args, cb)
}
fn stringify_value(&self, value: &dyn FluentType) -> std::borrow::Cow<'static, str> {
value.as_string_threadsafe(self)
}
}

View File

@ -1,65 +0,0 @@
//! `Entry` is used to store Messages, Terms and Functions in `FluentBundle` instances.
use std::borrow::Borrow;
use fluent_syntax::ast;
use crate::bundle::{FluentArgs, FluentBundleBase};
use crate::resource::FluentResource;
use crate::types::FluentValue;
pub type FluentFunction =
Box<dyn for<'a> Fn(&[FluentValue<'a>], &FluentArgs) -> FluentValue<'a> + Send + Sync>;
pub enum Entry {
Message([usize; 2]),
Term([usize; 2]),
Function(FluentFunction),
}
pub trait GetEntry {
fn get_entry_message(&self, id: &str) -> Option<&ast::Message>;
fn get_entry_term(&self, id: &str) -> Option<&ast::Term>;
fn get_entry_function(&self, id: &str) -> Option<&FluentFunction>;
}
impl<'bundle, R: Borrow<FluentResource>, M> GetEntry for FluentBundleBase<R, M> {
fn get_entry_message(&self, id: &str) -> Option<&ast::Message> {
self.entries.get(id).and_then(|entry| match *entry {
Entry::Message(pos) => {
let res = self.resources.get(pos[0])?.borrow();
if let Some(ast::ResourceEntry::Entry(ast::Entry::Message(ref msg))) =
res.ast().body.get(pos[1])
{
Some(msg)
} else {
None
}
}
_ => None,
})
}
fn get_entry_term(&self, id: &str) -> Option<&ast::Term> {
self.entries.get(id).and_then(|entry| match *entry {
Entry::Term(pos) => {
let res = self.resources.get(pos[0])?.borrow();
if let Some(ast::ResourceEntry::Entry(ast::Entry::Term(ref msg))) =
res.ast().body.get(pos[1])
{
Some(msg)
} else {
None
}
}
_ => None,
})
}
fn get_entry_function(&self, id: &str) -> Option<&FluentFunction> {
self.entries.get(id).and_then(|entry| match entry {
Entry::Function(function) => Some(function),
_ => None,
})
}
}

View File

@ -1,21 +0,0 @@
use crate::resolve::ResolverError;
use fluent_syntax::parser::ParserError;
#[derive(Debug, PartialEq)]
pub enum FluentError {
Overriding { kind: &'static str, id: String },
ParserError(ParserError),
ResolverError(ResolverError),
}
impl From<ResolverError> for FluentError {
fn from(error: ResolverError) -> Self {
FluentError::ResolverError(error)
}
}
impl From<ParserError> for FluentError {
fn from(error: ParserError) -> Self {
FluentError::ParserError(error)
}
}

View File

@ -1,125 +0,0 @@
//! Fluent is a modern localization system designed to improve how software is translated.
//!
//! The Rust implementation provides the low level components for syntax operations, like parser
//! and AST, and the core localization struct - [`FluentBundle`].
//!
//! [`FluentBundle`] is the low level container for storing and formatting localization messages
//! in a single locale.
//!
//! This crate provides also a number of structures needed for a localization API such as [`FluentResource`],
//! [`FluentMessage`], [`FluentArgs`], and [`FluentValue`].
//!
//! Together, they allow implementations to build higher-level APIs that use [`FluentBundle`]
//! and add user friendly helpers, framework bindings, error fallbacking,
//! language negotiation between user requested languages and available resources,
//! and I/O for loading selected resources.
//!
//! # Example
//!
//! ```
//! use fluent_bundle::{FluentBundle, FluentValue, FluentResource, FluentArgs};
//!
//! // Used to provide a locale for the bundle.
//! use unic_langid::langid;
//!
//! let ftl_string = String::from("
//! hello-world = Hello, world!
//! intro = Welcome, { $name }.
//! ");
//! let res = FluentResource::try_new(ftl_string)
//! .expect("Failed to parse an FTL string.");
//!
//! let langid_en = langid!("en-US");
//! let mut bundle = FluentBundle::new(&[langid_en]);
//!
//! bundle
//! .add_resource(res)
//! .expect("Failed to add FTL resources to the bundle.");
//!
//! let msg = bundle.get_message("hello-world")
//! .expect("Message doesn't exist.");
//! let mut errors = vec![];
//! let pattern = msg.value
//! .expect("Message has no value.");
//! let value = bundle.format_pattern(&pattern, None, &mut errors);
//!
//! assert_eq!(&value, "Hello, world!");
//!
//! let mut args = FluentArgs::new();
//! args.insert("name", FluentValue::from("John"));
//!
//! let msg = bundle.get_message("intro")
//! .expect("Message doesn't exist.");
//! let mut errors = vec![];
//! let pattern = msg.value.expect("Message has no value.");
//! let value = bundle.format_pattern(&pattern, Some(&args), &mut errors);
//!
//! // The FSI/PDI isolation marks ensure that the direction of
//! // the text from the variable is not affected by the translation.
//! assert_eq!(value, "Welcome, \u{2068}John\u{2069}.");
//! ```
//!
//! # Ergonomics & Higher Level APIs
//!
//! Reading the example, you may notice how verbose it feels.
//! Many core methods are fallible, others accumulate errors, and there
//! are intermediate structures used in operations.
//!
//! This is intentional as it serves as building blocks for variety of different
//! scenarios allowing implementations to handle errors, cache and
//! optimize results.
//!
//! At the moment it is expected that users will use
//! the `fluent-bundle` crate directly, while the ecosystem
//! matures and higher level APIs are being developed.
//!
//! [`FluentBundle`]: ./bundle/struct.FluentBundle.html
//! [`FluentResource`]: ./bundle/struct.FluentResource.html
//! [`FluentMessage`]: ./bundle/struct.FluentMessage.html
//! [`FluentArgs`]: ./bundle/type.FluentArgs.html
//! [`FluentValue`]: ./bundle/struct.FluentValue.html
#[macro_use]
extern crate rental;
use intl_memoizer::{IntlLangMemoizer, Memoizable};
use unic_langid::LanguageIdentifier;
mod bundle;
pub mod concurrent;
mod entry;
mod errors;
pub mod memoizer;
pub mod resolve;
mod resource;
pub mod types;
pub use bundle::{FluentArgs, FluentMessage};
pub use errors::FluentError;
pub use resource::FluentResource;
pub use types::FluentValue;
pub type FluentBundle<R> = bundle::FluentBundleBase<R, IntlLangMemoizer>;
impl memoizer::MemoizerKind for IntlLangMemoizer {
fn new(lang: LanguageIdentifier) -> Self
where
Self: Sized,
{
IntlLangMemoizer::new(lang)
}
fn with_try_get_threadsafe<I, R, U>(&self, args: I::Args, cb: U) -> Result<R, I::Error>
where
Self: Sized,
I: Memoizable + Send + Sync + 'static,
I::Args: Send + Sync + 'static,
U: FnOnce(&I) -> R,
{
self.with_try_get(args, cb)
}
fn stringify_value(&self, value: &dyn types::FluentType) -> std::borrow::Cow<'static, str> {
value.as_string(self)
}
}

View File

@ -1,18 +0,0 @@
use crate::types::FluentType;
use intl_memoizer::Memoizable;
use unic_langid::LanguageIdentifier;
pub trait MemoizerKind: 'static {
fn new(lang: LanguageIdentifier) -> Self
where
Self: Sized;
fn with_try_get_threadsafe<I, R, U>(&self, args: I::Args, cb: U) -> Result<R, I::Error>
where
Self: Sized,
I: Memoizable + Send + Sync + 'static,
I::Args: Send + Sync + 'static,
U: FnOnce(&I) -> R;
fn stringify_value(&self, value: &dyn FluentType) -> std::borrow::Cow<'static, str>;
}

View File

@ -1,350 +0,0 @@
//! The `ResolveValue` trait resolves Fluent AST nodes to [`FluentValues`].
//!
//! This is an internal API used by [`FluentBundle`] to evaluate Messages, Attributes and other
//! AST nodes to [`FluentValues`] which can be then formatted to strings.
//!
//! [`FluentValues`]: ../types/enum.FluentValue.html
//! [`FluentBundle`]: ../bundle/struct.FluentBundle.html
use std::borrow::Borrow;
use std::fmt::Write;
use fluent_syntax::ast;
use fluent_syntax::unicode::unescape_unicode;
use crate::bundle::{FluentArgs, FluentBundleBase};
use crate::entry::GetEntry;
use crate::memoizer::MemoizerKind;
use crate::resource::FluentResource;
use crate::types::DisplayableNode;
use crate::types::FluentValue;
const MAX_PLACEABLES: u8 = 100;
#[derive(Debug, PartialEq, Clone)]
pub enum ResolverError {
Reference(String),
MissingDefault,
Cyclic,
TooManyPlaceables,
}
/// State for a single `ResolveValue::to_value` call.
pub struct Scope<'bundle, R: Borrow<FluentResource>, M> {
/// The current `FluentBundleBase` instance.
pub bundle: &'bundle FluentBundleBase<R, M>,
/// The current arguments passed by the developer.
args: Option<&'bundle FluentArgs<'bundle>>,
/// Local args
local_args: Option<FluentArgs<'bundle>>,
/// The running count of resolved placeables. Used to detect the Billion
/// Laughs and Quadratic Blowup attacks.
placeables: u8,
/// Tracks hashes to prevent infinite recursion.
travelled: smallvec::SmallVec<[&'bundle ast::Pattern<'bundle>; 2]>,
/// Track errors accumulated during resolving.
pub errors: Vec<ResolverError>,
/// Makes the resolver bail.
pub dirty: bool,
}
impl<'bundle, R: Borrow<FluentResource>, M: MemoizerKind> Scope<'bundle, R, M> {
pub fn new(bundle: &'bundle FluentBundleBase<R, M>, args: Option<&'bundle FluentArgs>) -> Self {
Scope {
bundle,
args,
local_args: None,
placeables: 0,
travelled: Default::default(),
errors: vec![],
dirty: false,
}
}
// This method allows us to lazily add Pattern on the stack,
// only if the Pattern::resolve has been called on an empty stack.
//
// This is the case when pattern is called from Bundle and it
// allows us to fast-path simple resolutions, and only use the stack
// for placeables.
pub fn maybe_track(
&mut self,
pattern: &'bundle ast::Pattern,
placeable: &'bundle ast::Expression,
) -> FluentValue<'bundle> {
if self.travelled.is_empty() {
self.travelled.push(pattern);
}
let result = placeable.resolve(self);
if self.dirty {
return FluentValue::Error(placeable.into());
}
result
}
pub fn track(
&mut self,
pattern: &'bundle ast::Pattern,
entry: DisplayableNode<'bundle>,
) -> FluentValue<'bundle> {
if self.travelled.contains(&pattern) {
self.errors.push(ResolverError::Cyclic);
FluentValue::Error(entry)
} else {
self.travelled.push(pattern);
let result = pattern.resolve(self);
self.travelled.pop();
result
}
}
}
fn generate_ref_error<'source, R, M>(
scope: &mut Scope<'source, R, M>,
node: DisplayableNode<'source>,
) -> FluentValue<'source>
where
R: Borrow<FluentResource>,
{
scope
.errors
.push(ResolverError::Reference(node.get_error()));
FluentValue::Error(node)
}
// Converts an AST node to a `FluentValue`.
pub(crate) trait ResolveValue<'source> {
fn resolve<R, M: MemoizerKind>(
&'source self,
scope: &mut Scope<'source, R, M>,
) -> FluentValue<'source>
where
R: Borrow<FluentResource>;
}
impl<'source> ResolveValue<'source> for ast::Pattern<'source> {
fn resolve<R, M: MemoizerKind>(
&'source self,
scope: &mut Scope<'source, R, M>,
) -> FluentValue<'source>
where
R: Borrow<FluentResource>,
{
if scope.dirty {
return FluentValue::None;
}
if self.elements.len() == 1 {
return match self.elements[0] {
ast::PatternElement::TextElement(s) => {
if let Some(ref transform) = scope.bundle.transform {
transform(s).into()
} else {
s.into()
}
}
ast::PatternElement::Placeable(ref p) => scope.maybe_track(self, p),
};
}
let mut string = String::new();
for elem in &self.elements {
if scope.dirty {
return FluentValue::None;
}
match elem {
ast::PatternElement::TextElement(s) => {
if let Some(ref transform) = scope.bundle.transform {
string.push_str(&transform(s))
} else {
string.push_str(&s)
}
}
ast::PatternElement::Placeable(p) => {
scope.placeables += 1;
if scope.placeables > MAX_PLACEABLES {
scope.dirty = true;
scope.errors.push(ResolverError::TooManyPlaceables);
return FluentValue::None;
}
let needs_isolation = scope.bundle.use_isolating
&& match p {
ast::Expression::InlineExpression(
ast::InlineExpression::MessageReference { .. },
)
| ast::Expression::InlineExpression(
ast::InlineExpression::TermReference { .. },
)
| ast::Expression::InlineExpression(
ast::InlineExpression::StringLiteral { .. },
) => false,
_ => true,
};
if needs_isolation {
string.write_char('\u{2068}').expect("Writing failed");
}
let result = scope.maybe_track(self, p);
write!(string, "{}", result.as_string(scope)).expect("Writing failed");
if needs_isolation {
string.write_char('\u{2069}').expect("Writing failed");
}
}
}
}
string.into()
}
}
impl<'source> ResolveValue<'source> for ast::Expression<'source> {
fn resolve<R, M: MemoizerKind>(
&'source self,
scope: &mut Scope<'source, R, M>,
) -> FluentValue<'source>
where
R: Borrow<FluentResource>,
{
match self {
ast::Expression::InlineExpression(exp) => exp.resolve(scope),
ast::Expression::SelectExpression { selector, variants } => {
let selector = selector.resolve(scope);
match selector {
FluentValue::String(_) | FluentValue::Number(_) => {
for variant in variants {
let key = match variant.key {
ast::VariantKey::Identifier { name } => name.into(),
ast::VariantKey::NumberLiteral { value } => {
FluentValue::try_number(value)
}
};
if key.matches(&selector, &scope) {
return variant.value.resolve(scope);
}
}
}
_ => {}
}
for variant in variants {
if variant.default {
return variant.value.resolve(scope);
}
}
scope.errors.push(ResolverError::MissingDefault);
FluentValue::None
}
}
}
}
impl<'source> ResolveValue<'source> for ast::InlineExpression<'source> {
fn resolve<R, M: MemoizerKind>(
&'source self,
mut scope: &mut Scope<'source, R, M>,
) -> FluentValue<'source>
where
R: Borrow<FluentResource>,
{
match self {
ast::InlineExpression::StringLiteral { value } => unescape_unicode(value).into(),
ast::InlineExpression::MessageReference { id, attribute } => scope
.bundle
.get_entry_message(&id.name)
.and_then(|msg| {
if let Some(attr) = attribute {
msg.attributes
.iter()
.find(|a| a.id.name == attr.name)
.map(|attr| scope.track(&attr.value, self.into()))
} else {
msg.value
.as_ref()
.map(|value| scope.track(value, self.into()))
}
})
.unwrap_or_else(|| generate_ref_error(scope, self.into())),
ast::InlineExpression::NumberLiteral { value } => FluentValue::try_number(*value),
ast::InlineExpression::TermReference {
id,
attribute,
arguments,
} => {
let (_, resolved_named_args) = get_arguments(scope, arguments);
scope.local_args = Some(resolved_named_args);
let value = scope
.bundle
.get_entry_term(&id.name)
.and_then(|term| {
if let Some(attr) = attribute {
term.attributes
.iter()
.find(|a| a.id.name == attr.name)
.map(|attr| scope.track(&attr.value, self.into()))
} else {
Some(scope.track(&term.value, self.into()))
}
})
.unwrap_or_else(|| generate_ref_error(scope, self.into()));
scope.local_args = None;
value
}
ast::InlineExpression::FunctionReference { id, arguments } => {
let (resolved_positional_args, resolved_named_args) =
get_arguments(scope, arguments);
let func = scope.bundle.get_entry_function(id.name);
if let Some(func) = func {
func(resolved_positional_args.as_slice(), &resolved_named_args)
} else {
generate_ref_error(scope, self.into())
}
}
ast::InlineExpression::VariableReference { id } => {
let args = scope.local_args.as_ref().or(scope.args);
if let Some(arg) = args.and_then(|args| args.get(id.name)) {
arg.clone()
} else {
let entry: DisplayableNode = self.into();
if scope.local_args.is_none() {
scope
.errors
.push(ResolverError::Reference(entry.get_error()));
}
FluentValue::Error(entry)
}
}
ast::InlineExpression::Placeable { expression } => expression.resolve(scope),
}
}
}
fn get_arguments<'bundle, R, M: MemoizerKind>(
scope: &mut Scope<'bundle, R, M>,
arguments: &'bundle Option<ast::CallArguments<'bundle>>,
) -> (Vec<FluentValue<'bundle>>, FluentArgs<'bundle>)
where
R: Borrow<FluentResource>,
{
let mut resolved_positional_args = Vec::new();
let mut resolved_named_args = FluentArgs::new();
if let Some(ast::CallArguments { named, positional }) = arguments {
for expression in positional {
resolved_positional_args.push(expression.resolve(scope));
}
for arg in named {
resolved_named_args.insert(arg.name.name, arg.value.resolve(scope));
}
}
(resolved_positional_args, resolved_named_args)
}

View File

@ -1,41 +0,0 @@
use fluent_syntax::ast;
use fluent_syntax::parser::parse;
use fluent_syntax::parser::ParserError;
rental! {
mod rentals {
use super::*;
#[rental(covariant, debug)]
pub struct FluentResource {
string: String,
ast: ast::Resource<'string>,
}
}
}
/// A resource containing a list of localization messages.
#[derive(Debug)]
pub struct FluentResource(rentals::FluentResource);
impl FluentResource {
pub fn try_new(source: String) -> Result<Self, (Self, Vec<ParserError>)> {
let mut errors = None;
let res = rentals::FluentResource::new(source, |s| match parse(s) {
Ok(ast) => ast,
Err((ast, err)) => {
errors = Some(err);
ast
}
});
if let Some(errors) = errors {
Err((Self(res), errors))
} else {
Ok(Self(res))
}
}
pub fn ast(&self) -> &ast::Resource {
self.0.all().ast
}
}

View File

@ -1,264 +0,0 @@
mod number;
mod plural;
pub use number::*;
use plural::*;
use std::any::Any;
use std::borrow::{Borrow, Cow};
use std::default::Default;
use std::fmt;
use std::str::FromStr;
use fluent_syntax::ast;
use intl_pluralrules::{PluralCategory, PluralRuleType};
use crate::memoizer::MemoizerKind;
use crate::resolve::Scope;
use crate::resource::FluentResource;
#[derive(Debug, PartialEq, Clone)]
pub enum DisplayableNodeType<'source> {
Message(&'source str),
Term(&'source str),
Variable(&'source str),
Function(&'source str),
Expression,
}
#[derive(Debug, PartialEq, Clone)]
pub struct DisplayableNode<'source> {
node_type: DisplayableNodeType<'source>,
attribute: Option<&'source str>,
}
impl<'source> Default for DisplayableNode<'source> {
fn default() -> Self {
DisplayableNode {
node_type: DisplayableNodeType::Expression,
attribute: None,
}
}
}
impl<'source> DisplayableNode<'source> {
pub fn get_error(&self) -> String {
if self.attribute.is_some() {
format!("Unknown attribute: {}", self)
} else {
match self.node_type {
DisplayableNodeType::Message(..) => format!("Unknown message: {}", self),
DisplayableNodeType::Term(..) => format!("Unknown term: {}", self),
DisplayableNodeType::Variable(..) => format!("Unknown variable: {}", self),
DisplayableNodeType::Function(..) => format!("Unknown function: {}", self),
DisplayableNodeType::Expression => "Failed to resolve an expression.".to_string(),
}
}
}
}
impl<'source> fmt::Display for DisplayableNode<'source> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.node_type {
DisplayableNodeType::Message(id) => write!(f, "{}", id)?,
DisplayableNodeType::Term(id) => write!(f, "-{}", id)?,
DisplayableNodeType::Variable(id) => write!(f, "${}", id)?,
DisplayableNodeType::Function(id) => write!(f, "{}()", id)?,
DisplayableNodeType::Expression => f.write_str("???")?,
};
if let Some(attr) = self.attribute {
write!(f, ".{}", attr)?;
}
Ok(())
}
}
impl<'source> From<&ast::Expression<'source>> for DisplayableNode<'source> {
fn from(expr: &ast::Expression<'source>) -> Self {
match expr {
ast::Expression::InlineExpression(e) => e.into(),
ast::Expression::SelectExpression { .. } => DisplayableNode::default(),
}
}
}
impl<'source> From<&ast::InlineExpression<'source>> for DisplayableNode<'source> {
fn from(expr: &ast::InlineExpression<'source>) -> Self {
match expr {
ast::InlineExpression::MessageReference { id, attribute } => DisplayableNode {
node_type: DisplayableNodeType::Message(id.name),
attribute: attribute.as_ref().map(|attr| attr.name),
},
ast::InlineExpression::TermReference { id, attribute, .. } => DisplayableNode {
node_type: DisplayableNodeType::Term(id.name),
attribute: attribute.as_ref().map(|attr| attr.name),
},
ast::InlineExpression::VariableReference { id } => DisplayableNode {
node_type: DisplayableNodeType::Variable(id.name),
attribute: None,
},
ast::InlineExpression::FunctionReference { id, .. } => DisplayableNode {
node_type: DisplayableNodeType::Function(id.name),
attribute: None,
},
_ => DisplayableNode::default(),
}
}
}
pub trait FluentType: fmt::Debug + AnyEq + 'static {
fn duplicate(&self) -> Box<dyn FluentType>;
fn as_string(&self, intls: &intl_memoizer::IntlLangMemoizer) -> Cow<'static, str>;
fn as_string_threadsafe(
&self,
intls: &intl_memoizer::concurrent::IntlLangMemoizer,
) -> Cow<'static, str>;
}
impl PartialEq for dyn FluentType {
fn eq(&self, other: &Self) -> bool {
self.equals(other.as_any())
}
}
pub trait AnyEq: Any + 'static {
fn equals(&self, other: &dyn Any) -> bool;
fn as_any(&self) -> &dyn Any;
}
impl<T: Any + PartialEq> AnyEq for T {
fn equals(&self, other: &dyn Any) -> bool {
if let Some(that) = other.downcast_ref::<Self>() {
self == that
} else {
false
}
}
fn as_any(&self) -> &dyn Any {
self
}
}
/// The `FluentValue` enum represents values which can be formatted to a String.
///
/// The [`ResolveValue`][] trait from the [`resolve`][] module evaluates AST nodes into
/// `FluentValues` which can then be formatted to Strings using the i18n formatters stored by the
/// `FluentBundle` instance if required.
///
/// The arguments `HashMap` passed to [`FluentBundle::format`][] should also use `FluentValues`
/// as values of arguments.
///
/// [`ResolveValue`]: ../resolve/trait.ResolveValue.html
/// [`resolve`]: ../resolve
/// [`FluentBundle::format`]: ../bundle/struct.FluentBundle.html#method.format
#[derive(Debug)]
pub enum FluentValue<'source> {
String(Cow<'source, str>),
Number(FluentNumber),
Custom(Box<dyn FluentType>),
Error(DisplayableNode<'source>),
None,
}
impl<'s> PartialEq for FluentValue<'s> {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(FluentValue::String(s), FluentValue::String(s2)) => s == s2,
(FluentValue::Number(s), FluentValue::Number(s2)) => s == s2,
(FluentValue::Custom(s), FluentValue::Custom(s2)) => s == s2,
_ => false,
}
}
}
impl<'s> Clone for FluentValue<'s> {
fn clone(&self) -> Self {
match self {
FluentValue::String(s) => FluentValue::String(s.clone()),
FluentValue::Number(s) => FluentValue::Number(s.clone()),
FluentValue::Custom(s) => {
let new_value: Box<dyn FluentType> = s.duplicate();
FluentValue::Custom(new_value)
}
FluentValue::Error(e) => FluentValue::Error(e.clone()),
FluentValue::None => FluentValue::None,
}
}
}
impl<'source> FluentValue<'source> {
pub fn try_number<S: ToString>(v: S) -> Self {
let s = v.to_string();
if let Ok(num) = FluentNumber::from_str(&s.to_string()) {
num.into()
} else {
s.into()
}
}
pub fn matches<R: Borrow<FluentResource>, M: MemoizerKind>(
&self,
other: &FluentValue,
scope: &Scope<R, M>,
) -> bool {
match (self, other) {
(&FluentValue::String(ref a), &FluentValue::String(ref b)) => a == b,
(&FluentValue::Number(ref a), &FluentValue::Number(ref b)) => a == b,
(&FluentValue::String(ref a), &FluentValue::Number(ref b)) => {
let cat = match a.as_ref() {
"zero" => PluralCategory::ZERO,
"one" => PluralCategory::ONE,
"two" => PluralCategory::TWO,
"few" => PluralCategory::FEW,
"many" => PluralCategory::MANY,
"other" => PluralCategory::OTHER,
_ => return false,
};
scope
.bundle
.intls
.with_try_get_threadsafe::<PluralRules, _, _>(
(PluralRuleType::CARDINAL,),
|pr| pr.0.select(b) == Ok(cat),
)
.unwrap()
}
_ => false,
}
}
pub fn as_string<R: Borrow<FluentResource>, M: MemoizerKind>(
&self,
scope: &Scope<R, M>,
) -> Cow<'source, str> {
if let Some(formatter) = &scope.bundle.formatter {
if let Some(val) = formatter(self, &scope.bundle.intls) {
return val.into();
}
}
match self {
FluentValue::String(s) => s.clone(),
FluentValue::Number(n) => n.as_string(),
FluentValue::Error(d) => format!("{{{}}}", d.to_string()).into(),
FluentValue::Custom(s) => scope.bundle.intls.stringify_value(&**s),
FluentValue::None => "???".into(),
}
}
}
impl<'source> From<String> for FluentValue<'source> {
fn from(s: String) -> Self {
FluentValue::String(s.into())
}
}
impl<'source> From<&'source str> for FluentValue<'source> {
fn from(s: &'source str) -> Self {
FluentValue::String(s.into())
}
}
impl<'source> From<Cow<'source, str>> for FluentValue<'source> {
fn from(s: Cow<'source, str>) -> Self {
FluentValue::String(s)
}
}

View File

@ -1,249 +0,0 @@
use std::borrow::Cow;
use std::convert::TryInto;
use std::default::Default;
use std::str::FromStr;
use intl_pluralrules::operands::PluralOperands;
use crate::bundle::FluentArgs;
use crate::types::FluentValue;
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
pub enum FluentNumberStyle {
Decimal,
Currency,
Percent,
}
impl std::default::Default for FluentNumberStyle {
fn default() -> Self {
Self::Decimal
}
}
impl From<&str> for FluentNumberStyle {
fn from(input: &str) -> Self {
match input {
"decimal" => Self::Decimal,
"currency" => Self::Currency,
"percent" => Self::Percent,
_ => Self::default(),
}
}
}
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
pub enum FluentNumberCurrencyDisplayStyle {
Symbol,
Code,
Name,
}
impl std::default::Default for FluentNumberCurrencyDisplayStyle {
fn default() -> Self {
Self::Symbol
}
}
impl From<&str> for FluentNumberCurrencyDisplayStyle {
fn from(input: &str) -> Self {
match input {
"symbol" => Self::Symbol,
"code" => Self::Code,
"name" => Self::Name,
_ => Self::default(),
}
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct FluentNumberOptions {
pub style: FluentNumberStyle,
pub currency: Option<String>,
pub currency_display: FluentNumberCurrencyDisplayStyle,
pub use_grouping: bool,
pub minimum_integer_digits: Option<usize>,
pub minimum_fraction_digits: Option<usize>,
pub maximum_fraction_digits: Option<usize>,
pub minimum_significant_digits: Option<usize>,
pub maximum_significant_digits: Option<usize>,
}
impl Default for FluentNumberOptions {
fn default() -> Self {
Self {
style: Default::default(),
currency: None,
currency_display: Default::default(),
use_grouping: true,
minimum_integer_digits: None,
minimum_fraction_digits: None,
maximum_fraction_digits: None,
minimum_significant_digits: None,
maximum_significant_digits: None,
}
}
}
impl FluentNumberOptions {
pub fn merge(&mut self, opts: &FluentArgs) {
for (key, value) in opts {
match (*key, value) {
("style", FluentValue::String(n)) => {
self.style = n.as_ref().into();
}
("currency", FluentValue::String(n)) => {
self.currency = Some(n.to_string());
}
("currencyDisplay", FluentValue::String(n)) => {
self.currency_display = n.as_ref().into();
}
("minimumIntegerDigits", FluentValue::Number(n)) => {
self.minimum_integer_digits = Some(n.into());
}
("minimumFractionDigits", FluentValue::Number(n)) => {
self.minimum_fraction_digits = Some(n.into());
}
("maximumFractionDigits", FluentValue::Number(n)) => {
self.maximum_fraction_digits = Some(n.into());
}
("minimumSignificantDigits", FluentValue::Number(n)) => {
self.minimum_significant_digits = Some(n.into());
}
("maximumSignificantDigits", FluentValue::Number(n)) => {
self.maximum_significant_digits = Some(n.into());
}
_ => {}
}
}
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct FluentNumber {
pub value: f64,
pub options: FluentNumberOptions,
}
impl FluentNumber {
pub fn new(value: f64, options: FluentNumberOptions) -> Self {
Self { value, options }
}
pub fn as_string(&self) -> Cow<'static, str> {
let mut val = self.value.to_string();
if let Some(minfd) = self.options.minimum_fraction_digits {
if let Some(pos) = val.find('.') {
let frac_num = val.len() - pos - 1;
let missing = if frac_num > minfd {
0
} else {
minfd - frac_num
};
val = format!("{}{}", val, "0".repeat(missing));
} else {
val = format!("{}.{}", val, "0".repeat(minfd));
}
}
val.into()
}
}
impl FromStr for FluentNumber {
type Err = std::num::ParseFloatError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
f64::from_str(input).map(|n| {
let mfd = input.find('.').map(|pos| input.len() - pos - 1);
let opts = FluentNumberOptions {
minimum_fraction_digits: mfd,
..Default::default()
};
FluentNumber::new(n, opts)
})
}
}
impl<'l> From<FluentNumber> for FluentValue<'l> {
fn from(input: FluentNumber) -> Self {
FluentValue::Number(input)
}
}
macro_rules! from_num {
($num:ty) => {
impl From<$num> for FluentNumber {
fn from(n: $num) -> Self {
FluentNumber {
value: n as f64,
options: FluentNumberOptions::default(),
}
}
}
impl From<&$num> for FluentNumber {
fn from(n: &$num) -> Self {
FluentNumber {
value: *n as f64,
options: FluentNumberOptions::default(),
}
}
}
impl From<FluentNumber> for $num {
fn from(input: FluentNumber) -> Self {
input.value as $num
}
}
impl From<&FluentNumber> for $num {
fn from(input: &FluentNumber) -> Self {
input.value as $num
}
}
impl From<$num> for FluentValue<'_> {
fn from(n: $num) -> Self {
FluentValue::Number(n.into())
}
}
impl From<&$num> for FluentValue<'_> {
fn from(n: &$num) -> Self {
FluentValue::Number(n.into())
}
}
};
($($num:ty)+) => {
$(from_num!($num);)+
};
}
impl From<&FluentNumber> for PluralOperands {
fn from(input: &FluentNumber) -> Self {
let mut operands: PluralOperands = input
.value
.try_into()
.expect("Failed to generate operands out of FluentNumber");
if let Some(mfd) = input.options.minimum_fraction_digits {
if mfd > operands.v {
operands.f *= 10_usize.pow(mfd as u32 - operands.v as u32);
operands.v = mfd;
}
}
// XXX: Add support for other options.
operands
}
}
from_num!(i8 i16 i32 i64 i128 isize);
from_num!(u8 u16 u32 u64 u128 usize);
from_num!(f32 f64);
#[cfg(test)]
mod tests {
use crate::types::FluentValue;
#[test]
fn value_from_copy_ref() {
let x = 1i16;
let y = &x;
let z: FluentValue = y.into();
assert_eq!(z, FluentValue::try_number(1));
}
}

View File

@ -1,22 +0,0 @@
use fluent_langneg::{negotiate_languages, NegotiationStrategy};
use intl_memoizer::Memoizable;
use intl_pluralrules::{PluralRuleType, PluralRules as IntlPluralRules};
use unic_langid::LanguageIdentifier;
pub struct PluralRules(pub IntlPluralRules);
impl Memoizable for PluralRules {
type Args = (PluralRuleType,);
type Error = &'static str;
fn construct(lang: LanguageIdentifier, args: Self::Args) -> Result<Self, Self::Error> {
let default_lang: LanguageIdentifier = "en".parse().unwrap();
let pr_lang = negotiate_languages(
&[lang],
&IntlPluralRules::get_locales(args.0),
Some(&default_lang),
NegotiationStrategy::Lookup,
)[0]
.clone();
Ok(Self(IntlPluralRules::create(pr_lang, args.0)?))
}
}

View File

@ -1,37 +0,0 @@
use fluent_bundle::{FluentBundle, FluentResource};
use unic_langid::LanguageIdentifier;
#[test]
fn add_resource_override() {
let res = FluentResource::try_new("key = Value".to_string()).unwrap();
let res2 = FluentResource::try_new("key = Value 2".to_string()).unwrap();
let en_us: LanguageIdentifier = "en-US"
.parse()
.expect("Failed to parse a language identifier");
let mut bundle = FluentBundle::<&FluentResource>::new(&[en_us]);
bundle.add_resource(&res).expect("Failed to add a resource");
assert!(bundle.add_resource(&res2).is_err());
let mut errors = vec![];
let value = bundle
.get_message("key")
.expect("Failed to retireve a message")
.value
.expect("Failed to retireve a value of a message");
assert_eq!(bundle.format_pattern(value, None, &mut errors), "Value");
bundle.add_resource_overriding(&res2);
let value = bundle
.get_message("key")
.expect("Failed to retireve a message")
.value
.expect("Failed to retireve a value of a message");
assert_eq!(bundle.format_pattern(value, None, &mut errors), "Value 2");
assert!(errors.is_empty());
}

View File

@ -1,230 +0,0 @@
use fluent_bundle::memoizer::MemoizerKind;
use fluent_bundle::types::FluentType;
use fluent_bundle::FluentArgs;
use fluent_bundle::FluentBundle;
use fluent_bundle::FluentResource;
use fluent_bundle::FluentValue;
use unic_langid::langid;
#[test]
fn fluent_custom_type() {
#[derive(Debug, PartialEq)]
struct DateTime {
epoch: usize,
};
impl DateTime {
pub fn new(epoch: usize) -> Self {
Self { epoch }
}
}
impl FluentType for DateTime {
fn duplicate(&self) -> Box<dyn FluentType> {
Box::new(DateTime { epoch: self.epoch })
}
fn as_string(&self, _: &intl_memoizer::IntlLangMemoizer) -> std::borrow::Cow<'static, str> {
format!("{}", self.epoch).into()
}
fn as_string_threadsafe(
&self,
_: &intl_memoizer::concurrent::IntlLangMemoizer,
) -> std::borrow::Cow<'static, str> {
format!("{}", self.epoch).into()
}
}
let dt = FluentValue::Custom(Box::new(DateTime::new(10)));
let dt2 = FluentValue::Custom(Box::new(DateTime::new(10)));
let dt3 = FluentValue::Custom(Box::new(DateTime::new(15)));
let sv = FluentValue::from("foo");
assert_eq!(dt == dt2, true);
assert_eq!(dt == dt3, false);
assert_eq!(dt == sv, false);
}
#[test]
fn fluent_date_time_builtin() {
#[derive(Debug, PartialEq, Clone)]
enum DateTimeStyleValue {
Full,
Long,
Medium,
Short,
None,
}
impl std::default::Default for DateTimeStyleValue {
fn default() -> Self {
Self::None
}
}
impl<'l> From<&FluentValue<'l>> for DateTimeStyleValue {
fn from(input: &FluentValue) -> Self {
if let FluentValue::String(s) = input {
match s.as_ref() {
"full" => Self::Full,
"long" => Self::Long,
"medium" => Self::Medium,
"short" => Self::Short,
_ => Self::None,
}
} else {
Self::None
}
}
}
#[derive(Debug, PartialEq, Default, Clone)]
struct DateTimeOptions {
pub date_style: DateTimeStyleValue,
pub time_style: DateTimeStyleValue,
}
impl DateTimeOptions {
pub fn merge(&mut self, input: &FluentArgs) {
for (key, value) in input {
match *key {
"dateStyle" => self.date_style = value.into(),
"timeStyle" => self.time_style = value.into(),
_ => {}
}
}
}
}
impl<'l> From<&FluentArgs<'l>> for DateTimeOptions {
fn from(input: &FluentArgs) -> Self {
let mut opts = Self::default();
opts.merge(input);
opts
}
}
#[derive(Debug, PartialEq, Clone)]
struct DateTime {
epoch: usize,
options: DateTimeOptions,
};
impl DateTime {
pub fn new(epoch: usize, options: DateTimeOptions) -> Self {
Self { epoch, options }
}
}
impl FluentType for DateTime {
fn duplicate(&self) -> Box<dyn FluentType> {
Box::new(DateTime::new(self.epoch, DateTimeOptions::default()))
}
fn as_string(&self, _: &intl_memoizer::IntlLangMemoizer) -> std::borrow::Cow<'static, str> {
format!("2020-01-20 {}:00", self.epoch).into()
}
fn as_string_threadsafe(
&self,
_intls: &intl_memoizer::concurrent::IntlLangMemoizer,
) -> std::borrow::Cow<'static, str> {
format!("2020-01-20 {}:00", self.epoch).into()
}
}
let lang = langid!("en");
let mut bundle = FluentBundle::new(&[lang]);
let res = FluentResource::try_new(
r#"
key-explicit = Hello { DATETIME(12, dateStyle: "full") } World
key-ref = Hello { DATETIME($date, dateStyle: "full") } World
"#
.into(),
)
.unwrap();
bundle.add_resource(res).unwrap();
bundle.set_use_isolating(false);
bundle
.add_function("DATETIME", |positional, named| match positional.get(0) {
Some(FluentValue::Custom(custom)) => {
if let Some(that) = custom.as_ref().as_any().downcast_ref::<DateTime>() {
let mut dt = that.clone();
dt.options.merge(named);
FluentValue::Custom(Box::new(dt))
} else {
FluentValue::None
}
}
Some(FluentValue::Number(num)) => {
let num = num.value as usize;
FluentValue::Custom(Box::new(DateTime::new(num, named.into())))
}
_ => FluentValue::None,
})
.unwrap();
let mut errors = vec![];
let mut args = FluentArgs::new();
args.insert(
"date",
FluentValue::Custom(Box::new(DateTime::new(10, DateTimeOptions::default()))),
);
let msg = bundle.get_message("key-explicit").unwrap();
let val = bundle.format_pattern(msg.value.unwrap(), Some(&args), &mut errors);
assert_eq!(val, "Hello 2020-01-20 12:00 World");
let msg = bundle.get_message("key-ref").unwrap();
let val = bundle.format_pattern(msg.value.unwrap(), Some(&args), &mut errors);
assert_eq!(val, "Hello 2020-01-20 10:00 World");
}
#[test]
fn fluent_custom_number_format() {
fn custom_formatter<M: MemoizerKind>(num: &FluentValue, _intls: &M) -> Option<String> {
match num {
FluentValue::Number(_) => Some("CUSTOM".into()),
_ => None,
}
}
let res = FluentResource::try_new(
r#"
key-num-implicit = Hello { 5.000 } World
key-num-explicit = Hello { NUMBER(5, minimumFractionDigits: 2) } World
"#
.into(),
)
.unwrap();
let mut bundle = FluentBundle::default();
bundle.add_resource(res).unwrap();
bundle.set_use_isolating(false);
bundle
.add_function("NUMBER", |positional, named| match positional.get(0) {
Some(FluentValue::Number(n)) => {
let mut num = n.clone();
num.options.merge(named);
FluentValue::Number(num)
}
_ => FluentValue::None,
})
.unwrap();
let mut errors = vec![];
let msg = bundle.get_message("key-num-explicit").unwrap();
let val = bundle.format_pattern(msg.value.unwrap(), None, &mut errors);
assert_eq!(val, "Hello 5.00 World");
let msg = bundle.get_message("key-num-implicit").unwrap();
let val = bundle.format_pattern(msg.value.unwrap(), None, &mut errors);
assert_eq!(val, "Hello 5.000 World");
bundle.set_formatter(Some(custom_formatter));
let msg = bundle.get_message("key-num-implicit").unwrap();
let val = bundle.format_pattern(msg.value.unwrap(), None, &mut errors);
assert_eq!(val, "Hello CUSTOM World");
}

View File

@ -1,133 +0,0 @@
suites:
-
name: Variables
suites:
-
name: in values
resources:
-
source: |-
foo = Foo { $num }
bar = { foo }
baz =
.attr = Baz Attribute { $num }
qux = { "a" ->
*[a] Baz Variant A { $num }
}
tests:
-
name: can be used in the message value
asserts:
-
id: foo
args:
num: 3
value: Foo 3
-
name: can be used in the message value which is referenced
asserts:
-
id: bar
args:
num: 3
value: Foo 3
-
name: can be used in an attribute
asserts:
-
id: baz
attribute: attr
args:
num: 3
value: Baz Attribute 3
-
name: can be used in a variant
asserts:
-
id: qux
args:
num: 3
value: Baz Variant A 3
-
name: in selectors
resources:
-
source: |-
foo = { $num ->
*[3] Foo
}
tests:
-
name: can be used as a selector
asserts:
-
id: foo
args:
num: 3
value: Foo
-
name: in function calls
resources:
-
source: |-
foo = { NUMBER($num) }
bundles:
-
functions:
- NUMBER
tests:
-
name: can be a positional argument
asserts:
-
id: foo
args:
num: 3
value: 3
-
name: simple errors
resources:
-
source: |-
foo = { $arg }
tests:
-
name: falls back to argument's name if it's missing
asserts:
-
id: foo
value: "{$arg}"
errors:
-
type: Reference
desc: "Unknown variable: $arg"
-
name: and strings
resources:
-
source: |-
foo = { $arg }
tests:
-
name: can be a string
asserts:
-
id: foo
args:
arg: Argument
value: Argument
-
name: and numbers
resources:
-
source: |-
foo = { $arg }
tests:
-
name: can be a number
asserts:
-
id: foo
args:
arg: 1
value: 1

View File

@ -1,178 +0,0 @@
suites:
-
name: Attributes
suites:
-
name: missing
resources:
-
source: |-
foo = Foo
bar = Bar
.attr = Bar Attribute
baz = { foo } Baz
qux = { foo } Qux
.attr = Qux Attribute
ref-foo = { foo.missing }
ref-bar = { bar.missing }
ref-baz = { baz.missing }
ref-qux = { qux.missing }
tests:
-
name: falls back to id.attr for entities with string values and no attributes
asserts:
-
id: ref-foo
value: "{foo.missing}"
errors:
-
type: Reference
desc: "Unknown attribute: foo.missing"
-
name: falls back to id.attr for entities with string values and other attributes
asserts:
-
id: ref-bar
value: "{bar.missing}"
errors:
-
type: Reference
desc: "Unknown attribute: bar.missing"
-
name: falls back to id.attr for entities with pattern values and no attributes
asserts:
-
id: ref-baz
value: "{baz.missing}"
errors:
-
type: Reference
desc: "Unknown attribute: baz.missing"
-
name: falls back to id.attr for entities with pattern values and other attributes
asserts:
-
id: ref-qux
value: "{qux.missing}"
errors:
-
type: Reference
desc: "Unknown attribute: qux.missing"
-
name: with string values
resources:
-
source: |-
foo = Foo
.attr = Foo Attribute
bar = { foo } Bar
.attr = Bar Attribute
ref-foo = { foo.attr }
ref-bar = { bar.attr }
tests:
-
name: can be referenced for entities with string values
asserts:
-
id: ref-foo
value: Foo Attribute
-
name: can be formatted directly for entities with string values
asserts:
-
id: foo
attribute: attr
value: Foo Attribute
-
name: can be referenced for entities with pattern values
asserts:
-
id: ref-bar
value: Bar Attribute
-
name: can be formatted directly for entities with pattern values
asserts:
-
id: bar
attribute: attr
value: Bar Attribute
-
name: with simple pattern values
resources:
-
source: |-
foo = Foo
bar = Bar
.attr = { foo } Attribute
baz = { foo } Baz
.attr = { foo } Attribute
qux = Qux
.attr = { qux } Attribute
ref-bar = { bar.attr }
ref-baz = { baz.attr }
ref-qux = { qux.attr }
tests:
-
name: can be referenced for entities with string values
asserts:
-
id: ref-bar
value: Foo Attribute
-
name: can be formatted directly for entities with string values
asserts:
-
id: bar
attribute: attr
value: Foo Attribute
-
name: can be referenced for entities with simple pattern values
asserts:
-
id: ref-baz
value: Foo Attribute
-
name: can be formatted directly for entities with simple pattern values
asserts:
-
id: baz
attribute: attr
value: Foo Attribute
-
name: works with self-references
asserts:
-
id: ref-qux
value: Qux Attribute
-
name: can be formatted directly when it uses a self-reference
asserts:
-
id: qux
attribute: attr
value: Qux Attribute
-
name: with values with select expressions
resources:
-
source: |-
foo = Foo
.attr = { "a" ->
[a] A
*[b] B
}
ref-foo = { foo.attr }
tests:
-
name: can be referenced
asserts:
-
id: ref-foo
value: A
-
name: can be formatted directly
asserts:
-
id: foo
attribute: attr
value: A

View File

@ -1,30 +0,0 @@
suites:
-
name: Reference bombs
suites:
-
name: Billion Laughs
resources:
-
source: |-
lol0 = LOL
lol1 = {lol0} {lol0} {lol0} {lol0} {lol0} {lol0} {lol0} {lol0} {lol0} {lol0}
lol2 = {lol1} {lol1} {lol1} {lol1} {lol1} {lol1} {lol1} {lol1} {lol1} {lol1}
lol3 = {lol2} {lol2} {lol2} {lol2} {lol2} {lol2} {lol2} {lol2} {lol2} {lol2}
lol4 = {lol3} {lol3} {lol3} {lol3} {lol3} {lol3} {lol3} {lol3} {lol3} {lol3}
lol5 = {lol4} {lol4} {lol4} {lol4} {lol4} {lol4} {lol4} {lol4} {lol4} {lol4}
lol6 = {lol5} {lol5} {lol5} {lol5} {lol5} {lol5} {lol5} {lol5} {lol5} {lol5}
lol7 = {lol6} {lol6} {lol6} {lol6} {lol6} {lol6} {lol6} {lol6} {lol6} {lol6}
lol8 = {lol7} {lol7} {lol7} {lol7} {lol7} {lol7} {lol7} {lol7} {lol7} {lol7}
lol9 = {lol8} {lol8} {lol8} {lol8} {lol8} {lol8} {lol8} {lol8} {lol8} {lol8}
lolz = {lol9}
tests:
-
name: does not expand all placeables
asserts:
-
id: lolz
value: "{lol9}"
errors:
-
type: TooManyPlaceables

View File

@ -1,195 +0,0 @@
suites:
-
name: Bundle
suites:
-
name: addResource
resources:
-
source: |-
foo = Foo
-bar = Bar
tests:
-
name: adds messages
asserts:
-
id: foo
value: Foo
-
id: bar
missing: true
-
name: allowOverrides
resources:
-
source: key = Foo
tests:
-
name: addResource allowOverrides is false
resources:
-
source: key = Bar
bundles:
-
errors:
-
type: Overriding
asserts:
-
id: key
value: Foo
-
name: addResource allowOverrides is true
skip: true
resources:
-
source: key = Bar
asserts:
-
id: key
value: Bar
-
name: hasMessage
resources:
-
source: |-
foo = Foo
bar =
.attr = Bar Attr
-term = Term
# ERROR No value.
err1 =
# ERROR Broken value.
err2 = {}
# ERROR No attribute value.
err3 =
.attr =
# ERROR Broken attribute value.
err4 =
.attr1 = Attr
.attr2 = {}
errors:
-
type: Parser
-
type: Parser
-
type: Parser
-
type: Parser
tests:
-
name: returns true only for public messages
asserts:
-
id: foo
missing: false
-
name: returns false for terms and missing messages
asserts:
-
id: -term
missing: true
-
id: missing
missing: true
-
id: -missing
missing: true
-
name: returns false for broken messages
asserts:
-
id: err1
missing: true
-
id: err2
missing: true
-
id: err3
missing: true
-
id: err4
# XXX: Difference from JS. We handle partial messages
missing: false
-
name: getMessage
resources:
-
source: |-
foo = Foo
-bar = Bar
tests:
-
name: returns public messages
asserts:
-
id: foo
missing: false
-
name: returns undefined for terms and missing messages
asserts:
-
id: -bar
missing: true
-
id: baz
missing: true
-
id: -baz
missing: true
-
name: (Rust) Entries
resources:
-
source: |-
-foo = Bar
baz = { foo }
qux = { -bar }
fn = { FN() }
tests:
-
name: Entry mismatch doesn't leak
asserts:
-
id: baz
value: "{foo}"
errors:
-
type: Reference
desc: "Unknown message: foo"
-
name: Missing term
asserts:
-
id: qux
value: "{-bar}"
errors:
-
type: Reference
desc: "Unknown term: -bar"
-
name: Missing function
asserts:
-
id: fn
value: "{FN()}"
errors:
-
type: Reference
desc: "Unknown function: FN()"
-
name: (Rust) FluentBundle construction
resources:
-
source: |-
foo = Foo
bundles:
-
functions:
- SUM
- SUM
errors:
-
type: Overriding

View File

@ -1,6 +0,0 @@
# Those are default settings for all tests
bundle:
useIsolating: false
locales:
- en-US

View File

@ -1,21 +0,0 @@
suites:
-
name: Errors
resources:
-
source: |-
foo = {$one} and {$two}
tests:
-
name: Reporting into an array
asserts:
-
id: foo
value: "{$one} and {$two}"
errors:
-
type: Reference
desc: "Unknown variable: $one"
-
type: Reference
desc: "Unknown variable: $two"

View File

@ -1,85 +0,0 @@
suites:
-
name: Functions
suites:
-
name: missing
resources:
-
source: |-
foo = { MISSING("Foo") }
tests:
-
name: falls back to the name of the function
asserts:
-
id: foo
value: "{MISSING()}"
errors:
-
type: Reference
desc: "Unknown function: MISSING()"
-
name: arguments
resources:
-
source: |-
foo = Foo
.attr = Attribute
pass-nothing = { IDENTITY() }
pass-string = { IDENTITY("a") }
pass-number = { IDENTITY(1) }
pass-message = { IDENTITY(foo) }
pass-attr = { IDENTITY(foo.attr) }
pass-variable = { IDENTITY($var) }
pass-function-call = { IDENTITY(IDENTITY(1)) }
bundles:
-
functions:
- IDENTITY
tests:
-
name: falls back when arguments don't match the arity
asserts:
-
id: pass-nothing
## XXX: Difference from JS
value: "???"
-
name: accepts strings
asserts:
-
id: pass-string
value: a
-
name: accepts numbers
asserts:
-
id: pass-number
value: 1
-
name: accepts entities
asserts:
-
id: pass-message
value: Foo
-
name: accepts attributes
asserts:
-
id: pass-attr
value: Attribute
-
name: accepts variables
asserts:
-
id: pass-variable
args:
var: Variable
value: Variable
-
name: accepts function calls
asserts:
-
id: pass-function-call
value: 1

View File

@ -1,29 +0,0 @@
suites:
-
name: Runtime-specific functions
suites:
-
name: passing into the constructor
resources:
-
source: |-
foo = { CONCAT("Foo", "Bar") }
bar = { SUM(1, 2) }
bundles:
-
functions:
- CONCAT
- SUM
tests:
-
name: works for strings
asserts:
-
id: foo
value: FooBar
-
name: works for numbers
asserts:
-
id: bar
value: 3

View File

@ -1,100 +0,0 @@
suites:
-
name: Isolating interpolations
resources:
-
source: |-
foo = Foo
bar = { foo } Bar
baz = { $arg } Baz
qux = { bar } { baz }
bundles:
-
useIsolating: true
tests:
-
name: isolates interpolated message references
skip: true
asserts:
-
id: bar
value: "\u2068Foo\u2069 Bar"
-
name: isolates interpolated string-typed variables
asserts:
-
id: baz
args:
arg: Arg
value: "\u2068Arg\u2069 Baz"
-
name: isolates interpolated number-typed variables
asserts:
-
id: baz
args:
arg: 1
value: "\u20681\u2069 Baz"
-
name: isolates interpolated date-typed variables
skip: true
asserts:
-
id: baz
args:
arg: 1976-07-31
value: "\u20681976-07-31\u2069 Baz"
-
name: isolates complex interpolations
skip: true
asserts:
-
id: qux
args:
arg: Arg
value: "\u2068\u2068Foo\u2069 Bar\u2069 \u2068\u2068Arg\u2069 Baz\u2069"
-
name: Skip isolation cases
resources:
-
source: |-
-brand-short-name = Amaya
foo = { -brand-short-name }
bundles:
-
useIsolating: true
tests:
-
name: skips isolation if the only element is a placeable
asserts:
-
id: foo
value: "Amaya"
-
name: (Rust) Skip isolation of string literals and terms
resources:
-
source: |-
rs-bar = Foo { $foo } { "Bar" } baz
-rs-term = My Term
rs-baz = Foo { $foo } { -rs-term } baz
bundles:
-
useIsolating: true
tests:
-
name: skip isolation of string literals
asserts:
-
id: rs-bar
args:
foo: Test
value: "Foo \u2068Test\u2069 Bar baz"
-
name: skip isolation of term references
asserts:
-
id: rs-baz
args:
foo: Test
value: "Foo \u2068Test\u2069 My Term baz"

View File

@ -1,69 +0,0 @@
suites:
-
name: Literals as selectors
tests:
-
name: a matching string literal selector
resources:
-
source: |-
foo = { "a" ->
[a] A
*[b] B
}
asserts:
-
id: foo
value: A
-
name: a non-matching string literal selector
resources:
-
source: |-
foo = { "c" ->
[a] A
*[b] B
}
asserts:
-
id: foo
value: B
-
name: a matching number literal selector
resources:
-
source: |-
foo = { 0 ->
[0] A
*[1] B
}
asserts:
-
id: foo
value: A
-
name: a non-matching number literal selector
resources:
-
source: |-
foo = { 2 ->
[0] A
*[1] B
}
asserts:
-
id: foo
value: B
-
name: a number literal selector matching a plural category
resources:
-
source: |-
foo = { 1 ->
[one] A
*[other] B
}
asserts:
-
id: foo
value: A

View File

@ -1,392 +0,0 @@
suites:
-
name: Macros
suites:
-
name: References and calls
resources:
-
source: |-
-bar = Bar
term-ref = {-bar}
term-call = {-bar()}
tests:
-
name: terms can be referenced without parens
asserts:
-
id: term-ref
value: Bar
-
name: terms can be parameterized
asserts:
-
id: term-call
value: Bar
-
name: Passing arguments
resources:
-
source: |-
-foo = Foo {$arg}
ref-foo = {-foo}
call-foo-no-args = {-foo()}
call-foo-with-expected-arg = {-foo(arg: 1)}
call-foo-with-other-arg = {-foo(other: 3)}
tests:
-
name: Not parameterized, no externals
asserts:
-
id: ref-foo
value: Foo {$arg}
-
name: Not parameterized but with externals
asserts:
-
id: ref-foo
args:
arg: 1
value: Foo {$arg}
-
name: No arguments, no externals
asserts:
-
id: call-foo-no-args
value: Foo {$arg}
-
name: No arguments, but with externals
asserts:
-
id: call-foo-no-args
args:
arg: 1
value: Foo {$arg}
-
name: With expected args, no externals
asserts:
-
id: call-foo-with-expected-arg
value: Foo 1
-
name: With expected args, and with externals
asserts:
-
id: call-foo-with-expected-arg
args:
arg: 5
value: Foo 1
-
name: With other args, no externals
asserts:
-
id: call-foo-with-other-arg
value: Foo {$arg}
-
name: With other args, and with externals
asserts:
-
id: call-foo-with-other-arg
args:
arg: 5
value: Foo {$arg}
-
name: Nesting message references
resources:
-
source: |-
foo = Foo {$arg}
-bar = {foo}
ref-bar = {-bar}
call-bar = {-bar()}
call-bar-with-arg = {-bar(arg: 1)}
tests:
-
name: No parameterization, no externals
asserts:
-
id: ref-bar
value: Foo {$arg}
-
name: No parameterization, but with externals
asserts:
-
id: ref-bar
args:
arg: 5
value: Foo {$arg}
-
name: No arguments, no externals
asserts:
-
id: call-bar
value: Foo {$arg}
-
name: No arguments, but with externals
asserts:
-
id: call-bar
args:
arg: 5
value: Foo {$arg}
-
name: With arguments, no externals
asserts:
-
id: call-bar-with-arg
value: Foo 1
-
name: With arguments and with externals
asserts:
-
id: call-bar-with-arg
args:
arg: 5
value: Foo 1
-
name: Nesting term references
resources:
-
source: |-
-foo = Foo {$arg}
-bar = {-foo}
-baz = {-foo()}
-qux = {-foo(arg: 1)}
ref-bar = {-bar}
ref-baz = {-baz}
ref-qux = {-qux}
call-bar-no-args = {-bar()}
call-baz-no-args = {-baz()}
call-qux-no-args = {-qux()}
call-bar-with-arg = {-bar(arg: 2)}
call-baz-with-arg = {-baz(arg: 2)}
call-qux-with-arg = {-qux(arg: 2)}
call-qux-with-other = {-qux(other: 3)}
tests:
-
name: No parameterization, no parameterization, no externals
asserts:
-
id: ref-bar
value: Foo {$arg}
-
name: No parameterization, no parameterization, with externals
asserts:
-
id: ref-bar
args:
arg: 5
value: Foo {$arg}
-
name: No parameterization, no arguments, no externals
asserts:
-
id: ref-baz
value: Foo {$arg}
-
name: No parameterization, no arguments, with externals
asserts:
-
id: ref-baz
args:
arg: 5
value: Foo {$arg}
-
name: No parameterization, with arguments, no externals
asserts:
-
id: ref-qux
value: Foo 1
-
name: No parameterization, with arguments, with externals
asserts:
-
id: ref-qux
args:
arg: 5
value: Foo 1
-
name: No arguments, no parametrization, no externals
asserts:
-
id: call-bar-no-args
value: Foo {$arg}
-
name: No arguments, no parametrization, with externals
asserts:
-
id: call-bar-no-args
args:
arg: 5
value: Foo {$arg}
-
name: No arguments, no arguments, no externals
asserts:
-
id: call-baz-no-args
value: Foo {$arg}
-
name: No arguments, no arguments, with externals
asserts:
-
id: call-baz-no-args
args:
arg: 5
value: Foo {$arg}
-
name: No arguments, with arguments, no externals
asserts:
-
id: call-qux-no-args
value: Foo 1
-
name: No arguments, with arguments, with externals
asserts:
-
id: call-qux-no-args
args:
arg: 5
value: Foo 1
-
name: With arguments, no parametrization, no externals
asserts:
-
id: call-bar-with-arg
value: Foo {$arg}
-
name: With arguments, no parametrization, with externals
asserts:
-
id: call-bar-with-arg
args:
arg: 5
value: Foo {$arg}
-
name: With arguments, no arguments, no externals
asserts:
-
id: call-baz-with-arg
value: Foo {$arg}
-
name: With arguments, no arguments, with externals
asserts:
-
id: call-baz-with-arg
args:
arg: 5
value: Foo {$arg}
-
name: With arguments, with arguments, no externals
asserts:
-
id: call-qux-with-arg
value: Foo 1
-
name: With arguments, with arguments, with externals
asserts:
-
id: call-qux-with-arg
args:
arg: 5
value: Foo 1
-
name: With unexpected arguments, with arguments, no externals
asserts:
-
id: call-qux-with-other
value: Foo 1
-
name: With unexpected arguments, with arguments, with externals
asserts:
-
id: call-qux-with-other
args:
arg: 5
value: Foo 1
-
name: Parameterized term attributes
resources:
-
source: |-
-ship = Ship
.gender = {$style ->
*[traditional] neuter
[chicago] feminine
}
ref-attr = {-ship.gender ->
*[masculine] He
[feminine] She
[neuter] It
}
call-attr-no-args = {-ship.gender() ->
*[masculine] He
[feminine] She
[neuter] It
}
call-attr-with-expected-arg = {-ship.gender(style: "chicago") ->
*[masculine] He
[feminine] She
[neuter] It
}
call-attr-with-other-arg = {-ship.gender(other: 3) ->
*[masculine] He
[feminine] She
[neuter] It
}
tests:
-
name: Not parameterized, no externals
asserts:
-
id: ref-attr
value: It
-
name: Not parameterized but with externals
asserts:
-
id: ref-attr
args:
attr: chicago
value: It
-
name: No arguments, no externals
asserts:
-
id: call-attr-no-args
value: It
-
name: No arguments, but with externals
asserts:
-
id: call-attr-no-args
args:
style: chicago
value: It
-
name: With expected args, no externals
asserts:
-
id: call-attr-with-expected-arg
value: She
-
name: With expected args, and with externals
asserts:
-
id: call-attr-with-expected-arg
args:
style: chicago
value: She
-
name: With other args, no externals
asserts:
-
id: call-attr-with-other-arg
value: It
-
name: With other args, and with externals
asserts:
-
id: call-attr-with-other-arg
args:
style: chicago
value: It

View File

@ -1,218 +0,0 @@
suites:
-
name: Patterns
suites:
-
name: Simple string value
resources:
-
source: foo = Foo
tests:
-
name: returns the value
asserts:
-
id: foo
value: Foo
-
name: Complex string value
resources:
-
source: |-
foo = Foo
-bar = Bar
ref-message = { foo }
ref-term = { -bar }
ref-missing-message = { missing }
ref-missing-term = { -missing }
ref-malformed = { malformed
errors:
-
type: Parser
tests:
-
name: resolves the reference to a message
asserts:
-
id: ref-message
value: Foo
-
name: resolves the reference to a term
asserts:
-
id: ref-term
value: Bar
-
name: returns the id if a message reference is missing
asserts:
-
id: ref-missing-message
value: "{missing}"
errors:
-
type: Reference
desc: "Unknown message: missing"
-
name: returns the id if a term reference is missing
asserts:
-
id: ref-missing-term
value: "{-missing}"
errors:
-
type: Reference
desc: "Unknown term: -missing"
-
name: Complex string referencing a message with null value
resources:
-
source: |-
foo =
.attr = Foo Attr
bar = { foo } Bar
tests:
-
name: returns the null value
skip: true
asserts:
-
id: foo
value: Foo
-
name: formats the attribute
asserts:
-
id: foo
attribute: attr
value: Foo Attr
-
name: formats ??? when the referenced message has no value and no default
asserts:
-
id: bar
# XXX: Difference from JS
value: "{foo} Bar"
errors:
-
type: Reference
desc: "Unknown message: foo"
-
name: Cyclic reference
resources:
-
source: |-
foo = { bar }
bar = { foo }
tests:
-
name: returns ???
asserts:
-
id: foo
value: "{foo}"
errors:
-
type: Cyclic
-
name: Cyclic self-reference
resources:
-
source: foo = { foo }
tests:
-
name: returns the raw string
asserts:
-
id: foo
value: "{foo}"
errors:
-
type: Cyclic
-
name: Cyclic self-reference in a member
resources:
-
source: |-
foo =
{ $sel ->
*[a] { foo }
[b] Bar
}
bar = { foo }
tests:
-
name: returns ???
asserts:
-
id: foo
args:
sel: a
value: "{foo}"
errors:
-
type: Cyclic
-
name: returns the other member if requested
asserts:
-
id: foo
args:
sel: b
value: Bar
-
name: Cyclic reference in a selector
skip: true
resources:
-
source: |-
-foo =
{ -bar.attr ->
*[a] Foo
}
-bar = Bar
.attr = { -foo }
foo = { -foo }
tests:
-
name: returns the default variant
asserts:
-
id: foo
value: Foo
errors:
-
type: Cyclic
-
name: Cyclic self-reference in a selector
skip: true
resources:
-
source: |-
-foo =
{ -bar.attr ->
*[a] Foo
}
.attr = a
-bar =
{ -foo.attr ->
*[a] Bar
}
.attr = { -foo }
foo = { -foo }
bar = { -bar }
tests:
-
name: returns the default variant
asserts:
-
id: foo
value: Foo
errors:
-
type: Cyclic
-
name: can reference an attribute
asserts:
-
id: bar
value: Bar

View File

@ -1,154 +0,0 @@
suites:
-
name: Primitives
suites:
-
name: Numbers
resources:
-
source: |-
one = { 1 }
select = { 1 ->
*[0] Zero
[1] One
}
tests:
-
name: can be used in a placeable
asserts:
-
id: one
value: 1
-
name: can be used as a selector
asserts:
-
id: select
value: One
-
name: Simple string value
resources:
-
source: |-
foo = Foo
placeable-literal = { "Foo" } Bar
placeable-message = { foo } Bar
selector-literal = { "Foo" ->
*[Foo] Member 1
}
bar =
.attr = Bar Attribute
placeable-attr = { bar.attr }
-baz = Baz
.attr = BazAttribute
selector-attr = { -baz.attr ->
*[BazAttribute] Member 3
}
tests:
-
name: can be used as a value
asserts:
-
id: foo
value: Foo
-
name: can be used in a placeable
asserts:
-
id: placeable-literal
value: Foo Bar
-
name: can be a value of a message referenced in a placeable
asserts:
-
id: placeable-message
value: Foo Bar
-
name: can be a selector
asserts:
-
id: selector-literal
value: Member 1
-
name: can be used as an attribute value
asserts:
-
id: bar
attribute: attr
value: Bar Attribute
-
name: can be a value of an attribute used in a placeable
asserts:
-
id: placeable-attr
value: Bar Attribute
-
name: can be a value of an attribute used as a selector
asserts:
-
id: selector-attr
value: Member 3
-
name: Complex string value
resources:
-
source: |-
foo = Foo
bar = { foo }Bar
placeable-message = { bar }Baz
baz =
.attr = { bar }BazAttribute
-bazTerm = Value
.attr = { bar }BazAttribute
placeable-attr = { baz.attr }
# XXX: This is different from JS fixture which
# illegally uses message attribute as selector.
selector-attr = { -bazTerm.attr ->
[FooBarBazAttribute] FooBarBaz
*[other] Other
}
tests:
-
name: can be used as a value
asserts:
-
id: bar
value: FooBar
-
name: can be a value of a message referenced in a placeable
asserts:
-
id: placeable-message
value: FooBarBaz
-
name: can be used as an attribute value
asserts:
-
id: baz
attribute: attr
value: FooBarBazAttribute
-
name: can be a value of an attribute used in a placeable
asserts:
-
id: placeable-attr
value: FooBarBazAttribute
-
name: can be a value of an attribute used as a selector
asserts:
-
id: selector-attr
value: FooBarBaz
-
name: (Rust) Placeable
resources:
-
source: |-
foo = { { "Foo" } }
tests:
-
name: Placeable in placable work
asserts:
-
id: foo
value: Foo

View File

@ -1,151 +0,0 @@
suites:
-
name: Select expressions
tests:
-
name: missing selector
resources:
-
source: |-
select = {$none ->
[a] A
*[b] B
}
asserts:
-
id: select
value: B
errors:
-
type: Reference
desc: "Unknown variable: $none"
suites:
-
name: string selectors
tests:
-
name: matching selector
resources:
-
source: |-
select = {$selector ->
[a] A
*[b] B
}
asserts:
-
id: select
value: A
args:
selector: a
-
name: non-matching selector
resources:
-
source: |-
select = {$selector ->
[a] A
*[b] B
}
asserts:
-
id: select
value: B
args:
selector: c
-
name: number selectors
tests:
-
name: matching selector
resources:
-
source: |-
select = {$selector ->
[0] A
*[1] B
}
asserts:
-
id: select
value: A
args:
selector: 0
-
name: non-matching selector
resources:
-
source: |-
select = {$selector ->
[0] A
*[1] B
}
asserts:
-
id: select
value: B
args:
selector: 2
-
name: plural categories
tests:
-
name: matching number selector
resources:
-
source: |-
select = {$selector ->
[one] A
*[other] B
}
asserts:
-
id: select
value: A
args:
selector: 1
-
name: matching string selector
resources:
-
source: |-
select = {$selector ->
[one] A
*[other] B
}
asserts:
-
id: select
value: A
args:
selector: one
-
name: non-matching number selector
resources:
-
source: |-
select = {$selector ->
[one] A
*[default] D
}
asserts:
-
id: select
value: D
args:
selector: 2
-
name: non-matching string selector
resources:
-
source: |-
select = {$selector ->
[one] A
*[default] D
}
asserts:
-
id: select
value: D
args:
selector: other

View File

@ -1,48 +0,0 @@
suites:
-
name: Transformations
resources:
-
source: |-
foo = Faa
.bar = Bar {foo} Baz
bar = Bar {"Baz"}
qux = {"faa" ->
[faa] Faa
*[bar] Bar
}
arg = Faa {$arg}
bundles:
-
transform: example
tests:
-
name: transforms TextElements
asserts:
-
id: foo
value: FAA
-
id: foo
attribute: bar
value: BAr FAA BAz
-
name: does not transform StringLiterls
asserts:
-
id: bar
value: BAr Baz
-
name: does not transform VariantKeys
asserts:
-
id: qux
value: FAA
-
name: does not transform Variables
asserts:
-
id: arg
args:
arg: aaa
value: FAA aaa

View File

@ -1,74 +0,0 @@
suites:
-
name: Formatting values
resources:
-
source: |-
key1 = Value 1
key2 = { $sel ->
[a] A2
*[b] B2
}
key3 = Value { 3 }
key4 = { $sel ->
[a] A{ 4 }
*[b] B{ 4 }
}
key5 =
.a = A5
.b = B5
tests:
-
name: returns the value
asserts:
-
id: key1
value: Value 1
-
name: returns the default variant
asserts:
-
id: key2
value: B2
errors:
-
type: Reference
desc: "Unknown variable: $sel"
-
name: returns the value if it is a pattern
asserts:
-
id: key3
value: Value 3
-
name: returns the default variant if it is a pattern
asserts:
-
id: key4
value: B4
errors:
-
type: Reference
desc: "Unknown variable: $sel"
-
name: returns {???} when trying to format a null value
skip: true
asserts:
-
id: key5
value: "{???}"
errors:
-
type: Reference
desc: "Unknown variable: $sel"
-
name: allows to pass traits directly to bundle.formatPattern
asserts:
-
id: key5
attribute: a
value: "A5"
-
id: key5
attribute: b
value: "B5"

View File

@ -1,140 +0,0 @@
suites:
-
name: Referencing values
resources:
-
source: |-
key1 = Value 1
-key2 = { $sel ->
[a] A2
*[b] B2
}
key3 = Value { 3 }
-key4 = { $sel ->
[a] A{ 4 }
*[b] B{ 4 }
}
key5 =
.a = A5
.b = B5
ref1 = { key1 }
ref2 = { -key2 }
ref3 = { key3 }
ref4 = { -key4 }
ref5 = { key5 }
ref6 = { -key2(sel: "a") }
ref7 = { -key2(sel: "b") }
ref8 = { -key4(sel: "a") }
ref9 = { -key4(sel: "b") }
ref10 = { key5.a }
ref11 = { key5.b }
ref12 = { key5.c }
ref13 = { key6 }
ref14 = { key6.a }
ref15 = { -key6 }
ref16 = { -key6.a ->
*[a] A
}
tests:
-
name: references the value
asserts:
-
id: ref1
value: Value 1
-
name: references the default variant
asserts:
-
id: ref2
value: B2
-
name: references the value if it is a pattern
asserts:
-
id: ref3
value: Value 3
-
name: references the default variant if it is a pattern
asserts:
-
id: ref4
value: B4
-
name: falls back to id if there is no value
asserts:
-
id: ref5
value: "{key5}"
errors:
-
type: Reference
desc: "Unknown message: key5"
-
name: references the variants
asserts:
-
id: ref6
value: A2
-
id: ref7
value: B2
-
name: references the variants which are patterns
asserts:
-
id: ref8
value: A4
-
id: ref9
value: B4
-
name: references the attributes
asserts:
-
id: ref10
value: A5
-
id: ref11
value: B5
-
id: ref12
value: "{key5.c}"
errors:
-
type: Reference
desc: "Unknown attribute: key5.c"
-
name: missing message reference
asserts:
-
id: ref13
value: "{key6}"
errors:
-
type: Reference
desc: "Unknown message: key6"
-
id: ref14
value: "{key6.a}"
errors:
-
type: Reference
desc: "Unknown attribute: key6.a"
-
name: missing term reference
asserts:
-
id: ref15
value: "{-key6}"
errors:
-
type: Reference
desc: "Unknown term: -key6"
-
id: ref16
value: "A"
errors:
-
type: Reference
desc: "Unknown attribute: -key6.a"

Some files were not shown because too many files have changed in this diff Show More