From 7672e757434d94103a8544e8c68e1f2510493c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Neboj=C5=A1a=20=C4=86iri=C4=87?= Date: Thu, 15 Oct 2026 18:58:49 +0000 Subject: [PATCH 1/5] Inflection-213 Support building and testing with ICU 79 --- .github/workflows/cmake-multi-platform.yml | 31 +- .../create-distribution-packaging.yml | 36 +- .github/workflows/doxygen-gh-pages.yml | 8 +- .github/workflows/ubuntu-memory-check.yml | 8 +- CMakeLists.txt | 4 +- cmake/versions.mk | 2 +- documents/how_to_update_icu.md | 7 +- src/inflection/message2/MF2Factory.cpp | 958 ++++++++---------- src/inflection/message2/MF2Factory.hpp | 86 +- test/src/inflection/dialog/ListTest.cpp | 6 +- .../grammar/synthesis/QuantifyTest.cpp | 19 +- test/src/inflection/message2/Message2Test.cpp | 100 +- .../generateICUWrapper/generateICUWrapper.cpp | 1 + 13 files changed, 604 insertions(+), 662 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index d0fea3198..0086ad0e0 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: '1rc' 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..87adddaa0 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: '1rc' 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..a7a3c7ee2 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: '1rc' 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..200b40d4c 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: '1rc' 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..68a2f22d5 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,12 +69,12 @@ 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"; +// Per-function names passed to InflectionFunctionValue +static constexpr auto FN_INFLECTION = u"i:inflect"; +static constexpr auto FN_NUMERAL = u"i:numeral"; +static constexpr auto FN_QUANTIFY = u"i:quantify"; +static constexpr auto FN_LIST = u"i:list"; +static constexpr auto FN_PRONOUN = u"i:pronoun"; static UnicodeString xmlEscape(const std::u16string& s, bool attribute) { UnicodeString out; @@ -163,531 +161,520 @@ 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) { +// 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; } -private: - const ::inflection::dialog::SemanticFeatureModel* model; - MF2Factory::OutputMode mode; -}; + 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""); +} -Formatter* InflectionFormatterFactory::createFormatter(const Locale& locale, UErrorCode& 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 (U_FAILURE(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(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; +} - Formatter* result = new InflectionFormatter(GetSemanticFeatureModel(locale), mode); - if (result == nullptr) { - errorCode = U_MEMORY_ALLOCATION_ERROR; +class InvalidFormattableObject : public icu::message2::FormattableObject { +public: + const UnicodeString& tag() const override + { + static const UnicodeString* tagValue = new UnicodeString(u"inflection::invalid"); + return *tagValue; } - return result; -} + ~InvalidFormattableObject() override; +}; -FormattedPlaceholder InflectionFormatter::format( - FormattedPlaceholder&& arg, FunctionOptions&& options, - UErrorCode& errorCode) const -{ - if (U_FAILURE(errorCode)) { - return {}; +InvalidFormattableObject::~InvalidFormattableObject() = default; + +class InflectionFunctionValue : public FunctionValue { +public: + InflectionFunctionValue( + const FunctionContext& context, const FunctionValue& operand, const FunctionOptions& options, const char16_t* fnName) + { + UErrorCode status = U_ZERO_ERROR; + static const InvalidFormattableObject* kInvalidObject = new InvalidFormattableObject(); + innerValue = Formattable(kInvalidObject); + 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 { + dir = icu::U_MF_DIRECTIONALITY_UNKNOWN; + } + opts = options.mergeOptions(operand.getResolvedOptions(), status); + functionName = UnicodeString(fnName); + fallback = operand.isNullOperand() ? (UnicodeString(u"{:") + fnName + u"}") : operand.getFallback(); } - // Argument must be present - if (!arg.canFormat()) { - errorCode = U_MF_FORMATTING_ERROR; - return FormattedPlaceholder(FN_INFLECTION); + InflectionFunctionValue(const FunctionContext& context, const FunctionValue& operand, const FunctionOptions& options, + const char16_t* fnName, UnicodeString formattedVal, UnicodeString selectVal, bool selectable) + : InflectionFunctionValue(context, operand, options, fnName) + { + formatted = std::move(formattedVal); + selected = std::move(selectVal); + innerValue = Formattable(formatted); + canSelect = selectable; + valid = true; } - // Assumes the argument is not-yet-formatted - const Formattable& toFormat = arg.asFormattable(); - UnicodeString result; + ~InflectionFunctionValue() override; - 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); + UnicodeString formatToString(UErrorCode& status) const override + { + if (U_FAILURE(status)) { + return { }; } - 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 (!valid) { + status = U_MF_FORMATTING_ERROR; + return fallback; } - if (speakable != nullptr) { - result += emit(*speakable, mode); - } 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); - } - } else { - result += toFormat.getString(errorCode); + return formatted; } - return FormattedPlaceholder(arg, FormattedValue(std::move(result))); -} - -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; + 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 (!valid || !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; + bool valid = false; }; -Selector* InflectionSelectorFactory::createSelector( - const Locale& locale, UErrorCode& errorCode) const { - if (U_FAILURE(errorCode)) { - return nullptr; +InflectionFunctionValue::~InflectionFunctionValue() = default; + +static LocalPointer makeInvalidFunctionValue(const FunctionContext& context, const FunctionValue& operand, + const FunctionOptions& options, const char16_t* fnName, UErrorCode& errorCode) +{ + LocalPointer result(new InflectionFunctionValue(context, operand, options, fnName)); + if (!result.isValid()) { + errorCode = U_MEMORY_ALLOCATION_ERROR; } + return result; +} - Selector* result = new InflectionSelector(GetSemanticFeatureModel(locale)); - if (result == nullptr) { +static LocalPointer makeValidFunctionValue(const FunctionContext& context, const FunctionValue& operand, + const FunctionOptions& options, const char16_t* fnName, UnicodeString formattedVal, UnicodeString selectVal, bool selectable, + UErrorCode& errorCode) +{ + LocalPointer result( + new InflectionFunctionValue(context, operand, options, fnName, std::move(formattedVal), std::move(selectVal), selectable)); + 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); } - // Argument must be present - if (!arg.canFormat()) { - errorCode = U_MF_SELECTOR_ERROR; - return; + if (operand.isNullOperand()) { + return makeInvalidFunctionValue(context, operand, options, FN_INFLECTION, errorCode); } + const Locale& locale = context.getLocale(); + const auto* model = GetSemanticFeatureModel(locale); + const Formattable& toFormat = operand.unwrap(); - // 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) { + UErrorCode localStatus = U_ZERO_ERROR; + auto conceptObj = conceptFromOperand(toFormat, model, localStatus); + if (conceptObj == nullptr || U_FAILURE(localStatus)) { + return makeInvalidFunctionValue(context, operand, options, FN_INFLECTION, errorCode); + } + + 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; } - auto feature = conceptObj->getModel()->getFeature(key); - if (feature == nullptr) { - // A bare option must name a known grammatical feature. - errorCode = U_MF_SELECTOR_ERROR; - return; + auto constraint = conceptObj->getModel()->getFeature(key); + if (constraint == nullptr) { + return makeInvalidFunctionValue(context, operand, options, FN_INFLECTION, errorCode); + } + conceptObj->putConstraint(*constraint, resolveConstraintValue(model, *constraint, valPtr->unwrap(), localStatus)); + if (U_FAILURE(localStatus)) { + return makeInvalidFunctionValue(context, operand, options, FN_INFLECTION, 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 makeInvalidFunctionValue(context, operand, options, FN_INFLECTION, 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)); } + } 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 { + formatted = emit(inflection::dialog::SpeakableString(inputString), mode); } + return makeValidFunctionValue(context, operand, options, FN_INFLECTION, std::move(formatted), std::move(selected), true, errorCode); } -class NumeralFormatter : public Formatter { -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 { +class NumeralFunction : public Function { 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; - -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""); -} +Function* MF2Factory::createNumeralFunction(OutputMode mode) { return new NumeralFunction(mode); } -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 makeInvalidFunctionValue(context, operand, options, FN_NUMERAL, 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 makeInvalidFunctionValue(context, operand, options, FN_NUMERAL, 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 makeInvalidFunctionValue(context, operand, options, FN_NUMERAL, 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 makeInvalidFunctionValue(context, operand, options, FN_NUMERAL, errorCode); } UnicodeString out = emit(result, mode); - return FormattedPlaceholder(arg, FormattedValue(std::move(out))); + return makeValidFunctionValue(context, operand, options, FN_NUMERAL, 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); -} +Function* MF2Factory::createQuantifyFunction(OutputMode mode) { return new QuantifyFunction(mode); } -icu::message2::FormatterFactory* MF2Factory::createQuantifyFormatterFactory(OutputMode mode) { - return new QuantifyFormatterFactory(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 makeInvalidFunctionValue(context, operand, options, FN_QUANTIFY, 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 makeInvalidFunctionValue(context, operand, options, FN_QUANTIFY, 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 makeInvalidFunctionValue(context, operand, options, FN_QUANTIFY, 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 makeInvalidFunctionValue(context, operand, options, FN_QUANTIFY, 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); + num = std::make_unique(valueF.getDouble(localStatus), locale, locale); + } else { + num = std::make_unique(valueF.getInt64(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); + if (U_FAILURE(localStatus)) { + return makeInvalidFunctionValue(context, operand, options, FN_QUANTIFY, 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. + 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 makeInvalidFunctionValue(context, operand, options, FN_QUANTIFY, errorCode); + } + const UnicodeString& v = entry.second->unwrap().getString(localStatus); + if (U_FAILURE(localStatus)) { + return makeInvalidFunctionValue(context, operand, options, FN_QUANTIFY, 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 makeInvalidFunctionValue(context, operand, options, FN_QUANTIFY, errorCode); } UnicodeString out = emit(*result, mode); - return FormattedPlaceholder(arg, FormattedValue(std::move(out))); + return makeValidFunctionValue(context, operand, options, FN_QUANTIFY, 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 makeInvalidFunctionValue(context, operand, options, FN_LIST, 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 makeInvalidFunctionValue(context, operand, options, FN_LIST, 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 makeInvalidFunctionValue(context, operand, options, FN_LIST, 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 makeInvalidFunctionValue(context, operand, options, FN_LIST, 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 makeInvalidFunctionValue(context, operand, options, FN_LIST, 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 makeInvalidFunctionValue(context, operand, options, FN_LIST, errorCode); } } if (list == nullptr) { - errorCode = U_MF_FORMATTING_ERROR; - return FormattedPlaceholder(FN_LIST); + return makeInvalidFunctionValue(context, operand, options, FN_LIST, 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); } - // Apply grammeme constraints (options not in the control allow-list). + if (U_FAILURE(localStatus)) { + return makeInvalidFunctionValue(context, operand, options, FN_LIST, errorCode); + } + static constexpr std::u16string_view LIST_CONTROL[] = { - // This must be in sorted order. OPTION_WITH_AFTER_FIRST, OPTION_WITH_AFTER_LAST, OPTION_WITH_AVOID_ITEM_AFFIX_REDUNDANCY, @@ -700,248 +687,105 @@ 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) + if (inflection::util::ArrayUtils::contains(nameView)) { continue; } 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 makeInvalidFunctionValue(context, operand, options, FN_LIST, errorCode); + } + const UnicodeString& v = entry.second->unwrap().getString(localStatus); + if (U_FAILURE(localStatus)) { + return makeInvalidFunctionValue(context, operand, options, FN_LIST, 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 makeInvalidFunctionValue(context, operand, options, FN_LIST, errorCode); } UnicodeString out = emit(*result, mode); - return FormattedPlaceholder(arg, FormattedValue(std::move(out))); + return makeValidFunctionValue(context, operand, options, FN_LIST, 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) { + explicit PronounFunction(MF2Factory::OutputMode mode) + : mode(mode) + { } + ~PronounFunction() override = default; + LocalPointer call( + const FunctionContext& context, const FunctionValue& operand, const FunctionOptions& options, UErrorCode& errorCode) const override; private: - const inflection::dialog::SemanticFeatureModel* model{}; MF2Factory::OutputMode mode; }; -class PronounFormatterFactory : public FormatterFactory { -public: - explicit PronounFormatterFactory(MF2Factory::OutputMode mode) : mode(mode) { - } - - Formatter* createFormatter(const Locale&, UErrorCode&) 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); -} +Function* MF2Factory::createPronounFunction(OutputMode mode) { return new PronounFunction(mode); } -icu::message2::SelectorFactory* MF2Factory::createPronounSelectorFactory() { - return new PronounSelectorFactory(); -} - -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 makeInvalidFunctionValue(context, operand, options, FN_PRONOUN, 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 makeInvalidFunctionValue(context, operand, options, FN_PRONOUN, 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 makeInvalidFunctionValue(context, operand, options, FN_PRONOUN, 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 makeInvalidFunctionValue(context, operand, options, FN_PRONOUN, errorCode); + } + pronoun->putConstraint(*feature, resolveConstraintValue(model, *feature, valPtr->unwrap(), localStatus)); + if (U_FAILURE(localStatus)) { + return makeInvalidFunctionValue(context, operand, options, FN_PRONOUN, 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 makeInvalidFunctionValue(context, operand, options, FN_PRONOUN, 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); + } 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 makeInvalidFunctionValue(context, operand, options, FN_PRONOUN, 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 makeValidFunctionValue(context, operand, options, FN_PRONOUN, 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/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..169bd4bd0 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.
@@ -461,4 +460,89 @@ TEST_CASE("Message2Test#testLocales")
     }
     xmlCleanupParser();
     REQUIRE(executedFiles > 0);
+
+    // Verify strict error reporting on invalid option value types and best-effort fallback formatting.
+    {
+        std::map args;
+        args[UnicodeString(u"word")] = Formattable(UnicodeString(u"cat"));
+        args[UnicodeString(u"n")] = Formattable(static_cast(2));
+        Locale enUS("en_US");
+
+        for (const auto* pattern : { u"{$word :i:quantify withValue=$word}", u"{$word :i:inflect number=$n}" }) {
+            UErrorCode ec = U_ZERO_ERROR;
+            UParseError pe;
+            MessageFormatter::Builder b(ec);
+            b.setFunctionRegistry(plainRegistry)
+                .setLocale(enUS)
+                .setErrorHandlingBehavior(MessageFormatter::U_MF_STRICT)
+                .setPattern(UnicodeString(pattern), pe, ec);
+            MessageFormatter mf = b.build(ec);
+            if (U_SUCCESS(ec)) {
+                mf.formatToString(MessageArguments(args, ec), ec);
+            }
+            CHECK(U_FAILURE(ec));
+        }
+
+        {
+            UErrorCode ec = U_ZERO_ERROR;
+            UParseError pe;
+            MessageFormatter mf = MessageFormatter::Builder(ec)
+                                      .setFunctionRegistry(plainRegistry)
+                                      .setLocale(enUS)
+                                      .setBidiIsolationStrategy(icu::U_MF_BIDI_OFF)
+                                      .setPattern(UnicodeString(u"Hello {$word :i:inflect bogusFeature=value} and {:i:inflect}!"), pe, ec)
+                                      .build(ec);
+            REQUIRE(U_SUCCESS(ec));
+            UnicodeString out = mf.formatToString(MessageArguments(args, ec), ec);
+            REQUIRE(U_SUCCESS(ec));
+            CHECK(out == UnicodeString(u"Hello {$word} and {:i:inflect}!"));
+        }
+        {
+            // Default bidi isolation strategy (U_MF_BIDI_AUTO) with LTR locale and no u:dir option
+            // should treat locale-aware inflection functions as LTR (no FSI/PDI wrapping).
+            UErrorCode ec = U_ZERO_ERROR;
+            UParseError pe;
+            MessageFormatter mf = MessageFormatter::Builder(ec)
+                                      .setFunctionRegistry(plainRegistry)
+                                      .setLocale(enUS)
+                                      .setPattern(UnicodeString(u"You have {$n :i:numeral withStyle=asWords} messages."), pe, ec)
+                                      .build(ec);
+            REQUIRE(U_SUCCESS(ec));
+            UnicodeString out = mf.formatToString(MessageArguments(args, ec), ec);
+            REQUIRE(U_SUCCESS(ec));
+            CHECK(out == UnicodeString(u"You have two messages."));
+        }
+        {
+            // Local variable bound to an invalid inflection function call and referenced without a function annotation,
+            // as well as chained into another inflection function call.
+            for (const auto* pattern : {
+                     u".local $v = {$word :i:inflect bogusFeature=value} {{{$v}}}",
+                     u".local $v = {$word :i:numeral} {{{$v :i:numeral}}}",
+                 }) {
+                UErrorCode ec = U_ZERO_ERROR;
+                UParseError pe;
+                MessageFormatter mf = MessageFormatter::Builder(ec)
+                                          .setFunctionRegistry(plainRegistry)
+                                          .setLocale(enUS)
+                                          .setBidiIsolationStrategy(icu::U_MF_BIDI_OFF)
+                                          .setPattern(UnicodeString(pattern), pe, ec)
+                                          .build(ec);
+                REQUIRE(U_SUCCESS(ec));
+                UnicodeString out = mf.formatToString(MessageArguments(args, ec), ec);
+                REQUIRE(U_SUCCESS(ec));
+                CHECK(out == UnicodeString(u"{$v}"));
+
+                UErrorCode strictEc = U_ZERO_ERROR;
+                MessageFormatter strictMf = MessageFormatter::Builder(strictEc)
+                                                .setFunctionRegistry(plainRegistry)
+                                                .setLocale(enUS)
+                                                .setErrorHandlingBehavior(MessageFormatter::U_MF_STRICT)
+                                                .setPattern(UnicodeString(pattern), pe, strictEc)
+                                                .build(strictEc);
+                REQUIRE(U_SUCCESS(strictEc));
+                strictMf.formatToString(MessageArguments(args, strictEc), strictEc);
+                CHECK(strictEc == U_MF_FORMATTING_ERROR);
+            }
+        }
+    }
 }
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+", " "),

From 2d96ce44c1a67ed72e952142b7c31e9400f51fd5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Neboj=C5=A1a=20=C4=86iri=C4=87?= 
Date: Thu, 15 Oct 2026 22:21:42 +0000
Subject: [PATCH 2/5] Inflection-213 Address PR review comments on
 MessageFormat 2 tests and MF2Factory

---
 src/inflection/message2/MF2Factory.cpp        | 168 ++++++++----------
 test/resources/inflection/message2/de_DE.xml  |   8 +-
 test/resources/inflection/message2/en_US.xml  |  20 +--
 test/resources/inflection/message2/es_MX.xml  |  12 +-
 test/src/inflection/message2/Message2Test.cpp |  86 ---------
 5 files changed, 91 insertions(+), 203 deletions(-)

diff --git a/src/inflection/message2/MF2Factory.cpp b/src/inflection/message2/MF2Factory.cpp
index 68a2f22d5..80912a1c5 100644
--- a/src/inflection/message2/MF2Factory.cpp
+++ b/src/inflection/message2/MF2Factory.cpp
@@ -69,13 +69,6 @@ static constexpr auto TYPE_OR                            = u"or";
 // Bool option value
 static constexpr auto VALUE_TRUE                         = u"true";
 
-// Per-function names passed to InflectionFunctionValue
-static constexpr auto FN_INFLECTION = u"i:inflect";
-static constexpr auto FN_NUMERAL = u"i:numeral";
-static constexpr auto FN_QUANTIFY = u"i:quantify";
-static constexpr auto FN_LIST = u"i:list";
-static constexpr auto FN_PRONOUN = u"i:pronoun";
-
 static UnicodeString xmlEscape(const std::u16string& s, bool attribute) {
     UnicodeString out;
     for (char16_t c : s) {
@@ -238,26 +231,15 @@ static std::unique_ptr pronounFromOperand(
     return nullptr;
 }
 
-class InvalidFormattableObject : public icu::message2::FormattableObject {
-public:
-    const UnicodeString& tag() const override
-    {
-        static const UnicodeString* tagValue = new UnicodeString(u"inflection::invalid");
-        return *tagValue;
-    }
-    ~InvalidFormattableObject() override;
-};
-
-InvalidFormattableObject::~InvalidFormattableObject() = default;
-
 class InflectionFunctionValue : public FunctionValue {
 public:
-    InflectionFunctionValue(
-        const FunctionContext& context, const FunctionValue& operand, const FunctionOptions& options, const char16_t* fnName)
+    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)
     {
-        UErrorCode status = U_ZERO_ERROR;
-        static const InvalidFormattableObject* kInvalidObject = new InvalidFormattableObject();
-        innerValue = Formattable(kInvalidObject);
+        innerValue = Formattable(formatted);
         locale = context.getLocale();
         inputDir = context.getDirection();
         if (inputDir == icu::U_MF_BIDI_OPTION_LTR) {
@@ -269,20 +251,9 @@ class InflectionFunctionValue : public FunctionValue {
         } else {
             dir = icu::U_MF_DIRECTIONALITY_UNKNOWN;
         }
-        opts = options.mergeOptions(operand.getResolvedOptions(), status);
-        functionName = UnicodeString(fnName);
-        fallback = operand.isNullOperand() ? (UnicodeString(u"{:") + fnName + u"}") : operand.getFallback();
-    }
-
-    InflectionFunctionValue(const FunctionContext& context, const FunctionValue& operand, const FunctionOptions& options,
-        const char16_t* fnName, UnicodeString formattedVal, UnicodeString selectVal, bool selectable)
-        : InflectionFunctionValue(context, operand, options, fnName)
-    {
-        formatted = std::move(formattedVal);
-        selected = std::move(selectVal);
-        innerValue = Formattable(formatted);
-        canSelect = selectable;
-        valid = true;
+        opts = options.mergeOptions(operand.getResolvedOptions(), errorCode);
+        functionName = context.getCalledFunctionName();
+        fallback = operand.getFallback();
     }
 
     ~InflectionFunctionValue() override;
@@ -292,10 +263,6 @@ class InflectionFunctionValue : public FunctionValue {
         if (U_FAILURE(status)) {
             return { };
         }
-        if (!valid) {
-            status = U_MF_FORMATTING_ERROR;
-            return fallback;
-        }
         return formatted;
     }
 
@@ -306,7 +273,7 @@ class InflectionFunctionValue : public FunctionValue {
         if (U_FAILURE(status)) {
             return;
         }
-        if (!valid || !canSelect) {
+        if (!canSelect) {
             status = U_MF_SELECTOR_ERROR;
             return;
         }
@@ -322,27 +289,21 @@ class InflectionFunctionValue : public FunctionValue {
     UnicodeString formatted { };
     UnicodeString selected { };
     bool canSelect = false;
-    bool valid = false;
 };
 
 InflectionFunctionValue::~InflectionFunctionValue() = default;
 
-static LocalPointer makeInvalidFunctionValue(const FunctionContext& context, const FunctionValue& operand,
-    const FunctionOptions& options, const char16_t* fnName, UErrorCode& errorCode)
+static LocalPointer formattingError(UErrorCode& errorCode)
 {
-    LocalPointer result(new InflectionFunctionValue(context, operand, options, fnName));
-    if (!result.isValid()) {
-        errorCode = U_MEMORY_ALLOCATION_ERROR;
-    }
-    return result;
+    errorCode = U_MF_FORMATTING_ERROR;
+    return LocalPointer(nullptr);
 }
 
-static LocalPointer makeValidFunctionValue(const FunctionContext& context, const FunctionValue& operand,
-    const FunctionOptions& options, const char16_t* fnName, UnicodeString formattedVal, UnicodeString selectVal, bool selectable,
-    UErrorCode& errorCode)
+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, fnName, std::move(formattedVal), std::move(selectVal), selectable));
+        new InflectionFunctionValue(context, operand, options, std::move(formattedVal), std::move(selectVal), selectable, errorCode));
     if (!result.isValid()) {
         errorCode = U_MEMORY_ALLOCATION_ERROR;
     }
@@ -372,7 +333,7 @@ LocalPointer InflectionFunction::call(
         return LocalPointer(nullptr);
     }
     if (operand.isNullOperand()) {
-        return makeInvalidFunctionValue(context, operand, options, FN_INFLECTION, errorCode);
+        return formattingError(errorCode);
     }
     const Locale& locale = context.getLocale();
     const auto* model = GetSemanticFeatureModel(locale);
@@ -381,7 +342,7 @@ LocalPointer InflectionFunction::call(
     UErrorCode localStatus = U_ZERO_ERROR;
     auto conceptObj = conceptFromOperand(toFormat, model, localStatus);
     if (conceptObj == nullptr || U_FAILURE(localStatus)) {
-        return makeInvalidFunctionValue(context, operand, options, FN_INFLECTION, errorCode);
+        return formattingError(errorCode);
     }
 
     UnicodeString inputString;
@@ -391,27 +352,30 @@ LocalPointer InflectionFunction::call(
     const auto& opt = options.getOptions();
     for (const auto& [key, valPtr] : opt) {
         if (key == UnicodeString(OPTION_TO)) {
-            continue;
+            continue; // control option (the projection feature), not a grammeme constraint
         }
         auto constraint = conceptObj->getModel()->getFeature(key);
         if (constraint == nullptr) {
-            return makeInvalidFunctionValue(context, operand, options, FN_INFLECTION, errorCode);
+            // A bare option must name a known grammatical feature.
+            return formattingError(errorCode);
         }
         conceptObj->putConstraint(*constraint, resolveConstraintValue(model, *constraint, valPtr->unwrap(), localStatus));
         if (U_FAILURE(localStatus)) {
-            return makeInvalidFunctionValue(context, operand, options, FN_INFLECTION, errorCode);
+            return formattingError(errorCode);
         }
     }
     std::unique_ptr speakable;
     if (opt.contains(OPTION_TO)) {
         const UnicodeString& toName = opt.at(OPTION_TO)->unwrap().getString(localStatus);
         if (U_FAILURE(localStatus)) {
-            return makeInvalidFunctionValue(context, operand, options, FN_INFLECTION, errorCode);
+            return formattingError(errorCode);
         }
         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());
     }
@@ -421,9 +385,11 @@ LocalPointer InflectionFunction::call(
         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 makeValidFunctionValue(context, operand, options, FN_INFLECTION, std::move(formatted), std::move(selected), true, errorCode);
+    return makeFunctionValue(context, operand, options, std::move(formatted), std::move(selected), true, errorCode);
 }
 
 class NumeralFunction : public Function {
@@ -449,7 +415,7 @@ LocalPointer NumeralFunction::call(
         return LocalPointer(nullptr);
     }
     if (operand.isNullOperand()) {
-        return makeInvalidFunctionValue(context, operand, options, FN_NUMERAL, errorCode);
+        return formattingError(errorCode);
     }
     inflection::util::ULocale locale(context.getLocale().getName());
     const Formattable& toFormat = operand.unwrap();
@@ -461,7 +427,7 @@ LocalPointer NumeralFunction::call(
         num = std::make_unique(toFormat.getInt64(localStatus), locale, locale);
     }
     if (U_FAILURE(localStatus)) {
-        return makeInvalidFunctionValue(context, operand, options, FN_NUMERAL, errorCode);
+        return formattingError(errorCode);
     }
 
     std::u16string style;
@@ -478,15 +444,15 @@ LocalPointer NumeralFunction::call(
         hasVariant = true;
     }
     if (U_FAILURE(localStatus)) {
-        return makeInvalidFunctionValue(context, operand, options, FN_NUMERAL, errorCode);
+        return formattingError(errorCode);
     }
 
     inflection::dialog::SpeakableString result = numeralToSpeakable(*num, style, variant, hasVariant, localStatus);
     if (U_FAILURE(localStatus)) {
-        return makeInvalidFunctionValue(context, operand, options, FN_NUMERAL, errorCode);
+        return formattingError(errorCode);
     }
     UnicodeString out = emit(result, mode);
-    return makeValidFunctionValue(context, operand, options, FN_NUMERAL, std::move(out), { }, false, errorCode);
+    return makeFunctionValue(context, operand, options, std::move(out), { }, false, errorCode);
 }
 
 class QuantifyFunction : public Function {
@@ -512,7 +478,7 @@ LocalPointer QuantifyFunction::call(
         return LocalPointer(nullptr);
     }
     if (operand.isNullOperand()) {
-        return makeInvalidFunctionValue(context, operand, options, FN_QUANTIFY, errorCode);
+        return formattingError(errorCode);
     }
     const Locale& icuLoc = context.getLocale();
     const auto* model = GetSemanticFeatureModel(icuLoc);
@@ -522,15 +488,16 @@ LocalPointer QuantifyFunction::call(
     UErrorCode localStatus = U_ZERO_ERROR;
     std::unique_ptr unit = conceptFromOperand(operand.unwrap(), model, localStatus);
     if (unit == nullptr || U_FAILURE(localStatus)) {
-        return makeInvalidFunctionValue(context, operand, options, FN_QUANTIFY, errorCode);
+        return formattingError(errorCode);
     }
 
     const auto& opt = options.getOptions();
     if (!opt.contains(OPTION_WITH_VALUE)) {
-        return makeInvalidFunctionValue(context, operand, options, FN_QUANTIFY, errorCode);
+        return formattingError(errorCode);
     }
     if (opt.contains(OPTION_WITH_STYLE) || opt.contains(OPTION_WITH_VARIANT)) {
-        return makeInvalidFunctionValue(context, operand, options, FN_QUANTIFY, errorCode);
+        // reserved, not implemented, but perhaps it will be used in the future.
+        return formattingError(errorCode);
     }
 
     const Formattable& valueF = opt.at(OPTION_WITH_VALUE)->unwrap();
@@ -538,12 +505,15 @@ LocalPointer QuantifyFunction::call(
     if (valueF.getType() == UFMT_DOUBLE) {
         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(localStatus), locale, locale);
     }
     if (U_FAILURE(localStatus)) {
-        return makeInvalidFunctionValue(context, operand, options, FN_QUANTIFY, errorCode);
+        return formattingError(errorCode);
     }
 
+    // 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)
@@ -553,21 +523,21 @@ LocalPointer QuantifyFunction::call(
         std::u16string name(key.getBuffer(), key.length());
         const auto* feature = unit->getModel()->getFeature(name);
         if (feature == nullptr) {
-            return makeInvalidFunctionValue(context, operand, options, FN_QUANTIFY, errorCode);
+            return formattingError(errorCode); // unknown feature (mirror FormatTest strictness)
         }
         const UnicodeString& v = entry.second->unwrap().getString(localStatus);
         if (U_FAILURE(localStatus)) {
-            return makeInvalidFunctionValue(context, operand, options, FN_QUANTIFY, errorCode);
+            return formattingError(errorCode);
         }
         unit->putConstraint(*feature, std::u16string(v.getBuffer(), v.length()));
     }
 
     std::unique_ptr result(ccf->quantify(*num, unit.get()));
     if (result == nullptr) {
-        return makeInvalidFunctionValue(context, operand, options, FN_QUANTIFY, errorCode);
+        return formattingError(errorCode);
     }
     UnicodeString out = emit(*result, mode);
-    return makeValidFunctionValue(context, operand, options, FN_QUANTIFY, std::move(out), { }, false, errorCode);
+    return makeFunctionValue(context, operand, options, std::move(out), { }, false, errorCode);
 }
 
 class ListFunction : public Function {
@@ -593,7 +563,7 @@ LocalPointer ListFunction::call(
         return LocalPointer(nullptr);
     }
     if (operand.isNullOperand()) {
-        return makeInvalidFunctionValue(context, operand, options, FN_LIST, errorCode);
+        return formattingError(errorCode);
     }
     const Locale& icuLoc = context.getLocale();
     const auto* model = GetSemanticFeatureModel(icuLoc);
@@ -601,13 +571,13 @@ LocalPointer ListFunction::call(
 
     const Formattable& arrF = operand.unwrap();
     if (arrF.getType() != UFMT_ARRAY) {
-        return makeInvalidFunctionValue(context, operand, options, FN_LIST, errorCode);
+        return formattingError(errorCode);
     }
     UErrorCode localStatus = U_ZERO_ERROR;
     int32_t count = 0;
     const Formattable* elems = arrF.getArray(count, localStatus);
     if (U_FAILURE(localStatus)) {
-        return makeInvalidFunctionValue(context, operand, options, FN_LIST, errorCode);
+        return formattingError(errorCode);
     }
 
     const auto& opt = options.getOptions();
@@ -617,7 +587,7 @@ LocalPointer ListFunction::call(
     for (int32_t i = 0; i < count; i++) {
         UnicodeString elemStr = elems[i].getString(localStatus);
         if (U_FAILURE(localStatus)) {
-            return makeInvalidFunctionValue(context, operand, options, FN_LIST, errorCode);
+            return formattingError(errorCode);
         }
         items.push_back(
             std::make_unique(model, inflection::dialog::SpeakableString(elemStr)));
@@ -630,7 +600,7 @@ LocalPointer ListFunction::call(
     } else {
         const UnicodeString& typeV = opt.at(OPTION_WITH_TYPE)->unwrap().getString(localStatus);
         if (U_FAILURE(localStatus)) {
-            return makeInvalidFunctionValue(context, operand, options, FN_LIST, errorCode);
+            return formattingError(errorCode);
         }
         std::u16string typeVal(typeV.getBuffer(), typeV.length());
         if (typeVal == TYPE_AND) {
@@ -638,13 +608,14 @@ LocalPointer ListFunction::call(
         } else if (typeVal == TYPE_OR) {
             list.reset(ccf->createOrList(itemPtrs));
         } else {
-            return makeInvalidFunctionValue(context, operand, options, FN_LIST, errorCode);
+            return formattingError(errorCode);
         }
     }
     if (list == nullptr) {
-        return makeInvalidFunctionValue(context, operand, options, FN_LIST, errorCode);
+        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)->unwrap().getString(localStatus)));
     }
@@ -671,10 +642,12 @@ LocalPointer ListFunction::call(
         list->setAvoidItemAffixRedundancy(std::u16string(v.getBuffer(), static_cast(v.length())) == VALUE_TRUE);
     }
     if (U_FAILURE(localStatus)) {
-        return makeInvalidFunctionValue(context, operand, options, FN_LIST, errorCode);
+        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.
         OPTION_WITH_AFTER_FIRST,
         OPTION_WITH_AFTER_LAST,
         OPTION_WITH_AVOID_ITEM_AFFIX_REDUNDANCY,
@@ -688,25 +661,25 @@ LocalPointer ListFunction::call(
     for (const auto& entry : opt) {
         std::u16string_view nameView(entry.first.getBuffer(), static_cast(entry.first.length()));
         if (inflection::util::ArrayUtils::contains(nameView)) {
-            continue;
+            continue; // control options (exact allow-list)
         }
         const auto* feature = model->getFeature(nameView);
         if (feature == nullptr) {
-            return makeInvalidFunctionValue(context, operand, options, FN_LIST, errorCode);
+            return formattingError(errorCode); // unknown feature (mirror FormatTest/:quantify strictness)
         }
         const UnicodeString& v = entry.second->unwrap().getString(localStatus);
         if (U_FAILURE(localStatus)) {
-            return makeInvalidFunctionValue(context, operand, options, FN_LIST, errorCode);
+            return formattingError(errorCode);
         }
         list->putConstraint(*feature, std::u16string(v.getBuffer(), v.length()));
     }
 
     std::unique_ptr result(list->toSpeakableString());
     if (result == nullptr) {
-        return makeInvalidFunctionValue(context, operand, options, FN_LIST, errorCode);
+        return formattingError(errorCode);
     }
     UnicodeString out = emit(*result, mode);
-    return makeValidFunctionValue(context, operand, options, FN_LIST, std::move(out), { }, false, errorCode);
+    return makeFunctionValue(context, operand, options, std::move(out), { }, false, errorCode);
 }
 
 class PronounFunction : public Function {
@@ -732,18 +705,18 @@ LocalPointer PronounFunction::call(
         return LocalPointer(nullptr);
     }
     if (operand.isNullOperand()) {
-        return makeInvalidFunctionValue(context, operand, options, FN_PRONOUN, errorCode);
+        return formattingError(errorCode);
     }
     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 makeInvalidFunctionValue(context, operand, options, FN_PRONOUN, errorCode);
+        return formattingError(errorCode);
     }
 
     const auto& opt = options.getOptions();
     if (opt.contains(OPTION_TO) && opt.contains(OPTION_WITH_REFERENT)) {
-        return makeInvalidFunctionValue(context, operand, options, FN_PRONOUN, errorCode);
+        return formattingError(errorCode);
     }
     for (const auto& [key, valPtr] : opt) {
         if (key == UnicodeString(OPTION_TO) || key == UnicodeString(OPTION_WITH_REFERENT)) {
@@ -751,11 +724,11 @@ LocalPointer PronounFunction::call(
         }
         auto feature = pronoun->getModel()->getFeature(key);
         if (feature == nullptr) {
-            return makeInvalidFunctionValue(context, operand, options, FN_PRONOUN, errorCode);
+            return formattingError(errorCode);
         }
         pronoun->putConstraint(*feature, resolveConstraintValue(model, *feature, valPtr->unwrap(), localStatus));
         if (U_FAILURE(localStatus)) {
-            return makeInvalidFunctionValue(context, operand, options, FN_PRONOUN, errorCode);
+            return formattingError(errorCode);
         }
     }
 
@@ -763,16 +736,17 @@ LocalPointer PronounFunction::call(
     if (opt.contains(OPTION_TO)) {
         const UnicodeString& toName = opt.at(OPTION_TO)->unwrap().getString(localStatus);
         if (U_FAILURE(localStatus)) {
-            return makeInvalidFunctionValue(context, operand, options, FN_PRONOUN, errorCode);
+            return formattingError(errorCode);
         }
         auto feature = pronoun->getModel()->getFeature(toName);
         if (feature != nullptr) {
             speakable.reset(pronoun->getFeatureValue(*feature));
         }
+        // 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 makeInvalidFunctionValue(context, operand, options, FN_PRONOUN, errorCode);
+            return formattingError(errorCode);
         }
         speakable.reset(pronoun->toSpeakableString(*referent));
     } else {
@@ -785,7 +759,7 @@ LocalPointer PronounFunction::call(
         formatted = emit(*speakable, mode);
         selected = speakable->getPrint();
     }
-    return makeValidFunctionValue(context, operand, options, FN_PRONOUN, std::move(formatted), std::move(selected), true, errorCode);
+    return makeFunctionValue(context, operand, options, std::move(formatted), std::move(selected), true, errorCode);
 }
 
 }  // namespace inflection::message2
diff --git a/test/resources/inflection/message2/de_DE.xml b/test/resources/inflection/message2/de_DE.xml
index 64c96ccdc..996998885 100644
--- a/test/resources/inflection/message2/de_DE.xml
+++ b/test/resources/inflection/message2/de_DE.xml
@@ -9,8 +9,8 @@
             Bank
         
         .local $defArticleInPreposition = {$name :i:inflect number=singular case=accusative to=defArticleInPreposition} .match $defArticleInPreposition
-|in die| {{The defArticleInPreposition of '{$name}' is 'in die'.}}
-* {{The defArticleInPreposition of '{$name}' is unexpectedly '$defArticleInPreposition'.}}
+|in die| {{The defArticleInPreposition of '{$name :i:inflect}' is 'in die'.}}
+* {{The defArticleInPreposition of '{$name :i:inflect}' is unexpectedly '$defArticleInPreposition'.}}
 
         The defArticleInPreposition of 'Bank' is 'in die'.
     
@@ -19,8 +19,8 @@
             Fundort
         
         .local $withDefArticleInPreposition = {$name :i:inflect number=singular case=dative to=withDefArticleInPreposition} .match $withDefArticleInPreposition
-|im Fundort| {{The withDefArticleInPreposition of '{$name}' is 'im Fundort'.}}
-* {{The withDefArticleInPreposition of '{$name}' is something else.}}
+|im Fundort| {{The withDefArticleInPreposition of '{$name :i:inflect}' is 'im Fundort'.}}
+* {{The withDefArticleInPreposition of '{$name :i:inflect}' is something else.}}
 
         The withDefArticleInPreposition of 'Fundort' is 'im Fundort'.
     
diff --git a/test/resources/inflection/message2/en_US.xml b/test/resources/inflection/message2/en_US.xml
index e403b0900..485d13f93 100644
--- a/test/resources/inflection/message2/en_US.xml
+++ b/test/resources/inflection/message2/en_US.xml
@@ -8,7 +8,7 @@
         
             lights
         
-        The {$object} {is :i:inflect number=$object} on.
+        The {$object :i:inflect} {is :i:inflect number=$object} on.
         The lights are on.
     
     
@@ -23,8 +23,8 @@
             light
         
         .local $num = {$object :i:inflect to=number} .match $num
-plural {{The {$object} are on}}
-* {{The {$object} is on}}
+plural {{The {$object :i:inflect} are on}}
+* {{The {$object :i:inflect} is on}}
 
         The light is on
     
@@ -33,8 +33,8 @@ plural {{The {$object} are on}}
             lights
         
         .local $num = {$object :i:inflect to=number} .match $num
-plural {{The {$object} are on}}
-* {{The {$object} is on}}
+plural {{The {$object :i:inflect} are on}}
+* {{The {$object :i:inflect} is on}}
 
         The lights are on
     
@@ -43,8 +43,8 @@ plural {{The {$object} are on}}
             lights on the front porch
         
         .local $num = {$object :i:inflect to=number} .match $num
-plural {{The {$object} are on}}
-* {{The {$object} is on}}
+plural {{The {$object :i:inflect} are on}}
+* {{The {$object :i:inflect} is on}}
 
         The lights on the front porch are on
     
@@ -53,8 +53,8 @@ plural {{The {$object} are on}}
             light on the front porch
         
         .local $num = {$object :i:inflect to=number} .match $num
-plural {{The {$object} are on}}
-* {{The {$object} is on}}
+plural {{The {$object :i:inflect} are on}}
+* {{The {$object :i:inflect} is on}}
 
         The light on the front porch is on
     
@@ -125,7 +125,7 @@ plural {{The {$object} are on}}
         .local $v = {$word :i:inflect bogusFeature=value} .match $v
 * {{fallback}}
 
-        
+        
     
     
         
diff --git a/test/resources/inflection/message2/es_MX.xml b/test/resources/inflection/message2/es_MX.xml
index f94a33115..a7bcdf22e 100644
--- a/test/resources/inflection/message2/es_MX.xml
+++ b/test/resources/inflection/message2/es_MX.xml
@@ -16,12 +16,12 @@
             gato
         
         .local $var1 = {$name :i:inflect to=gender} .local $var2 = {$name :i:inflect to=number} .match $var1 $var2
-masculine 2 {{{$name} is Masculine & 2}}
-feminine singular {{{$name} is Feminine & Singular}}
-foo 4 {{{$name} is Foo & 4}}
-masculine singular {{{$name} is Masculine & Singular}}
-hello singular {{{$name} is Hello & Singular}}
-* * {{{$name} is other}}
+masculine 2 {{{$name :i:inflect} is Masculine & 2}}
+feminine singular {{{$name :i:inflect} is Feminine & Singular}}
+foo 4 {{{$name :i:inflect} is Foo & 4}}
+masculine singular {{{$name :i:inflect} is Masculine & Singular}}
+hello singular {{{$name :i:inflect} is Hello & Singular}}
+* * {{{$name :i:inflect} is other}}
 
         gato is Masculine & Singular
     
diff --git a/test/src/inflection/message2/Message2Test.cpp b/test/src/inflection/message2/Message2Test.cpp
index 169bd4bd0..427089e7a 100644
--- a/test/src/inflection/message2/Message2Test.cpp
+++ b/test/src/inflection/message2/Message2Test.cpp
@@ -410,7 +410,6 @@ 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.
@@ -460,89 +459,4 @@ TEST_CASE("Message2Test#testLocales")
     }
     xmlCleanupParser();
     REQUIRE(executedFiles > 0);
-
-    // Verify strict error reporting on invalid option value types and best-effort fallback formatting.
-    {
-        std::map args;
-        args[UnicodeString(u"word")] = Formattable(UnicodeString(u"cat"));
-        args[UnicodeString(u"n")] = Formattable(static_cast(2));
-        Locale enUS("en_US");
-
-        for (const auto* pattern : { u"{$word :i:quantify withValue=$word}", u"{$word :i:inflect number=$n}" }) {
-            UErrorCode ec = U_ZERO_ERROR;
-            UParseError pe;
-            MessageFormatter::Builder b(ec);
-            b.setFunctionRegistry(plainRegistry)
-                .setLocale(enUS)
-                .setErrorHandlingBehavior(MessageFormatter::U_MF_STRICT)
-                .setPattern(UnicodeString(pattern), pe, ec);
-            MessageFormatter mf = b.build(ec);
-            if (U_SUCCESS(ec)) {
-                mf.formatToString(MessageArguments(args, ec), ec);
-            }
-            CHECK(U_FAILURE(ec));
-        }
-
-        {
-            UErrorCode ec = U_ZERO_ERROR;
-            UParseError pe;
-            MessageFormatter mf = MessageFormatter::Builder(ec)
-                                      .setFunctionRegistry(plainRegistry)
-                                      .setLocale(enUS)
-                                      .setBidiIsolationStrategy(icu::U_MF_BIDI_OFF)
-                                      .setPattern(UnicodeString(u"Hello {$word :i:inflect bogusFeature=value} and {:i:inflect}!"), pe, ec)
-                                      .build(ec);
-            REQUIRE(U_SUCCESS(ec));
-            UnicodeString out = mf.formatToString(MessageArguments(args, ec), ec);
-            REQUIRE(U_SUCCESS(ec));
-            CHECK(out == UnicodeString(u"Hello {$word} and {:i:inflect}!"));
-        }
-        {
-            // Default bidi isolation strategy (U_MF_BIDI_AUTO) with LTR locale and no u:dir option
-            // should treat locale-aware inflection functions as LTR (no FSI/PDI wrapping).
-            UErrorCode ec = U_ZERO_ERROR;
-            UParseError pe;
-            MessageFormatter mf = MessageFormatter::Builder(ec)
-                                      .setFunctionRegistry(plainRegistry)
-                                      .setLocale(enUS)
-                                      .setPattern(UnicodeString(u"You have {$n :i:numeral withStyle=asWords} messages."), pe, ec)
-                                      .build(ec);
-            REQUIRE(U_SUCCESS(ec));
-            UnicodeString out = mf.formatToString(MessageArguments(args, ec), ec);
-            REQUIRE(U_SUCCESS(ec));
-            CHECK(out == UnicodeString(u"You have two messages."));
-        }
-        {
-            // Local variable bound to an invalid inflection function call and referenced without a function annotation,
-            // as well as chained into another inflection function call.
-            for (const auto* pattern : {
-                     u".local $v = {$word :i:inflect bogusFeature=value} {{{$v}}}",
-                     u".local $v = {$word :i:numeral} {{{$v :i:numeral}}}",
-                 }) {
-                UErrorCode ec = U_ZERO_ERROR;
-                UParseError pe;
-                MessageFormatter mf = MessageFormatter::Builder(ec)
-                                          .setFunctionRegistry(plainRegistry)
-                                          .setLocale(enUS)
-                                          .setBidiIsolationStrategy(icu::U_MF_BIDI_OFF)
-                                          .setPattern(UnicodeString(pattern), pe, ec)
-                                          .build(ec);
-                REQUIRE(U_SUCCESS(ec));
-                UnicodeString out = mf.formatToString(MessageArguments(args, ec), ec);
-                REQUIRE(U_SUCCESS(ec));
-                CHECK(out == UnicodeString(u"{$v}"));
-
-                UErrorCode strictEc = U_ZERO_ERROR;
-                MessageFormatter strictMf = MessageFormatter::Builder(strictEc)
-                                                .setFunctionRegistry(plainRegistry)
-                                                .setLocale(enUS)
-                                                .setErrorHandlingBehavior(MessageFormatter::U_MF_STRICT)
-                                                .setPattern(UnicodeString(pattern), pe, strictEc)
-                                                .build(strictEc);
-                REQUIRE(U_SUCCESS(strictEc));
-                strictMf.formatToString(MessageArguments(args, strictEc), strictEc);
-                CHECK(strictEc == U_MF_FORMATTING_ERROR);
-            }
-        }
-    }
 }

From 23b542e61aa569ebcf63b24eb5d9dadfd27ad441 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Neboj=C5=A1a=20=C4=86iri=C4=87?= 
Date: Tue, 20 Oct 2026 07:12:07 +0000
Subject: [PATCH 3/5] Inflection-213 Use XML hex entities for bidi isolates on
 bare variables in Message2Test

---
 test/resources/inflection/message2/de_DE.xml | 12 ++++-----
 test/resources/inflection/message2/en_US.xml | 28 ++++++++++----------
 test/resources/inflection/message2/es_MX.xml | 14 +++++-----
 3 files changed, 27 insertions(+), 27 deletions(-)

diff --git a/test/resources/inflection/message2/de_DE.xml b/test/resources/inflection/message2/de_DE.xml
index 996998885..1ecebaada 100644
--- a/test/resources/inflection/message2/de_DE.xml
+++ b/test/resources/inflection/message2/de_DE.xml
@@ -9,20 +9,20 @@
             Bank
         
         .local $defArticleInPreposition = {$name :i:inflect number=singular case=accusative to=defArticleInPreposition} .match $defArticleInPreposition
-|in die| {{The defArticleInPreposition of '{$name :i:inflect}' is 'in die'.}}
-* {{The defArticleInPreposition of '{$name :i:inflect}' is unexpectedly '$defArticleInPreposition'.}}
+|in die| {{The defArticleInPreposition of '{$name}' is 'in die'.}}
+* {{The defArticleInPreposition of '{$name}' is unexpectedly '$defArticleInPreposition'.}}
 
-        The defArticleInPreposition of 'Bank' is 'in die'.
+        The defArticleInPreposition of '⁨Bank⁩' is 'in die'.
     
     
         
             Fundort
         
         .local $withDefArticleInPreposition = {$name :i:inflect number=singular case=dative to=withDefArticleInPreposition} .match $withDefArticleInPreposition
-|im Fundort| {{The withDefArticleInPreposition of '{$name :i:inflect}' is 'im Fundort'.}}
-* {{The withDefArticleInPreposition of '{$name :i:inflect}' is something else.}}
+|im Fundort| {{The withDefArticleInPreposition of '{$name}' is 'im Fundort'.}}
+* {{The withDefArticleInPreposition of '{$name}' is something else.}}
 
-        The withDefArticleInPreposition of 'Fundort' is 'im Fundort'.
+        The withDefArticleInPreposition of '⁨Fundort⁩' is 'im Fundort'.
     
     
         
diff --git a/test/resources/inflection/message2/en_US.xml b/test/resources/inflection/message2/en_US.xml
index 485d13f93..6e49af874 100644
--- a/test/resources/inflection/message2/en_US.xml
+++ b/test/resources/inflection/message2/en_US.xml
@@ -8,8 +8,8 @@
         
             lights
         
-        The {$object :i:inflect} {is :i:inflect number=$object} on.
-        The lights are on.
+        The {$object} {is :i:inflect number=$object} on.
+        The ⁨lights⁩ are on.
     
     
         
@@ -23,40 +23,40 @@
             light
         
         .local $num = {$object :i:inflect to=number} .match $num
-plural {{The {$object :i:inflect} are on}}
-* {{The {$object :i:inflect} is on}}
+plural {{The {$object} are on}}
+* {{The {$object} is on}}
 
-        The light is on
+        The ⁨light⁩ is on
     
     
         
             lights
         
         .local $num = {$object :i:inflect to=number} .match $num
-plural {{The {$object :i:inflect} are on}}
-* {{The {$object :i:inflect} is on}}
+plural {{The {$object} are on}}
+* {{The {$object} is on}}
 
-        The lights are on
+        The ⁨lights⁩ are on
     
     
         
             lights on the front porch
         
         .local $num = {$object :i:inflect to=number} .match $num
-plural {{The {$object :i:inflect} are on}}
-* {{The {$object :i:inflect} is on}}
+plural {{The {$object} are on}}
+* {{The {$object} is on}}
 
-        The lights on the front porch are on
+        The ⁨lights on the front porch⁩ are on
     
     
         
             light on the front porch
         
         .local $num = {$object :i:inflect to=number} .match $num
-plural {{The {$object :i:inflect} are on}}
-* {{The {$object :i:inflect} is on}}
+plural {{The {$object} are on}}
+* {{The {$object} is on}}
 
-        The light on the front porch is on
+        The ⁨light on the front porch⁩ is on
     
     
         
diff --git a/test/resources/inflection/message2/es_MX.xml b/test/resources/inflection/message2/es_MX.xml
index a7bcdf22e..9ddb59d53 100644
--- a/test/resources/inflection/message2/es_MX.xml
+++ b/test/resources/inflection/message2/es_MX.xml
@@ -16,14 +16,14 @@
             gato
         
         .local $var1 = {$name :i:inflect to=gender} .local $var2 = {$name :i:inflect to=number} .match $var1 $var2
-masculine 2 {{{$name :i:inflect} is Masculine & 2}}
-feminine singular {{{$name :i:inflect} is Feminine & Singular}}
-foo 4 {{{$name :i:inflect} is Foo & 4}}
-masculine singular {{{$name :i:inflect} is Masculine & Singular}}
-hello singular {{{$name :i:inflect} is Hello & Singular}}
-* * {{{$name :i:inflect} is other}}
+masculine 2 {{{$name} is Masculine & 2}}
+feminine singular {{{$name} is Feminine & Singular}}
+foo 4 {{{$name} is Foo & 4}}
+masculine singular {{{$name} is Masculine & Singular}}
+hello singular {{{$name} is Hello & Singular}}
+* * {{{$name} is other}}
 
-        gato is Masculine & Singular
+        ⁨gato⁩ is Masculine & Singular
     
     
         

From b0c369a5ba26c35d5b74d764fb694f6365cff6b5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Neboj=C5=A1a=20=C4=86iri=C4=87?= 
Date: Tue, 20 Oct 2026 17:48:16 +0000
Subject: [PATCH 4/5] Inflection-213 Turn off bidi isolation in Message2Test
 and revert XML test changes

---
 test/resources/inflection/message2/de_DE.xml  |  4 ++--
 test/resources/inflection/message2/en_US.xml  | 10 +++++-----
 test/resources/inflection/message2/es_MX.xml  |  2 +-
 test/src/inflection/message2/Message2Test.cpp |  1 +
 4 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/test/resources/inflection/message2/de_DE.xml b/test/resources/inflection/message2/de_DE.xml
index 1ecebaada..64c96ccdc 100644
--- a/test/resources/inflection/message2/de_DE.xml
+++ b/test/resources/inflection/message2/de_DE.xml
@@ -12,7 +12,7 @@
 |in die| {{The defArticleInPreposition of '{$name}' is 'in die'.}}
 * {{The defArticleInPreposition of '{$name}' is unexpectedly '$defArticleInPreposition'.}}
 
-        The defArticleInPreposition of '⁨Bank⁩' is 'in die'.
+        The defArticleInPreposition of 'Bank' is 'in die'.
     
     
         
@@ -22,7 +22,7 @@
 |im Fundort| {{The withDefArticleInPreposition of '{$name}' is 'im Fundort'.}}
 * {{The withDefArticleInPreposition of '{$name}' is something else.}}
 
-        The withDefArticleInPreposition of '⁨Fundort⁩' is 'im Fundort'.
+        The withDefArticleInPreposition of 'Fundort' is 'im Fundort'.
     
     
         
diff --git a/test/resources/inflection/message2/en_US.xml b/test/resources/inflection/message2/en_US.xml
index 6e49af874..0fa7487bd 100644
--- a/test/resources/inflection/message2/en_US.xml
+++ b/test/resources/inflection/message2/en_US.xml
@@ -9,7 +9,7 @@
             lights
         
         The {$object} {is :i:inflect number=$object} on.
-        The ⁨lights⁩ are on.
+        The lights are on.
     
     
         
@@ -26,7 +26,7 @@
 plural {{The {$object} are on}}
 * {{The {$object} is on}}
 
-        The ⁨light⁩ is on
+        The light is on
     
     
         
@@ -36,7 +36,7 @@ plural {{The {$object} are on}}
 plural {{The {$object} are on}}
 * {{The {$object} is on}}
 
-        The ⁨lights⁩ are on
+        The lights are on
     
     
         
@@ -46,7 +46,7 @@ plural {{The {$object} are on}}
 plural {{The {$object} are on}}
 * {{The {$object} is on}}
 
-        The ⁨lights on the front porch⁩ are on
+        The lights on the front porch are on
     
     
         
@@ -56,7 +56,7 @@ plural {{The {$object} are on}}
 plural {{The {$object} are on}}
 * {{The {$object} is on}}
 
-        The ⁨light on the front porch⁩ is on
+        The light on the front porch is on
     
     
         
diff --git a/test/resources/inflection/message2/es_MX.xml b/test/resources/inflection/message2/es_MX.xml
index 9ddb59d53..f94a33115 100644
--- a/test/resources/inflection/message2/es_MX.xml
+++ b/test/resources/inflection/message2/es_MX.xml
@@ -23,7 +23,7 @@ masculine singular {{{$name} is Masculine & Singular}}
 hello singular {{{$name} is Hello & Singular}}
 * * {{{$name} is other}}
 
-        ⁨gato⁩ is Masculine & Singular
+        gato is Masculine & Singular
     
     
         
diff --git a/test/src/inflection/message2/Message2Test.cpp b/test/src/inflection/message2/Message2Test.cpp
index 427089e7a..ed331f1bf 100644
--- a/test/src/inflection/message2/Message2Test.cpp
+++ b/test/src/inflection/message2/Message2Test.cpp
@@ -410,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.

From 27cfb823848b510a59214016a5b854377390d187 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Neboj=C5=A1a=20=C4=86iri=C4=87?= 
Date: Thu, 22 Oct 2026 20:55:15 +0000
Subject: [PATCH 5/5] Inflection-213 Update CI workflows from ICU 79.1rc to
 79.1

---
 .github/workflows/cmake-multi-platform.yml          | 2 +-
 .github/workflows/create-distribution-packaging.yml | 2 +-
 .github/workflows/doxygen-gh-pages.yml              | 2 +-
 .github/workflows/ubuntu-memory-check.yml           | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml
index 0086ad0e0..9feb20727 100644
--- a/.github/workflows/cmake-multi-platform.yml
+++ b/.github/workflows/cmake-multi-platform.yml
@@ -29,7 +29,7 @@ on:
       - '!documents/**'
 env:
   ICU_MAJOR: '79'
-  ICU_MINOR: '1rc'
+  ICU_MINOR: '1'
 
 jobs:
   cache-icu:
diff --git a/.github/workflows/create-distribution-packaging.yml b/.github/workflows/create-distribution-packaging.yml
index 87adddaa0..d047134ae 100644
--- a/.github/workflows/create-distribution-packaging.yml
+++ b/.github/workflows/create-distribution-packaging.yml
@@ -34,7 +34,7 @@ jobs:
               build/*.tar.gz
     env:
       ICU_MAJOR: '79'
-      ICU_MINOR: '1rc'
+      ICU_MINOR: '1'
 
     steps:
       - name: Checkout
diff --git a/.github/workflows/doxygen-gh-pages.yml b/.github/workflows/doxygen-gh-pages.yml
index a7a3c7ee2..645385c4b 100644
--- a/.github/workflows/doxygen-gh-pages.yml
+++ b/.github/workflows/doxygen-gh-pages.yml
@@ -25,7 +25,7 @@ on:
 
 env:
   ICU_MAJOR: '79'
-  ICU_MINOR: '1rc'
+  ICU_MINOR: '1'
 
 jobs:
   cache-icu:
diff --git a/.github/workflows/ubuntu-memory-check.yml b/.github/workflows/ubuntu-memory-check.yml
index 200b40d4c..05583aa30 100644
--- a/.github/workflows/ubuntu-memory-check.yml
+++ b/.github/workflows/ubuntu-memory-check.yml
@@ -27,7 +27,7 @@ on:
       - '!documents/**'
 env:
   ICU_MAJOR: '79'
-  ICU_MINOR: '1rc'
+  ICU_MINOR: '1'
 
 jobs:
   cache-icu: