diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index d0fea3198..9feb20727 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -28,8 +28,8 @@ on: - '!tools/wikidata/**' - '!documents/**' env: - ICU_MAJOR: '78' - ICU_MINOR: '3' + ICU_MAJOR: '79' + ICU_MINOR: '1' jobs: cache-icu: @@ -43,13 +43,13 @@ jobs: - name: Download and install icu if: steps.cache.outputs.cache-hit != 'true' run: | - wget https://github.com/unicode-org/icu/releases/download/release-${ICU_MAJOR}.${ICU_MINOR}/icu4c-${ICU_MAJOR}.${ICU_MINOR}-Ubuntu22.04-x64.tgz + wget https://github.com/unicode-org/icu/releases/download/release-${ICU_MAJOR}.${ICU_MINOR}/icu4c-${ICU_MAJOR}.${ICU_MINOR}-Ubuntu24.04-x64.tgz export ICU=~/icu/ mkdir -p $ICU echo "ICU directory is $ICU" # Get the release and unpack. - cp icu4c-${ICU_MAJOR}.${ICU_MINOR}-Ubuntu22.04-x64.tgz $ICU + cp icu4c-${ICU_MAJOR}.${ICU_MINOR}-Ubuntu24.04-x64.tgz $ICU pushd $ICU pwd @@ -135,12 +135,25 @@ jobs: # Install all the required dependencies for the macos - name: Install ICU (Ubuntu/macos) run: | - if [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then - echo "ICU_ROOT=~/icu/icu/usr/local" >> $GITHUB_ENV - elif [[ "${{ matrix.os }}" == "macos-latest" ]]; then - brew list icu4c || brew install icu4c - echo "ICU_ROOT=$(brew --prefix icu4c)" >> $GITHUB_ENV + if [[ "${{ matrix.os }}" == "macos-latest" && "${{ steps.cache.outputs.cache-hit }}" != "true" ]]; then + pushd /tmp + curl -fLO https://github.com/unicode-org/icu/releases/download/release-${ICU_MAJOR}.${ICU_MINOR}/icu4c-${ICU_MAJOR}.${ICU_MINOR}-sources.tgz + tar -xzf icu4c-${ICU_MAJOR}.${ICU_MINOR}-sources.tgz + cd icu/source + ./runConfigureICU MacOSX --prefix="$HOME/icu/icu/usr/local" --enable-rpath --disable-samples --disable-tests --disable-extras --disable-icuio + make -j$(sysctl -n hw.ncpu) + make install + popd + rm -rf /tmp/icu /tmp/icu4c-${ICU_MAJOR}.${ICU_MINOR}-sources.tgz fi + echo "ICU_ROOT=$HOME/icu/icu/usr/local" >> $GITHUB_ENV + + - name: Save ICU cache (macOS) + if: matrix.os == 'macos-latest' && steps.cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ~/icu + key: ${{ runner.os }}-icu-${{ env.ICU_MAJOR }}-${{ env.ICU_MINOR }} - name: Install lld (Ubuntu) # Linux Release builds link with -fuse-ld=lld to avoid gold-plugin LTO version mismatches. diff --git a/.github/workflows/create-distribution-packaging.yml b/.github/workflows/create-distribution-packaging.yml index 4c5357f34..d047134ae 100644 --- a/.github/workflows/create-distribution-packaging.yml +++ b/.github/workflows/create-distribution-packaging.yml @@ -33,8 +33,8 @@ jobs: artifact_patterns: | build/*.tar.gz env: - ICU_MAJOR: '78' - ICU_MINOR: '3' + ICU_MAJOR: '79' + ICU_MINOR: '1' steps: - name: Checkout @@ -52,7 +52,7 @@ jobs: if: runner.os == 'Linux' run: | sudo apt update - sudo apt install -y cmake build-essential clang pkg-config + sudo apt install -y cmake build-essential clang lld pkg-config - name: Cache ICU (Ubuntu) if: runner.os == 'Linux' @@ -69,9 +69,9 @@ jobs: if: runner.os == 'Linux' && steps.cache-icu.outputs.cache-hit != 'true' run: | cd /tmp - wget https://github.com/unicode-org/icu/releases/download/release-${ICU_MAJOR}.${ICU_MINOR}/icu4c-${ICU_MAJOR}.${ICU_MINOR}-Ubuntu22.04-x64.tgz + wget https://github.com/unicode-org/icu/releases/download/release-${ICU_MAJOR}.${ICU_MINOR}/icu4c-${ICU_MAJOR}.${ICU_MINOR}-Ubuntu24.04-x64.tgz mkdir icu-install - tar -xzf icu4c-${ICU_MAJOR}.${ICU_MINOR}-Ubuntu22.04-x64.tgz -C icu-install + tar -xzf icu4c-${ICU_MAJOR}.${ICU_MINOR}-Ubuntu24.04-x64.tgz -C icu-install sudo mkdir -p /usr/local/icu-${ICU_MAJOR}_${ICU_MINOR} sudo cp -r icu-install/icu/usr/local/* /usr/local/icu-${ICU_MAJOR}_${ICU_MINOR}/ sudo ldconfig @@ -85,7 +85,27 @@ jobs: - name: Install system dependencies (macOS) if: runner.os == 'macOS' run: | - brew install icu4c libxml2 cmake pkg-config + brew install libxml2 cmake pkg-config + + - name: Cache ICU (macOS) + if: runner.os == 'macOS' + uses: actions/cache@v4 + id: cache-icu-macos + with: + path: ~/icu-${{ env.ICU_MAJOR }}_${{ env.ICU_MINOR }} + key: icu-${{ env.ICU_MAJOR }}_${{ env.ICU_MINOR }}-${{ runner.os }} + + - name: Install ICU (macOS) + if: runner.os == 'macOS' && steps.cache-icu-macos.outputs.cache-hit != 'true' + run: | + cd /tmp + curl -fLO https://github.com/unicode-org/icu/releases/download/release-${ICU_MAJOR}.${ICU_MINOR}/icu4c-${ICU_MAJOR}.${ICU_MINOR}-sources.tgz + tar -xzf icu4c-${ICU_MAJOR}.${ICU_MINOR}-sources.tgz + cd icu/source + ./runConfigureICU MacOSX --prefix="$HOME/icu-${ICU_MAJOR}_${ICU_MINOR}" --enable-rpath --disable-samples --disable-tests --disable-extras --disable-icuio + make -j$(sysctl -n hw.ncpu) + make install + rm -rf /tmp/icu /tmp/icu4c-${ICU_MAJOR}.${ICU_MINOR}-sources.tgz # Configure & Build - name: Configure & Build (Ubuntu) @@ -105,7 +125,7 @@ jobs: - name: Configure & Build (macOS) if: runner.os == 'macOS' run: | - ICU_PREFIX=$(brew --prefix icu4c) + ICU_PREFIX="$HOME/icu-${ICU_MAJOR}_${ICU_MINOR}" XML2_PREFIX=$(brew --prefix libxml2) export PKG_CONFIG_PATH="$ICU_PREFIX/lib/pkgconfig:$XML2_PREFIX/lib/pkgconfig:$PKG_CONFIG_PATH" mkdir -p build @@ -130,7 +150,7 @@ jobs: - name: Run tests (macOS) if: runner.os == 'macOS' run: | - ICU_PREFIX=$(brew --prefix icu4c) + ICU_PREFIX="$HOME/icu-${ICU_MAJOR}_${ICU_MINOR}" XML2_PREFIX=$(brew --prefix libxml2) export PKG_CONFIG_PATH="$ICU_PREFIX/lib/pkgconfig:$XML2_PREFIX/lib/pkgconfig:$PKG_CONFIG_PATH" export DYLD_LIBRARY_PATH="$ICU_PREFIX/lib:$DYLD_LIBRARY_PATH" diff --git a/.github/workflows/doxygen-gh-pages.yml b/.github/workflows/doxygen-gh-pages.yml index 5ddc15382..645385c4b 100644 --- a/.github/workflows/doxygen-gh-pages.yml +++ b/.github/workflows/doxygen-gh-pages.yml @@ -24,8 +24,8 @@ on: - '!documents/**' env: - ICU_MAJOR: '78' - ICU_MINOR: '3' + ICU_MAJOR: '79' + ICU_MINOR: '1' jobs: cache-icu: @@ -39,13 +39,13 @@ jobs: - name: Download and install icu if: steps.cache.outputs.cache-hit != 'true' run: | - wget https://github.com/unicode-org/icu/releases/download/release-${ICU_MAJOR}.${ICU_MINOR}/icu4c-${ICU_MAJOR}.${ICU_MINOR}-Ubuntu22.04-x64.tgz + wget https://github.com/unicode-org/icu/releases/download/release-${ICU_MAJOR}.${ICU_MINOR}/icu4c-${ICU_MAJOR}.${ICU_MINOR}-Ubuntu24.04-x64.tgz export ICU=~/icu/ mkdir -p $ICU echo "ICU directory is $ICU" # Get the release and unpack. - cp icu4c-${ICU_MAJOR}.${ICU_MINOR}-Ubuntu22.04-x64.tgz $ICU + cp icu4c-${ICU_MAJOR}.${ICU_MINOR}-Ubuntu24.04-x64.tgz $ICU pushd $ICU pwd diff --git a/.github/workflows/ubuntu-memory-check.yml b/.github/workflows/ubuntu-memory-check.yml index 3f429927c..05583aa30 100644 --- a/.github/workflows/ubuntu-memory-check.yml +++ b/.github/workflows/ubuntu-memory-check.yml @@ -26,8 +26,8 @@ on: - '!tools/wikidata/**' - '!documents/**' env: - ICU_MAJOR: '78' - ICU_MINOR: '3' + ICU_MAJOR: '79' + ICU_MINOR: '1' jobs: cache-icu: @@ -41,13 +41,13 @@ jobs: - name: Download and install icu if: steps.cache.outputs.cache-hit != 'true' run: | - wget https://github.com/unicode-org/icu/releases/download/release-${ICU_MAJOR}.${ICU_MINOR}/icu4c-${ICU_MAJOR}.${ICU_MINOR}-Ubuntu22.04-x64.tgz + wget https://github.com/unicode-org/icu/releases/download/release-${ICU_MAJOR}.${ICU_MINOR}/icu4c-${ICU_MAJOR}.${ICU_MINOR}-Ubuntu24.04-x64.tgz export ICU=~/icu/ mkdir -p $ICU echo "ICU directory is $ICU" # Get the release and unpack. - cp icu4c-${ICU_MAJOR}.${ICU_MINOR}-Ubuntu22.04-x64.tgz $ICU + cp icu4c-${ICU_MAJOR}.${ICU_MINOR}-Ubuntu24.04-x64.tgz $ICU pushd $ICU pwd diff --git a/CMakeLists.txt b/CMakeLists.txt index 790fd584b..b8439570f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -173,9 +173,9 @@ install(DIRECTORY ${INFLECTION_DATA_ROOT}/ TYPE DATA COMPONENT inflection_ # CPack Configuration for Ubuntu Packaging # # To update ICU version: -# 1. Update ICU_VERSION in the GitHub Actions workflow (.github/workflows/create-ubuntu-distribution-packaging.yml) +# 1. Update ICU_MAJOR and ICU_MINOR in the GitHub Actions workflows (.github/workflows/create-distribution-packaging.yml, etc.) # 2. Make sure the downloaded ICU binary file matches the filename format -# (e.g., icu4c-78_1-Ubuntu22.04-x64.tgz) +# (e.g., icu4c-79.1-Ubuntu24.04-x64.tgz) # 3. Update the line below to match the correct ICU dependency in Debian packaging # ---------------------------------------------------------------------------------------- diff --git a/cmake/versions.mk b/cmake/versions.mk index d5bf27f14..03b3f617b 100644 --- a/cmake/versions.mk +++ b/cmake/versions.mk @@ -2,5 +2,5 @@ # Copyright 2021-2024 Apple Inc. All rights reserved. # CATCH2_VERSION := 3.15.1 -ICU_VERSION_MINIMUM := 78.1 +ICU_VERSION_MINIMUM := 79.1 MARISA_VERSION := 0.3.1 diff --git a/documents/how_to_update_icu.md b/documents/how_to_update_icu.md index 79c41c737..d41aaea8c 100644 --- a/documents/how_to_update_icu.md +++ b/documents/how_to_update_icu.md @@ -10,15 +10,16 @@ Follow [ICU instructions](https://unicode-org.github.io/icu/download/) on how to Test your update locally and fix any problems - most likely to happen due to CLDR changes. If there are errors, you'll need to update the CI actions before submitting. -### Update Actions on GitHub -Edit all yml files under .github/workflows that contain: +### Update Minimum ICU Version and Actions on GitHub +1. Update `ICU_VERSION_MINIMUM` in `cmake/versions.mk` (e.g. `79.1`) and `CPACK_DEBIAN_PACKAGE_DEPENDS` in `CMakeLists.txt` (e.g. `libicu79 (>= ${ICU_VERSION_MINIMUM})`). +2. Edit all `.yml` files under `.github/workflows` that contain: ``` env: ICU_MAJOR: 'xx' ICU_MINOR: 'y' ``` -and update major and minor ICU versions, e.g. 78.2. +and update major and minor ICU versions, e.g. `79` and `1`. ### Clear the GitHub Cache if necessary Go to [Caches](https://github.com/unicode-org/inflection/actions/caches) page, and clear all ICU related caches on the right. diff --git a/src/inflection/message2/MF2Factory.cpp b/src/inflection/message2/MF2Factory.cpp index 02d0ba12f..80912a1c5 100644 --- a/src/inflection/message2/MF2Factory.cpp +++ b/src/inflection/message2/MF2Factory.cpp @@ -26,19 +26,17 @@ using inflection::dialog::SpeakableString; using U_ICU_NAMESPACE::Locale; +using U_ICU_NAMESPACE::LocalPointer; using U_ICU_NAMESPACE::UnicodeString; -using U_ICU_NAMESPACE::message2::Formatter; using U_ICU_NAMESPACE::message2::Formattable; -using U_ICU_NAMESPACE::message2::FormattedValue; -using U_ICU_NAMESPACE::message2::FormattedPlaceholder; -using U_ICU_NAMESPACE::message2::FormatterFactory; +using U_ICU_NAMESPACE::message2::Function; +using U_ICU_NAMESPACE::message2::FunctionContext; using U_ICU_NAMESPACE::message2::FunctionOptions; using U_ICU_NAMESPACE::message2::FunctionOptionsMap; +using U_ICU_NAMESPACE::message2::FunctionValue; using U_ICU_NAMESPACE::message2::MessageArguments; using U_ICU_NAMESPACE::message2::MessageFormatter; using U_ICU_NAMESPACE::message2::MFFunctionRegistry; -using U_ICU_NAMESPACE::message2::Selector; -using U_ICU_NAMESPACE::message2::SelectorFactory; namespace inflection::message2 { @@ -71,13 +69,6 @@ static constexpr auto TYPE_OR = u"or"; // Bool option value static constexpr auto VALUE_TRUE = u"true"; -// Per-function fallback names passed to FormattedPlaceholder -static constexpr auto FN_INFLECTION = u"inflection"; -static constexpr auto FN_NUMERAL = u"numeral"; -static constexpr auto FN_QUANTIFY = u"quantify"; -static constexpr auto FN_LIST = u"list"; -static constexpr auto FN_PRONOUN = u"pronoun"; - static UnicodeString xmlEscape(const std::u16string& s, bool attribute) { UnicodeString out; for (char16_t c : s) { @@ -163,528 +154,497 @@ resolveConstraintValue(const inflection::dialog::SemanticFeatureModel* model, return featureValue != nullptr ? featureValue->getPrint() : std::u16string(); } -class InflectionFormatterFactory : public FormatterFactory { -public: - explicit InflectionFormatterFactory(MF2Factory::OutputMode mode) : mode(mode) {} - Formatter* createFormatter(const Locale&, UErrorCode&) override; -private: - MF2Factory::OutputMode mode; -}; - -class InflectionSelectorFactory : public SelectorFactory { -public: - Selector* createSelector(const Locale&, UErrorCode&) const override; -}; - -icu::message2::FormatterFactory* MF2Factory::createInflectFormatterFactory(MF2Factory::OutputMode mode) { - return new InflectionFormatterFactory(mode); -} - -icu::message2::SelectorFactory* MF2Factory::createInflectSelectorFactory() { - return new InflectionSelectorFactory(); -} - -const inflection::dialog::CommonConceptFactory* GetCommonConceptFactory(const Locale& locale) { - return ::inflection::dialog::LocalizedCommonConceptFactoryProvider - ::getDefaultCommonConceptFactoryProvider() - ->getCommonConceptFactory(inflection::util::ULocale(locale.getName())); +const inflection::dialog::CommonConceptFactory* GetCommonConceptFactory(const Locale& locale) +{ + return ::inflection::dialog::LocalizedCommonConceptFactoryProvider::getDefaultCommonConceptFactoryProvider()->getCommonConceptFactory( + inflection::util::ULocale(locale.getName())); } -const inflection::dialog::SemanticFeatureModel* GetSemanticFeatureModel(const Locale& locale) { +const inflection::dialog::SemanticFeatureModel* GetSemanticFeatureModel(const Locale& locale) +{ return GetCommonConceptFactory(locale)->getSemanticFeatureModel(); } -class InflectionFormatter : public Formatter { -public: - FormattedPlaceholder format(FormattedPlaceholder&&, FunctionOptions&& opts, - UErrorCode& errorCode) const override; - InflectionFormatter(const inflection::dialog::SemanticFeatureModel* model, - MF2Factory::OutputMode mode) - : model(model), mode(mode) { - } -private: - const ::inflection::dialog::SemanticFeatureModel* model; - MF2Factory::OutputMode mode; -}; - -Formatter* InflectionFormatterFactory::createFormatter(const Locale& locale, UErrorCode& errorCode) +// Builds the SpeakableString for the numeral per style/variant (mirrors FormatTest::formatNumber). +static inflection::dialog::SpeakableString numeralToSpeakable(const inflection::dialog::NumberConcept& num, const std::u16string& style, + const std::u16string& variant, bool hasVariant, UErrorCode& errorCode) { - if (U_FAILURE(errorCode)) { - return nullptr; + if (style.empty()) { + std::unique_ptr s(num.toSpeakableString()); + return *s; } - - Formatter* result = new InflectionFormatter(GetSemanticFeatureModel(locale), mode); - if (result == nullptr) { - errorCode = U_MEMORY_ALLOCATION_ERROR; + if (style == STYLE_AS_WORDS) { + return SpeakableString(hasVariant ? num.asWords(variant) : num.getAsWords()); } - return result; + if (style == STYLE_AS_SPOKEN_WORDS) { + return hasVariant ? num.asSpokenWords(variant) : num.getAsSpokenWords(); + } + if (style == STYLE_AS_DIGITS) { + if (hasVariant) { + return SpeakableString(num.asDigits(variant)); + } + return num.getAsDigits(); + } + if (style == STYLE_AS_ORDINAL_DIGITS) { + return SpeakableString(hasVariant ? num.asDigits(variant) : num.getAsOrdinalDigits()); + } + errorCode = U_MF_FORMATTING_ERROR; + return SpeakableString(u""); } -FormattedPlaceholder InflectionFormatter::format( - FormattedPlaceholder&& arg, FunctionOptions&& options, - UErrorCode& errorCode) const +// Operand -> PronounConcept. String = a pronoun seed (PronounConcept(model, seed) THROWS on a bad +// seed, so it is caught and mapped). +static std::unique_ptr pronounFromOperand( + const Formattable& operand, const inflection::dialog::SemanticFeatureModel* formatterModel, UErrorCode& errorCode) { - if (U_FAILURE(errorCode)) { - return {}; + if (operand.getType() == UFMT_STRING) { + UnicodeString s = operand.getString(errorCode); + if (U_FAILURE(errorCode)) { + return nullptr; + } + try { + return std::make_unique( + *formatterModel, std::u16string_view(s.getBuffer(), static_cast(s.length()))); + } catch (const std::exception& e) { + inflection::util::TypeConversionUtils::convert(e, &errorCode); + return nullptr; + } } - - // Argument must be present - if (!arg.canFormat()) { + if (operand.getType() == UFMT_OBJECT) { + const icu::message2::FormattableObject* obj = operand.getObject(errorCode); + if (obj != nullptr && obj->tag() == UnicodeString(SemanticFeatureConceptArgument::TAG)) { + const auto* arg = static_cast(obj); + try { + // The carried concept must be a PronounConcept for :pronoun. + const auto* pronoun + = inflection::exception::ClassCastException::verifyCast(arg->getConcept()); + return std::unique_ptr(pronoun->clone()); + } catch (const std::exception& e) { + inflection::util::TypeConversionUtils::convert(e, &errorCode); + return nullptr; + } + } errorCode = U_MF_FORMATTING_ERROR; - return FormattedPlaceholder(FN_INFLECTION); + return nullptr; } + errorCode = U_MF_FORMATTING_ERROR; + return nullptr; +} - // Assumes the argument is not-yet-formatted - const Formattable& toFormat = arg.asFormattable(); - UnicodeString result; - - UErrorCode conceptCode = U_ZERO_ERROR; - auto conceptObj = conceptFromOperand(toFormat, model, conceptCode); - if (conceptObj != nullptr) { - UnicodeString inputString; - if (toFormat.getType() == UFMT_STRING) { - inputString = toFormat.getString(errorCode); - } - auto opt = options.getOptions(); - for (const auto& [key, value] : opt) { - if (key == UnicodeString(OPTION_TO)) { - continue; // control option (the projection feature), not a grammeme constraint - } - auto constraint = conceptObj->getModel()->getFeature(key); - if (constraint == nullptr) { - // A bare option must name a known grammatical feature. - errorCode = U_MF_FORMATTING_ERROR; - return FormattedPlaceholder(FN_INFLECTION); - } - conceptObj->putConstraint(*constraint, resolveConstraintValue(model, *constraint, value, errorCode)); - } - std::unique_ptr speakable; - if (opt.contains(OPTION_TO)) { - auto feature = conceptObj->getModel()->getFeature(opt.at(OPTION_TO).getString(errorCode)); - // Unknown to= feature: fall back to the uninflected form (intentional, not an error). - if (feature != nullptr) { - speakable.reset(conceptObj->getFeatureValue(*feature)); - } - } else { - speakable.reset(conceptObj->toSpeakableString()); - } - if (speakable != nullptr) { - result += emit(*speakable, mode); +class InflectionFunctionValue : public FunctionValue { +public: + InflectionFunctionValue(const FunctionContext& context, const FunctionValue& operand, const FunctionOptions& options, + UnicodeString formattedVal, UnicodeString selectVal, bool selectable, UErrorCode& errorCode) + : formatted(std::move(formattedVal)) + , selected(std::move(selectVal)) + , canSelect(selectable) + { + innerValue = Formattable(formatted); + locale = context.getLocale(); + inputDir = context.getDirection(); + if (inputDir == icu::U_MF_BIDI_OPTION_LTR) { + dir = icu::U_MF_DIRECTIONALITY_LTR; + } else if (inputDir == icu::U_MF_BIDI_OPTION_RTL) { + dir = icu::U_MF_DIRECTIONALITY_RTL; + } else if (inputDir == icu::U_MF_BIDI_OPTION_AUTO || inputDir == icu::U_MF_BIDI_OPTION_INHERIT) { + dir = locale.isRightToLeft() ? icu::U_MF_DIRECTIONALITY_RTL : icu::U_MF_DIRECTIONALITY_LTR; } else { - // conceptObj return nullptr When the constrained values is an empty set - // Fallback to the inputString, same as the non-string case. - result += emit(inflection::dialog::SpeakableString(inputString), mode); + dir = icu::U_MF_DIRECTIONALITY_UNKNOWN; } - } else { - result += toFormat.getString(errorCode); + opts = options.mergeOptions(operand.getResolvedOptions(), errorCode); + functionName = context.getCalledFunctionName(); + fallback = operand.getFallback(); } - return FormattedPlaceholder(arg, FormattedValue(std::move(result))); -} + ~InflectionFunctionValue() override; -class InflectionSelector : public Selector { -public: - void selectKey(FormattedPlaceholder &&arg, FunctionOptions &&options, - const UnicodeString *keys, int32_t keysLen, - UnicodeString *prefs, int32_t &prefsLen, - UErrorCode &errorCode) const override; + UnicodeString formatToString(UErrorCode& status) const override + { + if (U_FAILURE(status)) { + return { }; + } + return formatted; + } + + UBool isSelectable() const override { return canSelect; } - explicit InflectionSelector(const inflection::dialog::SemanticFeatureModel* model) - : model(model) { + void selectKeys(const UnicodeString* keys, int32_t keysLen, int32_t* prefs, int32_t& prefsLen, UErrorCode& status) const override + { + if (U_FAILURE(status)) { + return; + } + if (!canSelect) { + status = U_MF_SELECTOR_ERROR; + return; + } + prefsLen = 0; + for (int32_t i = 0; i < keysLen; i++) { + if (selected == keys[i]) { + prefs[prefsLen++] = i; + } + } } private: - const ::inflection::dialog::SemanticFeatureModel* model; + UnicodeString formatted { }; + UnicodeString selected { }; + bool canSelect = false; }; -Selector* InflectionSelectorFactory::createSelector( - const Locale& locale, UErrorCode& errorCode) const { - if (U_FAILURE(errorCode)) { - return nullptr; - } +InflectionFunctionValue::~InflectionFunctionValue() = default; - Selector* result = new InflectionSelector(GetSemanticFeatureModel(locale)); - if (result == nullptr) { +static LocalPointer formattingError(UErrorCode& errorCode) +{ + errorCode = U_MF_FORMATTING_ERROR; + return LocalPointer(nullptr); +} + +static LocalPointer makeFunctionValue(const FunctionContext& context, const FunctionValue& operand, + const FunctionOptions& options, UnicodeString formattedVal, UnicodeString selectVal, bool selectable, UErrorCode& errorCode) +{ + LocalPointer result( + new InflectionFunctionValue(context, operand, options, std::move(formattedVal), std::move(selectVal), selectable, errorCode)); + if (!result.isValid()) { errorCode = U_MEMORY_ALLOCATION_ERROR; } return result; } -void InflectionSelector::selectKey( - FormattedPlaceholder &&arg, FunctionOptions &&options, - const UnicodeString *keys, int32_t keysLen, - UnicodeString *prefs, int32_t &prefsLen, UErrorCode &errorCode) const { +class InflectionFunction : public Function { +public: + explicit InflectionFunction(MF2Factory::OutputMode mode) + : mode(mode) + { + } + ~InflectionFunction() override = default; + LocalPointer call( + const FunctionContext& context, const FunctionValue& operand, const FunctionOptions& options, UErrorCode& errorCode) const override; + +private: + MF2Factory::OutputMode mode; +}; + +Function* MF2Factory::createInflectFunction(OutputMode mode) { return new InflectionFunction(mode); } + +LocalPointer InflectionFunction::call( + const FunctionContext& context, const FunctionValue& operand, const FunctionOptions& options, UErrorCode& errorCode) const +{ if (U_FAILURE(errorCode)) { - return; + return LocalPointer(nullptr); + } + if (operand.isNullOperand()) { + return formattingError(errorCode); } - // Argument must be present - if (!arg.canFormat()) { - errorCode = U_MF_SELECTOR_ERROR; - return; + const Locale& locale = context.getLocale(); + const auto* model = GetSemanticFeatureModel(locale); + const Formattable& toFormat = operand.unwrap(); + + UErrorCode localStatus = U_ZERO_ERROR; + auto conceptObj = conceptFromOperand(toFormat, model, localStatus); + if (conceptObj == nullptr || U_FAILURE(localStatus)) { + return formattingError(errorCode); } - // Assumes the argument is not-yet-formatted - const Formattable& toFormat = arg.asFormattable(); - prefsLen = 0; - auto conceptObj = conceptFromOperand(toFormat, model, errorCode); - if (conceptObj == nullptr) { errorCode = U_MF_SELECTOR_ERROR; return; } - auto opt = options.getOptions(); - for (const auto& [key, value] : opt) { + UnicodeString inputString; + if (toFormat.getType() == UFMT_STRING) { + inputString = toFormat.getString(localStatus); + } + const auto& opt = options.getOptions(); + for (const auto& [key, valPtr] : opt) { if (key == UnicodeString(OPTION_TO)) { - continue; // control option (the projection feature), not a grammeme constraint + continue; // control option (the projection feature), not a grammeme constraint } - auto feature = conceptObj->getModel()->getFeature(key); - if (feature == nullptr) { + auto constraint = conceptObj->getModel()->getFeature(key); + if (constraint == nullptr) { // A bare option must name a known grammatical feature. - errorCode = U_MF_SELECTOR_ERROR; - return; + return formattingError(errorCode); + } + conceptObj->putConstraint(*constraint, resolveConstraintValue(model, *constraint, valPtr->unwrap(), localStatus)); + if (U_FAILURE(localStatus)) { + return formattingError(errorCode); } - conceptObj->putConstraint(*feature, resolveConstraintValue(model, *feature, value, errorCode)); } - UnicodeString feature; + std::unique_ptr speakable; if (opt.contains(OPTION_TO)) { - auto value = conceptObj->getModel()->getFeature(opt.at(OPTION_TO).getString(errorCode)); - // Unknown to= feature: leave projected value empty — matches only catch-all (intentional, not an error). - if (value != nullptr) { - std::unique_ptr result(conceptObj->getFeatureValue(*value)); - if (result != nullptr) { - feature = result->getPrint(); - } + const UnicodeString& toName = opt.at(OPTION_TO)->unwrap().getString(localStatus); + if (U_FAILURE(localStatus)) { + return formattingError(errorCode); } - } else { - std::unique_ptr ss(conceptObj->toSpeakableString()); - if (ss != nullptr) { - feature = ss->getPrint(); + auto feature = conceptObj->getModel()->getFeature(toName); + if (feature != nullptr) { + speakable.reset(conceptObj->getFeatureValue(*feature)); } + // Unknown to= feature: fall back to the uninflected form / match only catch-all + // (intentional, not an error). + } else { + speakable.reset(conceptObj->toSpeakableString()); } - for (int i = 0; i < keysLen; i++) { - if (feature == keys[i]) { - prefs[prefsLen++] = keys[i]; - } + UnicodeString formatted; + UnicodeString selected; + if (speakable != nullptr) { + formatted = emit(*speakable, mode); + selected = speakable->getPrint(); + } else { + // conceptObj returns nullptr when the constrained values are an empty set. + // Fallback to the inputString, same as the non-string case. + formatted = emit(inflection::dialog::SpeakableString(inputString), mode); } + return makeFunctionValue(context, operand, options, std::move(formatted), std::move(selected), true, errorCode); } -class NumeralFormatter : public Formatter { +class NumeralFunction : public Function { public: - FormattedPlaceholder format(FormattedPlaceholder&&, FunctionOptions&&, - UErrorCode&) const override; - NumeralFormatter(const inflection::util::ULocale& locale, MF2Factory::OutputMode mode) - : locale(locale), mode(mode) {} -private: - inflection::util::ULocale locale; - MF2Factory::OutputMode mode; -}; - -class NumeralFormatterFactory : public FormatterFactory { -public: - explicit NumeralFormatterFactory(MF2Factory::OutputMode mode) : mode(mode) {} - Formatter* createFormatter(const Locale& loc, UErrorCode& ec) override { - if (U_FAILURE(ec)) { - return nullptr; - } - return new NumeralFormatter(inflection::util::ULocale(loc.getName()), mode); + explicit NumeralFunction(MF2Factory::OutputMode mode) + : mode(mode) + { } - - ~NumeralFormatterFactory() override; + ~NumeralFunction() override = default; + LocalPointer call( + const FunctionContext& context, const FunctionValue& operand, const FunctionOptions& options, UErrorCode& errorCode) const override; private: MF2Factory::OutputMode mode; }; -NumeralFormatterFactory::~NumeralFormatterFactory() = default; +Function* MF2Factory::createNumeralFunction(OutputMode mode) { return new NumeralFunction(mode); } -icu::message2::FormatterFactory* MF2Factory::createNumeralFormatterFactory(MF2Factory::OutputMode mode) { - return new NumeralFormatterFactory(mode); -} - -// Builds the SpeakableString for the numeral per style/variant (mirrors FormatTest::formatNumber). -static inflection::dialog::SpeakableString -numeralToSpeakable(const inflection::dialog::NumberConcept& num, - const std::u16string& style, const std::u16string& variant, - bool hasVariant, UErrorCode& errorCode) { - if (style.empty()) { - std::unique_ptr s(num.toSpeakableString()); - return *s; - } - if (style == STYLE_AS_WORDS) { - return SpeakableString(hasVariant ? num.asWords(variant) : num.getAsWords()); - } - if (style == STYLE_AS_SPOKEN_WORDS) { - return hasVariant ? num.asSpokenWords(variant) : num.getAsSpokenWords(); - } - if (style == STYLE_AS_DIGITS) { - if (hasVariant) { - return SpeakableString(num.asDigits(variant)); - } - return num.getAsDigits(); - } - if (style == STYLE_AS_ORDINAL_DIGITS) { - return SpeakableString(hasVariant ? num.asDigits(variant) : num.getAsOrdinalDigits()); - } - errorCode = U_MF_FORMATTING_ERROR; - return SpeakableString(u""); -} - -FormattedPlaceholder NumeralFormatter::format( - FormattedPlaceholder&& arg, FunctionOptions&& options, - UErrorCode& errorCode) const +LocalPointer NumeralFunction::call( + const FunctionContext& context, const FunctionValue& operand, const FunctionOptions& options, UErrorCode& errorCode) const { if (U_FAILURE(errorCode)) { - return {}; + return LocalPointer(nullptr); } - if (!arg.canFormat()) { - errorCode = U_MF_FORMATTING_ERROR; - return FormattedPlaceholder(FN_NUMERAL); + if (operand.isNullOperand()) { + return formattingError(errorCode); } - const Formattable &toFormat = arg.asFormattable(); - + inflection::util::ULocale locale(context.getLocale().getName()); + const Formattable& toFormat = operand.unwrap(); + UErrorCode localStatus = U_ZERO_ERROR; std::unique_ptr num; if (toFormat.getType() == UFMT_DOUBLE) { - num = std::make_unique( - toFormat.getDouble(errorCode), locale, locale); + num = std::make_unique(toFormat.getDouble(localStatus), locale, locale); + } else { + num = std::make_unique(toFormat.getInt64(localStatus), locale, locale); } - else { - num = std::make_unique( - toFormat.getInt64(errorCode), locale, locale); + if (U_FAILURE(localStatus)) { + return formattingError(errorCode); } std::u16string style; std::u16string variant; bool hasVariant = false; - auto opt = options.getOptions(); + const auto& opt = options.getOptions(); if (opt.contains(OPTION_WITH_STYLE)) { - const UnicodeString &v = opt.at(OPTION_WITH_STYLE).getString(errorCode); + const UnicodeString& v = opt.at(OPTION_WITH_STYLE)->unwrap().getString(localStatus); style = std::u16string(v.getBuffer(), v.length()); } if (opt.contains(OPTION_WITH_VARIANT)) { - const UnicodeString &v = opt.at(OPTION_WITH_VARIANT).getString(errorCode); + const UnicodeString& v = opt.at(OPTION_WITH_VARIANT)->unwrap().getString(localStatus); variant = std::u16string(v.getBuffer(), v.length()); hasVariant = true; } + if (U_FAILURE(localStatus)) { + return formattingError(errorCode); + } - inflection::dialog::SpeakableString result = - numeralToSpeakable(*num, style, variant, hasVariant, errorCode); - if (U_FAILURE(errorCode)) { - return FormattedPlaceholder(FN_NUMERAL); + inflection::dialog::SpeakableString result = numeralToSpeakable(*num, style, variant, hasVariant, localStatus); + if (U_FAILURE(localStatus)) { + return formattingError(errorCode); } UnicodeString out = emit(result, mode); - return FormattedPlaceholder(arg, FormattedValue(std::move(out))); + return makeFunctionValue(context, operand, options, std::move(out), { }, false, errorCode); } -class QuantifyFormatter : public Formatter { +class QuantifyFunction : public Function { public: - FormattedPlaceholder format(FormattedPlaceholder&&, FunctionOptions&&, UErrorCode&) const override; - QuantifyFormatter(const inflection::dialog::SemanticFeatureModel* model, - const inflection::dialog::CommonConceptFactory* ccf, - const inflection::util::ULocale& locale, MF2Factory::OutputMode mode) - : model(model), ccf(ccf), locale(locale), mode(mode) {} -private: - const inflection::dialog::SemanticFeatureModel* model; - const inflection::dialog::CommonConceptFactory* ccf; - inflection::util::ULocale locale; - MF2Factory::OutputMode mode; -}; + explicit QuantifyFunction(MF2Factory::OutputMode mode) + : mode(mode) + { + } + ~QuantifyFunction() override = default; + LocalPointer call( + const FunctionContext& context, const FunctionValue& operand, const FunctionOptions& options, UErrorCode& errorCode) const override; -class QuantifyFormatterFactory : public FormatterFactory { -public: - explicit QuantifyFormatterFactory(MF2Factory::OutputMode mode) : mode(mode) {} - Formatter* createFormatter(const Locale&, UErrorCode&) override; private: MF2Factory::OutputMode mode; }; -Formatter* QuantifyFormatterFactory::createFormatter(const Locale& loc, UErrorCode& ec) { - if (U_FAILURE(ec)) { - return nullptr; - } - return new QuantifyFormatter(GetSemanticFeatureModel(loc), GetCommonConceptFactory(loc), - inflection::util::ULocale(loc.getName()), mode); -} - -icu::message2::FormatterFactory* MF2Factory::createQuantifyFormatterFactory(OutputMode mode) { - return new QuantifyFormatterFactory(mode); -} +Function* MF2Factory::createQuantifyFunction(OutputMode mode) { return new QuantifyFunction(mode); } -FormattedPlaceholder QuantifyFormatter::format(FormattedPlaceholder&& arg, FunctionOptions&& options, UErrorCode& errorCode) const { +LocalPointer QuantifyFunction::call( + const FunctionContext& context, const FunctionValue& operand, const FunctionOptions& options, UErrorCode& errorCode) const +{ if (U_FAILURE(errorCode)) { - return {}; + return LocalPointer(nullptr); } - if (!arg.canFormat()) { - errorCode = U_MF_FORMATTING_ERROR; - return FormattedPlaceholder(FN_QUANTIFY); + if (operand.isNullOperand()) { + return formattingError(errorCode); } - std::unique_ptr unit = - conceptFromOperand(arg.asFormattable(), model, errorCode); - if (unit == nullptr) { - return FormattedPlaceholder(FN_QUANTIFY); + const Locale& icuLoc = context.getLocale(); + const auto* model = GetSemanticFeatureModel(icuLoc); + const auto* ccf = GetCommonConceptFactory(icuLoc); + inflection::util::ULocale locale(icuLoc.getName()); + + UErrorCode localStatus = U_ZERO_ERROR; + std::unique_ptr unit = conceptFromOperand(operand.unwrap(), model, localStatus); + if (unit == nullptr || U_FAILURE(localStatus)) { + return formattingError(errorCode); } - auto opt = options.getOptions(); + const auto& opt = options.getOptions(); if (!opt.contains(OPTION_WITH_VALUE)) { - errorCode = U_MF_FORMATTING_ERROR; - return FormattedPlaceholder(FN_QUANTIFY); + return formattingError(errorCode); } if (opt.contains(OPTION_WITH_STYLE) || opt.contains(OPTION_WITH_VARIANT)) { // reserved, not implemented, but perhaps it will be used in the future. - errorCode = U_MF_FORMATTING_ERROR; - return FormattedPlaceholder(FN_QUANTIFY); + return formattingError(errorCode); } - const Formattable &valueF = opt.at(OPTION_WITH_VALUE); + const Formattable& valueF = opt.at(OPTION_WITH_VALUE)->unwrap(); std::unique_ptr num; if (valueF.getType() == UFMT_DOUBLE) { - num = std::make_unique(valueF.getDouble(errorCode), locale, locale); - } - else { + num = std::make_unique(valueF.getDouble(localStatus), locale, locale); + } else { // Integer-valued args (UFMT_INT64/UFMT_LONG) take this branch; only genuine doubles take the branch above. - num = std::make_unique(valueF.getInt64(errorCode), locale, locale); + num = std::make_unique(valueF.getInt64(localStatus), locale, locale); + } + if (U_FAILURE(localStatus)) { + return formattingError(errorCode); } - for (const auto &entry: opt) { - const UnicodeString &key = entry.first; - // Control options (exact allow-list, NOT a with-prefix test: some real - // semantic features are with-prefixed). withValue consumed above; - // withStyle/withVariant rejected above. + // Control options (exact allow-list, NOT a with-prefix test: some real semantic features + // are with-prefixed). withValue consumed above; withStyle/withVariant rejected above. + for (const auto& entry : opt) { + const UnicodeString& key = entry.first; if (key == UnicodeString(OPTION_WITH_VALUE) || key == UnicodeString(OPTION_WITH_STYLE) || key == UnicodeString(OPTION_WITH_VARIANT)) { continue; } std::u16string name(key.getBuffer(), key.length()); - const auto *feature = unit->getModel()->getFeature(name); + const auto* feature = unit->getModel()->getFeature(name); if (feature == nullptr) { - // unknown feature (mirror FormatTest strictness) - errorCode = U_MF_FORMATTING_ERROR; - return FormattedPlaceholder(FN_QUANTIFY); + return formattingError(errorCode); // unknown feature (mirror FormatTest strictness) + } + const UnicodeString& v = entry.second->unwrap().getString(localStatus); + if (U_FAILURE(localStatus)) { + return formattingError(errorCode); } - const UnicodeString &v = entry.second.getString(errorCode); unit->putConstraint(*feature, std::u16string(v.getBuffer(), v.length())); } std::unique_ptr result(ccf->quantify(*num, unit.get())); if (result == nullptr) { - errorCode = U_MF_FORMATTING_ERROR; - return FormattedPlaceholder(FN_QUANTIFY); + return formattingError(errorCode); } UnicodeString out = emit(*result, mode); - return FormattedPlaceholder(arg, FormattedValue(std::move(out))); + return makeFunctionValue(context, operand, options, std::move(out), { }, false, errorCode); } -class ListFormatter : public Formatter { -public: - FormattedPlaceholder format(FormattedPlaceholder&&, FunctionOptions&&, UErrorCode&) const override; - ListFormatter(const inflection::dialog::SemanticFeatureModel* model, - const inflection::dialog::CommonConceptFactory* ccf, MF2Factory::OutputMode mode) - : model(model), ccf(ccf), mode(mode) {} -private: - const inflection::dialog::SemanticFeatureModel* model; - const inflection::dialog::CommonConceptFactory* ccf; - MF2Factory::OutputMode mode; -}; - -class ListFormatterFactory : public FormatterFactory { +class ListFunction : public Function { public: - explicit ListFormatterFactory(MF2Factory::OutputMode mode) : mode(mode) { + explicit ListFunction(MF2Factory::OutputMode mode) + : mode(mode) + { } - - Formatter *createFormatter(const Locale &, UErrorCode &) override; + ~ListFunction() override = default; + LocalPointer call( + const FunctionContext& context, const FunctionValue& operand, const FunctionOptions& options, UErrorCode& errorCode) const override; private: MF2Factory::OutputMode mode; }; -Formatter *ListFormatterFactory::createFormatter(const Locale &loc, UErrorCode &ec) { - if (U_FAILURE(ec)) { - return nullptr; - } - return new ListFormatter(GetSemanticFeatureModel(loc), GetCommonConceptFactory(loc), mode); -} - -icu::message2::FormatterFactory *MF2Factory::createListFormatterFactory(OutputMode mode) { - return new ListFormatterFactory(mode); -} +Function* MF2Factory::createListFunction(OutputMode mode) { return new ListFunction(mode); } -FormattedPlaceholder ListFormatter::format(FormattedPlaceholder&& arg, FunctionOptions&& options, UErrorCode& errorCode) const { +LocalPointer ListFunction::call( + const FunctionContext& context, const FunctionValue& operand, const FunctionOptions& options, UErrorCode& errorCode) const +{ if (U_FAILURE(errorCode)) { - return {}; + return LocalPointer(nullptr); } - if (!arg.canFormat()) { - errorCode = U_MF_FORMATTING_ERROR; - return FormattedPlaceholder(FN_LIST); + if (operand.isNullOperand()) { + return formattingError(errorCode); } - const Formattable& arrF = arg.asFormattable(); + const Locale& icuLoc = context.getLocale(); + const auto* model = GetSemanticFeatureModel(icuLoc); + const auto* ccf = GetCommonConceptFactory(icuLoc); + + const Formattable& arrF = operand.unwrap(); if (arrF.getType() != UFMT_ARRAY) { - errorCode = U_MF_FORMATTING_ERROR; - return FormattedPlaceholder(FN_LIST); + return formattingError(errorCode); } + UErrorCode localStatus = U_ZERO_ERROR; int32_t count = 0; - const Formattable* elems = arrF.getArray(count, errorCode); - if (U_FAILURE(errorCode)) { - return FormattedPlaceholder(FN_LIST); + const Formattable* elems = arrF.getArray(count, localStatus); + if (U_FAILURE(localStatus)) { + return formattingError(errorCode); } - auto opt = options.getOptions(); + const auto& opt = options.getOptions(); - std::vector > items; + std::vector> items; std::vector itemPtrs; for (int32_t i = 0; i < count; i++) { - UnicodeString elemStr = elems[i].getString(errorCode); - if (U_FAILURE(errorCode)) { - return FormattedPlaceholder(FN_LIST); + UnicodeString elemStr = elems[i].getString(localStatus); + if (U_FAILURE(localStatus)) { + return formattingError(errorCode); } - items.push_back(std::make_unique( - model, inflection::dialog::SpeakableString(elemStr))); + items.push_back( + std::make_unique(model, inflection::dialog::SpeakableString(elemStr))); itemPtrs.push_back(items.back().get()); } std::unique_ptr list; if (!opt.contains(OPTION_WITH_TYPE)) { list = std::make_unique(model, itemPtrs); - } - else { - const UnicodeString& typeV = opt.at(OPTION_WITH_TYPE).getString(errorCode); + } else { + const UnicodeString& typeV = opt.at(OPTION_WITH_TYPE)->unwrap().getString(localStatus); + if (U_FAILURE(localStatus)) { + return formattingError(errorCode); + } std::u16string typeVal(typeV.getBuffer(), typeV.length()); if (typeVal == TYPE_AND) { list.reset(ccf->createAndList(itemPtrs)); - } - else if (typeVal == TYPE_OR) { + } else if (typeVal == TYPE_OR) { list.reset(ccf->createOrList(itemPtrs)); - } - else { - errorCode = U_MF_FORMATTING_ERROR; - return FormattedPlaceholder(FN_LIST); + } else { + return formattingError(errorCode); } } if (list == nullptr) { - errorCode = U_MF_FORMATTING_ERROR; - return FormattedPlaceholder(FN_LIST); + return formattingError(errorCode); } // Apply separator-setter options. if (opt.contains(OPTION_WITH_BEFORE_FIRST)) { - list->setBeforeFirst(inflection::dialog::SpeakableString(opt.at(OPTION_WITH_BEFORE_FIRST).getString(errorCode))); + list->setBeforeFirst(inflection::dialog::SpeakableString(opt.at(OPTION_WITH_BEFORE_FIRST)->unwrap().getString(localStatus))); } if (opt.contains(OPTION_WITH_AFTER_FIRST)) { - list->setAfterFirst(inflection::dialog::SpeakableString(opt.at(OPTION_WITH_AFTER_FIRST).getString(errorCode))); + list->setAfterFirst(inflection::dialog::SpeakableString(opt.at(OPTION_WITH_AFTER_FIRST)->unwrap().getString(localStatus))); } if (opt.contains(OPTION_WITH_ITEM_DELIMITER)) { - list->setItemDelimiter(inflection::dialog::SpeakableString(opt.at(OPTION_WITH_ITEM_DELIMITER).getString(errorCode))); + list->setItemDelimiter(inflection::dialog::SpeakableString(opt.at(OPTION_WITH_ITEM_DELIMITER)->unwrap().getString(localStatus))); } if (opt.contains(OPTION_WITH_BEFORE_LAST)) { - list->setBeforeLast(inflection::dialog::SpeakableString(opt.at(OPTION_WITH_BEFORE_LAST).getString(errorCode))); + list->setBeforeLast(inflection::dialog::SpeakableString(opt.at(OPTION_WITH_BEFORE_LAST)->unwrap().getString(localStatus))); } if (opt.contains(OPTION_WITH_AFTER_LAST)) { - list->setAfterLast(inflection::dialog::SpeakableString(opt.at(OPTION_WITH_AFTER_LAST).getString(errorCode))); + list->setAfterLast(inflection::dialog::SpeakableString(opt.at(OPTION_WITH_AFTER_LAST)->unwrap().getString(localStatus))); } if (opt.contains(OPTION_WITH_ITEM_PREFIX)) { - list->setItemPrefix(inflection::dialog::SpeakableString(opt.at(OPTION_WITH_ITEM_PREFIX).getString(errorCode))); + list->setItemPrefix(inflection::dialog::SpeakableString(opt.at(OPTION_WITH_ITEM_PREFIX)->unwrap().getString(localStatus))); } if (opt.contains(OPTION_WITH_ITEM_SUFFIX)) { - list->setItemSuffix(inflection::dialog::SpeakableString(opt.at(OPTION_WITH_ITEM_SUFFIX).getString(errorCode))); + list->setItemSuffix(inflection::dialog::SpeakableString(opt.at(OPTION_WITH_ITEM_SUFFIX)->unwrap().getString(localStatus))); } if (opt.contains(OPTION_WITH_AVOID_ITEM_AFFIX_REDUNDANCY)) { - const UnicodeString& v = opt.at(OPTION_WITH_AVOID_ITEM_AFFIX_REDUNDANCY).getString(errorCode); + const UnicodeString& v = opt.at(OPTION_WITH_AVOID_ITEM_AFFIX_REDUNDANCY)->unwrap().getString(localStatus); list->setAvoidItemAffixRedundancy(std::u16string(v.getBuffer(), static_cast(v.length())) == VALUE_TRUE); } + if (U_FAILURE(localStatus)) { + return formattingError(errorCode); + } + // Apply grammeme constraints (options not in the control allow-list). static constexpr std::u16string_view LIST_CONTROL[] = { // This must be in sorted order. @@ -700,248 +660,106 @@ FormattedPlaceholder ListFormatter::format(FormattedPlaceholder&& arg, FunctionO }; for (const auto& entry : opt) { std::u16string_view nameView(entry.first.getBuffer(), static_cast(entry.first.length())); - if (inflection::util::ArrayUtils::contains(nameView)) { // control options (exact allow-list) - continue; + if (inflection::util::ArrayUtils::contains(nameView)) { + continue; // control options (exact allow-list) } const auto* feature = model->getFeature(nameView); if (feature == nullptr) { - // unknown feature (mirror FormatTest/:quantify strictness) - errorCode = U_MF_FORMATTING_ERROR; - return FormattedPlaceholder(FN_LIST); + return formattingError(errorCode); // unknown feature (mirror FormatTest/:quantify strictness) + } + const UnicodeString& v = entry.second->unwrap().getString(localStatus); + if (U_FAILURE(localStatus)) { + return formattingError(errorCode); } - const UnicodeString& v = entry.second.getString(errorCode); list->putConstraint(*feature, std::u16string(v.getBuffer(), v.length())); } std::unique_ptr result(list->toSpeakableString()); if (result == nullptr) { - errorCode = U_MF_FORMATTING_ERROR; - return FormattedPlaceholder(FN_LIST); + return formattingError(errorCode); } UnicodeString out = emit(*result, mode); - return FormattedPlaceholder(arg, FormattedValue(std::move(out))); + return makeFunctionValue(context, operand, options, std::move(out), { }, false, errorCode); } -// Operand -> PronounConcept. String = a pronoun seed (PronounConcept(model, seed) THROWS on a bad -// seed, so it is caught and mapped). -static std::unique_ptr -pronounFromOperand(const Formattable& operand, - const inflection::dialog::SemanticFeatureModel* formatterModel, UErrorCode& errorCode) { - if (operand.getType() == UFMT_STRING) { - UnicodeString s = operand.getString(errorCode); - if (U_FAILURE(errorCode)) { - return nullptr; - } - try { - return std::make_unique( - *formatterModel, std::u16string_view(s.getBuffer(), static_cast(s.length()))); - } catch (const std::exception& e) { - inflection::util::TypeConversionUtils::convert(e, &errorCode); - return nullptr; - } - } - if (operand.getType() == UFMT_OBJECT) { - const icu::message2::FormattableObject* obj = operand.getObject(errorCode); - if (obj != nullptr && obj->tag() == UnicodeString(SemanticFeatureConceptArgument::TAG)) { - const auto* arg = static_cast(obj); - try { - // The carried concept must be a PronounConcept for :pronoun. - const auto* pronoun = inflection::exception::ClassCastException::verifyCast< - const inflection::dialog::PronounConcept*>(arg->getConcept()); - return std::unique_ptr(pronoun->clone()); - } catch (const std::exception& e) { - inflection::util::TypeConversionUtils::convert(e, &errorCode); - return nullptr; - } - } - errorCode = U_MF_FORMATTING_ERROR; - return nullptr; - } - errorCode = U_MF_FORMATTING_ERROR; - return nullptr; -} - -class PronounFormatter : public Formatter { +class PronounFunction : public Function { public: - FormattedPlaceholder format(FormattedPlaceholder&&, FunctionOptions&&, UErrorCode&) const override; - - PronounFormatter(const inflection::dialog::SemanticFeatureModel* model, MF2Factory::OutputMode mode) - : model(model), mode(mode) { - } - -private: - const inflection::dialog::SemanticFeatureModel* model{}; - MF2Factory::OutputMode mode; -}; - -class PronounFormatterFactory : public FormatterFactory { -public: - explicit PronounFormatterFactory(MF2Factory::OutputMode mode) : mode(mode) { + explicit PronounFunction(MF2Factory::OutputMode mode) + : mode(mode) + { } - - Formatter* createFormatter(const Locale&, UErrorCode&) override; + ~PronounFunction() override = default; + LocalPointer call( + const FunctionContext& context, const FunctionValue& operand, const FunctionOptions& options, UErrorCode& errorCode) const override; private: MF2Factory::OutputMode mode; }; -class PronounSelector : public Selector { -public: - void selectKey(FormattedPlaceholder&&, FunctionOptions&&, const UnicodeString*, int32_t, - UnicodeString*, int32_t&, UErrorCode&) const override; - - explicit PronounSelector(const inflection::dialog::SemanticFeatureModel* model) : model(model) { - } - -private: - const inflection::dialog::SemanticFeatureModel* model; -}; - -class PronounSelectorFactory : public SelectorFactory { -public: - Selector* createSelector(const Locale&, UErrorCode&) const override; -}; - -Formatter* PronounFormatterFactory::createFormatter(const Locale& loc, UErrorCode& ec) { - if (U_FAILURE(ec)) { - return nullptr; - } - return new PronounFormatter(GetSemanticFeatureModel(loc), mode); -} - -Selector* PronounSelectorFactory::createSelector(const Locale& loc, UErrorCode& ec) const { - if (U_FAILURE(ec)) { - return nullptr; - } - return new PronounSelector(GetSemanticFeatureModel(loc)); -} - -icu::message2::FormatterFactory* MF2Factory::createPronounFormatterFactory(OutputMode mode) { - return new PronounFormatterFactory(mode); -} - -icu::message2::SelectorFactory* MF2Factory::createPronounSelectorFactory() { - return new PronounSelectorFactory(); -} +Function* MF2Factory::createPronounFunction(OutputMode mode) { return new PronounFunction(mode); } -FormattedPlaceholder PronounFormatter::format(FormattedPlaceholder&& arg, FunctionOptions&& options, - UErrorCode& errorCode) const { +LocalPointer PronounFunction::call( + const FunctionContext& context, const FunctionValue& operand, const FunctionOptions& options, UErrorCode& errorCode) const +{ if (U_FAILURE(errorCode)) { - return {}; + return LocalPointer(nullptr); } - if (!arg.canFormat()) { - errorCode = U_MF_FORMATTING_ERROR; - return FormattedPlaceholder(FN_PRONOUN); + if (operand.isNullOperand()) { + return formattingError(errorCode); } - auto pronoun = pronounFromOperand(arg.asFormattable(), model, errorCode); - if (pronoun == nullptr) { - return FormattedPlaceholder(FN_PRONOUN); + const auto* model = GetSemanticFeatureModel(context.getLocale()); + UErrorCode localStatus = U_ZERO_ERROR; + auto pronoun = pronounFromOperand(operand.unwrap(), model, localStatus); + if (pronoun == nullptr || U_FAILURE(localStatus)) { + return formattingError(errorCode); } - auto opt = options.getOptions(); + const auto& opt = options.getOptions(); if (opt.contains(OPTION_TO) && opt.contains(OPTION_WITH_REFERENT)) { - errorCode = U_MF_FORMATTING_ERROR; - return FormattedPlaceholder(FN_PRONOUN); + return formattingError(errorCode); } - for (const auto& [key, value] : opt) { + for (const auto& [key, valPtr] : opt) { if (key == UnicodeString(OPTION_TO) || key == UnicodeString(OPTION_WITH_REFERENT)) { continue; } auto feature = pronoun->getModel()->getFeature(key); if (feature == nullptr) { - errorCode = U_MF_FORMATTING_ERROR; - return FormattedPlaceholder(FN_PRONOUN); + return formattingError(errorCode); + } + pronoun->putConstraint(*feature, resolveConstraintValue(model, *feature, valPtr->unwrap(), localStatus)); + if (U_FAILURE(localStatus)) { + return formattingError(errorCode); } - pronoun->putConstraint(*feature, resolveConstraintValue(model, *feature, value, errorCode)); } std::unique_ptr speakable; if (opt.contains(OPTION_TO)) { - auto feature = pronoun->getModel()->getFeature(opt.at(OPTION_TO).getString(errorCode)); - // Unknown to= feature: leave null -> fallback (intentional, not an error). + const UnicodeString& toName = opt.at(OPTION_TO)->unwrap().getString(localStatus); + if (U_FAILURE(localStatus)) { + return formattingError(errorCode); + } + auto feature = pronoun->getModel()->getFeature(toName); if (feature != nullptr) { speakable.reset(pronoun->getFeatureValue(*feature)); } - } - else if (opt.contains(OPTION_WITH_REFERENT)) { - auto referent = conceptFromOperand(opt.at(OPTION_WITH_REFERENT), model, errorCode); - if (referent == nullptr) { - return FormattedPlaceholder(FN_PRONOUN); + // Unknown to= feature: leave null -> fallback (intentional, not an error). + } else if (opt.contains(OPTION_WITH_REFERENT)) { + auto referent = conceptFromOperand(opt.at(OPTION_WITH_REFERENT)->unwrap(), model, localStatus); + if (referent == nullptr || U_FAILURE(localStatus)) { + return formattingError(errorCode); } speakable.reset(pronoun->toSpeakableString(*referent)); - } - else { + } else { speakable.reset(pronoun->toSpeakableString()); } - UnicodeString result; + UnicodeString formatted; + UnicodeString selected; if (speakable != nullptr) { - result += emit(*speakable, mode); - } - return FormattedPlaceholder(arg, FormattedValue(std::move(result))); -} - -void PronounSelector::selectKey(FormattedPlaceholder&& arg, FunctionOptions&& options, - const UnicodeString* keys, int32_t keysLen, - UnicodeString* prefs, int32_t& prefsLen, UErrorCode& errorCode) const { - if (U_FAILURE(errorCode)) { - return; - } - if (!arg.canFormat()) { - errorCode = U_MF_SELECTOR_ERROR; - return; - } - prefsLen = 0; - auto pronoun = pronounFromOperand(arg.asFormattable(), model, errorCode); - if (pronoun == nullptr) { - errorCode = U_MF_SELECTOR_ERROR; - return; - } - - auto opt = options.getOptions(); - if (opt.contains(OPTION_TO) && opt.contains(OPTION_WITH_REFERENT)) { - errorCode = U_MF_SELECTOR_ERROR; - return; - } - for (const auto& [key, value] : opt) { - if (key == UnicodeString(OPTION_TO) || key == UnicodeString(OPTION_WITH_REFERENT)) { - continue; - } - auto feature = pronoun->getModel()->getFeature(key); - if (feature == nullptr) { - errorCode = U_MF_SELECTOR_ERROR; - return; - } - pronoun->putConstraint(*feature, resolveConstraintValue(model, *feature, value, errorCode)); - } - - UnicodeString projected; - std::unique_ptr v; - if (opt.contains(OPTION_TO)) { - auto feature = pronoun->getModel()->getFeature(opt.at(OPTION_TO).getString(errorCode)); - if (feature != nullptr) { - v.reset(pronoun->getFeatureValue(*feature)); - } - } - else if (opt.contains(OPTION_WITH_REFERENT)) { - auto referent = conceptFromOperand(opt.at(OPTION_WITH_REFERENT), model, errorCode); - if (referent == nullptr) { - errorCode = U_MF_SELECTOR_ERROR; - return; - } - v.reset(pronoun->toSpeakableString(*referent)); - } - else { - v.reset(pronoun->toSpeakableString()); - } - if (v != nullptr) { - projected = v->getPrint(); - } - for (int i = 0; i < keysLen; i++) { - if (projected == keys[i]) { - prefs[prefsLen++] = keys[i]; - } + formatted = emit(*speakable, mode); + selected = speakable->getPrint(); } + return makeFunctionValue(context, operand, options, std::move(formatted), std::move(selected), true, errorCode); } } // namespace inflection::message2 diff --git a/src/inflection/message2/MF2Factory.hpp b/src/inflection/message2/MF2Factory.hpp index a622d0de1..6af329ded 100644 --- a/src/inflection/message2/MF2Factory.hpp +++ b/src/inflection/message2/MF2Factory.hpp @@ -9,22 +9,19 @@ #include /** - * @brief The MF2Factory provide factory method to create custom formatter and selector - * factory to work with icu::message2 library. - * The intend usage is to creat icu::message2::FormatterFactory - * or icu::message2::SelectorFactory and use it with - * icu::message2::MFFunctionRegistry::Builder - * to construct a custom function registry to build MessageFormatter + * @brief The MF2Factory provides factory methods to create custom functions + * to work with the icu::message2 library. + * The intended usage is to create an icu::message2::Function + * and use it with icu::message2::MFFunctionRegistry::Builder + * to construct a custom function registry to build a MessageFormatter * with MessageFormatter::Builder. * *

For example: *

*
  * auto customRegistry = icu::message2::MFFunctionRegistry::Builder(errorCode)
- *          .adoptFormatter(FunctionName("i:inflect"),
- *                          MF2Factory::createInflectFormatterFactory(), errorCode)
- *          .adoptSelector(FunctionName("i:inflect"),
- *                          MF2Factory::createInflectSelectorFactory(), errorCode)
+ *          .adoptFunction(FunctionName("i:inflect"),
+ *                         MF2Factory::createInflectFunction(), errorCode)
  *          .build();
  *
  * UParseError pe;
@@ -56,49 +53,38 @@
  */
 class INFLECTION_CLASS_API inflection::message2::MF2Factory {
 public:
-      /**
-       * Controls how formatters serialize their result:
-       * PLAIN emits the print form only; SSML emits Speech Synthesis
-       * Markup Language (SSML) markup — a <sub alias="..."> element
-       * when print differs from speak.
-       * @see https://www.w3.org/TR/speech-synthesis/
-       */
-      enum class OutputMode { PLAIN, SSML };
+    /**
+     * Controls how formatters serialize their result:
+     * PLAIN emits the print form only; SSML emits Speech Synthesis
+     * Markup Language (SSML) markup — a <sub alias="..."> element
+     * when print differs from speak.
+     * @see https://www.w3.org/TR/speech-synthesis/
+     */
+    enum class OutputMode { PLAIN, SSML };
 
-      /**
-       * Create an implementation of icu::message2::FormatterFactory*, based on the
-       * infleciton library, which can be passed to
-       * icu::messsage2::MFFunctionRegistry::Builder::adoptFormatter
-       * to register a custom formatter factory.
-       */
-      static icu::message2::FormatterFactory* createInflectFormatterFactory(OutputMode mode = OutputMode::PLAIN);
+    /**
+     * Create an implementation of icu::message2::Function*, based on the
+     * inflection library, which can be passed to
+     * icu::message2::MFFunctionRegistry::Builder::adoptFunction
+     * to register a custom function.
+     */
+    static icu::message2::Function* createInflectFunction(OutputMode mode = OutputMode::PLAIN);
 
-      /**
-       * Create an implementation of icu::message2::SelectorFactory*, based on the
-       * infleciton library, which can be passed to
-       * icu::messsage2::MFFunctionRegistry::Builder::adoptSelector
-       * to register a custom selector factory.
-       */
-      static icu::message2::SelectorFactory* createInflectSelectorFactory();
+    /**
+     * Create an implementation of icu::message2::Function* for the
+     * :i:numeral function (number formatting), based on the inflection library,
+     * which can be passed to
+     * icu::message2::MFFunctionRegistry::Builder::adoptFunction
+     * to register a custom function.
+     */
+    static icu::message2::Function* createNumeralFunction(OutputMode mode = OutputMode::PLAIN);
 
-      /**
-       * Create an implementation of icu::message2::FormatterFactory* for the
-       * :i:numeral function (number formatting), based on the inflection library,
-       * which can be passed to
-       * icu::messsage2::MFFunctionRegistry::Builder::adoptFormatter
-       * to register a custom formatter factory.
-       */
-      static icu::message2::FormatterFactory* createNumeralFormatterFactory(OutputMode mode = OutputMode::PLAIN);
+    /** Create a Function for the :i:quantify function (count + unit agreement). */
+    static icu::message2::Function* createQuantifyFunction(OutputMode mode = OutputMode::PLAIN);
 
-      /** Create a FormatterFactory for the :i:quantify function (count + unit agreement). */
-      static icu::message2::FormatterFactory* createQuantifyFormatterFactory(OutputMode mode = OutputMode::PLAIN);
+    /** Create a Function for the :i:list function (array → plain/and/or list). */
+    static icu::message2::Function* createListFunction(OutputMode mode = OutputMode::PLAIN);
 
-      /** Create a FormatterFactory for the :i:list function (array → plain/and/or list). */
-      static icu::message2::FormatterFactory* createListFormatterFactory(OutputMode mode = OutputMode::PLAIN);
-
-      /** Create a FormatterFactory for the :i:pronoun function. */
-      static icu::message2::FormatterFactory* createPronounFormatterFactory(OutputMode mode = OutputMode::PLAIN);
-
-      /** Create a SelectorFactory for the :i:pronoun function. */
-      static icu::message2::SelectorFactory* createPronounSelectorFactory();
+    /** Create a Function for the :i:pronoun function. */
+    static icu::message2::Function* createPronounFunction(OutputMode mode = OutputMode::PLAIN);
 };
diff --git a/test/resources/inflection/message2/en_US.xml b/test/resources/inflection/message2/en_US.xml
index e403b0900..0fa7487bd 100644
--- a/test/resources/inflection/message2/en_US.xml
+++ b/test/resources/inflection/message2/en_US.xml
@@ -125,7 +125,7 @@ plural {{The {$object} are on}}
         .local $v = {$word :i:inflect bogusFeature=value} .match $v
 * {{fallback}}
 
-        
+        
     
     
         
diff --git a/test/src/inflection/dialog/ListTest.cpp b/test/src/inflection/dialog/ListTest.cpp
index e2b458370..ba3961c01 100644
--- a/test/src/inflection/dialog/ListTest.cpp
+++ b/test/src/inflection/dialog/ListTest.cpp
@@ -720,9 +720,9 @@ TEST_CASE("ListTest#testListHi")
 
     REQUIRE(andList(factory, {u"1"}) == u"1");
     REQUIRE(andList(factory, {u"1", u"2"}) == u"1 और 2");
-    REQUIRE(andList(factory, {u"1", u"2", u"3"}) == u"1, 2, और 3");
-    REQUIRE(andList(factory, {u"1", u"2", u"3", u"4"}) == u"1, 2, 3, और 4");
-    REQUIRE(andList(factory, {u"Joe", u"John Junior", u"Jane", u"Joan"}) == u"Joe, John Junior, Jane, और Joan");
+    REQUIRE(andList(factory, { u"1", u"2", u"3" }) == u"1, 2 और 3");
+    REQUIRE(andList(factory, { u"1", u"2", u"3", u"4" }) == u"1, 2, 3 और 4");
+    REQUIRE(andList(factory, { u"Joe", u"John Junior", u"Jane", u"Joan" }) == u"Joe, John Junior, Jane और Joan");
 
     REQUIRE(orList(factory, {u"1"}) == u"1");
     REQUIRE(orList(factory, {u"1", u"2"}) == u"1 या 2");
diff --git a/test/src/inflection/grammar/synthesis/QuantifyTest.cpp b/test/src/inflection/grammar/synthesis/QuantifyTest.cpp
index ad651db5e..8732b2a88 100644
--- a/test/src/inflection/grammar/synthesis/QuantifyTest.cpp
+++ b/test/src/inflection/grammar/synthesis/QuantifyTest.cpp
@@ -585,23 +585,16 @@ TEST_CASE("QuantifyTest#testSerboCroatian")
     assertQuantity(conceptFactory, ::inflection::dialog::SpeakableString(u"4 брода", u"четири брода"), 4, ::inflection::dialog::SpeakableString(u"брод"));
     assertQuantity(conceptFactory, ::inflection::dialog::SpeakableString(u"5 бродова", u"пет бродова"), 5, ::inflection::dialog::SpeakableString(u"брод"));
     // A fraction falls in the CLDR "other" bucket yet must take the genitive singular.
-#if U_ICU_VERSION_MAJOR_NUM <= 78
-    // TODO remove this version check after version 79 is released, and update versions.mk with the new minimum version.
-    WARN("U_ICU_VERSION_MAJOR_NUM <= 78. Please test with a newer version of ICU.");
-#else
-    assertQuantity(conceptFactory, ::inflection::dialog::SpeakableString(u"1,1 брода", u"један зарез један брода"), 1.1, ::inflection::dialog::SpeakableString(u"брод"));
-#endif
+    assertQuantity(conceptFactory, ::inflection::dialog::SpeakableString(u"1,1 брода", u"један зарез један брода"), 1.1,
+        ::inflection::dialog::SpeakableString(u"брод"));
 
     // Feminine noun "земља" (land) through the same states.
     assertQuantity(conceptFactory, ::inflection::dialog::SpeakableString(u"1 земља", u"једна земља"), 1, ::inflection::dialog::SpeakableString(u"земља"));
     assertQuantity(conceptFactory, ::inflection::dialog::SpeakableString(u"2 земље", u"две земље"), 2, ::inflection::dialog::SpeakableString(u"земља"));
-    assertQuantity(conceptFactory, ::inflection::dialog::SpeakableString(u"5 земаља", u"пет земаља"), 5, ::inflection::dialog::SpeakableString(u"земља"));
-#if U_ICU_VERSION_MAJOR_NUM <= 78
-    // TODO remove this version check after version 79 is released, and update versions.mk with the new minimum version.
-    WARN("U_ICU_VERSION_MAJOR_NUM <= 78. Please test with a newer version of ICU.");
-#else
-    assertQuantity(conceptFactory, ::inflection::dialog::SpeakableString(u"1,1 земље", u"једна зарез једна земље"), 1.1, ::inflection::dialog::SpeakableString(u"земља"));
-#endif
+    assertQuantity(conceptFactory, ::inflection::dialog::SpeakableString(u"5 земаља", u"пет земаља"), 5,
+        ::inflection::dialog::SpeakableString(u"земља"));
+    assertQuantity(conceptFactory, ::inflection::dialog::SpeakableString(u"1,1 земље", u"једна зарез једна земље"), 1.1,
+        ::inflection::dialog::SpeakableString(u"земља"));
 
     // Verify oblique cases for 5+ (always genitive plural: бродова / земаља)
     assertQuantity(conceptFactory, u"5 бродова", u"пет бродова", 5, u"брод", u"locative");
diff --git a/test/src/inflection/message2/Message2Test.cpp b/test/src/inflection/message2/Message2Test.cpp
index 6b47320e2..ed331f1bf 100644
--- a/test/src/inflection/message2/Message2Test.cpp
+++ b/test/src/inflection/message2/Message2Test.cpp
@@ -275,14 +275,12 @@ TEST_CASE("Message2Test#testLocales")
     auto buildRegistry = [&](inflection::message2::MF2Factory::OutputMode mode) {
         UErrorCode ec = U_ZERO_ERROR;
         auto r = MFFunctionRegistry::Builder(ec)
-            .adoptFormatter(FunctionName("i:inflect"), inflection::message2::MF2Factory::createInflectFormatterFactory(mode), ec)
-            .adoptSelector(FunctionName("i:inflect"), inflection::message2::MF2Factory::createInflectSelectorFactory(), ec)
-            .adoptFormatter(FunctionName("i:numeral"), inflection::message2::MF2Factory::createNumeralFormatterFactory(mode), ec)
-            .adoptFormatter(FunctionName("i:quantify"), inflection::message2::MF2Factory::createQuantifyFormatterFactory(mode), ec)
-            .adoptFormatter(FunctionName("i:list"), inflection::message2::MF2Factory::createListFormatterFactory(mode), ec)
-            .adoptFormatter(FunctionName("i:pronoun"), inflection::message2::MF2Factory::createPronounFormatterFactory(mode), ec)
-            .adoptSelector(FunctionName("i:pronoun"), inflection::message2::MF2Factory::createPronounSelectorFactory(), ec)
-            .build();
+                     .adoptFunction(FunctionName("i:inflect"), inflection::message2::MF2Factory::createInflectFunction(mode), ec)
+                     .adoptFunction(FunctionName("i:numeral"), inflection::message2::MF2Factory::createNumeralFunction(mode), ec)
+                     .adoptFunction(FunctionName("i:quantify"), inflection::message2::MF2Factory::createQuantifyFunction(mode), ec)
+                     .adoptFunction(FunctionName("i:list"), inflection::message2::MF2Factory::createListFunction(mode), ec)
+                     .adoptFunction(FunctionName("i:pronoun"), inflection::message2::MF2Factory::createPronounFunction(mode), ec)
+                     .build();
         REQUIRE(U_SUCCESS(ec));
         return r;
     };
@@ -412,6 +410,7 @@ TEST_CASE("Message2Test#testLocales")
             MessageFormatter::Builder builder(testError);
             builder.setFunctionRegistry(hasSpeak ? ssmlRegistry : plainRegistry)
                 .setLocale(icuLocale)
+                .setBidiIsolationStrategy(icu::U_MF_BIDI_OFF)
                 .setPattern(UnicodeString(source.c_str()), pe, testError);
             if (errorExpected) {
                 // Surface MF errors (e.g. an unknown :i:inflect feature) via the UErrorCode.
diff --git a/tools/generateICUWrapper/generateICUWrapper.cpp b/tools/generateICUWrapper/generateICUWrapper.cpp
index 6b44a8430..49747b57c 100644
--- a/tools/generateICUWrapper/generateICUWrapper.cpp
+++ b/tools/generateICUWrapper/generateICUWrapper.cpp
@@ -276,6 +276,7 @@ static void RunOnFile(const char *className, const char *filePath, std::stringst
     vector normalizationMap = {
         RegexToMatch("\\bU_CAPI\\b", ""),
         RegexToMatch("\\bU_EXPORT2\\b", ""),
+        RegexToMatch("\\s*\\bU_LIFETIME_BOUND\\b", ""),
         RegexToMatch("\\s*/\\*[^\\*]*\\*/", ""),
         RegexToMatch("\\*", " *"),
         RegexToMatch("\\s+", " "),