diff --git a/icu4c/source/common/unicode/utypes.h b/icu4c/source/common/unicode/utypes.h
index f1a89770f1f7..347f3c896051 100644
--- a/icu4c/source/common/unicode/utypes.h
+++ b/icu4c/source/common/unicode/utypes.h
@@ -584,12 +584,13 @@ typedef enum UErrorCode {
     U_MF_OPERAND_MISMATCH_ERROR,     /**< An operand provided to a function does not have the required form for that function @internal ICU 75 technology preview @deprecated This API is for technology preview only. */
     U_MF_UNSUPPORTED_STATEMENT_ERROR, /**< A message includes a reserved statement. @internal ICU 75 technology preview @deprecated This API is for technology preview only. */
     U_MF_UNSUPPORTED_EXPRESSION_ERROR, /**< A message includes syntax reserved for future standardization or private implementation use. @internal ICU 75 technology preview @deprecated This API is for technology preview only. */
+    U_MF_DUPLICATE_VARIANT_ERROR, /**< A message includes a variant with the same key list as another variant. @internal ICU 76 technology preview @deprecated This API is for technology preview only. */
 #ifndef U_HIDE_DEPRECATED_API
     /**
      * One more than the highest normal formatting API error code.
      * @deprecated ICU 58 The numeric value may change over time, see ICU ticket #12420.
      */
-    U_FMT_PARSE_ERROR_LIMIT = 0x10121,
+    U_FMT_PARSE_ERROR_LIMIT = 0x10122,
 #endif  // U_HIDE_DEPRECATED_API
 
     /*
diff --git a/icu4c/source/common/utypes.cpp b/icu4c/source/common/utypes.cpp
index 715994d67f04..2f449ffea033 100644
--- a/icu4c/source/common/utypes.cpp
+++ b/icu4c/source/common/utypes.cpp
@@ -141,7 +141,8 @@ _uFmtErrorName[U_FMT_PARSE_ERROR_LIMIT - U_FMT_PARSE_ERROR_START] = {
     "U_MF_DUPLICATE_DECLARATION_ERROR",
     "U_MF_OPERAND_MISMATCH_ERROR",
     "U_MF_UNSUPPORTED_STATEMENT_ERROR",
-    "U_MF_UNSUPPORTED_EXPRESSION_ERROR"
+    "U_MF_UNSUPPORTED_EXPRESSION_ERROR",
+    "U_MF_DUPLICATE_VARIANT_ERROR"
 };
 
 static const char * const
diff --git a/icu4c/source/i18n/messageformat2_checker.cpp b/icu4c/source/i18n/messageformat2_checker.cpp
index 192167583fff..824b798410d5 100644
--- a/icu4c/source/i18n/messageformat2_checker.cpp
+++ b/icu4c/source/i18n/messageformat2_checker.cpp
@@ -22,6 +22,7 @@ Checks data model errors
 
 The following are checked here:
 Variant Key Mismatch
+Duplicate Variant
 Missing Fallback Variant (called NonexhaustivePattern here)
 Missing Selector Annotation
 Duplicate Declaration
@@ -162,6 +163,7 @@ void Checker::checkVariants(UErrorCode& status) {
 
     // Check that one variant includes only wildcards
     bool defaultExists = false;
+    bool duplicatesExist = false;
 
     for (int32_t i = 0; i < dataModel.numVariants(); i++) {
         const SelectorKeys& k = variants[i].getKeys();
@@ -173,10 +175,35 @@ void Checker::checkVariants(UErrorCode& status) {
             return;
         }
         defaultExists |= areDefaultKeys(keys, len);
+
+        // Check if this variant's keys are duplicated by any other variant's keys
+        if (!duplicatesExist) {
+            // This check takes quadratic time, but it can be optimized if checking
+            // this property turns out to be a bottleneck.
+            for (int32_t j = 0; j < i; j++) {
+                const SelectorKeys& k1 = variants[j].getKeys();
+                const Key* keys1 = k1.getKeysInternal();
+                bool allEqual = true;
+                // This variant was already checked,
+                // so we know keys1.len == len
+                for (int32_t kk = 0; kk < len; kk++) {
+                    if (!(keys[kk] == keys1[kk])) {
+                        allEqual = false;
+                        break;
+                    }
+                }
+                if (allEqual) {
+                    duplicatesExist = true;
+                }
+            }
+        }
+    }
+
+    if (duplicatesExist) {
+        errors.addError(StaticErrorType::DuplicateVariant, status);
     }
     if (!defaultExists) {
         errors.addError(StaticErrorType::NonexhaustivePattern, status);
-        return;
     }
 }
 
diff --git a/icu4c/source/i18n/messageformat2_data_model.cpp b/icu4c/source/i18n/messageformat2_data_model.cpp
index a37ba4cf44ec..d5651de44922 100644
--- a/icu4c/source/i18n/messageformat2_data_model.cpp
+++ b/icu4c/source/i18n/messageformat2_data_model.cpp
@@ -186,6 +186,9 @@ bool Key::operator==(const Key& other) const {
     if (isWildcard()) {
         return other.isWildcard();
     }
+    if (other.isWildcard()) {
+        return false;
+    }
     return (asLiteral() == other.asLiteral());
 }
 
@@ -829,23 +832,19 @@ const Expression& Binding::getValue() const {
         } else {
             const Operator* rator = rhs.getOperator(errorCode);
             bool hasOperator = U_SUCCESS(errorCode);
-            if (hasOperator && rator->isReserved()) {
-                errorCode = U_INVALID_STATE_ERROR;
+            // Clear error code -- the "error" from the absent operator
+            // is handled
+            errorCode = U_ZERO_ERROR;
+            b = Binding(variableName, std::move(rhs));
+            b.local = false;
+            if (hasOperator) {
+                rator = b.getValue().getOperator(errorCode);
+                U_ASSERT(U_SUCCESS(errorCode));
+                b.annotation = &rator->contents;
             } else {
-                // Clear error code -- the "error" from the absent operator
-                // is handled
-                errorCode = U_ZERO_ERROR;
-                b = Binding(variableName, std::move(rhs));
-                b.local = false;
-                if (hasOperator) {
-                    rator = b.getValue().getOperator(errorCode);
-                    U_ASSERT(U_SUCCESS(errorCode));
-                    b.annotation = std::get_if<Callable>(&(rator->contents));
-                } else {
-                    b.annotation = nullptr;
-                }
-                U_ASSERT(!hasOperator || b.annotation != nullptr);
+                b.annotation = nullptr;
             }
+            U_ASSERT(!hasOperator || b.annotation != nullptr);
         }
     }
     return b;
@@ -853,7 +852,8 @@ const Expression& Binding::getValue() const {
 
 const OptionMap& Binding::getOptionsInternal() const {
     U_ASSERT(annotation != nullptr);
-    return annotation->getOptions();
+    U_ASSERT(std::holds_alternative<Callable>(*annotation));
+    return std::get_if<Callable>(annotation)->getOptions();
 }
 
 void Binding::updateAnnotation() {
@@ -863,7 +863,7 @@ void Binding::updateAnnotation() {
         return;
     }
     U_ASSERT(U_SUCCESS(localErrorCode) && !rator->isReserved());
-    annotation = std::get_if<Callable>(&(rator->contents));
+    annotation = &rator->contents;
 }
 
 Binding::Binding(const Binding& other) : var(other.var), expr(other.expr), local(other.local) {
diff --git a/icu4c/source/i18n/messageformat2_errors.cpp b/icu4c/source/i18n/messageformat2_errors.cpp
index cbb9e1497d69..1fb31377cd1c 100644
--- a/icu4c/source/i18n/messageformat2_errors.cpp
+++ b/icu4c/source/i18n/messageformat2_errors.cpp
@@ -135,6 +135,10 @@ namespace message2 {
                 status = U_MF_VARIANT_KEY_MISMATCH_ERROR;
                 break;
             }
+            case StaticErrorType::DuplicateVariant: {
+                status = U_MF_DUPLICATE_VARIANT_ERROR;
+                break;
+            }
             case StaticErrorType::NonexhaustivePattern: {
                 status = U_MF_NONEXHAUSTIVE_PATTERN_ERROR;
                 break;
@@ -211,6 +215,10 @@ namespace message2 {
             dataModelError = true;
             break;
         }
+        case StaticErrorType::DuplicateVariant: {
+            dataModelError = true;
+            break;
+        }
         case StaticErrorType::NonexhaustivePattern: {
             dataModelError = true;
             break;
diff --git a/icu4c/source/i18n/messageformat2_errors.h b/icu4c/source/i18n/messageformat2_errors.h
index ef2ad20faddb..604c8bcf57ac 100644
--- a/icu4c/source/i18n/messageformat2_errors.h
+++ b/icu4c/source/i18n/messageformat2_errors.h
@@ -54,6 +54,7 @@ namespace message2 {
     enum StaticErrorType {
         DuplicateDeclarationError,
         DuplicateOptionName,
+        DuplicateVariant,
         MissingSelectorAnnotation,
         NonexhaustivePattern,
         SyntaxError,
diff --git a/icu4c/source/i18n/messageformat2_function_registry.cpp b/icu4c/source/i18n/messageformat2_function_registry.cpp
index e9bbc03e737e..17955760ecfb 100644
--- a/icu4c/source/i18n/messageformat2_function_registry.cpp
+++ b/icu4c/source/i18n/messageformat2_function_registry.cpp
@@ -7,11 +7,14 @@
 
 #if !UCONFIG_NO_MF2
 
+#include <math.h>
+
 #include "unicode/dtptngen.h"
 #include "unicode/messageformat2_data_model_names.h"
 #include "unicode/messageformat2_function_registry.h"
 #include "unicode/smpdtfmt.h"
 #include "charstr.h"
+#include "double-conversion.h"
 #include "messageformat2_allocation.h"
 #include "messageformat2_function_registry_internal.h"
 #include "messageformat2_macros.h"
@@ -421,12 +424,11 @@ static FormattedPlaceholder notANumber(const FormattedPlaceholder& input) {
     return FormattedPlaceholder(input, FormattedValue(UnicodeString("NaN")));
 }
 
-static FormattedPlaceholder stringAsNumber(const number::LocalizedNumberFormatter& nf, const FormattedPlaceholder& input, UErrorCode& errorCode) {
+static double parseNumberLiteral(const FormattedPlaceholder& input, UErrorCode& errorCode) {
     if (U_FAILURE(errorCode)) {
         return {};
     }
 
-    double numberValue;
     // Copying string to avoid GCC dangling-reference warning
     // (although the reference is safe)
     UnicodeString inputStr = input.asFormattable().getString(errorCode);
@@ -434,12 +436,39 @@ static FormattedPlaceholder stringAsNumber(const number::LocalizedNumberFormatte
     if (U_FAILURE(errorCode)) {
         return {};
     }
-    UErrorCode localErrorCode = U_ZERO_ERROR;
-    strToDouble(inputStr, numberValue, localErrorCode);
-    if (U_FAILURE(localErrorCode)) {
+
+    // Hack: Check for cases that are forbidden by the MF2 grammar
+    // but allowed by StringToDouble
+    int32_t len = inputStr.length();
+
+    if (len > 0 && ((inputStr[0] == '+')
+                    || (inputStr[0] == '0' && len > 1 && inputStr[1] != '.')
+                    || (inputStr[len - 1] == '.')
+                    || (inputStr[0] == '.'))) {
         errorCode = U_MF_OPERAND_MISMATCH_ERROR;
+        return 0;
+    }
+
+    // Otherwise, convert to double using double_conversion::StringToDoubleConverter
+    using namespace double_conversion;
+    int processedCharactersCount = 0;
+    StringToDoubleConverter converter(0, 0, 0, "", "");
+    double result =
+        converter.StringToDouble(reinterpret_cast<const uint16_t*>(inputStr.getBuffer()),
+                                 len,
+                                 &processedCharactersCount);
+    if (processedCharactersCount != len) {
+        errorCode = U_MF_OPERAND_MISMATCH_ERROR;
+    }
+    return result;
+}
+
+static FormattedPlaceholder tryParsingNumberLiteral(const number::LocalizedNumberFormatter& nf, const FormattedPlaceholder& input, UErrorCode& errorCode) {
+    double numberValue = parseNumberLiteral(input, errorCode);
+    if (U_FAILURE(errorCode)) {
         return notANumber(input);
     }
+
     UErrorCode savedStatus = errorCode;
     number::FormattedNumber result = nf.formatDouble(numberValue, errorCode);
     // Ignore U_USING_DEFAULT_WARNING
@@ -590,7 +619,7 @@ FormattedPlaceholder StandardFunctions::Number::format(FormattedPlaceholder&& ar
         }
         case UFMT_STRING: {
             // Try to parse the string as a number
-            return stringAsNumber(realFormatter, arg, errorCode);
+            return tryParsingNumberLiteral(realFormatter, arg, errorCode);
         }
         default: {
             // Other types can't be parsed as a number
diff --git a/icu4c/source/i18n/messageformat2_macros.h b/icu4c/source/i18n/messageformat2_macros.h
index ee8cf0779e6b..f06ed1a5a977 100644
--- a/icu4c/source/i18n/messageformat2_macros.h
+++ b/icu4c/source/i18n/messageformat2_macros.h
@@ -60,19 +60,11 @@ using namespace pluralimpl;
 // Fallback
 #define REPLACEMENT ((UChar32) 0xFFFD)
 
-// MessageFormat2 uses four keywords: `.input`, `.local`, `.when`, and `.match`.
+// MessageFormat2 uses three keywords: `.input`, `.local`, and `.match`.
 
-static constexpr UChar32 ID_INPUT[] = {
-    0x2E, 0x69, 0x6E, 0x70, 0x75, 0x74, 0 /* ".input" */
-};
-
-static constexpr UChar32 ID_LOCAL[] = {
-    0x2E, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0 /* ".local" */
-};
-
-static constexpr UChar32 ID_MATCH[] = {
-    0x2E, 0x6D, 0x61, 0x74, 0x63, 0x68, 0 /* ".match" */
-};
+static constexpr std::u16string_view ID_INPUT = u".input";
+static constexpr std::u16string_view ID_LOCAL = u".local";
+static constexpr std::u16string_view ID_MATCH = u".match";
 
 // Returns immediately if `errorCode` indicates failure
 #define CHECK_ERROR(errorCode)                                                                          \
diff --git a/icu4c/source/i18n/messageformat2_parser.cpp b/icu4c/source/i18n/messageformat2_parser.cpp
index fe5bfb484803..0635074f69c3 100644
--- a/icu4c/source/i18n/messageformat2_parser.cpp
+++ b/icu4c/source/i18n/messageformat2_parser.cpp
@@ -456,12 +456,11 @@ void Parser::parseToken(UChar32 c, UErrorCode& errorCode) {
    the string beginning at `source[index]`
    No postcondition -- a message can end with a '}' token
 */
-template <int32_t N>
-void Parser::parseToken(const UChar32 (&token)[N], UErrorCode& errorCode) {
+void Parser::parseToken(const std::u16string_view& token, UErrorCode& errorCode) {
     U_ASSERT(inBounds(source, index));
 
     int32_t tokenPos = 0;
-    while (tokenPos < N - 1) {
+    while (tokenPos < (int32_t) token.length()) {
         if (source[index] != token[tokenPos]) {
             ERROR(parseError, errorCode, index);
             return;
@@ -478,13 +477,12 @@ void Parser::parseToken(const UChar32 (&token)[N], UErrorCode& errorCode) {
    the string beginning at `source[index']`),
    then consumes optional whitespace again
 */
-template <int32_t N>
-void Parser::parseTokenWithWhitespace(const UChar32 (&token)[N], UErrorCode& errorCode) {
+void Parser::parseTokenWithWhitespace(const std::u16string_view& token, UErrorCode& errorCode) {
     // No need for error check or bounds check before parseOptionalWhitespace
     parseOptionalWhitespace(errorCode);
     // Establish precondition
     CHECK_BOUNDS(source, index, parseError, errorCode);
-    parseToken(token);
+    parseToken(token, errorCode);
     parseOptionalWhitespace(errorCode);
     // Guarantee postcondition
     CHECK_BOUNDS(source, index, parseError, errorCode);
@@ -641,80 +639,40 @@ FunctionName Parser::parseFunction(UErrorCode& errorCode) {
   Precondition: source[index] == BACKSLASH
 
   Consume an escaped character.
+  Corresponds to `escaped-char` in the grammar.
 
-  Generalized to handle `reserved-escape`, `text-escape`,
-  or `literal-escape`, depending on the `kind` argument.
-
-  Appends result to `str`
+  No postcondition (a message can end with an escaped char)
 */
-void Parser::parseEscapeSequence(EscapeKind kind,
-                                 UnicodeString &str,
-                                 UErrorCode& errorCode) {
+UnicodeString Parser::parseEscapeSequence(UErrorCode& errorCode) {
     U_ASSERT(inBounds(source, index));
     U_ASSERT(source[index] == BACKSLASH);
     normalizedInput += BACKSLASH;
     index++; // Skip the initial backslash
-    CHECK_BOUNDS(source, index, parseError, errorCode);
-
-    #define SUCCEED \
-       /* Append to the output string */                    \
-       str += source[index];                                \
-       /* Update normalizedInput */                         \
-       normalizedInput += source[index];                    \
-       /* Consume the character */                          \
-       index++;                                             \
-       /* Guarantee postcondition */                        \
-       CHECK_BOUNDS(source, index, parseError, errorCode);  \
-       return;
-
-    // Expect a '{', '|' or '}'
-    switch (source[index]) {
-    case LEFT_CURLY_BRACE:
-    case RIGHT_CURLY_BRACE: {
-        // Allowed in a `text-escape` or `reserved-escape`
-        switch (kind) {
-        case TEXT:
-        case RESERVED: {
-            SUCCEED;
+    UnicodeString str;
+    if (inBounds(source, index)) {
+        // Expect a '{', '|' or '}'
+        switch (source[index]) {
+        case LEFT_CURLY_BRACE:
+        case RIGHT_CURLY_BRACE:
+        case PIPE:
+        case BACKSLASH: {
+            /* Append to the output string */
+            str += source[index];
+            /* Update normalizedInput */
+            normalizedInput += source[index];
+            /* Consume the character */
+            index++;
+            return str;
         }
         default: {
+            // No other characters are allowed here
             break;
         }
         }
-        break;
     }
-    case PIPE: {
-        // Allowed in a `literal-escape` or `reserved-escape`
-        switch (kind) {
-           case LITERAL:
-           case RESERVED: {
-               SUCCEED;
-           }
-           default: {
-               break;
-           }
-        }
-        break;
-    }
-   case BACKSLASH: {
-       // Allowed in any escape sequence
-       SUCCEED;
-   }
-   default: {
-        // No other characters are allowed here
-        break;
-    }
-   }
    // If control reaches here, there was an error
    ERROR(parseError, errorCode, index);
-}
-
-/*
-  Consume an escaped pipe or backslash, matching the `literal-escape`
-  nonterminal in the grammar
-*/
-void Parser::parseLiteralEscape(UnicodeString &str, UErrorCode& errorCode) {
-    parseEscapeSequence(LITERAL, str, errorCode);
+   return str;
 }
 
 
@@ -736,7 +694,7 @@ Literal Parser::parseQuotedLiteral(UErrorCode& errorCode) {
             bool done = false;
             while (!done) {
                 if (source[index] == BACKSLASH) {
-                    parseLiteralEscape(contents, errorCode);
+                    contents += parseEscapeSequence(errorCode);
                 } else if (isQuotedChar(source[index])) {
                     contents += source[index];
                     normalizedInput += source[index];
@@ -1142,10 +1100,6 @@ Arbitrary lookahead is required to parse attribute lists, similarly to option li
     }
 }
 
-void Parser::parseReservedEscape(UnicodeString &str, UErrorCode& errorCode) {
-    parseEscapeSequence(RESERVED, str, errorCode);
-}
-
 /*
   Consumes a non-empty sequence of reserved-chars, reserved-escapes, and
   literals (as in 1*(reserved-char / reserved-escape / literal) in the `reserved-body` rule)
@@ -1177,8 +1131,7 @@ void Parser::parseReservedChunk(Reserved::Builder& result, UErrorCode& status) {
 
         if (source[index] == BACKSLASH) {
             // reserved-escape
-            parseReservedEscape(chunk, status);
-            result.add(Literal(false, chunk), status);
+            result.add(Literal(false, parseEscapeSequence(status)), status);
             chunk.setTo(u"", 0);
         } else if (source[index] == PIPE || isUnquotedStart(source[index])) {
             result.add(parseLiteral(status), status);
@@ -1718,15 +1671,17 @@ void Parser::parseUnsupportedStatement(UErrorCode& status) {
     dataModel.addUnsupportedStatement(builder.build(status), status);
 }
 
-// Terrible hack to get around the ambiguity between `matcher` and `reserved-statement`
-bool Parser::nextIsMatch() const {
-    for(int32_t i = 0; i < 6; i++) {
-        if (!inBounds(source, index + i) || source[index + i] != ID_MATCH[i]) {
+// Terrible hack to get around the ambiguity between unsupported keywords
+// and supported keywords
+bool Parser::nextIs(const std::u16string_view &keyword) const {
+    for(int32_t i = 0; i < (int32_t) keyword.length(); i++) {
+        if (!inBounds(source, index + i) || source[index + i] != keyword[i]) {
             return false;
         }
     }
     return true;
 }
+
 /*
   Consume a possibly-empty sequence of declarations separated by whitespace;
   each declaration matches the `declaration` nonterminal in the grammar
@@ -1740,19 +1695,17 @@ void Parser::parseDeclarations(UErrorCode& status) {
 
     while (source[index] == PERIOD) {
         CHECK_BOUNDS(source, index + 1, parseError, status);
-        if (source[index + 1] == ID_LOCAL[1]) {
+        // Check for unsupported statements first
+        // Lookahead is needed to disambiguate keyword from known keywords
+        if (!nextIs(ID_MATCH) && !nextIs(ID_LOCAL) && !nextIs(ID_INPUT)) {
+            parseUnsupportedStatement(status);
+        } else if (source[index + 1] == ID_LOCAL[1]) {
             parseLocalDeclaration(status);
         } else if (source[index + 1] == ID_INPUT[1]) {
             parseInputDeclaration(status);
         } else {
-            // Unsupported statement
-            // Lookahead is needed to disambiguate this from a `match`
-            if (!nextIsMatch()) {
-                parseUnsupportedStatement(status);
-            } else {
-                // Done parsing declarations
-                break;
-            }
+            // Done parsing declarations
+            break;
         }
 
         // Avoid looping infinitely
@@ -1765,49 +1718,22 @@ void Parser::parseDeclarations(UErrorCode& status) {
 }
 
 /*
-  Consume an escaped curly brace, or backslash, matching the `text-escape`
-  nonterminal in the grammar
-*/
-void Parser::parseTextEscape(UnicodeString &str, UErrorCode& status) {
-    parseEscapeSequence(TEXT, str, status);
-}
-
-/*
-  Consume a non-empty sequence of text characters and escaped text characters,
-  matching the `text` nonterminal in the grammar
+  Consume a text character
+  matching the `text-char` nonterminal in the grammar
 
-  No postcondition (a message can end with a text)
+  No postcondition (a message can end with a text-char)
 */
-UnicodeString Parser::parseText(UErrorCode& status) {
+UnicodeString Parser::parseTextChar(UErrorCode& status) {
     UnicodeString str;
-    if (!inBounds(source, index)) {
-        // Text can be empty
-        return str;
-    }
-
-    if (!(isTextChar(source[index] || source[index] == BACKSLASH))) {
-        // Error -- text is expected here
+    if (!inBounds(source, index) || !(isTextChar(source[index]))) {
+        // Error -- text-char is expected here
         ERROR(parseError, status, index);
-        return str;
-    }
-
-    while (true) {
-        if (source[index] == BACKSLASH) {
-            parseTextEscape(str, status);
-        } else if (isTextChar(source[index])) {
-            normalizedInput += source[index];
-            str += source[index];
-            index++;
-            maybeAdvanceLine();
-        } else {
-            break;
-        }
-        if (!inBounds(source, index)) {
-            // OK for text to end a message
-            break;
-        }
+    } else {
+        normalizedInput += source[index];
+        str += source[index];
+        index++;
+        maybeAdvanceLine();
     }
-
     return str;
 }
 
@@ -2026,9 +1952,22 @@ std::variant<Expression, Markup> Parser::parsePlaceholder(UErrorCode& status) {
         return exprFallback(status);
     }
 
-    // Check if it's markup or an expression
-    if (source[index + 1] == NUMBER_SIGN || source[index + 1] == SLASH) {
-        // Markup
+    // Need to look ahead arbitrarily since can appear before the '{' and '#'
+    // in markup
+    int32_t tempIndex = index + 1;
+    bool isMarkup = false;
+    while (inBounds(source, tempIndex)) {
+        if (source[tempIndex] == NUMBER_SIGN || source[tempIndex] == SLASH) {
+            isMarkup = true;
+            break;
+        }
+        if (!isWhitespace(source[tempIndex])){
+            break;
+        }
+        tempIndex++;
+    }
+
+    if (isMarkup) {
         return parseMarkup(status);
     }
     return parseExpression(status);
@@ -2058,9 +1997,18 @@ Pattern Parser::parseSimpleMessage(UErrorCode& status) {
                 }
                 break;
             }
+            case BACKSLASH: {
+                // Must be escaped-char
+                result.add(parseEscapeSequence(status), status);
+                break;
+            }
+            case RIGHT_CURLY_BRACE: {
+                // Distinguish unescaped '}' from end of quoted pattern
+                break;
+            }
             default: {
-                // Must be text
-                result.add(parseText(status), status);
+                // Must be text-char
+                result.add(parseTextChar(status), status);
                 break;
             }
             }
@@ -2232,21 +2180,31 @@ void Parser::parseBody(UErrorCode& status) {
 void Parser::parse(UParseError &parseErrorResult, UErrorCode& status) {
     CHECK_ERROR(status);
 
-    bool simple = true;
-    // Message can be empty, so we need to only look ahead
-    // if we know it's non-empty
+    bool complex = false;
+    // First, "look ahead" to determine if this is a simple or complex
+    // message. To do that, check the first non-whitespace character.
+    while (inBounds(source, index) && isWhitespace(source[index])) {
+        index++;
+    }
     if (inBounds(source, index)) {
         if (source[index] == PERIOD
             || (index < static_cast<uint32_t>(source.length()) + 1
                 && source[index] == LEFT_CURLY_BRACE
                 && source[index + 1] == LEFT_CURLY_BRACE)) {
-            // A complex message begins with a '.' or '{'
-            parseDeclarations(status);
-            parseBody(status);
-            simple = false;
+            complex = true;
         }
     }
-    if (simple) {
+    // Reset index
+    index = 0;
+
+    // Message can be empty, so we need to only look ahead
+    // if we know it's non-empty
+    if (complex) {
+        parseOptionalWhitespace(status);
+        parseDeclarations(status);
+        parseBody(status);
+        parseOptionalWhitespace(status);
+    } else {
         // Simple message
         // For normalization, quote the pattern
         normalizedInput += LEFT_CURLY_BRACE;
diff --git a/icu4c/source/i18n/messageformat2_parser.h b/icu4c/source/i18n/messageformat2_parser.h
index 92c0475d67db..9367c0e981dd 100644
--- a/icu4c/source/i18n/messageformat2_parser.h
+++ b/icu4c/source/i18n/messageformat2_parser.h
@@ -91,10 +91,6 @@ namespace message2 {
 	  parseError.postContext[0] = '\0';
 	}
 
-	// Used so `parseEscapeSequence()` can handle all types of escape sequences
-	// (literal, text, and reserved)
-	typedef enum { LITERAL, TEXT, RESERVED } EscapeKind;
-
 	static void translateParseError(const MessageParseError&, UParseError&);
 	static void setParseError(MessageParseError&, uint32_t);
 	void maybeAdvanceLine();
@@ -111,19 +107,16 @@ namespace message2 {
 	void parseOptionalWhitespace(UErrorCode&);
 	void parseToken(UChar32, UErrorCode&);
 	void parseTokenWithWhitespace(UChar32, UErrorCode&);
-	template <int32_t N>
-	void parseToken(const UChar32 (&)[N], UErrorCode&);
-	template <int32_t N>
-	void parseTokenWithWhitespace(const UChar32 (&)[N], UErrorCode&);
-        bool nextIsMatch() const;
+	void parseToken(const std::u16string_view&, UErrorCode&);
+	void parseTokenWithWhitespace(const std::u16string_view&, UErrorCode&);
+        bool nextIs(const std::u16string_view&) const;
 	UnicodeString parseName(UErrorCode&);
         UnicodeString parseIdentifier(UErrorCode&);
         UnicodeString parseDigits(UErrorCode&);
 	VariableName parseVariableName(UErrorCode&);
 	FunctionName parseFunction(UErrorCode&);
-	void parseEscapeSequence(EscapeKind, UnicodeString&, UErrorCode&);
-	void parseLiteralEscape(UnicodeString&, UErrorCode&);
-        Literal parseUnquotedLiteral(UErrorCode&);
+	UnicodeString parseEscapeSequence(UErrorCode&);
+	Literal parseUnquotedLiteral(UErrorCode&);
         Literal parseQuotedLiteral(UErrorCode&);
 	Literal parseLiteral(UErrorCode&);
         template<class T>
@@ -134,7 +127,6 @@ namespace message2 {
         void parseOption(OptionAdder<T>&, UErrorCode&);
         template<class T>
         void parseOptions(OptionAdder<T>&, UErrorCode&);
-	void parseReservedEscape(UnicodeString&, UErrorCode&);
 	void parseReservedChunk(Reserved::Builder&, UErrorCode&);
 	Reserved parseReserved(UErrorCode&);
         Reserved parseReservedBody(Reserved::Builder&, UErrorCode&);
@@ -143,8 +135,7 @@ namespace message2 {
         Markup parseMarkup(UErrorCode&);
 	Expression parseExpression(UErrorCode&);
         std::variant<Expression, Markup> parsePlaceholder(UErrorCode&);
-	void parseTextEscape(UnicodeString&, UErrorCode&);
-	UnicodeString parseText(UErrorCode&);
+	UnicodeString parseTextChar(UErrorCode&);
 	Key parseKey(UErrorCode&);
 	SelectorKeys parseNonEmptyKeys(UErrorCode&);
 	void errorPattern(UErrorCode& status);
diff --git a/icu4c/source/i18n/messageformat2_serializer.cpp b/icu4c/source/i18n/messageformat2_serializer.cpp
index 23c123e6f116..2d007fa4bf5a 100644
--- a/icu4c/source/i18n/messageformat2_serializer.cpp
+++ b/icu4c/source/i18n/messageformat2_serializer.cpp
@@ -35,12 +35,8 @@ void Serializer::emit(const UnicodeString& s) {
     result += s;
 }
 
-template <int32_t N>
-void Serializer::emit(const UChar32 (&token)[N]) {
-    // Don't emit the terminator
-    for (int32_t i = 0; i < N - 1; i++) {
-        emit(token[i]);
-    }
+void Serializer::emit(const std::u16string_view& token) {
+    result.append(token);
 }
 
 void Serializer::emit(const Literal& l) {
diff --git a/icu4c/source/i18n/messageformat2_serializer.h b/icu4c/source/i18n/messageformat2_serializer.h
index 4b72d1ca715f..38d3412665a4 100644
--- a/icu4c/source/i18n/messageformat2_serializer.h
+++ b/icu4c/source/i18n/messageformat2_serializer.h
@@ -38,8 +38,7 @@ namespace message2 {
 
         void whitespace();
         void emit(UChar32);
-        template <int32_t N>
-        void emit(const UChar32 (&)[N]);
+        void emit(const std::u16string_view&);
         void emit(const UnicodeString&);
         void emit(const Literal&);
         void emit(const Key&);
diff --git a/icu4c/source/i18n/unicode/messageformat2_data_model.h b/icu4c/source/i18n/unicode/messageformat2_data_model.h
index 942a03f73591..65dc836caeb3 100644
--- a/icu4c/source/i18n/unicode/messageformat2_data_model.h
+++ b/icu4c/source/i18n/unicode/messageformat2_data_model.h
@@ -2599,7 +2599,7 @@ namespace message2 {
             // If non-null, the referent is a member of `expr` so
             // its lifetime is the same as the lifetime of the enclosing Binding
             // (as long as there's no mutation)
-            const Callable* annotation = nullptr;
+            const std::variant<Reserved, Callable>* annotation = nullptr;
 
             const OptionMap& getOptionsInternal() const;
 
diff --git a/icu4c/source/test/intltest/messageformat2test_read_json.cpp b/icu4c/source/test/intltest/messageformat2test_read_json.cpp
index 33e65a92ce85..0c65764e7fb5 100644
--- a/icu4c/source/test/intltest/messageformat2test_read_json.cpp
+++ b/icu4c/source/test/intltest/messageformat2test_read_json.cpp
@@ -18,52 +18,44 @@ using namespace nlohmann;
 
 using namespace icu::message2;
 
-static UErrorCode getExpectedErrorFromString(const std::string& errorName) {
-    if (errorName == "Variant Key Mismatch") {
+static UErrorCode getExpectedRuntimeErrorFromString(const std::string& errorName) {
+    if (errorName == "syntax-error") {
+        return U_MF_SYNTAX_ERROR;
+    }
+    if (errorName == "variant-key-mismatch") {
         return U_MF_VARIANT_KEY_MISMATCH_ERROR;
     }
-    if (errorName == "Missing Fallback Variant") {
+    if (errorName == "missing-fallback-variant") {
         return U_MF_NONEXHAUSTIVE_PATTERN_ERROR;
     }
-    if (errorName == "Missing Selector Annotation") {
+    if (errorName == "missing-selector-annotation") {
         return U_MF_MISSING_SELECTOR_ANNOTATION_ERROR;
     }
-    if (errorName == "Duplicate Declaration") {
-        return U_MF_DUPLICATE_DECLARATION_ERROR;
-    }
-    if (errorName == "Unsupported Statement") {
-        return U_MF_UNSUPPORTED_STATEMENT_ERROR;
-    }
-// Arbitrary default
-    return U_MF_DUPLICATE_OPTION_NAME_ERROR;
-}
-
-static UErrorCode getExpectedRuntimeErrorFromString(const std::string& errorName) {
-    if (errorName == "parse-error" || errorName == "empty-token" || errorName == "extra-content") {
-        return U_MF_SYNTAX_ERROR;
-    }
-    if (errorName == "key-mismatch") {
-        return U_MF_VARIANT_KEY_MISMATCH_ERROR;
-    }
-    if (errorName == "missing-var" || errorName == "unresolved-var") {
+    if (errorName == "unresolved-variable") {
         return U_MF_UNRESOLVED_VARIABLE_ERROR;
     }
-    if (errorName == "unsupported-annotation") {
+    if (errorName == "unsupported-expression") {
         return U_MF_UNSUPPORTED_EXPRESSION_ERROR;
     }
-    if (errorName == "bad-input" || errorName == "RangeError") {
+    if (errorName == "bad-operand") {
         return U_MF_OPERAND_MISMATCH_ERROR;
     }
     if (errorName == "bad-option") {
         return U_MF_FORMATTING_ERROR;
     }
-    if (errorName == "missing-func") {
+    if (errorName == "unknown-function") {
         return U_MF_UNKNOWN_FUNCTION_ERROR;
     }
     if (errorName == "duplicate-declaration") {
         return U_MF_DUPLICATE_DECLARATION_ERROR;
     }
-    if (errorName == "selector-error") {
+    if (errorName == "duplicate-option-name") {
+        return U_MF_DUPLICATE_OPTION_NAME_ERROR;
+    }
+    if (errorName == "duplicate-variant") {
+        return U_MF_DUPLICATE_VARIANT_ERROR;
+    }
+    if (errorName == "bad-selector") {
         return U_MF_SELECTOR_ERROR;
     }
     if (errorName == "formatting-error") {
@@ -88,56 +80,86 @@ static void makeTestName(char* buffer, size_t size, std::string fileName, int32_
     snprintf(buffer, size, "test from file: %s[%u]", fileName.c_str(), ++testNum);
 }
 
-static bool setArguments(TestCase::Builder& test, const json::object_t& params, UErrorCode& errorCode) {
+static bool setArguments(TestMessageFormat2& t,
+                         TestCase::Builder& test,
+                         const std::vector<json>& params,
+                         UErrorCode& errorCode) {
     if (U_FAILURE(errorCode)) {
         return true;
     }
+    bool schemaError = false;
     for (auto argsIter = params.begin(); argsIter != params.end(); ++argsIter) {
-        const UnicodeString argName = u_str(argsIter->first);
-        // Determine type of value
-        if (argsIter->second.is_number()) {
-            test.setArgument(argName,
-                             argsIter->second.template get<double>());
-        } else if (argsIter->second.is_string()) {
-            test.setArgument(argName,
-                             u_str(argsIter->second.template get<std::string>()));
-        } else if (argsIter->second.is_object()) {
-            // Dates: represent in tests as { "date" : timestamp }, to distinguish
-            // from number values
-            auto obj = argsIter->second.template get<json::object_t>();
-            if (obj["date"].is_number()) {
-                test.setDateArgument(argName, obj["date"]);
-            } else if (obj["decimal"].is_string()) {
-                // Decimal strings: represent in tests as { "decimal" : string },
-                // to distinguish from string values
-                test.setDecimalArgument(argName, obj["decimal"].template get<std::string>(), errorCode);
+        auto j_object = argsIter->template get<json::object_t>();
+        if (!j_object["name"].is_null()) {
+            const UnicodeString argName = u_str(j_object["name"].template get<std::string>());
+            if (!j_object["value"].is_null()) {
+                json val = j_object["value"];
+                // Determine type of value
+                if (val.is_number()) {
+                    test.setArgument(argName,
+                                     val.template get<double>());
+                } else if (val.is_string()) {
+                    test.setArgument(argName,
+                                     u_str(val.template get<std::string>()));
+                } else if (val.is_object()) {
+                    // Dates: represent in tests as { "date" : timestamp }, to distinguish
+                    // from number values
+                    auto obj = val.template get<json::object_t>();
+                    if (obj["date"].is_number()) {
+                        test.setDateArgument(argName, val["date"]);
+                    } else if (obj["decimal"].is_string()) {
+                        // Decimal strings: represent in tests as { "decimal" : string },
+                        // to distinguish from string values
+                        test.setDecimalArgument(argName, obj["decimal"].template get<std::string>(), errorCode);
+                    }
+                } else if (val.is_boolean() || val.is_null()) {
+                    return false; // For now, boolean and null arguments are unsupported
+                }
+            } else {
+               schemaError = true;
+               break;
             }
-        } else if (argsIter->second.is_boolean() || argsIter->second.is_null()) {
-            return false; // For now, boolean and null arguments are unsupported
+        } else {
+            schemaError = true;
+            break;
+        }
+    }
+    if (schemaError) {
+        t.logln("Warning: test with missing 'name' or 'value' in params");
+        if (U_SUCCESS(errorCode)) {
+            errorCode = U_ILLEGAL_ARGUMENT_ERROR;
         }
     }
     return true;
 }
 
+
+/*
+  Test files are expected to follow the schema in:
+  https://github.com/unicode-org/conformance/blob/main/schema/message_fmt2/testgen_schema.json
+  as of https://github.com/unicode-org/conformance/pull/255
+*/
 static void runValidTest(TestMessageFormat2& icuTest,
                          const std::string& testName,
+                         const std::string& defaultError,
                          const json& j,
                          IcuTestErrorCode& errorCode) {
     auto j_object = j.template get<json::object_t>();
     std::string messageText;
 
+    // src can be a single string or an array of strings
     if (!j_object["src"].is_null()) {
-        messageText = j_object["src"].template get<std::string>();
-    } else {
-        if (!j_object["srcs"].is_null()) {
-            auto strings = j_object["srcs"].template get<std::vector<std::string>>();
+        if (j_object["src"].is_string()) {
+            messageText = j_object["src"].template get<std::string>();
+        } else {
+            auto strings = j_object["src"].template get<std::vector<std::string>>();
             for (const auto &piece : strings) {
                 messageText += piece;
             }
         }
-        // Otherwise, it should probably be an error, but we just
-        // treat this as the empty string
     }
+    // Otherwise, it should probably be an error, but we just
+    // treat this as the empty string
 
     TestCase::Builder test = successTest(testName, messageText);
 
@@ -160,16 +182,17 @@ static void runValidTest(TestMessageFormat2& icuTest,
     }
 
     if (!j_object["params"].is_null()) {
-        // Map from string to json
-        auto params = j_object["params"].template get<json::object_t>();
-        if (!setArguments(test, params, errorCode)) {
+        // `params` is an array of objects
+        auto params = j_object["params"].template get<std::vector<json>>();
+        if (!setArguments(icuTest, test, params, errorCode)) {
             return; // Skip tests with unsupported arguments
         }
     }
 
-    if (!j_object["errors"].is_null()) {
+    bool expectedError = false;
+    if (!j_object["expErrors"].is_null()) {
         // Map from string to string
-        auto errors = j_object["errors"].template get<std::vector<std::map<std::string, std::string>>>();
+        auto errors = j_object["expErrors"].template get<std::vector<std::map<std::string, std::string>>>();
         // We only emit the first error, so we just hope the first error
         // in the list in the test is also the error we emit
         U_ASSERT(errors.size() > 0);
@@ -182,22 +205,18 @@ static void runValidTest(TestMessageFormat2& icuTest,
             return;
         }
         test.setExpectedError(getExpectedRuntimeErrorFromString(errorType));
+        expectedError = true;
+    } else if (defaultError.length() > 0) {
+        test.setExpectedError(getExpectedRuntimeErrorFromString(defaultError));
+        expectedError = true;
     }
-    TestCase t = test.build();
-    TestUtils::runTestCase(icuTest, t, errorCode);
-}
-
-static void runSyntaxErrorTest(TestMessageFormat2& icuTest,
-                         const std::string& testName,
-                         const json& j,
-                         IcuTestErrorCode& errorCode) {
-    auto messageText = j["src"].template get<std::string>();
 
-    TestCase::Builder test = successTest(testName, messageText)
-        .setExpectedError(U_MF_SYNTAX_ERROR);
-
-    auto j_object = j.template get<json::object_t>();
+    // If no expected result and no error, then set the test builder to expect success
+    if (j_object["exp"].is_null() && !expectedError) {
+        test.setNoSyntaxError();
+    }
 
+    // Check for expected diagnostic values
     int32_t lineNumber = 0;
     int32_t offset = -1;
     if (!j_object["char"].is_null()) {
@@ -209,117 +228,14 @@ static void runSyntaxErrorTest(TestMessageFormat2& icuTest,
     if (offset != -1) {
         test.setExpectedLineNumberAndOffset(lineNumber, offset);
     }
-    TestCase t = test.build();
-    TestUtils::runTestCase(icuTest, t, errorCode);
-}
-
-// File name is relative to message2/ in the test data directory
-static void runICU4JSyntaxTestsFromJsonFile(TestMessageFormat2& t,
-                                            const std::string& fileName,
-                                            IcuTestErrorCode& errorCode) {
-    const char* testDataDirectory = IntlTest::getSharedTestData(errorCode);
-    CHECK_ERROR(errorCode);
-
-    std::string testFileName(testDataDirectory);
-    testFileName.append("message2/");
-    testFileName.append(fileName);
-    std::ifstream testFile(testFileName);
-    json data = json::parse(testFile);
-
-    // Map from string to json, where the strings are the function names
-    auto tests = data.template get<json::object_t>();
-    for (auto iter = tests.begin(); iter != tests.end(); ++iter) {
-        int32_t testNum = 0;
-        auto categoryName = iter->first;
-        t.logln("ICU4J syntax tests:");
-        t.logln(u_str(iter->second.dump()));
-
-        // Array of tests
-        auto testsForThisCategory = iter->second.template get<std::vector<std::string>>();
-
-        TestCase::Builder test;
-        test.setNoSyntaxError();
-        for (auto testsIter = testsForThisCategory.begin();
-             testsIter != testsForThisCategory.end();
-             ++testsIter) {
-            char testName[100];
-            makeTestName(testName, sizeof(testName), categoryName, ++testNum);
-            t.logln(testName);
-
-            // Tests here are just strings, and we test that they run without syntax errors
-            test.setPattern(u_str(*testsIter));
-            TestCase testCase = test.build();
-            TestUtils::runTestCase(t, testCase, errorCode);
-        }
-
-    }
-}
-
-// File name is relative to message2/ in the test data directory
-static void runICU4JSelectionTestsFromJsonFile(TestMessageFormat2& t,
-                                            const std::string& fileName,
-                                            IcuTestErrorCode& errorCode) {
-    const char* testDataDirectory = IntlTest::getSharedTestData(errorCode);
-    CHECK_ERROR(errorCode);
-
-    std::string testFileName(testDataDirectory);
-    testFileName.append("message2/");
-    testFileName.append(fileName);
-    std::ifstream testFile(testFileName);
-    json data = json::parse(testFile);
-
-    int32_t testNum = 0;
 
-    for (auto iter = data.begin(); iter != data.end(); ++iter) {
-        // Each test has a "shared" and a "variations" field
-        auto j_object = iter->get<json::object_t>();
-        auto shared = j_object["shared"];
-        auto variations = j_object["variations"];
 
-        // Skip ignored tests
-        if (!j_object["ignoreCpp"].is_null()) {
-            return;
-        }
-
-        // shared has a "srcs" field
-        auto strings = shared["srcs"].template get<std::vector<std::string>>();
-        std::string messageText;
-        for (const auto &piece : strings) {
-            messageText += piece;
-        }
-
-        t.logln(u_str("ICU4J selectors tests: " + fileName));
-        t.logln(u_str(iter->dump()));
-
-        TestCase::Builder test;
-        char testName[100];
-        makeTestName(testName, sizeof(testName), fileName, ++testNum);
-        test.setName(testName);
-
-        // variations has "params" and "exp" fields, and an optional "locale"
-        for (auto variationsIter = variations.begin(); variationsIter != variations.end(); ++variationsIter) {
-            auto variation = variationsIter->get<json::object_t>();
-            auto params = variation["params"];
-            auto exp = variation["exp"];
-
-            test.setExpected(u_str(exp));
-            test.setPattern(u_str(messageText));
-            test.setExpectSuccess();
-            setArguments(test, params, errorCode);
-
-            if (!variation["locale"].is_null()) {
-                std::string localeStr = variation["locale"].template get<std::string>();
-                test.setLocale(Locale(localeStr.c_str()));
-            }
-
-            TestCase testCase = test.build();
-            TestUtils::runTestCase(t, testCase, errorCode);
-        }
-    }
+    TestCase t = test.build();
+    TestUtils::runTestCase(icuTest, t, errorCode);
 }
 
 // File name is relative to message2/ in the test data directory
-static void runValidTestsFromJsonFile(TestMessageFormat2& t,
+static void runTestsFromJsonFile(TestMessageFormat2& t,
                                       const std::string& fileName,
                                       IcuTestErrorCode& errorCode) {
     const char* testDataDirectory = IntlTest::getSharedTestData(errorCode);
@@ -334,156 +250,41 @@ static void runValidTestsFromJsonFile(TestMessageFormat2& t,
     int32_t testNum = 0;
     char testName[100];
 
-    for (auto iter = data.begin(); iter != data.end(); ++iter) {
-        makeTestName(testName, sizeof(testName), fileName, ++testNum);
-        t.logln(testName);
-
-        t.logln(u_str(iter->dump()));
-
-        runValidTest(t, testName, *iter, errorCode);
-    }
-}
-
-// File name is relative to message2/ in the test data directory
-static void runDataModelErrorTestsFromJsonFile(TestMessageFormat2& t,
-                                               const std::string& fileName,
-                                               IcuTestErrorCode& errorCode) {
-    const char* testDataDirectory = IntlTest::getSharedTestData(errorCode);
-    CHECK_ERROR(errorCode);
-
-    std::string dataModelErrorsFileName(testDataDirectory);
-    dataModelErrorsFileName.append("message2/");
-    dataModelErrorsFileName.append(fileName);
-    std::ifstream dataModelErrorsFile(dataModelErrorsFileName);
-    json data = json::parse(dataModelErrorsFile);
-
-    // Do tests for data model errors
-    // This file is an object where the keys are error names
-    // and the values are arrays of strings.
-    // The whole file can be represented
-    // as a map from strings to a vector of strings.
-    using dataModelErrorType = std::map<std::string, std::vector<std::string>>;
-    auto dataModelErrorTests = data.template get<dataModelErrorType>();
-    for (auto iter = dataModelErrorTests.begin(); iter != dataModelErrorTests.end(); ++iter) {
-        auto errorName = iter->first;
-        auto messages = iter->second;
-
-        UErrorCode expectedError = getExpectedErrorFromString(errorName);
-        int32_t testNum = 0;
-        char testName[100];
-        TestCase::Builder testBuilder;
-        for (auto messagesIter = messages.begin(); messagesIter != messages.end(); ++messagesIter) {
-            makeTestName(testName, sizeof(testName), errorName, testNum);
-            testBuilder.setName(testName);
-            t.logln(u_str(fileName + ": " + testName));
-            testNum++;
-            UnicodeString messageText = u_str(*messagesIter);
-            t.logln(messageText);
-
-            TestCase test = testBuilder.setPattern(messageText)
-                .setExpectedError(expectedError)
-                .build();
-            TestUtils::runTestCase(t, test, errorCode);
+    auto j_object = data.template get<json::object_t>();
+
+    // Some files have an expected error
+    std::string defaultError;
+    if (!j_object["defaultTestProperties"].is_null()
+        && !j_object["defaultTestProperties"]["expErrors"].is_null()) {
+        auto expErrors = j_object["defaultTestProperties"]["expErrors"];
+        // expErrors might also be a boolean, in which case we ignore it --
+        // so we have to check if it's an array
+        if (expErrors.is_array()) {
+            auto expErrorsObj = expErrors.template get<std::vector<json>>();
+            if (expErrorsObj.size() > 0) {
+                if (!expErrorsObj[0]["type"].is_null()) {
+                    defaultError = expErrorsObj[0]["type"].template get<std::string>();
+                }
+            }
         }
     }
 
-}
-
-// File name is relative to message2/ in the test data directory
-static void runSyntaxErrorTestsFromJsonFile(TestMessageFormat2& t,
-                                            const std::string& fileName,
-                                            IcuTestErrorCode& errorCode) {
-    const char* testDataDirectory = IntlTest::getSharedTestData(errorCode);
-    CHECK_ERROR(errorCode);
-
-    std::string syntaxErrorsFileName(testDataDirectory);
-    syntaxErrorsFileName.append("message2/");
-    syntaxErrorsFileName.append(fileName);
-    std::ifstream syntaxErrorsFile(syntaxErrorsFileName);
-    json data = json::parse(syntaxErrorsFile);
-
-    // Do tests for syntax errors
-    // This file is just an array of strings
-    int32_t testNum = 0;
-    char testName[100];
-    TestCase::Builder testBuilder;
-    for (auto iter = data.begin(); iter != data.end(); ++iter) {
-        makeTestName(testName, sizeof(testName), fileName, ++testNum);
-        testBuilder.setName(testName);
-        t.logln(testName);
-
-        json json_string = *iter;
-        UnicodeString cpp_string = u_str(json_string.template get<std::string>());
-
-        t.logln(cpp_string);
-        TestCase test = testBuilder.setPattern(cpp_string)
-            .setExpectedError(U_MF_SYNTAX_ERROR)
-            .build();
-        TestUtils::runTestCase(t, test, errorCode);
-    }
-}
-
-// File name is relative to message2/ in the test data directory
-static void runSyntaxTestsWithDiagnosticsFromJsonFile(TestMessageFormat2& t,
-                                                      const std::string& fileName,
-                                                      IcuTestErrorCode& errorCode) {
-    const char* testDataDirectory = IntlTest::getSharedTestData(errorCode);
-    CHECK_ERROR(errorCode);
-
-    std::string testFileName(testDataDirectory);
-    testFileName.append("message2/");
-    testFileName.append(fileName);
-    std::ifstream testFile(testFileName);
-    json data = json::parse(testFile);
-
-    int32_t testNum = 0;
-    char testName[100];
-
-    for (auto iter = data.begin(); iter != data.end(); ++iter) {
-        makeTestName(testName, sizeof(testName), fileName, ++testNum);
-        t.logln(testName);
-        t.logln(u_str(iter->dump()));
-
-        runSyntaxErrorTest(t, testName, *iter, errorCode);
-    }
-}
-
-// File name is relative to message2/ in the test data directory
-static void runFunctionTestsFromJsonFile(TestMessageFormat2& t,
-                                         const std::string& fileName,
-                                         IcuTestErrorCode& errorCode) {
-    // Get the test data directory
-    const char* testDataDirectory = IntlTest::getSharedTestData(errorCode);
-    CHECK_ERROR(errorCode);
-
-    std::string functionTestsFileName(testDataDirectory);
-    functionTestsFileName.append("message2/");
-    functionTestsFileName.append(fileName);
-    std::ifstream functionTestsFile(functionTestsFileName);
-    json data = json::parse(functionTestsFile);
-
-    // Map from string to json, where the strings are the function names
-    auto tests = data.template get<json::object_t>();
-    for (auto iter = tests.begin(); iter != tests.end(); ++iter) {
-        int32_t testNum = 0;
-        auto functionName = iter->first;
-        t.logln(u_str("Function tests: " + fileName));
-        t.logln(u_str(iter->second.dump()));
-
-        // Array of tests
-        auto testsForThisFunction = iter->second.template get<std::vector<json>>();
-        for (auto testsIter = testsForThisFunction.begin();
-             testsIter != testsForThisFunction.end();
-             ++testsIter) {
-            char testName[100];
-            makeTestName(testName, sizeof(testName), functionName, ++testNum);
+    if (!j_object["tests"].is_null()) {
+        auto tests = j_object["tests"].template get<std::vector<json>>();
+        for (auto iter = tests.begin(); iter != tests.end(); ++iter) {
+            makeTestName(testName, sizeof(testName), fileName, ++testNum);
             t.logln(testName);
 
-            runValidTest(t, testName, *testsIter, errorCode);
-        }
+            t.logln(u_str(iter->dump()));
 
+            runValidTest(t, testName, defaultError, *iter, errorCode);
+        }
+    } else {
+        // Test doesn't follow schema -- probably an error
+        t.logln("Warning: no tests in filename: ");
+        t.logln(u_str(fileName));
+        (UErrorCode&) errorCode = U_ILLEGAL_ARGUMENT_ERROR;
     }
-
 }
 
 void TestMessageFormat2::jsonTestsFromFiles(IcuTestErrorCode& errorCode) {
@@ -493,27 +294,34 @@ void TestMessageFormat2::jsonTestsFromFiles(IcuTestErrorCode& errorCode) {
     // Tests directly under testdata/message2 are specific to ICU4C.
 
     // Do spec tests for syntax errors
-    runSyntaxErrorTestsFromJsonFile(*this, "spec/syntax-errors.json", errorCode);
-    runSyntaxErrorTestsFromJsonFile(*this, "more-syntax-errors.json", errorCode);
+    runTestsFromJsonFile(*this, "spec/syntax-errors.json", errorCode);
 
     // Do tests for data model errors
-    runDataModelErrorTestsFromJsonFile(*this, "spec/data-model-errors.json", errorCode);
-    runDataModelErrorTestsFromJsonFile(*this, "more-data-model-errors.json", errorCode);
+    runTestsFromJsonFile(*this, "spec/data-model-errors.json", errorCode);
+    runTestsFromJsonFile(*this, "more-data-model-errors.json", errorCode);
+
+    // Do tests for reserved statements/expressions
+    runTestsFromJsonFile(*this, "spec/unsupported-expressions.json", errorCode);
+    runTestsFromJsonFile(*this, "spec/unsupported-statements.json", errorCode);
 
     // Do valid spec tests
-    runValidTestsFromJsonFile(*this, "spec/test-core.json", errorCode);
+    runTestsFromJsonFile(*this, "spec/syntax.json", errorCode);
 
     // Do valid function tests
-    runFunctionTestsFromJsonFile(*this, "spec/test-functions.json", errorCode);
+    runTestsFromJsonFile(*this, "spec/functions/date.json", errorCode);
+    runTestsFromJsonFile(*this, "spec/functions/datetime.json", errorCode);
+    runTestsFromJsonFile(*this, "spec/functions/integer.json", errorCode);
+    runTestsFromJsonFile(*this, "spec/functions/number.json", errorCode);
+    runTestsFromJsonFile(*this, "spec/functions/string.json", errorCode);
+    runTestsFromJsonFile(*this, "spec/functions/time.json", errorCode);
 
     // Other tests (non-spec)
-    runFunctionTestsFromJsonFile(*this, "more-functions.json", errorCode);
-    runValidTestsFromJsonFile(*this, "valid-tests.json", errorCode);
-    runValidTestsFromJsonFile(*this, "resolution-errors.json", errorCode);
-    runValidTestsFromJsonFile(*this, "reserved-syntax.json", errorCode);
-    runValidTestsFromJsonFile(*this, "matches-whitespace.json", errorCode);
-    runValidTestsFromJsonFile(*this, "alias-selector-annotations.json", errorCode);
-    runValidTestsFromJsonFile(*this, "runtime-errors.json", errorCode);
+    runTestsFromJsonFile(*this, "more-functions.json", errorCode);
+    runTestsFromJsonFile(*this, "valid-tests.json", errorCode);
+    runTestsFromJsonFile(*this, "resolution-errors.json", errorCode);
+    runTestsFromJsonFile(*this, "matches-whitespace.json", errorCode);
+    runTestsFromJsonFile(*this, "alias-selector-annotations.json", errorCode);
+    runTestsFromJsonFile(*this, "runtime-errors.json", errorCode);
 
     // Re: the expected output for the first test in this file:
     // Note: the more "correct" fallback output seems like it should be "1.000 3" (ignoring the
@@ -523,10 +331,10 @@ void TestMessageFormat2::jsonTestsFromFiles(IcuTestErrorCode& errorCode) {
     // Probably this is going to change anyway so that any data model error gets replaced
     // with a fallback for the whole message.
     // The second test has a similar issue with the output.
-    runValidTestsFromJsonFile(*this, "tricky-declarations.json", errorCode);
+    runTestsFromJsonFile(*this, "tricky-declarations.json", errorCode);
 
     // Markup is ignored when formatting to string
-    runValidTestsFromJsonFile(*this, "markup.json", errorCode);
+    runTestsFromJsonFile(*this, "markup.json", errorCode);
 
     // TODO(duplicates): currently the expected output is based on using
     // the last definition of the duplicate-declared variable;
@@ -534,24 +342,24 @@ void TestMessageFormat2::jsonTestsFromFiles(IcuTestErrorCode& errorCode) {
     // however if https://github.com/unicode-org/message-format-wg/pull/704 lands,
     // it'll be a moot point since the output will be expected to be the fallback string
     // (This applies to the expected output for all the U_DUPLICATE_DECLARATION_ERROR tests)
-    runValidTestsFromJsonFile(*this, "duplicate-declarations.json", errorCode);
+    runTestsFromJsonFile(*this, "duplicate-declarations.json", errorCode);
 
     // TODO(options):
     // Bad options. The spec is unclear about this
     // -- see https://github.com/unicode-org/message-format-wg/issues/738
     // The current behavior is to set a U_MF_FORMATTING_ERROR for any invalid options.
-    runValidTestsFromJsonFile(*this, "invalid-options.json", errorCode);
+    runTestsFromJsonFile(*this, "invalid-options.json", errorCode);
 
-    runSyntaxTestsWithDiagnosticsFromJsonFile(*this, "syntax-errors-end-of-input.json", errorCode);
-    runSyntaxTestsWithDiagnosticsFromJsonFile(*this, "syntax-errors-diagnostics.json", errorCode);
-    runSyntaxTestsWithDiagnosticsFromJsonFile(*this, "invalid-number-literals-diagnostics.json", errorCode);
-    runSyntaxTestsWithDiagnosticsFromJsonFile(*this, "syntax-errors-diagnostics-multiline.json", errorCode);
+    runTestsFromJsonFile(*this, "syntax-errors-end-of-input.json", errorCode);
+    runTestsFromJsonFile(*this, "syntax-errors-diagnostics.json", errorCode);
+    runTestsFromJsonFile(*this, "invalid-number-literals-diagnostics.json", errorCode);
+    runTestsFromJsonFile(*this, "syntax-errors-diagnostics-multiline.json", errorCode);
 
     // ICU4J tests
-    runFunctionTestsFromJsonFile(*this, "icu-test-functions.json", errorCode);
-    runICU4JSyntaxTestsFromJsonFile(*this, "icu-parser-tests.json", errorCode);
-    runICU4JSelectionTestsFromJsonFile(*this, "icu-test-selectors.json", errorCode);
-    runValidTestsFromJsonFile(*this, "icu-test-previous-release.json", errorCode);
+    runTestsFromJsonFile(*this, "icu-test-functions.json", errorCode);
+    runTestsFromJsonFile(*this, "icu-parser-tests.json", errorCode);
+    runTestsFromJsonFile(*this, "icu-test-selectors.json", errorCode);
+    runTestsFromJsonFile(*this, "icu-test-previous-release.json", errorCode);
 }
 
 #endif /* #if !UCONFIG_NO_MF2 */
diff --git a/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFDataModelFormatter.java b/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFDataModelFormatter.java
index e33a7c9804a8..2ba70854d093 100644
--- a/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFDataModelFormatter.java
+++ b/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFDataModelFormatter.java
@@ -436,10 +436,8 @@ private static Object resolveLiteralOrVariable(
             Map<String, Object> arguments) {
         if (value instanceof Literal) {
             String val = ((Literal) value).value;
-            Number nr = OptUtils.asNumber(val);
-            if (nr != null) {
-                return nr;
-            }
+            // "The resolution of a text or literal MUST resolve to a string."
+            // https://github.com/unicode-org/message-format-wg/blob/main/spec/formatting.md#literal-resolution
             return val;
         } else if (value instanceof VariableRef) {
             String varName = ((VariableRef) value).name;
diff --git a/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFParser.java b/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFParser.java
index 073aafd8ef32..a5c826f87397 100644
--- a/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFParser.java
+++ b/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFParser.java
@@ -46,22 +46,32 @@ public static MFDataModel.Message parse(String input) throws MFParseException {
     // Parser proper
     private MFDataModel.Message parseImpl() throws MFParseException {
         MFDataModel.Message result;
+        // Determine if message is simple or complex; this requires
+        // looking through whitespace.
+        int savedPosition = input.getPosition();
+        skipOptionalWhitespaces();
         int cp = input.peekChar();
         if (cp == '.') { // declarations or .match
+            // No need to restore whitespace
             result = getComplexMessage();
         } else if (cp == '{') { // `{` or `{{`
             cp = input.readCodePoint();
             cp = input.peekChar();
             if (cp == '{') { // `{{`, complex body without declarations
                 input.backup(1); // let complexBody deal with the wrapping {{ and }}
+                // abnf: complex-message   = [s] *(declaration [s]) complex-body [s]
                 MFDataModel.Pattern pattern = getQuotedPattern();
+                skipOptionalWhitespaces();
                 result = new MFDataModel.PatternMessage(new ArrayList<>(), pattern);
             } else { // placeholder
-                input.backup(1); // We want the '{' present, to detect the part as placeholder.
+                // Restore whitespace if applicable
+                input.gotoPosition(savedPosition);
                 MFDataModel.Pattern pattern = getPattern();
                 result = new MFDataModel.PatternMessage(new ArrayList<>(), pattern);
             }
         } else {
+            // Restore whitespace if applicable
+            input.gotoPosition(savedPosition);
             MFDataModel.Pattern pattern = getPattern();
             result = new MFDataModel.PatternMessage(new ArrayList<>(), pattern);
         }
@@ -586,9 +596,10 @@ private MFDataModel.Message getComplexMessage() throws MFParseException {
         } else { // Expect {{...}} or end of message
             skipOptionalWhitespaces();
             int cp = input.peekChar();
-            // complex-message   = *(declaration [s]) complex-body
+            // abnf: complex-message   = [s] *(declaration [s]) complex-body [s]
             checkCondition(cp != EOF, "Expected a quoted pattern or .match; got end-of-input");
             MFDataModel.Pattern pattern = getQuotedPattern();
+            skipOptionalWhitespaces(); // Trailing whitespace is allowed
             checkCondition(input.atEnd(), "Content detected after the end of the message.");
             return new MFDataModel.PatternMessage(declarations, pattern);
         }
@@ -648,9 +659,7 @@ private MFDataModel.Variant getVariant() throws MFParseException {
             }
             keys.add(key);
         }
-        // Only want to skip whitespace if we parsed at least one key --
-        // otherwise, we might fail to catch trailing whitespace at the end of
-        // the message, which is a parse error
+        // Only want to skip whitespace if we parsed at least one key
         if (!keys.isEmpty()) {
             skipOptionalWhitespaces();
         }
@@ -712,7 +721,7 @@ private MFDataModel.Declaration getDeclaration() throws MFParseException {
         MFDataModel.Expression expression;
         switch (declName) {
             case "input":
-                skipMandatoryWhitespaces();
+                skipOptionalWhitespaces();
                 expression = getPlaceholder();
                 String inputVarName = null;
                 checkCondition(expression instanceof MFDataModel.VariableExpression,
diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/CoreTest.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/CoreTest.java
index 3305f2435a50..0dd3bf6be59c 100644
--- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/CoreTest.java
+++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/CoreTest.java
@@ -4,11 +4,14 @@
 package com.ibm.icu.dev.test.message2;
 
 import java.io.Reader;
+import java.lang.reflect.Type;
+import java.util.Map;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
+import com.google.gson.reflect.TypeToken;
 import com.ibm.icu.dev.test.CoreTestFmwk;
 
 @SuppressWarnings({"static-method", "javadoc"})
@@ -16,14 +19,30 @@
 public class CoreTest extends CoreTestFmwk {
     private static final String[] JSON_FILES = {"alias-selector-annotations.json",
                                                 "duplicate-declarations.json",
+                                                "icu-parser-tests.json",
+                                                "icu-test-functions.json",
+                                                "icu-test-previous-release.json",
+                                                "icu-test-selectors.json",
+                                                "invalid-number-literals-diagnostics.json",
                                                 "invalid-options.json",
                                                 "markup.json",
                                                 "matches-whitespace.json",
-                                                "reserved-syntax.json",
+                                                "more-data-model-errors.json",
+                                                "more-functions.json",
                                                 "resolution-errors.json",
                                                 "runtime-errors.json",
-                                                "spec/test-core.json",
+                                                "spec/data-model-errors.json",
+                                                "spec/syntax-errors.json",
+                                                "spec/syntax.json",
+                                                "spec/functions/date.json",
+                                                "spec/functions/datetime.json",
+                                                "spec/functions/integer.json",
+                                                "spec/functions/number.json",
+                                                "spec/functions/string.json",
+                                                "spec/functions/time.json",
                                                 "syntax-errors-diagnostics.json",
+                                                "syntax-errors-diagnostics-multiline.json",
+                                                "syntax-errors-end-of-input.json",
                                                 "tricky-declarations.json",
                                                 "valid-tests.json"};
 
@@ -31,9 +50,9 @@ public class CoreTest extends CoreTestFmwk {
     public void test() throws Exception {
         for (String jsonFile : JSON_FILES) {
             try (Reader reader = TestUtils.jsonReader(jsonFile)) {
-                Unit[] unitList = TestUtils.GSON.fromJson(reader, Unit[].class);
-                for (Unit unit : unitList) {
-                    TestUtils.runTestCase(unit);
+                MF2Test tests = TestUtils.GSON.fromJson(reader, MF2Test.class);
+                for (Unit unit : tests.tests) {
+                    TestUtils.runTestCase(tests.defaultTestProperties, unit);
                 }
             }
         }
diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/DataModelErrorsTest.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/DataModelErrorsTest.java
deleted file mode 100644
index d8ba5699c260..000000000000
--- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/DataModelErrorsTest.java
+++ /dev/null
@@ -1,44 +0,0 @@
-// © 2024 and later: Unicode, Inc. and others.
-// License & terms of use: https://www.unicode.org/copyright.html
-
-package com.ibm.icu.dev.test.message2;
-
-import java.io.Reader;
-import java.lang.reflect.Type;
-import java.util.Map;
-import java.util.Map.Entry;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import com.google.gson.reflect.TypeToken;
-import com.ibm.icu.dev.test.CoreTestFmwk;
-import com.ibm.icu.message2.MessageFormatter;
-
-@SuppressWarnings({"static-method", "javadoc"})
-@RunWith(JUnit4.class)
-public class DataModelErrorsTest extends CoreTestFmwk {
-    private static final String[] JSON_FILES = {"spec/data-model-errors.json",
-                                                "more-data-model-errors.json"};
-
-    @Test
-    public void test() throws Exception {
-        for (String jsonFile : JSON_FILES) {
-            try (Reader reader = TestUtils.jsonReader(jsonFile)) {
-                Type mapType = new TypeToken<Map<String, String[]>>(){/* not code */}.getType();
-                Map<String, String[]> unitList = TestUtils.GSON.fromJson(reader, mapType);
-                for (Entry<String, String[]> tests : unitList.entrySet()) {
-                    for (String pattern : tests.getValue()) {
-                        try {
-                            MessageFormatter.builder().setPattern(pattern).build().formatToString(null);
-                            fail("Undetected errors in '" + tests.getKey() + "': '" + pattern + "'");
-                        } catch (Exception e) {
-                            // We expected an error, so it's all good
-                        }
-                    }
-                }
-            }
-        }
-    }
-}
diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/DefaultTestProperties.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/DefaultTestProperties.java
new file mode 100644
index 000000000000..d0b463baf1f3
--- /dev/null
+++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/DefaultTestProperties.java
@@ -0,0 +1,23 @@
+// © 2024 and later: Unicode, Inc. and others.
+// License & terms of use: https://www.unicode.org/copyright.html
+
+package com.ibm.icu.dev.test.message2;
+
+import java.util.List;
+import java.util.Map;
+import java.util.StringJoiner;
+
+// See https://github.com/unicode-org/conformance/blob/main/schema/message_fmt2/testgen_schema.json
+
+// Class corresponding to the json test files.
+// Since this is serialized by Gson, the field names should match the keys in the .json files.
+class DefaultTestProperties {
+    // Unused fields ignored
+    final String locale;
+    final Object[] expErrors;
+
+    DefaultTestProperties(String locale, Object[] expErrors) {
+        this.locale = locale;
+        this.expErrors = expErrors;
+    }
+}
diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/FirstReleaseTests.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/FirstReleaseTests.java
deleted file mode 100644
index 994366a559aa..000000000000
--- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/FirstReleaseTests.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// © 2024 and later: Unicode, Inc. and others.
-// License & terms of use: https://www.unicode.org/copyright.html
-
-package com.ibm.icu.dev.test.message2;
-
-import java.io.Reader;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import com.ibm.icu.dev.test.CoreTestFmwk;
-
-/*
- * This is the equivalent of the `FromJsonTest` class in the previous release.
- * That class was originally a json file, converted to some hard-coded tests in the Java class.
- * Now that we can use gson for testing we reverted those tests back to json, tested in this class.
- */
-@SuppressWarnings({"static-method", "javadoc"})
-@RunWith(JUnit4.class)
-public class FirstReleaseTests extends CoreTestFmwk {
-    private static final String JSON_FILE = "icu-test-previous-release.json";
-
-    @Test
-    public void test() throws Exception {
-        try (Reader reader = TestUtils.jsonReader(JSON_FILE)) {
-            Unit[] unitList = TestUtils.GSON.fromJson(reader, Unit[].class);
-            for (Unit unit : unitList) {
-                TestUtils.runTestCase(unit);
-            }
-        }
-    }
-}
diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/FunctionsTest.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/FunctionsTest.java
deleted file mode 100644
index c76c60a50327..000000000000
--- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/FunctionsTest.java
+++ /dev/null
@@ -1,38 +0,0 @@
-// © 2024 and later: Unicode, Inc. and others.
-// License & terms of use: https://www.unicode.org/copyright.html
-
-package com.ibm.icu.dev.test.message2;
-
-import java.io.Reader;
-import java.lang.reflect.Type;
-import java.util.Map;
-import java.util.Map.Entry;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import com.google.gson.reflect.TypeToken;
-import com.ibm.icu.dev.test.CoreTestFmwk;
-
-@SuppressWarnings({"static-method", "javadoc"})
-@RunWith(JUnit4.class)
-public class FunctionsTest extends CoreTestFmwk {
-    private static final String[] JSON_FILES = {"spec/test-functions.json",
-                                                "more-functions.json"};
-
-    @Test
-    public void test() throws Exception {
-        for (String jsonFile : JSON_FILES) {
-            try (Reader reader = TestUtils.jsonReader(jsonFile)) {
-                Type mapType = new TypeToken<Map<String, Unit[]>>(){/* not code */}.getType();
-                Map<String, Unit[]> unitList = TestUtils.GSON.fromJson(reader, mapType);
-                for (Entry<String, Unit[]> testGroup : unitList.entrySet()) {
-                    for (Unit unit : testGroup.getValue()) {
-                        TestUtils.runTestCase(unit);
-                    }
-                }
-            }
-        }
-    }
-}
diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/IcuFunctionsTest.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/IcuFunctionsTest.java
deleted file mode 100644
index f286d5c2c743..000000000000
--- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/IcuFunctionsTest.java
+++ /dev/null
@@ -1,38 +0,0 @@
-// © 2024 and later: Unicode, Inc. and others.
-// License & terms of use: https://www.unicode.org/copyright.html
-
-package com.ibm.icu.dev.test.message2;
-
-import java.io.Reader;
-import java.lang.reflect.Type;
-import java.util.Map;
-import java.util.Map.Entry;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import com.google.gson.reflect.TypeToken;
-import com.ibm.icu.dev.test.CoreTestFmwk;
-
-@SuppressWarnings({"static-method", "javadoc"})
-@RunWith(JUnit4.class)
-public class IcuFunctionsTest extends CoreTestFmwk {
-    private static final String JSON_FILE = "icu-test-functions.json";
-
-    @Test
-    public void test() throws Exception {
-        try (Reader reader = TestUtils.jsonReader(JSON_FILE)) {
-            Type mapType =
-                    new TypeToken<Map<String, Unit[]>>() {
-                        /* not code */
-                    }.getType();
-            Map<String, Unit[]> unitList = TestUtils.GSON.fromJson(reader, mapType);
-            for (Entry<String, Unit[]> testGroup : unitList.entrySet()) {
-                for (Unit unit : testGroup.getValue()) {
-                    TestUtils.runTestCase(unit);
-                }
-            }
-        }
-    }
-}
diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/MF2Test.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/MF2Test.java
new file mode 100644
index 000000000000..502fde2a6093
--- /dev/null
+++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/MF2Test.java
@@ -0,0 +1,24 @@
+// © 2024 and later: Unicode, Inc. and others.
+// License & terms of use: https://www.unicode.org/copyright.html
+
+package com.ibm.icu.dev.test.message2;
+
+import java.util.List;
+import java.util.Map;
+import java.util.StringJoiner;
+
+// See https://github.com/unicode-org/conformance/blob/main/schema/message_fmt2/testgen_schema.json
+
+// Class corresponding to the json test files.
+// Since this is serialized by Gson, the field names should match the keys in the .json files.
+class MF2Test {
+    // Unused fields ignored
+    final DefaultTestProperties defaultTestProperties;
+    final Unit[] tests;
+
+    MF2Test(DefaultTestProperties defaultTestProperties,
+            Unit[] tests) {
+        this.defaultTestProperties = defaultTestProperties;
+        this.tests = tests;
+    }
+}
diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Param.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Param.java
new file mode 100644
index 000000000000..9c77fe73443b
--- /dev/null
+++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Param.java
@@ -0,0 +1,23 @@
+// © 2024 and later: Unicode, Inc. and others.
+// License & terms of use: https://www.unicode.org/copyright.html
+
+package com.ibm.icu.dev.test.message2;
+
+import java.util.List;
+import java.util.Map;
+import java.util.StringJoiner;
+
+// See https://github.com/unicode-org/conformance/blob/main/schema/message_fmt2/testgen_schema.json
+
+// Class corresponding to the json test files.
+// Since this is serialized by Gson, the field names should match the keys in the .json files.
+class Param {
+    // Unused fields ignored
+    final String name;
+    final Object value;
+
+    Param(String name, Object value) {
+        this.name = name;
+        this.value = value;
+    }
+}
diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/ParserSmokeTest.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/ParserSmokeTest.java
index 7b2a3b988c9e..638b38521b27 100644
--- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/ParserSmokeTest.java
+++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/ParserSmokeTest.java
@@ -29,16 +29,5 @@ public void testNullInput() throws Exception {
         MFParser.parse(null);
     }
 
-    @Test
-    public void test() throws Exception {
-        try (Reader reader = TestUtils.jsonReader(JSON_FILE)) {
-            Type mapType = new TypeToken<Map<String, String[]>>(){/* not code */}.getType();
-            Map<String, String[]> unitList = TestUtils.GSON.fromJson(reader, mapType);
-            for (Entry<String, String[]> testGroup : unitList.entrySet()) {
-                for (String unit : testGroup.getValue()) {
-                    MFParser.parse(unit);
-                }
-            }
-        }
-    }
+    // Other tests in CoreTest.java
 }
diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/SelectorsWithVariousArgumentsTest.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/SelectorsWithVariousArgumentsTest.java
deleted file mode 100644
index 8762f4757259..000000000000
--- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/SelectorsWithVariousArgumentsTest.java
+++ /dev/null
@@ -1,39 +0,0 @@
-// © 2024 and later: Unicode, Inc. and others.
-// License & terms of use: https://www.unicode.org/copyright.html
-
-package com.ibm.icu.dev.test.message2;
-
-import java.io.Reader;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import com.ibm.icu.dev.test.CoreTestFmwk;
-
-@SuppressWarnings({"static-method", "javadoc"})
-@RunWith(JUnit4.class)
-public class SelectorsWithVariousArgumentsTest extends CoreTestFmwk {
-    private static final String JSON_FILE = "icu-test-selectors.json";
-
-    @Test
-    public void test() throws Exception {
-        try (Reader reader = TestUtils.jsonReader(JSON_FILE)) {
-            TestWithVariations[] unitList =
-                    TestUtils.GSON.fromJson(reader, TestWithVariations[].class);
-            for (TestWithVariations testWithVar : unitList) {
-                Unit sharedUnit = testWithVar.shared;
-                for (Unit variation : testWithVar.variations) {
-                    Unit mergedUnit = sharedUnit.merge(variation);
-                    TestUtils.runTestCase(mergedUnit);
-                }
-            }
-        }
-    }
-
-    class TestWithVariations {
-        String comment;
-        Unit shared;
-        Unit[] variations;
-    }
-}
diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Sources.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Sources.java
new file mode 100644
index 000000000000..39a931a5a589
--- /dev/null
+++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Sources.java
@@ -0,0 +1,26 @@
+// © 2024 and later: Unicode, Inc. and others.
+// License & terms of use: https://www.unicode.org/copyright.html
+
+package com.ibm.icu.dev.test.message2;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.StringJoiner;
+
+// Class corresponding to the json test files.
+// See Unit.java and StringToListAdapter.java for how this is used.
+// Workaround for not being able to get the class of a generic type.
+
+class Sources {
+    final List<String> sources;
+
+    Sources(List<String> sources) {
+        this.sources = sources;
+    }
+
+    @Override
+    public String toString() {
+        return ("[" + sources.toString() + "]");
+    }
+}
diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/StringToListAdapter.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/StringToListAdapter.java
new file mode 100644
index 000000000000..facc9f00c2df
--- /dev/null
+++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/StringToListAdapter.java
@@ -0,0 +1,52 @@
+// © 2024 and later: Unicode, Inc. and others.
+// License & terms of use: https://www.unicode.org/copyright.html
+
+package com.ibm.icu.dev.test.message2;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import com.google.gson.stream.JsonToken;
+
+// Helper class that converts a single String to a List<String>
+// so that the `src` property can be either a single string or an array of strings.
+// Used in the TestUtils class.
+
+// Uses ArrayList instead of List so that when registering, it's possible
+// to get ArrayList<String>.class
+public class StringToListAdapter extends TypeAdapter<Sources> {
+    public Sources read(JsonReader reader) throws IOException {
+        if (reader.peek() == JsonToken.NULL) {
+            reader.nextNull();
+            return null;
+        }
+        if (reader.peek() == JsonToken.BEGIN_ARRAY) {
+            ArrayList<String> result = new ArrayList<String>();
+            reader.beginArray();
+            while (reader.hasNext()) {
+                result.add(reader.nextString());
+            }
+            reader.endArray();
+            return new Sources(result);
+        }
+        if (reader.peek() == JsonToken.STRING) {
+            String str = reader.nextString();
+            ArrayList<String> result = new ArrayList<String>();
+            result.add(str);
+            return new Sources(result);
+        }
+        throw new IOException();
+    }
+    public void write(JsonWriter writer, Sources value) throws IOException {
+        writer.beginArray();
+        for (String s : value.sources) {
+            writer.value(s);
+        }
+        writer.endArray();
+    }
+}
+
diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/SyntaxErrorsTest.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/SyntaxErrorsTest.java
deleted file mode 100644
index 204f79555161..000000000000
--- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/SyntaxErrorsTest.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// © 2024 and later: Unicode, Inc. and others.
-// License & terms of use: https://www.unicode.org/copyright.html
-
-package com.ibm.icu.dev.test.message2;
-
-import java.io.Reader;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import com.ibm.icu.dev.test.CoreTestFmwk;
-import com.ibm.icu.message2.MessageFormatter;
-
-@SuppressWarnings({"static-method", "javadoc"})
-@RunWith(JUnit4.class)
-public class SyntaxErrorsTest extends CoreTestFmwk {
-    private static final String[] JSON_FILES = {"more-syntax-errors.json",
-                                                "spec/syntax-errors.json"};
-
-    @Test
-    public void test() throws Exception {
-        for (String jsonFile : JSON_FILES) {
-            try (Reader reader = TestUtils.jsonReader(jsonFile)) {
-                String[] srcList = TestUtils.GSON.fromJson(reader, String[].class);
-                for (String source : srcList) {
-                    try {
-                        MessageFormatter.builder().setPattern(source).build();
-                        fail("Pattern expected to fail, but didn't: '" + source + "'");
-                    } catch (Exception e) {
-                        // If we get here it is fine
-                    }
-                }
-            }
-        }
-    }
-}
diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/TestUtils.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/TestUtils.java
index 572da833e67a..4a276216462c 100644
--- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/TestUtils.java
+++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/TestUtils.java
@@ -17,6 +17,7 @@
 import java.util.Date;
 import java.util.Locale;
 import java.util.Map;
+import java.util.TreeMap;
 
 import org.junit.Ignore;
 
@@ -28,7 +29,11 @@
 /** Utility class, has no test methods. */
 @Ignore("Utility class, has no test methods.")
 public class TestUtils {
-    static final Gson GSON = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create();
+
+    static final Gson GSON = new GsonBuilder()
+        .setDateFormat("yyyy-MM-dd HH:mm:ss")
+        .registerTypeAdapter(Sources.class, new StringToListAdapter())
+        .create();
 
     // ======= Legacy TestCase utilities, no json-compatible ========
 
@@ -72,62 +77,73 @@ private static String reportCase(TestCase testCase) {
 
     // ======= Same functionality with Unit, usable with JSON ========
 
-    static void rewriteDates(Map<String, Object> params) {
+    static void rewriteDates(Param[] params) {
         // For each value in `params` that's a map with the single key
         // `date` and a double value d,
         // return a map with that value changed to Date(d)
         // In JSON this looks like:
-        //    "params": {"exp": { "date": 1722746637000 } }
-        for (Map.Entry<String, Object> pair : params.entrySet()) {
-            if (pair.getValue() instanceof Map) {
-                Map innerMap = (Map) pair.getValue();
+        //    "params": [{"name": "exp"}, { "value": { "date": 1722746637000 } }]
+        for (int i = 0; i < params.length; i++) {
+            Param pair = params[i];
+            if (pair.value instanceof Map) {
+                Map innerMap = (Map) pair.value;
                 if (innerMap.size() == 1 && innerMap.containsKey("date") && innerMap.get("date") instanceof Double) {
                     Long dateValue = Double.valueOf((Double) innerMap.get("date")).longValue();
-                    params.put(pair.getKey(), new Date(dateValue));
+                    params[i] = new Param(pair.name, new Date(dateValue));
                 }
             }
         }
     }
 
-    static void rewriteDecimals(Map<String, Object> params) {
+    static void rewriteDecimals(Param[] params) {
         // For each value in `params` that's a map with the single key
         // `decimal` and a string value s
         // return a map with that value changed to Decimal(s)
         // In JSON this looks like:
-        //    "params": {"val": {"decimal": "1234567890123456789.987654321"}},
-        for (Map.Entry<String, Object> pair : params.entrySet()) {
-            if (pair.getValue() instanceof Map) {
-                Map innerMap = (Map) pair.getValue();
+        //    "params": [{"name": "val"}, {"value": {"decimal": "1234567890123456789.987654321"}}]
+        for (int i = 0; i < params.length; i++) {
+            Param pair = params[i];
+            if (pair.value instanceof Map) {
+                Map innerMap = (Map) pair.value;
                 if (innerMap.size() == 1 && innerMap.containsKey("decimal")
                     && innerMap.get("decimal") instanceof String) {
                     String decimalValue = (String) innerMap.get("decimal");
-                    params.put(pair.getKey(), new com.ibm.icu.math.BigDecimal(decimalValue));
+                    params[i] = new Param(pair.name, new com.ibm.icu.math.BigDecimal(decimalValue));
                 }
             }
         }
     }
 
+    static Map<String, Object> paramsToMap(Param[] params) {
+        if (params == null) {
+            return null;
+        }
+        TreeMap<String, Object> result = new TreeMap<String, Object>();
+        for (Param pair : params) {
+            result.put(pair.name, pair.value);
+        }
+        return result;
+    }
 
-    static boolean expectsErrors(Unit unit) {
-        return unit.errors != null && !unit.errors.isEmpty();
+    static boolean expectsErrors(DefaultTestProperties defaults, Unit unit) {
+        return (unit.expErrors != null && !unit.expErrors.isEmpty())
+            || (defaults.expErrors != null && defaults.expErrors.length > 0);
     }
 
-    static void runTestCase(Unit unit) {
-        runTestCase(unit, null);
+    static void runTestCase(DefaultTestProperties defaults, Unit unit) {
+        runTestCase(defaults, unit, null);
     }
 
-    static void runTestCase(Unit unit, Map<String, Object> params) {
+    static void runTestCase(DefaultTestProperties defaults, Unit unit, Param[] params) {
         if (unit.ignoreJava != null) {
             return;
         }
 
         StringBuilder pattern = new StringBuilder();
-        if (unit.srcs != null) {
-            for (String src : unit.srcs) {
+        if (unit.src != null) {
+            for (String src : unit.src.sources) {
                 pattern.append(src);
             }
-        } else if (unit.src != null) {
-            pattern.append(unit.src);
         }
 
         // We can call the "complete" constructor with null values, but we want to test that
@@ -136,6 +152,8 @@ static void runTestCase(Unit unit, Map<String, Object> params) {
                 MessageFormatter.builder().setPattern(pattern.toString());
         if (unit.locale != null && !unit.locale.isEmpty()) {
             mfBuilder.setLocale(Locale.forLanguageTag(unit.locale));
+        } else if (defaults.locale != null) {
+            mfBuilder.setLocale(Locale.forLanguageTag(defaults.locale));
         } else {
             mfBuilder.setLocale(Locale.US);
         }
@@ -147,16 +165,18 @@ static void runTestCase(Unit unit, Map<String, Object> params) {
                 rewriteDates(params);
                 rewriteDecimals(params);
             }
-            String result = mf.formatToString(params);
-            if (expectsErrors(unit)) {
+            String result = mf.formatToString(paramsToMap(params));
+            if (expectsErrors(defaults, unit)) {
                 fail(reportCase(unit)
                         + "\nExpected error, but it didn't happen.\n"
                         + "Result: '" + result + "'");
             } else {
-                assertEquals(reportCase(unit), unit.exp, result);
+                if (unit.exp != null) {
+                    assertEquals(reportCase(unit), unit.exp, result);
+                }
             }
         } catch (IllegalArgumentException | NullPointerException e) {
-            if (!expectsErrors(unit)) {
+            if (!expectsErrors(defaults, unit)) {
                 fail(reportCase(unit)
                         + "\nNo error was expected here, but it happened:\n"
                         + e.getMessage());
diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Unit.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Unit.java
index 3ff18ecd7cc2..810594f29253 100644
--- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Unit.java
+++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Unit.java
@@ -10,29 +10,27 @@
 // Class corresponding to the json test files.
 // Since this is serialized by Gson, the field names should match the keys in the .json files.
 class Unit {
-    final String src;
-    final List<String> srcs;
+    // For why this is not an ArrayList<String>, see StringToListAdapter.java
+    final Sources src;
     final String locale;
-    final Map<String, Object> params;
+    final Param[] params;
     final String exp;
     final String ignoreJava;
-    final List<Error> errors;
+    final List<Error> expErrors;
 
     Unit(
-            String src,
-            List<String> srcs,
+            Sources src,
             String locale,
-            Map<String, Object> params,
+            Param[] params,
             String exp,
             String ignoreJava,
-            List<Error> errors) {
+            List<Error> expErrors) {
         this.src = src;
-        this.srcs = srcs;
         this.locale = locale;
         this.params = params;
         this.exp = exp;
         this.ignoreJava = ignoreJava;
-        this.errors = errors;
+        this.expErrors = expErrors;
     }
 
     class Error {
@@ -56,7 +54,9 @@ public String toString() {
     @Override
     public String toString() {
         StringJoiner result = new StringJoiner(", ", "UnitTest {", "}");
-        result.add("src=" + escapeString(src));
+        if (src != null) {
+            result.add("src=" + src.sources.toString());
+        }
         if (params != null) {
             result.add("params=" + params);
         }
@@ -75,14 +75,13 @@ public String toString() {
      * @return a new unit created by merging `this` unit and `other`
      */
     public Unit merge(Unit other) {
-        String newSrc = other.src != null ? other.src : this.src;
-        List<String> newSrcs = other.srcs != null ? other.srcs : this.srcs;
+        Sources newSrc = other.src != null ? other.src : this.src;
         String newLocale = other.locale != null ? other.locale : this.locale;
-        Map<String, Object> newParams = other.params != null ? other.params : this.params;
+        Param[] newParams = other.params != null ? other.params : this.params;
         String newExp = other.exp != null ? other.exp : this.exp;
         String newIgnore = other.ignoreJava != null ? other.ignoreJava : this.ignoreJava;
-        List<Error> newErrors = other.errors != null ? other.errors : this.errors;
-        return new Unit(newSrc, newSrcs, newLocale, newParams, newExp, newIgnore, newErrors);
+        List<Error> newExpErrors = other.expErrors != null ? other.expErrors : this.expErrors;
+        return new Unit(newSrc, newLocale, newParams, newExp, newIgnore, newExpErrors);
     }
 
     private static String escapeString(String str) {
diff --git a/testdata/message2/README.txt b/testdata/message2/README.txt
index 17c7c656c320..b803ebc0425e 100644
--- a/testdata/message2/README.txt
+++ b/testdata/message2/README.txt
@@ -1,19 +1,14 @@
 © 2024 and later: Unicode, Inc. and others.
 License & terms of use: http://www.unicode.org/copyright.html
 
-The format of the JSON files in this directory follows the same format as `test-core.json`
-in the spec, described in:
+The format of the JSON files in this directory and subdirectories
+follow the test schema defined in the Conformance repository:
 
-https://github.com/unicode-org/message-format-wg/blob/main/test/README.md
+https://github.com/unicode-org/conformance/blob/main/schema/message_fmt2/testgen_schema.json
 
-The `parts` field is not used.
+(as of https://github.com/unicode-org/conformance/pull/255 or later).
 
-# JSON extensions
-
-An additional `comment` field may be present, which is only for human readers.
-
-A "srcs" field, whose value is an array of strings, may be present instead
-of "src". The strings are concatenated to get the message.
+# JSON notes
 
 In the "params" field, a date parameter can be expressed as:
 { "date": n }
@@ -46,43 +41,3 @@ If present, "char" reflects the expected character offset and "line"
 reflects the expected line number in the parse error.
 The files with "diagnostics" in the name have these fields filled in.
 
-# ICU4C vs. ICU4J tests
-
-The following tests are run in both ICU4C and ICU4J:
-
-* alias-selector-annotations.json
-* duplicate-declarations.json
-* icu-parser-tests.json
-  - Two tests removed while single-sourcing tests, because a `{{}}` message body
-  had to be added to get it to parse in ICU4C, and this broke the test in ICU4J.
-  These tests are in icu-parser-tests-old.json
-* icu-test-functions.json
-  - Some tests marked as ignored
-* icu-test-previous-release.json
-  - Some tests marked as ignored
-* icu-test-selectors.json
-* markup.json
-* matches-whitespace.json
-  - Some tests marked as ignored
-* more-data-model-errors.json
-* more-syntax-errors.json
-* reserved-syntax.json
-  - All tests marked as ignored in Java (resolution errors are suppressed)
-* resolution-errors.json
-  - All tests marked as ignored in Java (resolution errors are suppressed)
-* runtime-errors.json
-  - All tests marked as ignored in Java (message function errors are suppressed)
-* syntax-errors-diagnostics.json
-* tricky-declarations.json
-* valid-tests.json
-  - Some tests marked as ignored
-* spec/*
-  - Some tests in test-core.json and test-functions.json marked as ignored
-
-The following tests are only run in ICU4C, either because ICU4J doesn't check
-for invalid options, or because ICU4J doesn't report line/column numbers for
-parse errors:
-* invalid-number-literals-diagnostics.json
-* invalid-options.json
-* syntax-errors-diagnostics-multiline.json
-* syntax-errors-end-of-input.json
diff --git a/testdata/message2/alias-selector-annotations.json b/testdata/message2/alias-selector-annotations.json
index 71ad134d821d..c064d40d4c82 100644
--- a/testdata/message2/alias-selector-annotations.json
+++ b/testdata/message2/alias-selector-annotations.json
@@ -1,4 +1,10 @@
-[
+{
+  "scenario": "Selector annotations",
+  "description": "Tests for indirectly annotated selectors",
+  "defaultTestProperties": {
+    "locale": "en-US"
+  },
+  "tests": [
     {
         "src": ".local $one = {|The one| :string}\n   .match {$one}\n   1 {{Value is one}}\n  * {{Value is not one}}",
         "exp": "Value is not one"
@@ -7,4 +13,6 @@
         "src": ".local $one = {|The one| :string}\n  .local $two = {$one}\n  .match {$two}\n  1 {{Value is one}}\n   * {{Value is not one}}",
         "exp": "Value is not one"
     }
-]
+  ]
+}
+
diff --git a/testdata/message2/duplicate-declarations.json b/testdata/message2/duplicate-declarations.json
index 6ea54daa003e..cd3acc1576d3 100644
--- a/testdata/message2/duplicate-declarations.json
+++ b/testdata/message2/duplicate-declarations.json
@@ -1,25 +1,43 @@
-[
+{
+  "scenario": "Duplicate declaration errors",
+  "description": "Tests that should trigger a duplicate declaration error",
+  "defaultTestProperties": {
+    "locale": "en-US",
+    "expErrors": [
+      {
+        "type": "duplicate-declaration"
+      }
+    ]
+  },
+  "tests": [
     {
         "src": ".local $foo = {$foo} .local $foo = {42} {{bar {$foo}}}",
-        "params": { "foo": "foo" },
-        "exp": "bar 42",
-        "errors": [{ "type": "duplicate-declaration" }]
+        "params": [{ "name": "foo", "value": "foo" }],
+        "exp": "bar 42"
     },
     {
         "src": ".local $foo = {42} .local $foo = {42} {{bar {$foo}}}",
-        "params": { "foo": "foo" },
-        "exp": "bar 42",
-        "errors": [{ "type": "duplicate-declaration" }]
+        "params": [{ "name": "foo", "value": "foo" }],
+        "exp": "bar 42"
     },
     {
         "src": ".local $foo = {:unknown} .local $foo = {42} {{bar {$foo}}}",
-        "params": { "foo": "foo" },
-        "exp": "bar 42",
-        "errors": [{ "type": "duplicate-declaration" }]
+        "params": [{ "name": "foo", "value": "foo" }],
+        "exp": "bar 42"
     },
     {
         "src": ".local $x = {42} .local $y = {$x} .local $x = {13} {{{$x} {$y}}}",
-        "exp": "13 42",
-        "errors": [{ "type": "duplicate-declaration" }]
+        "exp": "13 42"
+    },
+    {
+        "src": ".local $foo = {$foo} {{bar {$foo}}}",
+        "params": [{ "name": "foo", "value": "foo" }],
+        "exp": "bar foo"
+    },
+    {
+        "src": ".local $foo = {$bar} .local $bar = {$baz} {{bar {$foo}}}",
+        "params": [{ "name": "baz", "value": "foo" }],
+        "exp": "bar {$bar}"
     }
-]
+  ]
+}
diff --git a/testdata/message2/icu-parser-tests.json b/testdata/message2/icu-parser-tests.json
index 0cdfe0a467aa..a28cfc61687f 100644
--- a/testdata/message2/icu-parser-tests.json
+++ b/testdata/message2/icu-parser-tests.json
@@ -1,61 +1,61 @@
 {
-  "Simple messages": [
-    "",
-    "Hello",
-    "Hello world!",
-    "Hello \t \n \r \\{ world!",
-    "Hello world {:datetime}",
-    "Hello world {foo}",
-    "Hello {0} world",
-    "Hello {123} world",
-    "Hello {-123} world",
-    "Hello {3.1416} world",
-    "Hello {-3.1416} world",
-    "Hello {123E+2} world",
-    "Hello {123E-2} world",
-    "Hello {123.456E+2} world",
-    "Hello {123.456E-2} world",
-    "Hello {-123.456E+2} world",
-    "Hello {-123.456E-2} world",
-    "Hello {-123E+2} world",
-    "Hello {-123E-2} world",
-    "Hello world {$exp}",
-    "Hello world {$exp :datetime}",
-    "Hello world {|2024-02-27| :datetime}",
-    "Hello world {$exp :datetime style=long} and more",
-    "Hello world {$exp :function number=1234} and more",
-    "Hello world {$exp :function unquoted=left   } and more",
-    "Hello world {$exp :function quoted=|Something| } and more",
-    "Hello world {$exp :function quoted=|Something with spaces| } and more",
-    "Hello world {$exp :function quoted=|Something with \\| spaces and \\| escapes| } and more",
-    "Hello world {$exp :function number=1234 unquoted=left quoted=|Something|}",
-    "Hello world {$exp :function number=1234 unquoted=left quoted=|Something longer|}",
-    "Hello world {$exp :function number=1234 unquoted=left quoted=|Something \\| longer|}"
-  ],
-  "Attributes": [
-    "Hello world {$exp}",
-    "Hello world {$exp @attr}",
-    "Hello world {$exp @valid @attr=a @attrb=123 @atrn=|foo bar|}",
-    "Hello world {$exp :date @valid @attr=aaaa @attrb=123 @atrn=|foo bar|}",
-    "Hello world {$exp :date year=numeric month=long day=numeric int=12 @valid @attr=a @attrb=123 @atrn=|foo bar|}"
-    ],
-  "Reserved and private": [
-    "Reserved {$exp &foo |something more protected|} and more",
-    "Reserved {$exp %foo |something quoted \\| inside|} and more",
-    "{{.starting with dot is OK here}}",
-    "{{Some string pattern, with {$foo} and {$exp :date style=long}!}}"
-  ],
-  "Simple messages, with declarations": [
-    ".input {$pi :number} {{}}",
-    ".input {$exp :date} {{}}",
-    ".local $foo = {$exp} {{}}",
-    ".local $foo = {$exp :date} {{}}",
-    ".local $foo = {$exp :date year=numeric month=long day=numeric} {{}}",
-    ".local $bar = {$foo :date month=medium} {{}}",
-    ".something |reserved=| {$foo :date} {{}}"
-  ],
-  "Multiple declarations in one message": [
-    ".input {$a :date} .local $exp = {$a :date style=full} {{Your card expires on {$exp}!}}",
-    ".input {$a :date} .local $b = {$a :date year=numeric month=long day=numeric} .local $c = {$b :date month=medium} .someting |reserved = \\| and more| {$x :date} {$y :date} {$z :number} {{}}"
+  "scenario": "Valid tests",
+  "description": "Additional valid tests",
+  "defaultTestProperties": {
+    "locale": "en-US"
+  },
+  "tests": [
+    { "src": "" },
+    { "src": "Hello" },
+    { "src": "Hello world!" },
+    { "src": "Hello \t \n \r \\{ world!" },
+    { "src": "Hello world {:datetime}" },
+    { "src": "Hello world {foo}" },
+    { "src": "Hello {0} world" },
+    { "src": "Hello {123} world" },
+    { "src": "Hello {-123} world" },
+    { "src": "Hello {3.1416} world" },
+    { "src": "Hello {-3.1416} world" },
+    { "src": "Hello {123E+2} world" },
+    { "src": "Hello {123E-2} world" },
+    { "src": "Hello {123.456E+2} world" },
+    { "src": "Hello {123.456E-2} world" },
+    { "src": "Hello {-123.456E+2} world" },
+    { "src": "Hello {-123.456E-2} world" },
+    { "src": "Hello {-123E+2} world" },
+    { "src": "Hello {-123E-2} world" },
+    { "src": "Hello world {$exp}" },
+    { "src": "Hello world {$exp :datetime}" },
+    { "src": "Hello world {|2024-02-27| :datetime}" },
+    { "src": "Hello world {$exp :datetime style=long} and more" },
+    { "src": "Hello world {$exp :function number=1234} and more" },
+    { "src": "Hello world {$exp :function unquoted=left   } and more" },
+    { "src": "Hello world {$exp :function quoted=|Something| } and more" },
+    { "src": "Hello world {$exp :function quoted=|Something with spaces| } and more" },
+    { "src": "Hello world {$exp :function quoted=|Something with \\| spaces and \\| escapes| } and more" },
+    { "src": "Hello world {$exp :function number=1234 unquoted=left quoted=|Something|}" },
+    { "src": "Hello world {$exp :function number=1234 unquoted=left quoted=|Something longer|}" },
+    { "src": "Hello world {$exp :function number=1234 unquoted=left quoted=|Something \\| longer|}" },
+    { "src": "Hello world {$exp}" },
+    { "src": "Hello world {$exp @attr}" },
+    { "src": "Hello world {$exp @valid @attr=a @attrb=123 @atrn=|foo bar|}" },
+    { "src": "Hello world {$exp :date @valid @attr=aaaa @attrb=123 @atrn=|foo bar|}" },
+    { "src": "Hello world {$exp :date year=numeric month=long day=numeric int=12 @valid @attr=a @attrb=123 @atrn=|foo bar|}" },
+    { "src": "Reserved {$exp &foo |something more protected|} and more" },
+    { "src": "Reserved {$exp %foo |something quoted \\| inside|} and more" },
+    { "src": "{{.starting with dot is OK here}}" },
+    { "src": "{{Some string pattern \\}, with {$foo} and {$exp :date style=long}!}}" },
+    { "src": ".input {$pi :number} {{}}" },
+    { "src": ".input {$exp :date} {{}}" },
+    { "src": ".local $foo = {$exp} {{}}" },
+    { "src": ".local $foo = {$exp :date} {{}}" },
+    { "src": ".local $foo = {$exp :date year=numeric month=long day=numeric} {{}}" },
+    { "src": ".local $bar = {$foo :date month=medium} {{}}" },
+    { "src": ".something |reserved=| {$foo :date} {{}}" },
+    { "src": ".input {$a :date} .local $exp = {$a :date style=full} {{Your card expires on {$exp}!}}" },
+    { "src": ".input {$a :date} .local $b = {$a :date year=numeric month=long day=numeric} .local $c = {$b :date month=medium} .someting |reserved = \\| and more| {$x :date} {$y :date} {$z :number} {{}}" },
+    { "src": ".input {$x :number} {{_}}" },
+    { "src": ".local $foo = {|1|} {{_}}" },
+    { "src": ".unsupported |statement| {$x :number} {{_}}" }
   ]
 }
diff --git a/testdata/message2/icu-test-functions.json b/testdata/message2/icu-test-functions.json
index 2dfd91cb8030..a97446addf0e 100644
--- a/testdata/message2/icu-test-functions.json
+++ b/testdata/message2/icu-test-functions.json
@@ -1,95 +1,98 @@
 {
-  "Date and time formats": [
+  "scenario": "Function tests",
+  "description": "Tests for ICU-specific formatting behavior.",
+  "defaultTestProperties": {
+    "locale": "en-US"
+  },
+  "tests": [
     {
       "src": "Expires on {$exp}",
       "exp": "Expires on 8/3/24, 9:43 PM",
       "comment": "Modified from ICU4J copy to add params (likewise with the other date/time tests); 1722746637000 is 2024-08-03 21:43:57 PDT",
-      "params": {"exp": { "date": 1722746637000 } }
+      "params": [{ "name": "exp", "value": { "date": 1722746637000 } }]
     },
     {
       "src": "Expires on {$exp :datetime}",
       "exp": "Expires on 8/3/24, 9:43 PM",
-      "params": {"exp": { "date": 1722746637000 } }
+      "params": [{ "name": "exp", "value": { "date": 1722746637000 } }]
     },
     {
       "src": "Expires on {$exp :datetime icu:skeleton=yMMMMdjmsSSEE}",
       "exp": "Expires on Sat, August 3, 2024 at 9:43:57.00 PM",
-      "params": {"exp": { "date": 1722746637000 } },
+      "params": [{ "name": "exp", "value": { "date": 1722746637000 } }],
       "ignoreCpp": "ICU-22754 Skeleton option not implemented yet"
     },
     {
       "src": "Expires on {$exp :datetime dateStyle=full}",
       "exp": "Expires on Saturday, August 3, 2024",
-      "params": {"exp": { "date": 1722746637000 } }
+      "params": [{ "name": "exp", "value": { "date": 1722746637000 } }]
     },
     {
       "src": "Expires on {$exp :datetime dateStyle=long}",
       "exp": "Expires on August 3, 2024",
-      "params": {"exp": { "date": 1722746637000 } }
+      "params": [{ "name": "exp", "value": { "date": 1722746637000 } }]
     },
     {
       "src": "Expires on {$exp :datetime dateStyle=medium}",
       "exp": "Expires on Aug 3, 2024",
-      "params": {"exp": { "date": 1722746637000 } }
+      "params": [{ "name": "exp", "value": { "date": 1722746637000 } }]
     },
     {
       "src": "Expires on {$exp :datetime timeStyle=long}",
       "exp": "Expires on 9:43:57 PM PDT",
-      "params": {"exp": { "date": 1722746637000 } }
+      "params": [{ "name": "exp", "value": { "date": 1722746637000 } }]
     },
     {
       "src": "Expires on {$exp :datetime timeStyle=medium}",
       "exp": "Expires on 9:43:57 PM",
-      "params": {"exp": { "date": 1722746637000 } }
+      "params": [{ "name": "exp", "value": { "date": 1722746637000 } }]
     },
     {
       "src": "Expires on {$exp :datetime timeStyle=short}",
       "exp": "Expires on 9:43 PM",
-      "params": {"exp": { "date": 1722746637000 } }
+      "params": [{ "name": "exp", "value": { "date": 1722746637000 } }]
     },
     {
       "src": "Expires on {$exp :datetime dateStyle=full timeStyle=medium}",
       "exp": "Expires on Saturday, August 3, 2024 at 9:43:57 PM",
-      "params": {"exp": { "date": 1722746637000 } }
+      "params": [{ "name": "exp", "value": { "date": 1722746637000 } }]
     },
     {
       "src": "Expires on {$exp :datetime year=numeric month=long}",
       "exp": "Expires on August 2024",
-      "params": {"exp": { "date": 1722746637000 } }
+      "params": [{ "name": "exp", "value": { "date": 1722746637000 } }]
     },
     {
       "src": "Expires on {$exp :datetime year=numeric month=medium day=numeric weekday=long hour=numeric minute=numeric}",
       "exp": "Expires on 3 Saturday 2024, 9:43 PM",
-      "params": {"exp": { "date": 1722746637000 } }
+      "params": [{ "name": "exp", "value": { "date": 1722746637000 } }]
     },
     {
       "comment": "Make sure we ignore date / time fields if needed",
       "src": "Expires on {$exp :date year=numeric month=medium day=numeric weekday=long hour=numeric minute=numeric}",
       "exp": "Expires on 3 Saturday 2024",
-      "params": {"exp": { "date": 1722746637000 } },
+      "params": [{ "name": "exp", "value": { "date": 1722746637000 } }],
       "ignoreCpp": "ICU-22754 ICU4C doesn't accept field options for `:date` or `:time` -- see spec"
     },
     {
       "comment": "Make sure we ignore date / time fields if needed",
       "src": "Expires at {$exp :time year=numeric month=medium day=numeric weekday=long hour=numeric minute=numeric}",
       "exp": "Expires at 9:43 PM",
-      "params": {"exp": { "date": 1722746637000 } },
+      "params": [{ "name": "exp", "value": { "date": 1722746637000 } }],
       "ignoreCpp": "ICU-22754 ICU4C doesn't accept field options for `:date` or `:time` -- see spec"
     },
     {
       "comment": "Make sure we ignore date / time fields if needed",
       "src": "Expires at {$exp :time style=long dateStyle=full timeStyle=medium}",
       "exp": "Expires at 9:43:57 PM PDT",
-      "params": {"exp": { "date": 1722746637000 } }
+      "params": [{ "name": "exp", "value": { "date": 1722746637000 } }]
     },
     {
       "comment": "Make sure we ignore date / time fields if needed",
       "src": "Expires on {$exp :date style=long dateStyle=full timeStyle=medium}",
       "exp": "Expires on August 3, 2024",
-      "params": {"exp": { "date": 1722746637000 } }
-    }
-  ],
-  "Literals" : [
+      "params": [{ "name": "exp", "value": { "date": 1722746637000 } }]
+    },
     {
       "src": "Expires on {|2025-02-27| :datetime dateStyle=full}",
       "exp": "Expires on Thursday, February 27, 2025"
@@ -115,12 +118,10 @@
       "src": "Expires at {|2024-07-02T19:23:45+03:30| :datetime timeStyle=full}",
       "exp": "Expires at 7:23:45 PM GMT+03:30",
       "ignoreCpp": "ICU-22754 Time zones not working yet (bug)"
-    }
-  ],
-  "Chaining" : [
+    },
     {
       "comment": "Horibly long, but I really wanted to test multiple declarations with overrides, and you can't join strings in JSON",
-      "srcs": [
+      "src": [
         ".input {$exp :datetime timeStyle=short}\n",
         ".input {$user :string}\n",
         ".local $longExp = {$exp :datetime dateStyle=long}\n",
@@ -128,30 +129,30 @@
         "{{Hello John, you want '{$exp}', '{$longExp}', or '{$zooExp}' or even '{$exp :datetime dateStyle=full}'?}}"
       ],
       "exp": "Hello John, you want '9:43 PM', 'August 3, 2024 at 9:43 PM', or '8/3/24, 9:43:57 PM Pacific Daylight Time' or even 'Saturday, August 3, 2024 at 9:43 PM'?",
-      "params": {"exp": { "date": 1722746637000 }, "user": "John", "tsOver" : "full" },
+      "params": [{"name": "exp", "value": { "date": 1722746637000 }},
+                 {"name": "user", "value": "John"},
+                 {"name": "tsOver", "value": "full" }],
       "ignoreCpp": "ICU-22754 ICU4C doesn't implement this kind of function composition yet. See https://github.com/unicode-org/message-format-wg/issues/515"
     },
     {
-      "srcs": [
+      "src": [
         ".input {$exp :datetime year=numeric month=numeric day=|2-digit|}\n",
         ".local $longExp = {$exp :datetime month=long weekday=long}\n",
         "{{Expires on '{$exp}' ('{$longExp}').}}"
       ],
       "exp": "Expires on '8/03/2024' ('Saturday, August 03, 2024').",
-      "params": {"exp": { "date": 1722746637000 } }
-    }
-  ],
-  "Number formatter" : [
+      "params": [{ "name": "exp", "value": { "date": 1722746637000 } }]
+    },
     {
       "src": "Format {$val} number",
-      "params": { "val": 31 },
+      "params": [{ "name": "val", "value": 31 }],
       "exp": "Format 31 number"
     },
     {
       "src": "Format {123456789.9876} number",
       "locale": "en-IN",
-      "exp": "Format 12,34,56,789.9876 number",
-      "ignoreCpp": "ICU-22754 No default formatting for numbers, so it's formatted as a literal string. Is this in the spec?"
+      "exp": "Format 123456789.9876 number",
+      "comment": "Number literals are not formatted as numbers by default"
     },
     {
       "src": "Format {|3.1416|} number",
@@ -161,8 +162,8 @@
     {
       "src": "Format {|3.1416|} number",
       "locale": "ar-AR-u-nu-arab",
-      "exp": "Format ٣٫١٤١٦ number",
-      "ignoreCpp": "ICU-22754 No default formatting for numbers, so it's formatted as a literal string. Is this in the spec?"
+      "exp": "Format 3.1416 number",
+      "comment": "Number literals are not formatted as numbers by default"
     },
     {
       "src": "Format {3.1415926 :number}",
diff --git a/testdata/message2/icu-test-previous-release.json b/testdata/message2/icu-test-previous-release.json
index cc863411db95..0a1e27dff6e2 100644
--- a/testdata/message2/icu-test-previous-release.json
+++ b/testdata/message2/icu-test-previous-release.json
@@ -1,38 +1,10 @@
-[
-    {
-      "src": "hello",
-      "exp": "hello"
-    },
-    {
-      "src": "hello {|world|}",
-      "exp": "hello world"
-    },
-    {
-      "src": "hello {||}",
-      "exp": "hello "
-    },
-    {
-      "src": "hello {$place}",
-      "params": { "place": "world" },
-      "exp": "hello world"
-    },
-    {
-      "src": "hello {$place}",
-      "exp": "hello {$place}",
-      "errors": [{ "type": "unresolved-var" }],
-      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-    },
-    {
-      "src": "{$one} and {$two}",
-      "params" : { "one": 1.3, "two": 4.2 },
-      "exp": "1.3 and 4.2"
-    },
-    {
-      "src": "{$one} et {$two}",
-      "locale": "fr",
-      "params": { "one": 1.3, "two": 4.2 },
-      "exp": "1,3 et 4,2"
-    },
+{
+  "scenario": "Tests from original ICU4J release",
+  "description": "Tests taken from the September 2022 MF2 ICU4J release",
+  "defaultTestProperties": {
+    "locale": "en-US"
+  },
+  "tests": [
     {
       "src": "hello {|4.2| :number}",
       "exp": "hello 4.2"
@@ -43,254 +15,108 @@
       "exp": "hello \u0664\u066B\u0662"
     },
     {
-      "src": "hello {|foo| :number}",
-      "exp": "hello {|foo|}",
-      "errors": [{ "type": "bad-input" }],
-      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-    },
-    {
-      "src": "hello {:number}",
-      "exp": "hello {:number}",
-      "comment": "This is different from JS, should be an error.",
-      "errors": [{ "type": "bad-input" }]
-    },
-    {
-      "src": "hello {|4.2| :number minimumFractionDigits=2}",
-      "exp": "hello 4.20"
-    },
-    {
-      "src": "hello {|4.2| :number minimumFractionDigits=|2|}",
-      "exp": "hello 4.20"
-    },
-    {
-      "src": "hello {|4.2| :number minimumFractionDigits=$foo}",
-      "params": { "foo": 2.0 },
-      "exp": "hello 4.20"
-    },
-    {
-      "src": "hello {|4.2| :number minimumFractionDigits=$foo}",
-      "params": { "foo": "2" },
-      "exp": "hello 4.20",
-      "errorsJs": ["invalid-type"]
-    },
-    {
-      "src": ".local $foo = {|bar|} {{bar {$foo}}}",
-      "exp": "bar bar"
-    },
-    {
+      "comment": "This is not an error! foo is not used before the local declaration, so the local declaration of $foo shadows the input variable.",
       "src": ".local $foo = {bar} {{bar {$foo}}}",
-     "params": { "foo": "foo" },
-     "expectedJs": "bar foo",
-     "comment": "It is undefined if we allow arguments to override local variables, or it is an error.  And undefined who wins if that happens, the local variable of the argument.",
-      "exp": "bar bar"
-    },
-    {
-      "src": ".local $foo = {$bar} {{bar {$foo}}}",
-      "params": { "bar": "foo" },
-      "exp": "bar foo"
+      "exp": "bar bar",
+      "params": [{ "name": "foo", "value": "foo" }]
     },
     {
       "src": ".local $foo = {$bar :number} {{bar {$foo}}}",
-      "params": { "bar": 4.2 },
+      "params": [{ "name": "bar", "value": 4.2 }],
       "exp": "bar 4.2"
     },
-    {
-      "src": ".local $foo = {$bar :number minimumFractionDigits=2} {{bar {$foo}}}",
-      "params": { "bar": 4.2 },
-      "exp": "bar 4.20"
-    },
-    {
-      "ignoreJava": "Maybe. Because `minimumFractionDigits=foo`",
-      "src": ".local $foo = {$bar :number minimumFractionDigits=foo} {{bar {$foo}}}",
-      "params": { "bar": 4.2 },
-      "exp": "bar 4.2",
-      "errors": [{ "type": "bad-option" }]
-    },
-    {
-      "src": ".local $foo = {$bar :number} {{bar {$foo}}}",
-      "params": { "bar": "foo" },
-      "exp": "bar {$bar}",
-      "errors": [{ "type": "bad-input" }],
-      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-    },
     {
       "src": ".local $bar = {$baz} .local $foo = {$bar} {{bar {$foo}}}",
-      "params": { "baz": "foo" },
+      "params": [{ "name": "baz", "value": "foo" }],
       "exp": "bar foo"
     },
-    {
-      "patternJs": ".match {$foo} 1 {{one}} * {{other}}",
-      "src": ".match {$foo :string} 1 {{one}} * {{other}}",
-      "params": { "foo": "1" },
-      "exp": "one"
-    },
     {
       "src": ".match {$foo :number} 1 {{one}} * {{other}}",
-      "params": { "foo": "1" },
+      "params": [{ "name": "foo", "value": "1" }],
       "exp": "one",
       "ignoreJava": "See ICU-22809"
     },
     {
       "src": ".match {$foo :string} 1 {{one}} * {{other}}",
-      "params": { "foo": "1" },
-      "exp": "one"
-    },
-    {
-      "patternJs": ".match {$foo} 1 {{one}} * {{other}}",
-      "src": ".match {$foo :number} 1 {{one}} * {{other}}",
-      "params": { "foo": 1 },
+      "params": [{ "name": "foo", "value": "1" }],
       "exp": "one"
     },
     {
       "src": ".match {$foo :number} 1 {{one}} * {{other}}",
-      "params": { "foo": 1 },
+      "params": [{ "name": "foo", "value": 1 }],
       "exp": "one"
     },
     {
       "ignoreJava": "Can't pass null in a map",
+      "ignoreCpp": "Same as Java",
       "src": ".match {$foo} 1 {{one}} * {{other}}",
-      "params": { "foo": null },
+      "params": [{ "name": "foo", "value": null }],
       "exp": "other"
     },
     {
-      "srcJs": ".match {$foo} 1 {{one}} * {{other}}",
       "src": ".match {$foo :number} 1 {{one}} * {{other}}",
       "exp": "other",
-      "errors": [{ "type":  "missing-var" }]
-    },
-    {
-      "srcJs": ".match {$foo} one {{one}} * {{other}}",
-      "src": ".match {$foo :number} one {{one}} * {{other}}",
-      "params": { "foo": 1 },
-      "exp": "one"
-    },
-    {
-      "srcJs": ".match {$foo} 1 {{=1}} one {{one}} * {{other}}",
-      "src": ".match {$foo :number} 1 {{=1}} one {{one}} * {{other}}",
-      "params": { "foo": 1 },
-      "exp": "=1"
-    },
-    {
-      "srcJs": ".match {$foo} one {{one}} 1 {{=1}} * {{other}}",
-      "src": ".match {$foo :number} one {{one}} 1 {{=1}} * {{other}}",
-      "params": { "foo": 1 },
-      "exp": "=1"
-    },
-    {
-      "srcJs": ".match {$foo} {$bar} one one {{one one}} one * {{one other}} * * {{other}}",
-      "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}",
-      "params": { "foo": 1, "bar": 1 },
-      "exp": "one one"
-    },
-    {
-      "srcJs": ".match {$foo} {$bar} one one {{one one}} one * {{one other}} * * {{other}}",
-      "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}",
-      "params": { "foo": 1, "bar": 2 },
-      "exp": "one other"
-    },
-    {
-      "srcJs": ".match {$foo} {$bar} one one {{one one}} one * {{one other}} * * {{other}}",
-      "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}",
-      "params": { "foo": 2, "bar": 2 },
-      "exp": "other"
+      "expErrors": [{ "type":  "unresolved-variable" }]
     },
     {
-      "srcJs": ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}",
       "src": ".local $foo = {$bar} .match {$foo :number} one {{one}} * {{other}}",
-      "params": { "bar": 1 },
+      "params": [{ "name": "bar", "value": 1 }],
       "exp": "one"
     },
     {
-      "srcJs": ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}",
       "src": ".local $foo = {$bar} .match {$foo :number} one {{one}} * {{other}}",
-      "params": { "bar": 2 },
+      "params": [{ "name": "bar", "value": 2 }],
       "exp": "other"
     },
     {
-      "srcJs": ".local $bar = {$none} .match {$foo} one {{one}} * {{{$bar}}}",
       "src": ".local $bar = {$none} .match {$foo :number} one {{one}} * {{{$bar}}}",
-      "params": { "foo": 1, "none": "" },
+      "params": [{ "name": "foo", "value": 1 }, {"name": "none", "value": "" }],
       "exp": "one"
     },
     {
-      "srcJs": ".local $bar = {$none} .match {$foo} one {{one}} * {{{$bar}}}",
       "src": ".local $bar = {$none :number} .match {$foo :string} one {{one}} * {{{$bar}}}",
-      "params": { "foo": 2 },
+      "params": [{ "name": "foo", "value": 2 }],
       "exp": "{$none}",
-      "errors": [{ "type": "unresolved-var" }],
+      "expErrors": [{ "type": "unresolved-variable" }],
       "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
     },
     {
       "src": "{{#tag}}",
       "exp": "#tag"
     },
-    {
-      "src": "{#tag}",
-      "exp": ""
-    },
-    {
-      "src": "{#tag}content",
-      "exp": "content"
-    },
     {
       "src": "{#tag}content{/tag}",
       "exp": "content"
     },
-    {
-      "src": "{#tag}content",
-      "exp": "content"
-    },
-    {
-      "comment": "When we format markup to string we generate no output",
-      "src": "{#tag foo=bar}",
-      "exp": ""
-    },
     {
       "src": "{#tag foo=foo bar=$bar}",
-      "params": { "bar": "b a r" },
+      "params": [{ "name": "bar", "value": "b a r" }],
       "exp": ""
     },
     {
       "src": "bad {#markup/} test",
-      "exp": "bad  test",
-      "errorsJs": [{ "type":  "extra-content" }]
-    },
-    {
-      "src": "{#tag foo=bar}",
-      "exp": "",
-      "errorsJs": [{ "type":  "extra-content" }]
-    },
-    {
-      "src": "no braces",
-      "exp": "no braces",
-      "errorsJs": [{ "type":  "parse-error" }, { "type": "junk-element" }]
+      "exp": "bad  test"
     },
     {
       "src": "no braces {$foo}",
-      "params": { "foo": 2 },
-      "exp": "no braces 2",
-      "errorsJs": [{ "type":  "parse-error" }, { "type": "junk-element" }]
+      "params": [{ "name": "foo", "value": 2 }],
+      "exp": "no braces 2"
     },
     {
       "src": "empty { }",
       "exp": "empty ",
-      "errors": [{ "type": "parse-error" }, { "type": "junk-element" }],
+      "expErrors": [{ "type": "syntax-error" }],
       "ignoreCpp": "Fallback is unclear. See https://github.com/unicode-org/message-format-wg/issues/703"
     },
     {
       "src": "bad {:}",
       "exp": "bad {:}",
-      "errors": [{ "type": "empty-token" }, { "type": "missing-func" }]
-    },
-    {
-      "src": "bad {placeholder}",
-      "exp": "bad placeholder",
-      "errorsJs": [{ "type": "parse-error" }, { "type": "extra-content" }, { "type": "junk-element" }]
+      "expErrors": [{ "type": "syntax-error" }, { "type": "unknown-function" }]
     },
     {
       "src": "{bad {$placeholder option}}",
       "exp": "bad {$placeholder}",
-      "errors": [{ "type": "extra-content" }, { "type": "extra-content" }, { "type": "missing-var" }],
+      "expErrors": [{ "type": "syntax-error"}, { "type": "unresolved-variable" }],
       "ignoreCpp": "Fallback is unclear. See https://github.com/unicode-org/message-format-wg/issues/703"
     },
     {
@@ -300,14 +126,14 @@
     {
       "src": ".match {$foo :string} * * {{foo}}",
       "exp": "foo",
-      "errors": [{ "type": "key-mismatch" }, { "type": "missing-var" }],
+      "expErrors": [{ "type": "variant-key-mismatch" }, { "type": "unresolved-variable" }],
       "ignoreCpp": "Fallback is unclear. See https://github.com/unicode-org/message-format-wg/issues/735"
     },
     {
       "src": ".match {$foo :string} {$bar :string} * {{foo}}",
       "exp": "foo",
-      "errors": [{ "type": "key-mismatch" }, { "type": "missing-var" }, { "type": "missing-var" }],
+      "expErrors": [{ "type": "variant-key-mismatch" }, { "type": "unresolved-variable" }],
       "ignoreCpp": "Fallback is unclear. See https://github.com/unicode-org/message-format-wg/issues/735"
     }
-]
-
+  ]
+}
diff --git a/testdata/message2/icu-test-selectors.json b/testdata/message2/icu-test-selectors.json
index efe67e7c4c9e..102bdfd88f50 100644
--- a/testdata/message2/icu-test-selectors.json
+++ b/testdata/message2/icu-test-selectors.json
@@ -1,46 +1,171 @@
-[
+{
+  "scenario": "Match tests",
+  "description": "Tests for various match constructs",
+  "defaultTestProperties": {
+    "locale": "en-US"
+  },
+ "tests": [
+  {
+    "comment": "Testing simple plural",
+    "src": [
+        ".match {$count :number}\n",
+        "one {{{$count} file}}\n",
+        " *  {{{$count} files}}"
+     ],
+     "params": [{"name": "count", "value": 0}],
+     "exp": "0 files"
+  },
   {
     "comment": "Testing simple plural",
-    "shared": {
-      "srcs": [
+    "src": [
         ".match {$count :number}\n",
         "one {{{$count} file}}\n",
         " *  {{{$count} files}}"
-      ]
-    },
-    "variations" : [
-      { "params": { "count": 0 }, "exp": "0 files" },
-      { "params": { "count": 1 }, "exp": "1 file" },
-      { "params": { "count": 3 }, "exp": "3 files" },
-      { "params": { "count": 0 }, "locale": "fr", "exp": "0 file" },
-      { "params": { "count": 1 }, "locale": "fr", "exp": "1 file" },
-      { "params": { "count": 3 }, "locale": "fr", "exp": "3 files" }
-    ]
+     ],
+     "params": [{"name": "count", "value": 1}],
+     "exp": "1 file"
+  },
+  {
+    "comment": "Testing simple plural",
+    "src": [
+        ".match {$count :number}\n",
+        "one {{{$count} file}}\n",
+        " *  {{{$count} files}}"
+     ],
+     "params": [{"name": "count", "value": 3}],
+     "exp": "3 files"
+  },
+  {
+    "comment": "Testing simple plural",
+    "locale": "fr",
+    "src": [
+        ".match {$count :number}\n",
+        "one {{{$count} file}}\n",
+        " *  {{{$count} files}}"
+     ],
+     "params": [{"name": "count", "value": 0}],
+     "exp": "0 file"
+  },
+  {
+    "comment": "Testing simple plural",
+    "locale": "fr",
+    "src": [
+        ".match {$count :number}\n",
+        "one {{{$count} file}}\n",
+        " *  {{{$count} files}}"
+     ],
+     "params": [{"name": "count", "value": 1}],
+     "exp": "1 file"
+  },
+  {
+    "comment": "Testing simple plural",
+    "locale": "fr",
+    "src": [
+        ".match {$count :number}\n",
+        "one {{{$count} file}}\n",
+        " *  {{{$count} files}}"
+     ],
+     "params": [{"name": "count", "value": 3}],
+     "exp": "3 files"
+  },
+  {
+    "comment": "Testing simple plural, but swap variant order",
+    "src": [
+        ".match {$count :number}\n",
+        " *  {{You deleted {$count} files}}\n",
+        "one {{You deleted {$count} file}}"
+    ],
+    "params": [{"name": "count", "value": 1}],
+    "exp": "You deleted 1 file"
   },
   {
     "comment": "Testing simple plural, but swap variant order",
-    "shared": {
-      "srcs": [
+    "src": [
         ".match {$count :number}\n",
         " *  {{You deleted {$count} files}}\n",
         "one {{You deleted {$count} file}}"
-      ]
-    },
-    "variations" : [
-      {
-        "params": { "count": 1 },
-        "exp": "You deleted 1 file"
-      },
-      {
-        "params": { "count": 3 },
-        "exp": "You deleted 3 files"
-      }
-    ]
+    ],
+    "params": [{"name": "count", "value": 3}],
+    "exp": "You deleted 3 files"
+  },
+  {
+    "comment": "Ordinal, with mixed order and exact matches",
+    "src": [
+        ".match {$place :number select=ordinal}\n",
+        "*   {{You finished in the {$place}th place}}\n",
+        "two {{You finished in the {$place}nd place}}\n",
+        "one {{You finished in the {$place}st place}}\n",
+        "1   {{You got the gold medal}}\n",
+        "2   {{You got the silver medal}}\n",
+        "3   {{You got the bronze medal}}\n",
+        "few {{You finished in the {$place}rd place}}"
+    ],
+    "params": [{"name": "place", "value": 1}],
+    "exp": "You got the gold medal"
+  },
+  {
+    "comment": "Ordinal, with mixed order and exact matches",
+    "src": [
+        ".match {$place :number select=ordinal}\n",
+        "*   {{You finished in the {$place}th place}}\n",
+        "two {{You finished in the {$place}nd place}}\n",
+        "one {{You finished in the {$place}st place}}\n",
+        "1   {{You got the gold medal}}\n",
+        "2   {{You got the silver medal}}\n",
+        "3   {{You got the bronze medal}}\n",
+        "few {{You finished in the {$place}rd place}}"
+    ],
+    "params": [{"name": "place", "value": 2}],
+    "exp": "You got the silver medal"
+  },
+  {
+    "comment": "Ordinal, with mixed order and exact matches",
+    "src": [
+        ".match {$place :number select=ordinal}\n",
+        "*   {{You finished in the {$place}th place}}\n",
+        "two {{You finished in the {$place}nd place}}\n",
+        "one {{You finished in the {$place}st place}}\n",
+        "1   {{You got the gold medal}}\n",
+        "2   {{You got the silver medal}}\n",
+        "3   {{You got the bronze medal}}\n",
+        "few {{You finished in the {$place}rd place}}"
+    ],
+    "params": [{"name": "place", "value": 3}],
+    "exp": "You got the bronze medal"
+  },
+  {
+    "comment": "Ordinal, with mixed order and exact matches",
+    "src": [
+        ".match {$place :number select=ordinal}\n",
+        "*   {{You finished in the {$place}th place}}\n",
+        "two {{You finished in the {$place}nd place}}\n",
+        "one {{You finished in the {$place}st place}}\n",
+        "1   {{You got the gold medal}}\n",
+        "2   {{You got the silver medal}}\n",
+        "3   {{You got the bronze medal}}\n",
+        "few {{You finished in the {$place}rd place}}"
+    ],
+    "params": [{"name": "place", "value": 7}],
+    "exp": "You finished in the 7th place"
+  },
+  {
+    "comment": "Ordinal, with mixed order and exact matches",
+    "src": [
+        ".match {$place :number select=ordinal}\n",
+        "*   {{You finished in the {$place}th place}}\n",
+        "two {{You finished in the {$place}nd place}}\n",
+        "one {{You finished in the {$place}st place}}\n",
+        "1   {{You got the gold medal}}\n",
+        "2   {{You got the silver medal}}\n",
+        "3   {{You got the bronze medal}}\n",
+        "few {{You finished in the {$place}rd place}}"
+    ],
+    "params": [{"name": "place", "value": 21}],
+    "exp": "You finished in the 21st place"
   },
   {
     "comment": "Ordinal, with mixed order and exact matches",
-    "shared": {
-      "srcs": [
+    "src": [
         ".match {$place :number select=ordinal}\n",
         "*   {{You finished in the {$place}th place}}\n",
         "two {{You finished in the {$place}nd place}}\n",
@@ -49,83 +174,199 @@
         "2   {{You got the silver medal}}\n",
         "3   {{You got the bronze medal}}\n",
         "few {{You finished in the {$place}rd place}}"
-      ]
-    },
-    "variations" : [
-      { "params": { "place": 1 }, "exp": "You got the gold medal" },
-      { "params": { "place": 2 }, "exp": "You got the silver medal" },
-      { "params": { "place": 3 }, "exp": "You got the bronze medal" },
-      { "params": { "place": 7 }, "exp": "You finished in the 7th place" },
-      { "params": { "place": 21 }, "exp": "You finished in the 21st place" },
-      { "params": { "place": 22 }, "exp": "You finished in the 22nd place" },
-      { "params": { "place": 23 }, "exp": "You finished in the 23rd place" },
-      { "params": { "place": 28 }, "exp": "You finished in the 28th place" }
-    ]
+    ],
+    "params": [{"name": "place", "value": 22}],
+    "exp": "You finished in the 22nd place"
+  },
+  {
+    "comment": "Ordinal, with mixed order and exact matches",
+    "src": [
+        ".match {$place :number select=ordinal}\n",
+        "*   {{You finished in the {$place}th place}}\n",
+        "two {{You finished in the {$place}nd place}}\n",
+        "one {{You finished in the {$place}st place}}\n",
+        "1   {{You got the gold medal}}\n",
+        "2   {{You got the silver medal}}\n",
+        "3   {{You got the bronze medal}}\n",
+        "few {{You finished in the {$place}rd place}}"
+    ],
+    "params": [{"name": "place", "value": 23}],
+    "exp": "You finished in the 23rd place"
+  },
+  {
+    "comment": "Ordinal, with mixed order and exact matches",
+    "src": [
+        ".match {$place :number select=ordinal}\n",
+        "*   {{You finished in the {$place}th place}}\n",
+        "two {{You finished in the {$place}nd place}}\n",
+        "one {{You finished in the {$place}st place}}\n",
+        "1   {{You got the gold medal}}\n",
+        "2   {{You got the silver medal}}\n",
+        "3   {{You got the bronze medal}}\n",
+        "few {{You finished in the {$place}rd place}}"
+    ],
+    "params": [{"name": "place", "value": 28}],
+    "exp": "You finished in the 28th place"
+  },
+  {
+    "comment": "Plural combinations, mixed order",
+    "src": [
+        ".match {$fileCount :number} {$folderCount :number}\n",
+        "  *   *   {{You found {$fileCount} files in {$folderCount} folders}}\n",
+        "  one one {{You found {$fileCount} file in {$folderCount} folder}}\n",
+        "  one *   {{You found {$fileCount} file in {$folderCount} folders}}\n",
+        "  *   one {{You found {$fileCount} files in {$folderCount} folder}}"
+    ],
+    "params": [{"name": "fileCount", "value": 1},
+               {"name": "folderCount", "value": 1}],
+    "exp": "You found 1 file in 1 folder"
+  },
+  {
+    "comment": "Plural combinations, mixed order",
+    "src": [
+        ".match {$fileCount :number} {$folderCount :number}\n",
+        "  *   *   {{You found {$fileCount} files in {$folderCount} folders}}\n",
+        "  one one {{You found {$fileCount} file in {$folderCount} folder}}\n",
+        "  one *   {{You found {$fileCount} file in {$folderCount} folders}}\n",
+        "  *   one {{You found {$fileCount} files in {$folderCount} folder}}"
+    ],
+    "params": [{"name": "fileCount", "value": 1},
+               {"name": "folderCount", "value": 5}],
+    "exp": "You found 1 file in 5 folders"
+  },
+  {
+    "comment": "Plural combinations, mixed order",
+    "src": [
+        ".match {$fileCount :number} {$folderCount :number}\n",
+        "  *   *   {{You found {$fileCount} files in {$folderCount} folders}}\n",
+        "  one one {{You found {$fileCount} file in {$folderCount} folder}}\n",
+        "  one *   {{You found {$fileCount} file in {$folderCount} folders}}\n",
+        "  *   one {{You found {$fileCount} files in {$folderCount} folder}}"
+    ],
+    "params": [{"name": "fileCount", "value": 7},
+               {"name": "folderCount", "value": 1}],
+    "exp": "You found 7 files in 1 folder"
   },
   {
     "comment": "Plural combinations, mixed order",
-    "shared": {
-      "srcs": [
+    "src": [
         ".match {$fileCount :number} {$folderCount :number}\n",
         "  *   *   {{You found {$fileCount} files in {$folderCount} folders}}\n",
         "  one one {{You found {$fileCount} file in {$folderCount} folder}}\n",
         "  one *   {{You found {$fileCount} file in {$folderCount} folders}}\n",
         "  *   one {{You found {$fileCount} files in {$folderCount} folder}}"
-      ]
-    },
-    "variations" : [
-      { "params": { "fileCount": 1, "folderCount": 1 }, "exp": "You found 1 file in 1 folder" },
-      { "params": { "fileCount": 1, "folderCount": 5 }, "exp": "You found 1 file in 5 folders" },
-      { "params": { "fileCount": 7, "folderCount": 1 }, "exp": "You found 7 files in 1 folder" },
-      { "params": { "fileCount": 7, "folderCount": 3 }, "exp": "You found 7 files in 3 folders" }
-    ]
+    ],
+    "params": [{"name": "fileCount", "value": 7},
+               {"name": "folderCount", "value": 3}],
+    "exp": "You found 7 files in 3 folders"
+  },
+  {
+    "comment": "Test that the selection honors the formatting option (`1.00 dollars`)",
+      "src": [
+        ".local $c = {$price :number minimumFractionDigits=$minF}\n",
+        ".match {$c}\n",
+        "  one {{{$c} dollar}}\n",
+        "   *  {{{$c} dollars}}"
+      ],
+      "params": [{ "name": "price", "value": 1 },
+                 { "name": "minF", "value": 0 }],
+      "exp": "1 dollar"
   },
   {
     "comment": "Test that the selection honors the formatting option (`1.00 dollars`)",
-    "shared": {
-      "srcs": [
+      "src": [
         ".local $c = {$price :number minimumFractionDigits=$minF}\n",
         ".match {$c}\n",
         "  one {{{$c} dollar}}\n",
         "   *  {{{$c} dollars}}"
-      ]
-    },
-    "variations" : [
-      { "params": { "price": 1, "minF": 0 }, "exp": "1 dollar" },
-      { "params": { "price": 1, "minF": 2 }, "exp": "1.00 dollars" }
-    ]
+      ],
+      "params": [{ "name": "price", "value": 1},
+                 { "name": "minF", "value": 2 }],
+      "exp": "1.00 dollars"
   },
   {
     "comment": "Test that the selection honors the formatting option (`1.00 dollars`)",
-    "shared": {
-      "srcs": [
+      "src": [
         ".local $c = {$price :number maximumFractionDigits=$maxF}\n",
         ".match {$c}\n",
         "  one {{{$c} dollar}}\n",
         "   *  {{{$c} dollars}}"
-      ]
-    },
-    "variations" : [
-      { "params": { "price": 1.25, "maxF": 0 }, "exp": "1 dollar" },
-      { "params": { "price": 1.25, "maxF": 2 }, "exp": "1.25 dollars" }
-    ]
+      ],
+      "params": [{ "name": "price", "value": 1.25},
+                 { "name": "maxF", "value": 0 }],
+      "exp": "1 dollar"
+  },
+  {
+    "comment": "Test that the selection honors the formatting option (`1.00 dollars`)",
+      "src": [
+        ".local $c = {$price :number maximumFractionDigits=$maxF}\n",
+        ".match {$c}\n",
+        "  one {{{$c} dollar}}\n",
+        "   *  {{{$c} dollars}}"
+      ],
+      "params": [{ "name": "price", "value": 1.25},
+                 { "name": "maxF", "value": 2 }],
+      "exp": "1.25 dollars"
+  },
+  {
+    "comment": "Test that the selection honors the `:integer` over options",
+      "src": [
+        ".local $c = {$price :integer maximumFractionDigits=$maxF}\n",
+        ".match {$c}\n",
+        "  one {{{$c} dollar}}\n",
+        "   *  {{{$c} dollars}}"
+      ],
+      "params": [{ "name": "price", "value": 1},
+                 { "name": "maxF", "value": 0 }],
+      "exp": "1 dollar"
+  },
+  {
+    "comment": "Test that the selection honors the `:integer` over options",
+      "src": [
+        ".local $c = {$price :integer maximumFractionDigits=$maxF}\n",
+        ".match {$c}\n",
+        "  one {{{$c} dollar}}\n",
+        "   *  {{{$c} dollars}}"
+      ],
+      "params": [{ "name": "price", "value": 1},
+                 { "name": "maxF", "value": 2 }],
+      "exp": "1 dollar"
+  },
+  {
+    "comment": "Test that the selection honors the `:integer` over options",
+      "src": [
+        ".local $c = {$price :integer maximumFractionDigits=$maxF}\n",
+        ".match {$c}\n",
+        "  one {{{$c} dollar}}\n",
+        "   *  {{{$c} dollars}}"
+      ],
+      "params": [{ "name": "price", "value": 1.25},
+                 { "name": "maxF", "value": 0 }],
+      "exp": "1 dollar"
+  },
+  {
+    "comment": "Test that the selection honors the `:integer` over options",
+      "src": [
+        ".local $c = {$price :integer maximumFractionDigits=$maxF}\n",
+        ".match {$c}\n",
+        "  one {{{$c} dollar}}\n",
+        "   *  {{{$c} dollars}}"
+      ],
+      "params": [{ "name": "price", "value": 1.25 },
+                 { "name": "maxF", "value": 2 }],
+      "exp": "1 dollar"
   },
   {
     "comment": "Test that the selection honors the `:integer` over options",
-    "shared": {
-      "srcs": [
+      "src": [
         ".local $c = {$price :integer maximumFractionDigits=$maxF}\n",
         ".match {$c}\n",
         "  one {{{$c} dollar}}\n",
         "   *  {{{$c} dollars}}"
-      ]
-    },
-    "variations" : [
-      { "params": { "price": 1, "maxF": 0 }, "exp": "1 dollar" },
-      { "params": { "price": 1, "maxF": 2 }, "exp": "1 dollar" },
-      { "params": { "price": 1.25, "maxF": 0 }, "exp": "1 dollar" },
-      { "params": { "price": 1.25, "maxF": 2 }, "exp": "1 dollar" },
-      { "params": { "price": 4.12345, "maxF": 4 }, "exp": "4 dollars" }
-    ]
+      ],
+      "params": [{ "name": "price", "value": 4.12345 },
+                 { "name": "maxF", "value": 4 }],
+      "exp": "4 dollars"
   }
-]
+ ]
+}
diff --git a/testdata/message2/invalid-number-literals-diagnostics.json b/testdata/message2/invalid-number-literals-diagnostics.json
index 1c1e84f53f37..d35c16b23386 100644
--- a/testdata/message2/invalid-number-literals-diagnostics.json
+++ b/testdata/message2/invalid-number-literals-diagnostics.json
@@ -1,4 +1,15 @@
-[
+{
+  "scenario": "Number literal syntax errors",
+  "description": "Syntax errors with number literals; for ICU4C, the character offset in the parse error is checked",
+  "defaultTestProperties": {
+    "locale": "en-US",
+    "expErrors": [
+      {
+        "type": "syntax-error"
+      }
+    ]
+  },
+  "tests": [
     { "src": "{00}", "char": 2},
     { "src": "{042}", "char": 2},
     { "src": "{1.}", "char": 3},
@@ -10,4 +21,5 @@
     { "src": "{1e+}", "char": 4},
     { "src": "{1e-}", "char": 4},
     { "src": "{1.0e2.0}", "char": 6}
-]
+  ]
+}
diff --git a/testdata/message2/invalid-options.json b/testdata/message2/invalid-options.json
index 8b4740ee9bcb..698583cd5578 100644
--- a/testdata/message2/invalid-options.json
+++ b/testdata/message2/invalid-options.json
@@ -1,34 +1,47 @@
-[
-    { "src": ".local $foo = {1 :number minimumIntegerDigits=-1} {{bar {$foo}}}",
-      "errors": [{"type": "bad-option"}],
+{
+  "scenario": "Bad options for built-in functions",
+  "description": "Tests for the bad-option error; only run in ICU4C for now",
+  "defaultTestProperties": {
+    "locale": "en-US",
+    "expErrors": [
+      {
+        "type": "bad-option"
+      }
+    ]
+  },
+  "tests": [
+    { "comment": "Neither impl validates options right now; see https://github.com/unicode-org/message-format-wg/issues/738",
+      "src": ".local $foo = {1 :number minimumIntegerDigits=-1} {{bar {$foo}}}",
+      "ignoreCpp": "ICU4C doesn't validate options",
       "ignoreJava": "ICU4J doesn't validate options"
     },
     { "src": ".local $foo = {1 :number minimumIntegerDigits=foo} {{bar {$foo}}}",
-      "errors": [{"type": "bad-option"}],
+      "ignoreCpp": "ICU4C doesn't validate options",
       "ignoreJava": "ICU4J doesn't validate options"
     },
     { "src": ".local $foo = {1 :number minimumFractionDigits=foo} {{bar {$foo}}}",
-      "errors": [{"type": "bad-option"}],
+      "ignoreCpp": "ICU4C doesn't validate options",
       "ignoreJava": "ICU4J doesn't validate options"
     },
     { "src": ".local $foo = {1 :number maximumFractionDigits=foo} {{bar {$foo}}}",
-      "errors": [{"type": "bad-option"}],
+      "ignoreCpp": "ICU4C doesn't validate options",
       "ignoreJava": "ICU4J doesn't validate options"
     },
     { "src": ".local $foo = {1 :number minimumSignificantDigits=foo} {{bar {$foo}}}",
-      "errors": [{"type": "bad-option"}],
+      "ignoreCpp": "ICU4C doesn't validate options",
       "ignoreJava": "ICU4J doesn't validate options"
     },
     { "src": ".local $foo = {1 :number maximumSignificantDigits=foo} {{bar {$foo}}}",
-      "errors": [{"type": "bad-option"}],
+      "ignoreCpp": "ICU4C doesn't validate options",
       "ignoreJava": "ICU4J doesn't validate options"
     },
     { "src": ".local $foo = {1 :integer minimumIntegerDigits=foo} {{bar {$foo}}}",
-      "errors": [{"type": "bad-option"}],
+      "ignoreCpp": "ICU4C doesn't validate options",
       "ignoreJava": "ICU4J doesn't validate options"
     },
     { "src": ".local $foo = {1 :integer maximumSignificantDigits=foo} {{bar {$foo}}}",
-      "errors": [{"type": "bad-option"}],
+      "ignoreCpp": "ICU4C doesn't validate options",
       "ignoreJava": "ICU4J doesn't validate options"
     }
-]
+  ]
+}
diff --git a/testdata/message2/markup.json b/testdata/message2/markup.json
index 52c86b7aa637..29b4408e2c4a 100644
--- a/testdata/message2/markup.json
+++ b/testdata/message2/markup.json
@@ -1,14 +1,15 @@
-[
-    { "src": "{#tag}", "exp": "" },
+{
+  "scenario": "Markup",
+  "description": "Tests for valid markup strings",
+  "defaultTestProperties": {
+    "locale": "en-US"
+  },
+  "tests": [
     { "src": "{#tag/}", "exp": "" },
     { "src": "{/tag}", "exp": "" },
-    { "src": "{#tag}content", "exp": "content" },
     { "src": "{#tag}content{/tag}", "exp": "content" },
-    { "src": "{/tag}content", "exp": "content" },
-    { "src": "{#tag foo=bar}", "exp": "" },
-    { "src": "{/tag foo=bar}", "exp": "" },
-    { "src": "{#tag foo=bar/}", "exp": "" },
     { "src": "{#tag foo=|foo| bar=$bar}",
-      "params": { "bar": "b a r" },
+      "params": [{ "name": "bar", "value": "b a r" }],
       "exp": "" }
-]
+  ]
+}
diff --git a/testdata/message2/matches-whitespace.json b/testdata/message2/matches-whitespace.json
index d0b2c4ecdfe7..a0af4c4d143e 100644
--- a/testdata/message2/matches-whitespace.json
+++ b/testdata/message2/matches-whitespace.json
@@ -1,4 +1,10 @@
-[
+{
+  "scenario": "Matches with whitespace",
+  "description": "Tests for valid match constructs with whitespace in various places",
+  "defaultTestProperties": {
+    "locale": "en-US"
+  },
+  "tests": [
     { "src": ".match {one :string} {bar :string} one * {{one}} * * {{other}}",
       "exp": "one" },
     { "src": ".match {foo :string} {bar :string}one * {{one}} * * {{other}}",
@@ -23,4 +29,6 @@
     },
     { "src": ".match {foo :string} {bar :string} one *  {{one}} * * {{foo}}",
       "exp": "foo" }
-]
+  ]
+}
+
diff --git a/testdata/message2/more-data-model-errors.json b/testdata/message2/more-data-model-errors.json
index 0c86c58a5a12..14c33c66d353 100644
--- a/testdata/message2/more-data-model-errors.json
+++ b/testdata/message2/more-data-model-errors.json
@@ -1,34 +1,205 @@
 {
-    "Variant Key Mismatch": [
-        ".match {$foo :number} {$bar :number}  one{{one}}",
-        ".match {$foo :number} {$bar :number}  one {{one}}",
-        ".match {$foo :number} {$bar :number}  one  {{one}}",
-        ".match {$foo :number}  * * {{foo}}",
-        ".match {$one :number}\n  1 2 {{Too many}}\n   * {{Otherwise}}",
-        ".match {$one :number} {$two :number}\n  1 2 {{Two keys}}\n  * {{Missing a key}}\n  * * {{Otherwise}}",
-        ".match {$foo :x} {$bar :x} * {{foo}}"
-    ],
-    "Missing Fallback Variant": [
-        ".match {$one :number}\n   1 {{Value is one}}\n 2 {{Value is two}}",
-        ".match {$one :number} {$two :number}\n  1 * {{First is one}}\n  * 1 {{Second is one}}"
-    ],
-    "Missing Selector Annotation": [
-        ".match {$one}\n   1 {{Value is one}}\n    * {{Value is not one}}",
-        ".local $one = {|The one|}\n   .match {$one}\n  1 {{Value is one}}\n   * {{Value is not one}}",
-        ".match {|horse| ^private}\n   1 {{The value is one.}}   * {{The value is not one.}}",
-        ".match {$foo !select}  |1| {{one}}  * {{other}}",
-        ".match {$foo ^select}  |1| {{one}}  * {{other}}",
-        ".input {$foo} .match {$foo} one {{one}} * {{other}}",
-        ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}"
-    ],
-    "Duplicate Declaration": [
-        ".local $x = {|1|} .input {$x :number} {{{$x}}}",
-        ".input {$x :number} .input {$x :string} {{{$x}}}"
-    ],
-    "Duplicate Option Name": [
-        "{:foo a=1 b=2 a=1}",
-        "{:foo a=1 a=1}",
-        "{:foo a=1 a=2}",
-        "{|x| :foo a=1 a=2}"
-    ]
+  "scenario": "Additional data model errors",
+  "defaultTestProperties": {
+    "locale": "en-US"
+  },
+  "tests": [
+    {
+      "src": ".match {$foo :number} {$bar :number}  one{{one}}",
+      "expErrors": [
+        {
+          "type": "variant-key-mismatch"
+        }
+      ]
+    },
+
+    {
+      "src": ".match {$foo :number} {$bar :number}  one {{one}}",
+      "expErrors": [
+        {
+          "type": "variant-key-mismatch"
+        }
+      ]
+    },
+
+    {
+      "src": ".match {$foo :number} {$bar :number}  one  {{one}}",
+      "expErrors": [
+        {
+          "type": "variant-key-mismatch"
+        }
+      ]
+    },
+
+    {
+      "src": ".match {$foo :number}  * * {{foo}}",
+      "expErrors": [
+        {
+          "type": "variant-key-mismatch"
+        }
+      ]
+    },
+
+    {
+      "src": ".match {$one :number}\n  1 2 {{Too many}}\n   * {{Otherwise}}",
+      "expErrors": [
+        {
+          "type": "variant-key-mismatch"
+        }
+      ]
+    },
+
+    {
+      "src": ".match {$one :number} {$two :number}\n  1 2 {{Two keys}}\n  * {{Missing a key}}\n  * * {{Otherwise}}",
+      "expErrors": [
+        {
+          "type": "variant-key-mismatch"
+        }
+      ]
+    },
+
+    {
+      "src": ".match {$foo :x} {$bar :x} * {{foo}}",
+      "expErrors": [
+        {
+          "type": "variant-key-mismatch"
+        }
+      ]
+    },
+
+    {
+      "src": ".match {$one :number}\n   1 {{Value is one}}\n 2 {{Value is two}}",
+      "expErrors": [
+        {
+          "type": "missing-fallback-variant"
+        }
+      ]
+    },
+
+    {
+      "src": ".match {$one :number} {$two :number}\n  1 * {{First is one}}\n  * 1 {{Second is one}}",
+      "expErrors": [
+        {
+          "type": "missing-fallback-variant"
+        }
+      ]
+    },
+
+    {
+      "src": ".match {$one}\n   1 {{Value is one}}\n    * {{Value is not one}}",
+      "expErrors": [
+        {
+          "type": "missing-selector-annotation"
+        }
+      ]
+    },
+
+    {
+      "src": ".local $one = {|The one|}\n   .match {$one}\n  1 {{Value is one}}\n   * {{Value is not one}}",
+      "expErrors": [
+        {
+          "type": "missing-selector-annotation"
+        }
+      ]
+    },
+
+    {
+      "src": ".match {|horse| ^private}\n   1 {{The value is one.}}   * {{The value is not one.}}",
+      "expErrors": [
+        {
+          "type": "missing-selector-annotation"
+        }
+      ]
+    },
+
+    {
+      "src": ".match {$foo !select}  |1| {{one}}  * {{other}}",
+      "expErrors": [
+        {
+          "type": "missing-selector-annotation"
+        }
+      ]
+    },
+
+    {
+      "src": ".match {$foo ^select}  |1| {{one}}  * {{other}}",
+      "expErrors": [
+        {
+          "type": "missing-selector-annotation"
+        }
+      ]
+    },
+
+    {
+      "src": ".input {$foo} .match {$foo} one {{one}} * {{other}}",
+      "expErrors": [
+        {
+          "type": "missing-selector-annotation"
+        }
+      ]
+    },
+
+    {
+      "src": ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}",
+      "expErrors": [
+        {
+          "type": "missing-selector-annotation"
+        }
+      ]
+    },
+
+    {
+      "src": ".local $x = {|1|} .input {$x :number} {{{$x}}}",
+      "expErrors": [
+        {
+          "type": "duplicate-declaration"
+        }
+      ]
+    },
+
+    {
+      "src": ".input {$x :number} .input {$x :string} {{{$x}}}",
+      "expErrors": [
+        {
+          "type": "duplicate-declaration"
+        }
+      ]
+    },
+
+    {
+      "src": "{:foo a=1 b=2 a=1}",
+      "expErrors": [
+        {
+          "type": "duplicate-option-name"
+        }
+      ]
+    },
+
+    {
+      "src": "{:foo a=1 a=1}",
+      "expErrors": [
+        {
+          "type": "duplicate-option-name"
+        }
+      ]
+    },
+
+    {
+      "src": "{:foo a=1 a=2}",
+      "expErrors": [
+        {
+          "type": "duplicate-option-name"
+        }
+      ]
+    },
+
+    {
+      "src": "{|x| :foo a=1 a=2}",
+      "expErrors": [
+        {
+          "type": "duplicate-option-name"
+        }
+      ]
+    }
+  ]
 }
diff --git a/testdata/message2/more-functions.json b/testdata/message2/more-functions.json
index 99640a4daed3..83a55151f786 100644
--- a/testdata/message2/more-functions.json
+++ b/testdata/message2/more-functions.json
@@ -1,5 +1,10 @@
 {
-  "number": [
+  "scenario": "Function tests 2",
+  "description": "More tests for ICU-specific formatting behavior.",
+  "defaultTestProperties": {
+    "locale": "en-US"
+  },
+  "tests": [
     {
       "src": "Format {123456789.9876 :number} number",
       "locale": "en-IN",
@@ -19,54 +24,54 @@
       "comment": "From ICU4J tests, with explicit formatter added"
     },
     {
-      "srcs": [".local $dateStr = {$date :datetime}\n",
+      "src": [".local $dateStr = {$date :datetime}\n",
                "{{Testing date formatting: {$dateStr :datetime}.}}"],
       "exp": "Testing date formatting: 23.11.2022, 19:42.",
       "locale": "ro",
-      "params": {"date": {"date": 1669261357000}}
+      "params": [{ "name": "date", "value": { "date": 1669261357000 }}]
     },
     {
       "src": "Testing date formatting: {$date :date style=long}.",
       "exp": "Testing date formatting: November 23, 2022.",
-      "params": {"date": {"date": 1669261357000}}
+      "params": [{ "name": "date", "value": { "date": 1669261357000 } }]
     },
     {
       "src": "Testing date formatting: {$date :date style=medium}.",
       "exp": "Testing date formatting: Nov 23, 2022.",
-      "params": {"date": {"date": 1669261357000}}
+      "params": [{ "name": "date", "value": { "date": 1669261357000 } }]
     },
     {
       "src": "Testing date formatting: {$date :date style=short}.",
       "exp": "Testing date formatting: 11/23/22.",
-      "params": {"date": {"date": 1669261357000}}
+      "params": [{ "name": "date", "value": { "date": 1669261357000 } }]
     },
     {
       "src": "Testing date formatting: {$date :time style=long}.",
       "exp": "Testing date formatting: 7:42:37\u202FPM PST.",
-      "params": {"date": {"date": 1669261357000}}
+      "params": [{ "name": "date", "value": { "date": 1669261357000 } }]
     },
     {
       "src": "Testing date formatting: {$date :time style=medium}.",
       "exp": "Testing date formatting: 7:42:37\u202FPM.",
-      "params": {"date": {"date": 1669261357000}}
+      "params": [{ "name": "date", "value": { "date": 1669261357000 } }]
     },
     {
       "src": "Testing date formatting: {$date :time style=short}.",
       "exp": "Testing date formatting: 7:42\u202FPM.",
-      "params": {"date": {"date": 1669261357000}}
+      "params": [{ "name": "date", "value": { "date": 1669261357000 } }]
     },
     {
-      "srcs": [".local $num = {|42| :number}\n",
+      "src": [".local $num = {|42| :number}\n",
                "{{Testing date formatting: {$num :datetime}}}"],
       "exp": "Testing date formatting: {|42|}",
-      "errors": [{"type": "bad-input"}]
+      "expErrors": [{"type": "bad-operand"}]
     },
     {
       "src": "From literal: {|123456789,531| :number}!",
       "exp": "From literal: {|123456789,531|}!",
       "locale": "en-IN",
-      "params": {"val": 1234567890.97531},
-      "errors": [{"type": "bad-input"}],
+      "params": [{ "name": "val", "value": 1234567890.97531 }],
+      "expErrors": [{"type": "bad-operand"}],
       "comment": "Should fail because number literals are not treated as localized numbers",
       "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
     },
@@ -74,32 +79,32 @@
       "src": "From literal: {|123456789.531| :number}!",
       "exp": "From literal: \u1041\u1042\u1043,\u1044\u1045\u1046,\u1047\u1048\u1049.\u1045\u1043\u1041!",
       "locale": "my",
-      "params": {"val": 1234567890.97531}
+      "params": [{ "name": "val", "value": 1234567890.97531 }]
     },
     {
       "src": "Default double: {$val}!",
       "exp": "Default double: 1,23,45,67,890.97531!",
       "locale": "en-IN",
-      "params": {"val": 1234567890.97531},
+      "params": [{ "name": "val", "value": 1234567890.97531 }],
       "comment": "The next few tests check that numeric variables are formatted without specifying :number"
     },
     {
       "src": "Default double: {$val}!",
       "exp": "Default double: 1.234.567.890,97531!",
       "locale": "ro",
-      "params": {"val": 1234567890.97531}
+      "params": [{ "name": "val", "value": 1234567890.97531 }]
     },
     {
       "src": "Default float: {$val}!",
       "exp": "Default float: 3,141593!",
       "locale": "ro",
-      "params": {"val": 3.1415926535}
+      "params": [{"name": "val", "value": 3.1415926535}]
     },
     {
       "src": "Default int64: {$val}!",
       "exp": "Default int64: 1.234.567.890.123.456.800!",
       "locale": "ro",
-      "params": {"val": 1234567890123456789},
+      "params": [{ "name": "val", "value": 1234567890123456789 }],
       "comment": "Rounded due to JSON not supporting full 64-bit ints",
       "ignoreJava": "See https://unicode-org.atlassian.net/browse/ICU-22754?focusedCommentId=175932"
     },
@@ -107,7 +112,7 @@
       "src": "Default int64: {$val}!",
       "exp": "Default int64: 1.234.567.890.123.456.770!",
       "locale": "ro",
-      "params": {"val": 1234567890123456789},
+      "params": [{ "name": "val", "value": 1234567890123456789 }],
       "comment": "Rounded due to JSON not supporting full 64-bit ints",
       "ignoreCpp": "See https://unicode-org.atlassian.net/browse/ICU-22754?focusedCommentId=175932"
     },
@@ -115,7 +120,7 @@
       "src": "Default number: {$val}!",
       "exp": "Default number: 1.234.567.890.123.456.789,987654!",
       "locale": "ro",
-      "params": {"val": {"decimal": "1234567890123456789.987654321"}}
+      "params": [{ "name": "val", "value": {"decimal": "1234567890123456789.987654321"} }]
     }
   ]
 }
diff --git a/testdata/message2/more-syntax-errors.json b/testdata/message2/more-syntax-errors.json
deleted file mode 100644
index 48f145b452c0..000000000000
--- a/testdata/message2/more-syntax-errors.json
+++ /dev/null
@@ -1,5 +0,0 @@
-[
-  ".input {$x :number}",
-  ".local $foo = {|1|}",
-  ".unsupported |statement| {$x :number}"
-]
diff --git a/testdata/message2/reserved-syntax.json b/testdata/message2/reserved-syntax.json
deleted file mode 100644
index 9efd6fe6b9c9..000000000000
--- a/testdata/message2/reserved-syntax.json
+++ /dev/null
@@ -1,40 +0,0 @@
-[
-    { "src": "hello {|4.2| %number}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" },
-    { "src": "hello {|4.2| %n|um|ber}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "{+42}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {|4.2| &num|be|r}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {|4.2| ^num|be|r}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {|4.2| +num|be|r}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {|4.2| ?num|be||r|s}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {|foo| !number}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {|foo| *number}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {?number}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "{<tag}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": ".local $bar = {$none ~plural} .match {foo :string}  * {{{$bar}}}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {|4.2| %num\\\\ber}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {|4.2| %num\\{be\\|r}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {|4.2| %num\\\\\\}ber}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {|4.2| !}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {|4.2| %}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {|4.2| *}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {|4.2| ^abc|123||5|\\\\}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {|4.2| ^ abc|123||5|\\\\}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {|4.2| ^ abc|123||5|\\\\ \\|def |3.14||2|}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {|4.2| ? }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {|4.2| %xyzz }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {|4.2| >xyzz   }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {$foo ~xyzz }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {$x   <xyzz   }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "{>xyzz }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "{  !xyzz   }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "{~xyzz }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "{ <xyzz   }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {|4.2| !xy z z }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {|4.2| *num \\\\ b er}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {|4.2| %num \\\\ b |3.14| r    }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {|4.2|    +num xx \\\\ b |3.14| r  }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {$foo    +num x \\\\ abcde |3.14| r  }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {$foo    >num x \\\\ abcde |aaa||3.14||42| r  }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  },
-    { "src": "hello {$foo    >num x \\\\ abcde |aaa||3.14| |42| r  }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations"  }
-]
-
diff --git a/testdata/message2/resolution-errors.json b/testdata/message2/resolution-errors.json
index 077e8b0e9c3b..610588e0cf5e 100644
--- a/testdata/message2/resolution-errors.json
+++ b/testdata/message2/resolution-errors.json
@@ -1,14 +1,21 @@
-[
-    { "src": "{$oops}", "exp": "{$oops}", "errors": [{ "type": "unresolved-var" }], "ignoreJava": "ICU4J doesn't signal unresolved variable errors?"},
-    { "src": ".input {$x :number} {{{$x}}}", "exp": "{$x}", "errors": [{ "type": "unresolved-var" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"},
-    { "src": ".local $foo = {$bar} .match {$foo :number}  one {{one}}  * {{other}}", "exp": "other", "errors": [{ "type": "unresolved-var" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"},
-    { "src": ".local $bar = {$none :number} .match {$foo :string}  one {{one}}  * {{{$bar}}}", "exp": "{$none}", "errors": [{ "type": "unresolved-var" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"},
-    { "src": "The value is {horse :func}.", "exp": "The value is {|horse|}.", "errors": [{ "type": "missing-func" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"},
-    { "src": ".matc {-1} {{hello}}", 
-      "errors": [{ "type": "unsupported-statement" }],
+{
+  "scenario": "Resolution errors",
+  "description": "Tests for unknown variables and functions",
+  "defaultTestProperties": {
+    "locale": "en-US"
+  },
+  "tests": [
+    { "src": "{$oops}", "exp": "{$oops}", "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "ICU4J doesn't signal unresolved variable errors?"},
+    { "src": ".input {$x :number} {{{$x}}}", "exp": "{$x}", "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"},
+    { "src": ".local $foo = {$bar} .match {$foo :number}  one {{one}}  * {{other}}", "exp": "other", "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"},
+    { "src": ".local $bar = {$none :number} .match {$foo :string}  one {{one}}  * {{{$bar}}}", "exp": "{$none}", "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"},
+    { "src": "The value is {horse :func}.", "exp": "The value is {|horse|}.", "expErrors": [{ "type": "unknown-function" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"},
+    { "src": ".matc {-1} {{hello}}",
+      "expErrors": [{ "type": "unsupported-statement" }],
       "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
     },
-    { "src": ".m {-1} {{hello}}", 
-      "errors": [{ "type": "unsupported-statement" }],
+    { "src": ".m {-1} {{hello}}",
+      "expErrors": [{ "type": "unsupported-statement" }],
       "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }
-]
+  ]
+}
diff --git a/testdata/message2/runtime-errors.json b/testdata/message2/runtime-errors.json
index 9c7bfbc58443..b1bb0cd491a0 100644
--- a/testdata/message2/runtime-errors.json
+++ b/testdata/message2/runtime-errors.json
@@ -1,26 +1,27 @@
-[
+{
+  "scenario": "Runtime errors",
+  "description": "Tests for bad-selector and bad-operand errors",
+  "defaultTestProperties": {
+    "locale": "en-US"
+  },
+  "tests": [
     {
         "src": ".match {|horse| :date}\n   1 {{The value is one.}}\n  * {{Formatter used as selector.}}",
         "exp": "Formatter used as selector.",
-        "errors": [{"type": "selector-error"}],
-        "ignoreJava": "ICU4J doesn't signal runtime errors?"
-    },
-    {
-        "src": ".match {|horse| :string}\n  1 {{The value is one.}}\n  * {{Selector used as formatter: {|horse| :string}}}",
-        "exp": "Selector used as formatter: {|horse|}",
-        "errors": [{"type": "formatting-error"}],
+        "expErrors": [{"type": "bad-selector"}],
         "ignoreJava": "ICU4J doesn't signal runtime errors?"
     },
     {
         "src": ".match {|horse| :number}\n  1 {{The value is one.}}\n  * {{horse is not a number.}}",
         "exp": "horse is not a number.",
-        "errors": [{"type": "selector-error"}],
+        "expErrors": [{"type": "bad-selector"}],
         "ignoreJava": "ICU4J doesn't signal runtime errors?"
     },
     {
         "src": ".local $sel = {|horse| :number}\n  .match {$sel}\n    1 {{The value is one.}}\n   * {{horse is not a number.}}",
         "exp": "horse is not a number.",
-        "errors": [{"type": "selector-error"}],
+        "expErrors": [{"type": "bad-selector"}],
         "ignoreJava": "ICU4J doesn't signal runtime errors?"
     }
-]
+  ]
+}
diff --git a/testdata/message2/spec/data-model-errors.json b/testdata/message2/spec/data-model-errors.json
index 0a6bd67641b6..86a674c43961 100644
--- a/testdata/message2/spec/data-model-errors.json
+++ b/testdata/message2/spec/data-model-errors.json
@@ -1,32 +1,185 @@
 {
-    "Variant Key Mismatch": [
-        ".match {$foo :x} * * {{foo}}",
-        ".match {$foo :x} {$bar :x} * {{foo}}"
-    ],
-    "Missing Fallback Variant": [
-        ".match {:foo} 1 {{_}}",
-        ".match {:foo} other {{_}}",
-        ".match {:foo} {:bar} * 1 {{_}} 1 * {{_}}"
-    ],
-    "Missing Selector Annotation": [
-        ".match {$foo} one {{one}} * {{other}}",
-        ".input {$foo} .match {$foo} one {{one}} * {{other}}",
-        ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}"
-    ],
-    "Duplicate Declaration": [
-        ".input {$foo} .input {$foo} {{_}}",
-        ".input {$foo} .local $foo = {42} {{_}}",
-        ".local $foo = {42} .input {$foo} {{_}}",
-        ".local $foo = {:unknown} .local $foo = {42} {{_}}",
-        ".local $foo = {$bar} .local $bar = {42} {{_}}",
-        ".local $foo = {$foo} {{_}}",
-        ".local $foo = {$bar} .local $bar = {$baz} {{_}}",
-        ".local $foo = {$bar :func} .local $bar = {$baz} {{_}}",
-        ".local $foo = {42 :func opt=$foo} {{_}}",
-        ".local $foo = {42 :func opt=$bar} .local $bar = {42} {{_}}"
-    ],
-    "Duplicate Option Name": [
-            "bad {:placeholder option=x option=x}",
-            "bad {:placeholder ns:option=x ns:option=y}"
-    ]
+  "$schema": "https://raw.githubusercontent.com/unicode-org/message-format-wg/main/test/schemas/v0/tests.schema.json",
+  "scenario": "Data model errors",
+  "defaultTestProperties": {
+    "locale": "en-US"
+  },
+  "tests": [
+    {
+      "src": ".match {$foo :x} * * {{foo}}",
+      "expErrors": [
+        {
+          "type": "variant-key-mismatch"
+        }
+      ]
+    },
+    {
+      "src": ".match {$foo :x} {$bar :x} * {{foo}}",
+      "expErrors": [
+        {
+          "type": "variant-key-mismatch"
+        }
+      ]
+    },
+    {
+      "src": ".match {:foo} 1 {{_}}",
+      "expErrors": [
+        {
+          "type": "missing-fallback-variant"
+        }
+      ]
+    },
+    {
+      "src": ".match {:foo} other {{_}}",
+      "expErrors": [
+        {
+          "type": "missing-fallback-variant"
+        }
+      ]
+    },
+    {
+      "src": ".match {:foo} {:bar} * 1 {{_}} 1 * {{_}}",
+      "expErrors": [
+        {
+          "type": "missing-fallback-variant"
+        }
+      ]
+    },
+    {
+      "src": ".match {$foo} one {{one}} * {{other}}",
+      "expErrors": [
+        {
+          "type": "missing-selector-annotation"
+        }
+      ]
+    },
+    {
+      "src": ".input {$foo} .match {$foo} one {{one}} * {{other}}",
+      "expErrors": [
+        {
+          "type": "missing-selector-annotation"
+        }
+      ]
+    },
+    {
+      "src": ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}",
+      "expErrors": [
+        {
+          "type": "missing-selector-annotation"
+        }
+      ]
+    },
+    {
+      "src": ".input {$foo} .input {$foo} {{_}}",
+      "expErrors": [
+        {
+          "type": "duplicate-declaration"
+        }
+      ]
+    },
+    {
+      "src": ".input {$foo} .local $foo = {42} {{_}}",
+      "expErrors": [
+        {
+          "type": "duplicate-declaration"
+        }
+      ]
+    },
+    {
+      "src": ".local $foo = {42} .input {$foo} {{_}}",
+      "expErrors": [
+        {
+          "type": "duplicate-declaration"
+        }
+      ]
+    },
+    {
+      "src": ".local $foo = {:unknown} .local $foo = {42} {{_}}",
+      "expErrors": [
+        {
+          "type": "duplicate-declaration"
+        }
+      ]
+    },
+    {
+      "src": ".local $foo = {$bar} .local $bar = {42} {{_}}",
+      "expErrors": [
+        {
+          "type": "duplicate-declaration"
+        }
+      ]
+    },
+    {
+      "src": ".local $foo = {$foo} {{_}}",
+      "expErrors": [
+        {
+          "type": "duplicate-declaration"
+        }
+      ]
+    },
+    {
+      "src": ".local $foo = {$bar} .local $bar = {$baz} {{_}}",
+      "expErrors": [
+        {
+          "type": "duplicate-declaration"
+        }
+      ]
+    },
+    {
+      "src": ".local $foo = {$bar :func} .local $bar = {$baz} {{_}}",
+      "expErrors": [
+        {
+          "type": "duplicate-declaration"
+        }
+      ]
+    },
+    {
+      "src": ".local $foo = {42 :func opt=$foo} {{_}}",
+      "expErrors": [
+        {
+          "type": "duplicate-declaration"
+        }
+      ]
+    },
+    {
+      "src": ".local $foo = {42 :func opt=$bar} .local $bar = {42} {{_}}",
+      "expErrors": [
+        {
+          "type": "duplicate-declaration"
+        }
+      ]
+    },
+    {
+      "src": "bad {:placeholder option=x option=x}",
+      "expErrors": [
+        {
+          "type": "duplicate-option-name"
+        }
+      ]
+    },
+    {
+      "src": "bad {:placeholder ns:option=x ns:option=y}",
+      "expErrors": [
+        {
+          "type": "duplicate-option-name"
+        }
+      ]
+    },
+    {
+      "src": ".match {$var :string} * {{The first default}} * {{The second default}}",
+      "expErrors": [
+        {
+          "type": "duplicate-variant"
+        }
+      ]
+    },
+    {
+      "src": ".match {$x :string} {$y :string} * foo {{The first foo variant}} bar * {{The bar variant}} * |foo| {{The second foo variant}} * * {{The default variant}}",
+      "expErrors": [
+        {
+          "type": "duplicate-variant"
+        }
+      ]
+    }
+  ]
 }
diff --git a/testdata/message2/spec/functions/date.json b/testdata/message2/spec/functions/date.json
new file mode 100644
index 000000000000..dd14e6785fb6
--- /dev/null
+++ b/testdata/message2/spec/functions/date.json
@@ -0,0 +1,46 @@
+{
+  "scenario": "Date function",
+  "description": "The built-in formatter for dates.",
+  "defaultTestProperties": {
+    "locale": "en-US",
+    "expErrors": []
+  },
+  "tests": [
+    {
+      "src": "{:date}",
+      "exp": "{:date}",
+      "expErrors": [
+        {
+          "type": "bad-operand"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": "{horse :date}",
+      "exp": "{|horse|}",
+      "expErrors": [
+        {
+          "type": "bad-operand"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": "{|2006-01-02| :date}"
+    },
+    {
+      "src": "{|2006-01-02T15:04:06| :date}"
+    },
+    {
+      "src": "{|2006-01-02| :date style=long}"
+    },
+    {
+      "src": ".local $d = {|2006-01-02| :date style=long} {{{$d :date}}}"
+    },
+    {
+      "src": ".local $t = {|2006-01-02T15:04:06| :time} {{{$t :date}}}",
+      "ignoreJava": "ICU4J doesn't support this kind of composition"
+    }
+  ]
+}
diff --git a/testdata/message2/spec/functions/datetime.json b/testdata/message2/spec/functions/datetime.json
new file mode 100644
index 000000000000..bdfea3096cda
--- /dev/null
+++ b/testdata/message2/spec/functions/datetime.json
@@ -0,0 +1,68 @@
+{
+  "scenario": "Datetime function",
+  "description": "The built-in formatter for datetimes.",
+  "defaultTestProperties": {
+    "locale": "en-US",
+    "expErrors": []
+  },
+  "tests": [
+    {
+      "src": "{:datetime}",
+      "exp": "{:datetime}",
+      "expErrors": [
+        {
+          "type": "bad-operand"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": "{$x :datetime}",
+      "exp": "{$x}",
+      "params": [
+        {
+          "name": "x",
+          "value": true
+        }
+      ],
+      "expErrors": [
+        {
+          "type": "bad-operand"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": "{horse :datetime}",
+      "exp": "{|horse|}",
+      "expErrors": [
+        {
+          "type": "bad-operand"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": "{|2006-01-02T15:04:06| :datetime}"
+    },
+    {
+      "src": "{|2006-01-02T15:04:06| :datetime year=numeric month=|2-digit|}"
+    },
+    {
+      "src": "{|2006-01-02T15:04:06| :datetime dateStyle=long}"
+    },
+    {
+      "src": "{|2006-01-02T15:04:06| :datetime timeStyle=medium}"
+    },
+    {
+      "src": "{$dt :datetime}",
+      "params": [
+        {
+          "type": "datetime",
+          "name": "dt",
+          "value": "2006-01-02T15:04:06"
+        }
+      ]
+    }
+  ]
+}
diff --git a/testdata/message2/spec/functions/integer.json b/testdata/message2/spec/functions/integer.json
new file mode 100644
index 000000000000..c8e75077a221
--- /dev/null
+++ b/testdata/message2/spec/functions/integer.json
@@ -0,0 +1,32 @@
+{
+  "$schema": "https://raw.githubusercontent.com/unicode-org/message-format-wg/main/test/schemas/v0/tests.schema.json",
+  "scenario": "Integer function",
+  "description": "The built-in formatter for integers.",
+  "defaultTestProperties": {
+    "locale": "en-US"
+  },
+  "tests": [
+    {
+      "src": "hello {4.2 :integer}",
+      "exp": "hello 4"
+    },
+    {
+      "src": "hello {-4.20 :integer}",
+      "exp": "hello -4"
+    },
+    {
+      "src": "hello {0.42e+1 :integer}",
+      "exp": "hello 4"
+    },
+    {
+      "src": ".match {$foo :integer} one {{one}} * {{other}}",
+      "params": [
+        {
+          "name": "foo",
+          "value": 1.2
+        }
+      ],
+      "exp": "one"
+    }
+  ]
+}
diff --git a/testdata/message2/spec/functions/number.json b/testdata/message2/spec/functions/number.json
new file mode 100644
index 000000000000..1b81c705622b
--- /dev/null
+++ b/testdata/message2/spec/functions/number.json
@@ -0,0 +1,407 @@
+{
+  "scenario": "Number function",
+  "description": "The built-in formatter for numbers.",
+  "defaultTestProperties": {
+    "locale": "en-US"
+  },
+  "tests": [
+    {
+      "src": "hello {4.2 :number}",
+      "exp": "hello 4.2"
+    },
+    {
+      "src": "hello {-4.20 :number}",
+      "exp": "hello -4.2"
+    },
+    {
+      "src": "hello {0.42e+1 :number}",
+      "exp": "hello 4.2"
+    },
+    {
+      "src": "hello {foo :number}",
+      "exp": "hello {|foo|}",
+      "expErrors": [
+        {
+          "type": "bad-operand"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": "invalid number literal {|.1| :number}",
+      "exp": "invalid number literal {|.1|}",
+      "expErrors": [
+        {
+          "type": "bad-operand"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": "invalid number literal {|1.| :number}",
+      "exp": "invalid number literal {|1.|}",
+      "expErrors": [
+        {
+          "type": "bad-operand"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": "invalid number literal {|01| :number}",
+      "exp": "invalid number literal {|01|}",
+      "expErrors": [
+        {
+          "type": "bad-operand"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": "invalid number literal {|+1| :number}",
+      "exp": "invalid number literal {|+1|}",
+      "expErrors": [
+        {
+          "type": "bad-operand"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": "invalid number literal {|0x1| :number}",
+      "exp": "invalid number literal {|0x1|}",
+      "expErrors": [
+        {
+          "type": "bad-operand"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": "hello {:number}",
+      "exp": "hello {:number}",
+      "expErrors": [
+        {
+          "type": "bad-operand"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": "hello {4.2 :number minimumFractionDigits=2}",
+      "exp": "hello 4.20"
+    },
+    {
+      "src": "hello {|4.2| :number minimumFractionDigits=|2|}",
+      "exp": "hello 4.20"
+    },
+    {
+      "src": "hello {4.2 :number minimumFractionDigits=$foo}",
+      "params": [
+        {
+          "name": "foo",
+          "value": 2
+        }
+      ],
+      "exp": "hello 4.20"
+    },
+    {
+      "src": "hello {|4.2| :number minimumFractionDigits=$foo}",
+      "params": [
+        {
+          "name": "foo",
+          "value": "2"
+        }
+      ],
+      "exp": "hello 4.20"
+    },
+    {
+      "src": ".local $foo = {$bar :number} {{bar {$foo}}}",
+      "params": [
+        {
+          "name": "bar",
+          "value": 4.2
+        }
+      ],
+      "exp": "bar 4.2"
+    },
+    {
+      "src": ".local $foo = {$bar :number minimumFractionDigits=2} {{bar {$foo}}}",
+      "params": [
+        {
+          "name": "bar",
+          "value": 4.2
+        }
+      ],
+      "exp": "bar 4.20"
+    },
+    {
+      "src": ".local $foo = {$bar :number minimumFractionDigits=foo} {{bar {$foo}}}",
+      "params": [
+        {
+          "name": "bar",
+          "value": 4.2
+        }
+      ],
+      "exp": "bar {$bar}",
+      "expErrors": [
+        {
+          "type": "bad-option"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": ".local $foo = {$bar :number} {{bar {$foo}}}",
+      "params": [
+        {
+          "name": "bar",
+          "value": "foo"
+        }
+      ],
+      "exp": "bar {$bar}",
+      "expErrors": [
+        {
+          "type": "bad-operand"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": ".input {$foo :number} {{bar {$foo}}}",
+      "params": [
+        {
+          "name": "foo",
+          "value": 4.2
+        }
+      ],
+      "exp": "bar 4.2"
+    },
+    {
+      "src": ".input {$foo :number minimumFractionDigits=2} {{bar {$foo}}}",
+      "params": [
+        {
+          "name": "foo",
+          "value": 4.2
+        }
+      ],
+      "exp": "bar 4.20"
+    },
+    {
+      "src": ".input {$foo :number minimumFractionDigits=foo} {{bar {$foo}}}",
+      "params": [
+        {
+          "name": "foo",
+          "value": 4.2
+        }
+      ],
+      "exp": "bar {$foo}",
+      "expErrors": [
+        {
+          "type": "bad-option"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": ".input {$foo :number} {{bar {$foo}}}",
+      "params": [
+        {
+          "name": "foo",
+          "value": "foo"
+        }
+      ],
+      "exp": "bar {$foo}",
+      "expErrors": [
+        {
+          "type": "bad-operand"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": ".match {$foo :number} one {{one}} * {{other}}",
+      "params": [
+        {
+          "name": "foo",
+          "value": 1
+        }
+      ],
+      "exp": "one"
+    },
+    {
+      "src": ".match {$foo :number} 1 {{=1}} one {{one}} * {{other}}",
+      "params": [
+        {
+          "name": "foo",
+          "value": 1
+        }
+      ],
+      "exp": "=1"
+    },
+    {
+      "src": ".match {$foo :number} one {{one}} 1 {{=1}} * {{other}}",
+      "params": [
+        {
+          "name": "foo",
+          "value": 1
+        }
+      ],
+      "exp": "=1"
+    },
+    {
+      "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}",
+      "params": [
+        {
+          "name": "foo",
+          "value": 1
+        },
+        {
+          "name": "bar",
+          "value": 1
+        }
+      ],
+      "exp": "one one"
+    },
+    {
+      "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}",
+      "params": [
+        {
+          "name": "foo",
+          "value": 1
+        },
+        {
+          "name": "bar",
+          "value": 2
+        }
+      ],
+      "exp": "one other"
+    },
+    {
+      "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}",
+      "params": [
+        {
+          "name": "foo",
+          "value": 2
+        },
+        {
+          "name": "bar",
+          "value": 2
+        }
+      ],
+      "exp": "other"
+    },
+    {
+      "src": ".input {$foo :number} .match {$foo} one {{one}} * {{other}}",
+      "params": [
+        {
+          "name": "foo",
+          "value": 1
+        }
+      ],
+      "exp": "one"
+    },
+    {
+      "src": ".local $foo = {$bar :number} .match {$foo} one {{one}} * {{other}}",
+      "params": [
+        {
+          "name": "bar",
+          "value": 1
+        }
+      ],
+      "exp": "one"
+    },
+    {
+      "src": ".input {$foo :number} .local $bar = {$foo} .match {$bar} one {{one}} * {{other}}",
+      "params": [
+        {
+          "name": "foo",
+          "value": 1
+        }
+      ],
+      "exp": "one"
+    },
+    {
+      "src": ".input {$bar :number} .match {$bar} one {{one}} * {{other}}",
+      "params": [
+        {
+          "name": "bar",
+          "value": 2
+        }
+      ],
+      "exp": "other"
+    },
+    {
+      "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}",
+      "params": [
+        {
+          "name": "bar",
+          "value": 1
+        }
+      ],
+      "exp": "one"
+    },
+    {
+      "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}",
+      "params": [
+        {
+          "name": "bar",
+          "value": 2
+        }
+      ],
+      "exp": "other"
+    },
+    {
+      "src": ".input {$none} .match {$foo :number} one {{one}} * {{{$none}}}",
+      "params": [
+        {
+          "name": "foo",
+          "value": 1
+        }
+      ],
+      "exp": "one"
+    },
+    {
+      "src": ".local $bar = {$none} .match {$foo :number} one {{one}} * {{{$bar}}}",
+      "params": [
+        {
+          "name": "foo",
+          "value": 1
+        }
+      ],
+      "exp": "one"
+    },
+    {
+      "src": ".local $bar = {$none} .match {$foo :number} one {{one}} * {{{$bar}}}",
+      "params": [
+        {
+          "name": "foo",
+          "value": 2
+        }
+      ],
+      "exp": "{$none}",
+      "expErrors": [
+        {
+          "type": "unresolved-variable"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": "{42 :number @foo @bar=13}",
+      "exp": "42",
+      "expParts": [
+        {
+          "type": "number",
+          "source": "|42|",
+          "parts": [
+            {
+              "type": "integer",
+              "value": "42"
+            }
+          ]
+        }
+      ]
+    }
+  ]
+}
diff --git a/testdata/message2/spec/functions/string.json b/testdata/message2/spec/functions/string.json
new file mode 100644
index 000000000000..5858079b99a6
--- /dev/null
+++ b/testdata/message2/spec/functions/string.json
@@ -0,0 +1,51 @@
+{
+  "scenario": "String function",
+  "description": "The built-in formatter for strings.",
+  "defaultTestProperties": {
+    "locale": "en-US"
+  },
+  "tests": [
+    {
+      "src": ".match {$foo :string} |1| {{one}} * {{other}}",
+      "params": [
+        {
+          "name": "foo",
+          "value": "1"
+        }
+      ],
+      "exp": "one"
+    },
+    {
+      "src": ".match {$foo :string} 1 {{one}} * {{other}}",
+      "params": [
+        {
+          "name": "foo",
+          "value": 1
+        }
+      ],
+      "exp": "one",
+      "ignoreJava": ":string doesn't stringify numbers?"
+    },
+    {
+      "src": ".match {$foo :string} 1 {{one}} * {{other}}",
+      "params": [
+        {
+          "name": "foo",
+          "value": null
+        }
+      ],
+      "exp": "other",
+      "ignoreCpp": "Can't handle null value for input variable"
+    },
+    {
+      "src": ".match {$foo :string} 1 {{one}} * {{other}}",
+      "exp": "other",
+      "expErrors": [
+        {
+          "type": "unresolved-variable"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    }
+  ]
+}
diff --git a/testdata/message2/spec/functions/time.json b/testdata/message2/spec/functions/time.json
new file mode 100644
index 000000000000..845934a5e16a
--- /dev/null
+++ b/testdata/message2/spec/functions/time.json
@@ -0,0 +1,43 @@
+{
+  "scenario": "Time function",
+  "description": "The built-in formatter for times.",
+  "defaultTestProperties": {
+    "locale": "en-US",
+    "expErrors": []
+  },
+  "tests": [
+    {
+      "src": "{:time}",
+      "exp": "{:time}",
+      "expErrors": [
+        {
+          "type": "bad-operand"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": "{horse :time}",
+      "exp": "{|horse|}",
+      "expErrors": [
+        {
+          "type": "bad-operand"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": "{|2006-01-02T15:04:06| :time}"
+    },
+    {
+      "src": "{|2006-01-02T15:04:06| :time style=medium}"
+    },
+    {
+      "src": ".local $t = {|2006-01-02T15:04:06| :time style=medium} {{{$t :time}}}"
+    },
+    {
+      "src": ".local $d = {|2006-01-02T15:04:06| :date} {{{$d :time}}}",
+      "ignoreJava": "ICU4J doesn't support this kind of composition"
+    }
+  ]
+}
diff --git a/testdata/message2/spec/syntax-errors.json b/testdata/message2/spec/syntax-errors.json
index fc4537131c83..34d9aa484572 100644
--- a/testdata/message2/spec/syntax-errors.json
+++ b/testdata/message2/spec/syntax-errors.json
@@ -1,56 +1,180 @@
-[
-  ".",
-  "{",
-  "}",
-  "{}",
-  "{{",
-  "{{}",
-  "{{}}}",
-  "{|foo| #markup}",
-  "{{missing end brace}",
-  "{{missing end braces",
-  "{{missing end {$braces",
-  "{{extra}} content",
-  "empty { } placeholder",
-  "missing space {42:func}",
-  "missing space {|foo|:func}",
-  "missing space {|foo|@bar}",
-  "missing space {:func@bar}",
-  "missing space {:func @bar@baz}",
-  "missing space {:func @bar=42@baz}",
-  "missing space {+reserved@bar}",
-  "missing space {&private@bar}",
-  "bad {:} placeholder",
-  "bad {\\u0000placeholder}",
-  "no-equal {|42| :number minimumFractionDigits 2}",
-  "bad {:placeholder option=}",
-  "bad {:placeholder option value}",
-  "bad {:placeholder option:value}",
-  "bad {:placeholder option}",
-  "bad {:placeholder:}",
-  "bad {::placeholder}",
-  "bad {:placeholder::foo}",
-  "bad {:placeholder option:=x}",
-  "bad {:placeholder :option=x}",
-  "bad {:placeholder option::x=y}",
-  "bad {$placeholder option}",
-  "bad {:placeholder @attribute=}",
-  "bad {:placeholder @attribute=@foo}",
-  "no {placeholder end",
-  "no {$placeholder end",
-  "no {:placeholder end",
-  "no {|placeholder| end",
-  "no {|literal} end",
-  "no {|literal or placeholder end",
-  ".local bar = {|foo|} {{_}}",
-  ".local #bar = {|foo|} {{_}}",
-  ".local $bar {|foo|} {{_}}",
-  ".local $bar = |foo| {{_}}",
-  ".match {#foo} * {{foo}}",
-  ".match {} * {{foo}}",
-  ".match {|foo| :x} {|bar| :x} ** {{foo}}",
-  ".match * {{foo}}",
-  ".match {|x| :x} * foo",
-  ".match {|x| :x} * {{foo}} extra",
-  ".match |x| * {{foo}}"
-]
+{
+  "$schema": "https://raw.githubusercontent.com/unicode-org/message-format-wg/main/test/schemas/v0/tests.schema.json",
+  "scenario": "Syntax errors",
+  "description": "Strings that produce syntax errors when parsed.",
+  "defaultTestProperties": {
+    "locale": "en-US",
+    "expErrors": [
+      {
+        "type": "syntax-error"
+      }
+    ]
+  },
+  "tests": [
+    {
+      "src": "."
+    },
+    {
+      "src": "{"
+    },
+    {
+      "src": "}"
+    },
+    {
+      "src": "{}"
+    },
+    {
+      "src": "{{"
+    },
+    {
+      "src": "{{}"
+    },
+    {
+      "src": "{{}}}"
+    },
+    {
+      "src": "{|foo| #markup}"
+    },
+    {
+      "src": "{{missing end brace}"
+    },
+    {
+      "src": "{{missing end braces"
+    },
+    {
+      "src": "{{missing end {$braces"
+    },
+    {
+      "src": "{{extra}} content"
+    },
+    {
+      "src": "empty { } placeholder"
+    },
+    {
+      "src": "missing space {42:func}"
+    },
+    {
+      "src": "missing space {|foo|:func}"
+    },
+    {
+      "src": "missing space {|foo|@bar}"
+    },
+    {
+      "src": "missing space {:func@bar}"
+    },
+    {
+      "src": "missing space {:func @bar@baz}"
+    },
+    {
+      "src": "missing space {:func @bar=42@baz}"
+    },
+    {
+      "src": "missing space {+reserved@bar}"
+    },
+    {
+      "src": "missing space {&private@bar}"
+    },
+    {
+      "src": "bad {:} placeholder"
+    },
+    {
+      "src": "bad {\\u0000placeholder}"
+    },
+    {
+      "src": "no-equal {|42| :number minimumFractionDigits 2}"
+    },
+    {
+      "src": "bad {:placeholder option=}"
+    },
+    {
+      "src": "bad {:placeholder option value}"
+    },
+    {
+      "src": "bad {:placeholder option:value}"
+    },
+    {
+      "src": "bad {:placeholder option}"
+    },
+    {
+      "src": "bad {:placeholder:}"
+    },
+    {
+      "src": "bad {::placeholder}"
+    },
+    {
+      "src": "bad {:placeholder::foo}"
+    },
+    {
+      "src": "bad {:placeholder option:=x}"
+    },
+    {
+      "src": "bad {:placeholder :option=x}"
+    },
+    {
+      "src": "bad {:placeholder option::x=y}"
+    },
+    {
+      "src": "bad {$placeholder option}"
+    },
+    {
+      "src": "bad {:placeholder @attribute=}"
+    },
+    {
+      "src": "bad {:placeholder @attribute=@foo}"
+    },
+    {
+      "src": "{ @misplaced = attribute }"
+    },
+    {
+      "src": "no {placeholder end"
+    },
+    {
+      "src": "no {$placeholder end"
+    },
+    {
+      "src": "no {:placeholder end"
+    },
+    {
+      "src": "no {|placeholder| end"
+    },
+    {
+      "src": "no {|literal} end"
+    },
+    {
+      "src": "no {|literal or placeholder end"
+    },
+    {
+      "src": ".local bar = {|foo|} {{_}}"
+    },
+    {
+      "src": ".local #bar = {|foo|} {{_}}"
+    },
+    {
+      "src": ".local $bar {|foo|} {{_}}"
+    },
+    {
+      "src": ".local $bar = |foo| {{_}}"
+    },
+    {
+      "src": ".match {#foo} * {{foo}}"
+    },
+    {
+      "src": ".match {} * {{foo}}"
+    },
+    {
+      "src": ".match {|foo| :x} {|bar| :x} ** {{foo}}"
+    },
+    {
+      "src": ".match * {{foo}}"
+    },
+    {
+      "src": ".match {|x| :x} * foo"
+    },
+    {
+      "src": ".match {|x| :x} * {{foo}} extra"
+    },
+    {
+      "src": ".match |x| * {{foo}}"
+    }
+  ]
+}
diff --git a/testdata/message2/spec/syntax.json b/testdata/message2/spec/syntax.json
new file mode 100644
index 000000000000..1b901eb639d7
--- /dev/null
+++ b/testdata/message2/spec/syntax.json
@@ -0,0 +1,970 @@
+{
+  "$schema": "https://raw.githubusercontent.com/unicode-org/message-format-wg/main/test/schemas/v0/tests.schema.json",
+  "scenario": "Syntax",
+  "description": "Test cases that do not depend on any registry definitions.",
+  "defaultTestProperties": {
+    "locale": "en-US"
+  },
+  "tests": [
+    {
+      "description": "message -> simple-message -> <empty>",
+      "src": "",
+      "exp": ""
+    },
+    {
+      "description": "message -> simple-message -> simple-start pattern -> simple-start-char",
+      "src": "a",
+      "exp": "a"
+    },
+    {
+      "description": "message -> simple-message -> simple-start pattern -> simple-start-char pattern -> ...",
+      "src": "hello",
+      "exp": "hello"
+    },
+    {
+      "description": "message -> simple-message -> simple-start pattern -> escaped-char",
+      "src": "\\\\",
+      "exp": "\\"
+    },
+    {
+      "description": "message -> simple-message -> simple-start pattern -> simple-start-char pattern -> ... -> simple-start-char *text-char placeholder",
+      "src": "hello {world}",
+      "exp": "hello world"
+    },
+    {
+      "description": "message -> simple-message -> simple-start pattern -> simple-start-char pattern -> ... -> simple-start-char *text-char placeholder",
+      "src": "hello {|world|}",
+      "exp": "hello world"
+    },
+    {
+      "description": "message -> simple-message -> s simple-start pattern -> s simple-start-char pattern -> ...",
+      "src": "\n hello\t",
+      "exp": "\n hello\t"
+    },
+    {
+      "src": "hello {$place}",
+      "params": [
+        {
+          "name": "place",
+          "value": "world"
+        }
+      ],
+      "exp": "hello world"
+    },
+    {
+      "src": "hello {$place-.}",
+      "params": [
+        {
+          "name": "place-.",
+          "value": "world"
+        }
+      ],
+      "exp": "hello world"
+    },
+    {
+      "src": "hello {$place}",
+      "expErrors": [
+        {
+          "type": "unresolved-variable"
+        }
+      ],
+      "exp": "hello {$place}",
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "message -> simple-message -> simple-start pattern -> placeholder -> expression -> literal-expression -> \"{\" literal \"}\"",
+      "src": "{a}",
+      "exp": "a"
+    },
+    {
+      "description": "... -> literal-expression -> \"{\" literal s annotation \"}\" -> \"{\" literal s function \"}\" -> \"{\" literal s \":\" identifier \"}\" -> \"{\" literal s \":\" name \"}\"",
+      "src": "{a :f}",
+      "exp": "{|a|}",
+      "expErrors": [{ "type": "unknown-function" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... -> \"{\" literal s \":\" namespace \":\" name \"}\"",
+      "src": "{a :u:f}",
+      "exp": "{|a|}",
+      "expErrors": [{ "type": "unknown-function" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "message -> simple-message -> simple-start pattern -> placeholder -> expression -> variable-expression -> \"{\" variable \"}\"",
+      "src": "{$x}",
+      "exp": "{$x}",
+      "expErrors": [{ "type": "unresolved-variable" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... -> variable-expression -> \"{\" variable s annotation \"}\" -> \"{\" variable s function \"}\" -> \"{\" variable s \":\" identifier \"}\" -> \"{\" variable s \":\" name \"}\"",
+      "src": "{$x :f}",
+      "exp": "{$x}",
+      "expErrors": [{ "type": "unresolved-variable" }, { "type": "unknown-function" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... -> \"{\" variable s \":\" namespace \":\" name \"}\"",
+      "src": "{$x :u:f}",
+      "exp": "{$x}",
+      "expErrors": [{ "type": "unresolved-variable" }, { "type": "unknown-function" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... -> annotation-expression -> function -> \"{\" \":\" namespace \":\" name \"}\"",
+      "src": "{:u:f}",
+      "exp": "{:u:f}",
+      "expErrors": [{ "type": "unknown-function" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... -> annotation-expression -> function -> \"{\" \":\" name \"}\"",
+      "src": "{:f}",
+      "exp": "{:f}",
+      "expErrors": [{ "type": "unknown-function" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "message -> complex-message -> complex-body -> quoted-pattern -> \"{{\" pattern \"}}\" -> \"{{\"\"}}\"",
+      "src": "{{}}",
+      "exp": ""
+    },
+    {
+      "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"#\" identifier \"}\"", 
+      "src": "{#tag}",
+      "exp": "",
+      "expParts": [
+        {
+          "type": "markup",
+          "kind": "open",
+          "name": "tag"
+        }
+      ]
+    },
+    {
+      "description": "message -> complex-message -> *(declaration [s]) complex-body -> declaration complex-body -> input-declaration complex-body -> input variable-expression complex-body",
+      "src": ".input{$x}{{}}",
+      "exp": ""
+    },
+    {
+      "description": "message -> complex-message -> s *(declaration [s]) complex-body s -> s declaration complex-body s -> s input-declaration complex-body s -> s input variable-expression complex-body s",
+      "src": "\t.input{$x}{{}}\n",
+      "exp": ""
+    },
+    {
+      "description": "message -> complex-message -> *(declaration [s]) complex-body -> declaration declaration complex-body -> input-declaration input-declaration complex-body -> input variable-expression input variable-expression complex-body",
+      "src": ".input{$x}.input{$y}{{}}",
+      "exp": ""
+    },
+    {
+      "description": "message -> complex-message -> *(declaration [s]) complex-body -> declaration s declaration complex-body -> input-declaration s input-declaration complex-body -> input variable-expression s input variable-expression complex-body",
+      "src": ".input{$x} .input{$y}{{}}",
+      "exp": ""
+    },
+    {
+      "description": "message -> complex-message -> s *(declaration [s]) complex-body s -> s complex-body s",
+      "src": "  {{}}  ",
+      "exp": ""
+    },
+    {
+      "description": "message -> complex-message -> *(declaration [s]) complex-body -> declaration declaration complex-body -> local-declaration input-declaration complex-body -> local s variable [s] \"=\" [s] expression input variable-expression complex-body",
+      "src": ".local $x ={a}.input{$y}{{}}",
+      "exp": ""
+    },
+    {
+      "description": "message -> complex-message -> *(declaration [s]) complex-body -> declaration complex-body -> reserved-statement  complex-body -> reserved-keyword expression -> \".\" name expression complex-body",
+      "src": ".n{a}{{}}",
+      "exp": "",
+      "expErrors": [ { "type": "unsupported-statement" } ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "message -> complex-message -> complex-body -> matcher -> match-statement variant -> match selector key quoted-pattern -> \".match\" expression literal quoted-pattern",
+      "src": ".match{a :f}a{{}}*{{}}",
+      "exp": "",
+      "expErrors": [ { "type": "unknown-function" } ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... input-declaration -> input s variable-expression ...",
+      "src": ".input {$x}{{}}",
+      "exp": ""
+    },
+    {
+      "description": "... local-declaration -> local s variable s \"=\" expression ...",
+      "src": ".local $x ={a}{{}}",
+      "exp": ""
+    },
+    {
+      "description": "... local-declaration -> local s variable \"=\" s expression ...",
+      "src": ".local $x= {a}{{}}",
+      "exp": ""
+    },
+    {
+      "description": "... local-declaration -> local s variable s \"=\" expression ...",
+      "src": ".local $x = {a}{{}}",
+      "exp": ""
+    },
+    {
+      "description": "... matcher -> match-statement [s] variant -> match 1*([s] selector) variant -> match selector selector variant -> match selector selector variant key s key quoted-pattern",
+      "src": ".match{a :f}{b :f}a b{{}}* *{{}}",
+      "exp": "",
+      "expErrors": [ { "type": "unknown-function" } ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... matcher -> match-statement [s] variant -> match 1*([s] selector) variant -> match selector variant variant ...",
+      "src": ".match{a :f}a{{}}b{{}}*{{}}",
+      "exp": "",
+      "expErrors": [ { "type": "unknown-function" } ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... variant -> key s quoted-pattern -> ...",
+      "src": ".match{a :f}a {{}}*{{}}",
+      "exp": "",
+      "expErrors": [ { "type": "unknown-function" } ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... variant -> key s key s quoted-pattern -> ...",
+      "src": ".match{a :f}{b :f}a b {{}}* *{{}}",
+      "exp": "",
+      "expErrors": [ { "type": "unknown-function" } ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... key -> \"*\" ...",
+      "src": ".match{a :f}*{{}}",
+      "exp": "",
+      "expErrors": [ { "type": "unknown-function" } ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "simple-message -> simple-start pattern -> placeholder -> expression -> literal-expression -> \"{\" s literal \"}\"",
+      "src": "{ a}",
+      "exp": "a"
+    },
+    {
+      "description": "... literal-expression -> \"{\" literal s attribute \"}\" -> \"{\" literal s \"@\" identifier \"}\"",
+      "src": "{a @c}",
+      "exp": "a"
+    },
+    {
+      "description": "... -> literal-expression -> \"{\" literal s \"}\"",
+      "src": "{a }",
+      "exp": "a"
+    },
+    {
+      "description": "simple-message -> simple-start pattern -> placeholder -> expression -> variable-expression -> \"{\" s variable \"}\"",
+      "src": "{ $x}",
+      "exp": "{$x}",
+      "expErrors": [{ "type": "unresolved-variable" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... variable-expression -> \"{\" variable s attribute \"}\" -> \"{\" variable s \"@\" identifier \"}\"",
+      "src": "{$x @c}",
+      "exp": "{$x}",
+      "expErrors": [{ "type": "unresolved-variable" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... -> variable-expression -> \"{\" variable s \"}\"",
+      "src": "{$x }",
+      "exp": "{$x}",
+      "expErrors": [{ "type": "unresolved-variable" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "simple-message -> simple-start pattern -> placeholder -> expression -> annotation-expression -> \"{\" s annotation \"}\"",
+      "src": "{ :f}",
+      "exp": "{:f}",
+      "expErrors": [{ "type": "unknown-function" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... annotation-expression -> \"{\" annotation s attribute \"}\" -> \"{\" annotation s \"@\" identifier \"}\"",
+      "src": "{:f @c}",
+      "exp": "{:f}",
+      "expErrors": [{ "type": "unknown-function" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... -> annotation-expression -> \"{\" annotation s \"}\"",
+      "src": "{:f }",
+      "exp": "{:f}",
+      "expErrors": [{ "type": "unknown-function" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... annotation -> private-use-annotation -> private-start",
+      "src": "{^}",
+      "exp": "{^}",
+      "expErrors": [{ "type": "unsupported-expression" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... annotation -> reserved-annotation -> reserved-annotation-start",
+      "src": "{!}",
+      "exp": "{!}",
+      "expErrors": [{ "type": "unsupported-expression" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" s \"#\" identifier \"}\"", 
+      "src": "{ #a}",
+      "exp": ""
+    },
+    {
+      "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"#\" identifier option \"}\" -> \"{\" \"#\" identifier identifier \"=\" literal \"}\"",
+      "src": "{#tag foo=bar}",
+      "exp": "",
+      "expParts": [
+        {
+          "type": "markup",
+          "kind": "open",
+          "name": "tag",
+          "options": {
+            "foo": "bar"
+          }
+        }
+      ]
+    },
+    {
+      "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"#\" identifier attribute \"}\" -> \"{\" \"#\" identifier identifier \"=\" literal \"}\"",
+      "src": "{#a @c}",
+      "exp": ""
+    },
+    {
+      "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"#\" identifier s \"}\" -> \"{\" \"#\" identifier identifier \"=\" literal \"}\"",
+      "src": "{#a }",
+      "exp": ""
+    },
+    {
+      "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"#\" identifier \"/\" \"}\" -> \"{\" \"#\" identifier identifier \"=\" literal \"}\"",
+      "src": "{#a/}",
+      "exp": ""
+    },
+    {
+      "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"/\" identifier \"}\"",
+      "src": "{/a}",
+      "exp": ""
+    },
+    {
+      "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" s \"/\" identifier \"}\"",
+      "src": "{ /a}",
+      "exp": ""
+    },
+    {
+      "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"/\" identifier option \"}\"",
+      "src": "{/tag foo=bar}",
+      "exp": "",
+      "expParts": [
+        {
+          "type": "markup",
+          "kind": "close",
+          "name": "tag",
+          "options": {
+            "foo": "bar"
+          }
+        }
+      ]
+    },
+    {
+      "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"/\" identifier s \"}\"",
+      "src": "{/a }",
+      "exp": ""
+    },
+    {
+      "description": "... annotation-expression -> function -> \":\" identifier option",
+      "src": "{:f k=v}",
+      "exp": "{:f}",
+      "expErrors": [{ "type": "unknown-function" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... option -> identifier s \"=\" literal",
+      "src": "{:f k =v}",
+      "exp": "{:f}",
+      "expErrors": [{ "type": "unknown-function" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... option -> identifier \"=\" s literal",
+      "src": "{:f k= v}",
+      "exp": "{:f}",
+      "expErrors": [{ "type": "unknown-function" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... option -> identifier s \"=\" s literal",
+      "src": "{:f k = v}",
+      "exp": "{:f}",
+      "expErrors": [{ "type": "unknown-function" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... attribute -> \"@\" identifier \"=\" literal ...",
+      "src": "{a @c=d}",
+      "exp": "a"
+    },
+    {
+      "description": "... attribute -> \"@\" identifier s \"=\" literal ...",
+      "src": "{a @c =d}",
+      "exp": "a"
+    },
+    {
+      "description": "... attribute -> \"@\" identifier \"=\" s literal ...",
+      "src": "{a @c= d}",
+      "exp": "a"
+    },
+    {
+      "description": "... attribute -> \"@\" identifier s \"=\" s literal ...",
+      "src": "{a @c = d}",
+      "exp": "a"
+    },
+    {
+      "description": "... attribute -> \"@\" identifier s \"=\" s variable ...",
+      "src": "{42 @foo=$bar}",
+      "exp": "42",
+      "expParts": [
+        {
+          "type": "string",
+          "source": "|42|",
+          "value": "42"
+        }
+      ]
+    },
+    {
+      "description": "... literal -> quoted-literal -> \"|\" \"|\" ...",
+      "src": "{||}",
+      "exp": ""
+    },
+    {
+      "description": "... quoted-literal -> \"|\" quoted-char \"|\"",
+      "src": "{|a|}",
+      "exp": "a"
+    },
+    {
+      "description": "... quoted-literal -> \"|\" escaped-char \"|\"",
+      "src": "{|\\\\|}",
+      "exp": "\\"
+    },
+    {
+      "description": "... quoted-literal -> \"|\" quoted-char escaped-char \"|\"",
+      "src": "{|a\\\\|}",
+      "exp": "a\\"
+    },
+    {
+      "description": "... unquoted-literal -> number-literal -> %x30",
+      "src": "{0}",
+      "exp": "0"
+    },
+    {
+      "description": "... unquoted-literal -> number-literal -> \"-\" %x30",
+      "src": "{-0}",
+      "exp": "-0"
+    },
+    {
+      "description": "... unquoted-literal -> number-literal -> (%x31-39 *DIGIT) -> %x31",
+      "src": "{1}",
+      "exp": "1"
+    },
+    {
+      "description": "... unquoted-literal -> number-literal -> (%x31-39 *DIGIT) -> %x31 DIGIT -> 11",
+      "src": "{11}",
+      "exp": "11"
+    },
+    {
+      "description": "... unquoted-literal -> number-literal -> %x30 \".\" 1*DIGIT -> 0 \".\" 1",
+      "src": "{0.1}",
+      "exp": "0.1"
+    },
+    {
+      "description": "... unquoted-literal -> number-literal -> %x30 \".\" 1*DIGIT -> %x30 \".\" DIGIT DIGIT -> 0 \".\" 1 2",
+      "src": "{0.12}",
+      "exp": "0.12"
+    },
+    {
+      "description": "... unquoted-literal -> number-literal -> %x30 %i\"e\" 1*DIGIT -> %x30 \"e\" DIGIT",
+      "src": "{0e1}",
+      "exp": "0e1"
+    },
+    {
+      "description": "... unquoted-literal -> number-literal -> %x30 %i\"e\" 1*DIGIT -> %x30 \"E\" DIGIT",
+      "src": "{0E1}",
+      "exp": "0E1"
+    },
+    {
+      "description": "... unquoted-literal -> number-literal -> %x30 %i\"e\" \"-\" 1*DIGIT ...",
+      "src": "{0E-1}",
+      "exp": "0E-1"
+    },
+    {
+      "description": "... unquoted-literal -> number-literal -> %x30 %i\"e\" \"+\" 1*DIGIT ...",
+      "src": "{0E-1}",
+      "exp": "0E-1"
+    },
+    {
+      "description": "... reserved-statement -> reserved-keyword s reserved-body 1*([s] expression) -> reserved-keyword s reserved-body expression -> \".\" name s reserved-body-part expression -> \".\" name s reserved-char expression ...",
+      "src": ".n .{a}{{}}",
+      "exp": "",
+      "expErrors": [ { "type": "unsupported-statement" } ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... reserved-statement -> reserved-keyword reserved-body 1*([s] expression) -> reserved-keyword s reserved-body s expression -> \".\" name s reserved-body-part expression -> \".\" name s reserved-char expression ...",
+      "src": ".n. {a}{{}}",
+      "exp": "",
+      "expErrors": [ { "type": "unsupported-statement" } ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... reserved-statement -> reserved-keyword reserved-body 1*([s] expression) -> reserved-keyword reserved-body expression expression -> \".\" name reserved-body-part expression expression -> \".\" name s reserved-char expression expression ...",
+      "src": ".n.{a}{b}{{}}",
+      "exp": "",
+      "expErrors": [ { "type": "unsupported-statement" } ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... reserved-annotation -> reserved-annotation-start reserved-body -> \"!\" reserved-body-part -> \"!\" reserved-char ...",
+      "src": "{!.}",
+      "exp": "{!}",
+      "expErrors": [{ "type": "unsupported-expression" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... reserved-annotation -> reserved-annotation-start s reserved-body -> \"!\" s reserved-body-part -> \"!\" s reserved-char ...",
+      "src": "{! .}",
+      "exp": "{!}",
+      "expErrors": [{ "type": "unsupported-expression" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... reserved-annotation-start ...",
+      "src": "{%}",
+      "exp": "{%}",
+      "expErrors": [{ "type": "unsupported-expression" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... reserved-annotation-start ...",
+      "src": "{*}",
+      "exp": "{*}",
+      "expErrors": [{ "type": "unsupported-expression" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... reserved-annotation-start ...",
+      "src": "{+}",
+      "exp": "{+}",
+      "expErrors": [{ "type": "unsupported-expression" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... reserved-annotation-start ...",
+      "src": "{<}",
+      "exp": "{<}",
+      "expErrors": [{ "type": "unsupported-expression" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... reserved-annotation-start ...",
+      "src": "{>}",
+      "exp": "{>}",
+      "expErrors": [{ "type": "unsupported-expression" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... reserved-annotation-start ...",
+      "src": "{?}",
+      "exp": "{?}",
+      "expErrors": [{ "type": "unsupported-expression" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... reserved-annotation-start ...",
+      "src": "{~}",
+      "exp": "{~}",
+      "expErrors": [{ "type": "unsupported-expression" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... private-use-annotation -> private-start reserved-body -> \"^\" reserved-body-part -> \"^\" reserved-char ...",
+      "src": "{^.}",
+      "exp": "{^}",
+      "expErrors": [{ "type": "unsupported-expression" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... private-use-annotation -> private-start s reserved-body -> \"^\" s reserved-body-part -> \"^\" s reserved-char ...",
+      "src": "{^ .}",
+      "exp": "{^}",
+      "expErrors": [{ "type": "unsupported-expression" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... private-start ...",
+      "src": "{&}",
+      "exp": "{&}",
+      "expErrors": [{ "type": "unsupported-expression" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... reserved-annotation -> reserved-annotation-start reserved-body -> \"!\" reserved-body-part reserved-body-part -> \"!\" reserved-char escaped-char ...",
+      "src": "{!.\\{}",
+      "exp": "{!}",
+      "expErrors": [{ "type": "unsupported-expression" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... reserved-annotation -> reserved-annotation-start reserved-body -> \"!\" reserved-body-part s reserved-body-part -> \"!\" reserved-char s escaped-char ...",
+      "src": "{!. \\{}",
+      "exp": "{!}",
+      "expErrors": [{ "type": "unsupported-expression" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "description": "... reserved-annotation -> reserved-annotation-start reserved-body -> \"!\" reserved-body-part -> \"!\" quoted-literal ...",
+      "src": "{!|a|}",
+      "exp": "{!}",
+      "expErrors": [{ "type": "unsupported-expression" }],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": "hello { world\t\n}",
+      "exp": "hello world"
+    },
+    {
+      "src": "hello {\u3000world\r}",
+      "exp": "hello world"
+    },
+    {
+      "src": "{$one} and {$two}",
+      "params": [
+        {
+          "name": "one",
+          "value": 1.3
+        },
+        {
+          "name": "two",
+          "value": 4.2
+        }
+      ],
+      "exp": "1.3 and 4.2"
+    },
+    {
+      "src": "{$one} et {$two}",
+      "locale": "fr",
+      "params": [
+        {
+          "name": "one",
+          "value": 1.3
+        },
+        {
+          "name": "two",
+          "value": 4.2
+        }
+      ],
+      "exp": "1,3 et 4,2"
+    },
+    {
+      "src": ".local $foo = {bar} {{bar {$foo}}}",
+      "exp": "bar bar"
+    },
+    {
+      "src": ".local $foo = {|bar|} {{bar {$foo}}}",
+      "exp": "bar bar"
+    },
+    {
+      "src": ".local $foo = {|bar|} {{bar {$foo}}}",
+      "params": [
+        {
+          "name": "foo",
+          "value": "foo"
+        }
+      ],
+      "exp": "bar bar"
+    },
+    {
+      "src": ".local $foo = {$bar} {{bar {$foo}}}",
+      "params": [
+        {
+          "name": "bar",
+          "value": "foo"
+        }
+      ],
+      "exp": "bar foo"
+    },
+    {
+      "src": ".local $foo = {$baz} .local $bar = {$foo} {{bar {$bar}}}",
+      "params": [
+        {
+          "name": "baz",
+          "value": "foo"
+        }
+      ],
+      "exp": "bar foo"
+    },
+    {
+      "src": ".input {$foo} {{bar {$foo}}}",
+      "params": [
+        {
+          "name": "foo",
+          "value": "foo"
+        }
+      ],
+      "exp": "bar foo"
+    },
+    {
+      "src": ".input {$foo} .local $bar = {$foo} {{bar {$bar}}}",
+      "params": [
+        {
+          "name": "foo",
+          "value": "foo"
+        }
+      ],
+      "exp": "bar foo"
+    },
+    {
+      "src": ".local $foo = {$baz} .local $bar = {$foo} {{bar {$bar}}}",
+      "params": [
+        {
+          "name": "baz",
+          "value": "foo"
+        }
+      ],
+      "exp": "bar foo"
+    },
+    {
+      "src": ".local $x = {42} .local $y = {$x} {{{$x} {$y}}}",
+      "exp": "42 42"
+    },
+    {
+      "src": "{#tag}content",
+      "exp": "content",
+      "expParts": [
+        {
+          "type": "markup",
+          "kind": "open",
+          "name": "tag"
+        },
+        {
+          "type": "literal",
+          "value": "content"
+        }
+      ]
+    },
+    {
+      "src": "{#ns:tag}content{/ns:tag}",
+      "exp": "content",
+      "expParts": [
+        {
+          "type": "markup",
+          "kind": "open",
+          "name": "ns:tag"
+        },
+        {
+          "type": "literal",
+          "value": "content"
+        },
+        {
+          "type": "markup",
+          "kind": "close",
+          "name": "ns:tag"
+        }
+      ]
+    },
+    {
+      "src": "{/tag}content",
+      "exp": "content",
+      "expParts": [
+        {
+          "type": "markup",
+          "kind": "close",
+          "name": "tag"
+        },
+        {
+          "type": "literal",
+          "value": "content"
+        }
+      ]
+    },
+    {
+      "src": "{#tag foo=bar/}",
+      "exp": "",
+      "expParts": [
+        {
+          "type": "markup",
+          "kind": "standalone",
+          "name": "tag",
+          "options": {
+            "foo": "bar"
+          }
+        }
+      ]
+    },
+    {
+      "src": "{#tag a:foo=|foo| b:bar=$bar}",
+      "params": [
+        {
+          "name": "bar",
+          "value": "b a r"
+        }
+      ],
+      "exp": "",
+      "expParts": [
+        {
+          "type": "markup",
+          "kind": "open",
+          "name": "tag",
+          "options": {
+            "a:foo": "foo",
+            "b:bar": "b a r"
+          }
+        }
+      ]
+    },
+    {
+      "src": "{42 @foo @bar=13}",
+      "exp": "42",
+      "expParts": [
+        {
+          "type": "string",
+          "source": "|42|",
+          "value": "42"
+        }
+      ]
+    },
+    {
+      "src": "foo {+reserved}",
+      "exp": "foo {+}",
+      "expParts": [
+        {
+          "type": "literal",
+          "value": "foo "
+        },
+        {
+          "type": "fallback",
+          "source": "+"
+        }
+      ],
+      "expErrors": [
+        {
+          "type": "unsupported-expression"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": "foo {&private}",
+      "exp": "foo {&}",
+      "expParts": [
+        {
+          "type": "literal",
+          "value": "foo "
+        },
+        {
+          "type": "fallback",
+          "source": "&"
+        }
+      ],
+      "expErrors": [
+        {
+          "type": "unsupported-expression"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": "foo {?reserved @a @b=$c}",
+      "exp": "foo {?}",
+      "expParts": [
+        {
+          "type": "literal",
+          "value": "foo "
+        },
+        {
+          "type": "fallback",
+          "source": "?"
+        }
+      ],
+      "expErrors": [
+        {
+          "type": "unsupported-expression"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": ".foo {42} {{bar}}",
+      "exp": "bar",
+      "expParts": [
+        {
+          "type": "literal",
+          "value": "bar"
+        }
+      ],
+      "expErrors": [
+        {
+          "type": "unsupported-statement"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": ".foo{42}{{bar}}",
+      "exp": "bar",
+      "expParts": [
+        {
+          "type": "literal",
+          "value": "bar"
+        }
+      ],
+      "expErrors": [
+        {
+          "type": "unsupported-statement"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": ".foo |}lit{| {42}{{bar}}",
+      "exp": "bar",
+      "expParts": [
+        {
+          "type": "literal",
+          "value": "bar"
+        }
+      ],
+      "expErrors": [
+        {
+          "type": "unsupported-statement"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": ".l $y = {|bar|} {{}}",
+      "exp": "",
+      "expParts": [
+        {
+          "type": "literal",
+          "value": "bar"
+        }
+      ],
+      "expErrors": [
+        {
+          "type": "unsupported-statement"
+        }
+      ],
+      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
+    },
+    {
+      "src": "{{trailing whitespace}} \n",
+      "exp": "trailing whitespace"
+    }
+  ]
+}
diff --git a/testdata/message2/spec/test-core.json b/testdata/message2/spec/test-core.json
deleted file mode 100644
index 2ee0786e62de..000000000000
--- a/testdata/message2/spec/test-core.json
+++ /dev/null
@@ -1,219 +0,0 @@
-[
-  { "src": "hello", "exp": "hello" },
-  { "src": "hello {world}", "exp": "hello world" },
-  {
-    "src": "hello { world\t\n}",
-    "exp": "hello world",
-    "cleanSrc": "hello {world}"
-  },
-  {
-    "src": "hello {\u3000world\r}",
-    "exp": "hello world",
-    "cleanSrc": "hello {world}"
-  },
-  { "src": "hello {|world|}", "exp": "hello world" },
-  { "src": "hello {||}", "exp": "hello " },
-  {
-    "src": "hello {$place}",
-    "params": { "place": "world" },
-    "exp": "hello world"
-  },
-  {
-    "src": "hello {$place-.}",
-    "params": { "place-.": "world" },
-    "exp": "hello world"
-  },
-  {
-    "src": "hello {$place}",
-    "errors": [{ "type": "unresolved-var" }],
-    "exp": "hello {$place}",
-    "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-  },
-  {
-    "src": "{$one} and {$two}",
-    "params": { "one": 1.3, "two": 4.2 },
-    "exp": "1.3 and 4.2"
-  },
-  {
-    "src": "{$one} et {$two}",
-    "locale": "fr",
-    "params": { "one": 1.3, "two": 4.2 },
-    "exp": "1,3 et 4,2"
-  },
-  { "src": ".local $foo = {bar} {{bar {$foo}}}", "exp": "bar bar" },
-  { "src": ".local $foo = {|bar|} {{bar {$foo}}}", "exp": "bar bar" },
-  {
-    "src": ".local $foo = {|bar|} {{bar {$foo}}}",
-    "params": { "foo": "foo" },
-    "exp": "bar bar"
-  },
-  {
-    "src": ".local $foo = {$bar} {{bar {$foo}}}",
-    "params": { "bar": "foo" },
-    "exp": "bar foo"
-  },
-  {
-    "src": ".local $foo = {$baz} .local $bar = {$foo} {{bar {$bar}}}",
-    "params": { "baz": "foo" },
-    "exp": "bar foo"
-  },
-  {
-    "src": ".input {$foo} {{bar {$foo}}}",
-    "params": { "foo": "foo" },
-    "exp": "bar foo"
-  },
-  {
-    "src": ".input {$foo} .local $bar = {$foo} {{bar {$bar}}}",
-    "params": { "foo": "foo" },
-    "exp": "bar foo"
-  },
-  {
-    "src": ".local $foo = {$baz} .local $bar = {$foo} {{bar {$bar}}}",
-    "params": { "baz": "foo" },
-    "exp": "bar foo"
-  },
-  { "src": ".local $x = {42} .local $y = {$x} {{{$x} {$y}}}", "exp": "42 42" },
-  {
-    "src": "{#tag}",
-    "exp": "",
-    "parts": [{ "type": "markup", "kind": "open", "name": "tag" }]
-  },
-  {
-    "src": "{#tag}content",
-    "exp": "content",
-    "parts": [
-      { "type": "markup", "kind": "open", "name": "tag" },
-      { "type": "literal", "value": "content" }
-    ]
-  },
-  {
-    "src": "{#ns:tag}content{/ns:tag}",
-    "exp": "content",
-    "parts": [
-      { "type": "markup", "kind": "open", "name": "ns:tag" },
-      { "type": "literal", "value": "content" },
-      { "type": "markup", "kind": "close", "name": "ns:tag" }
-    ]
-  },
-  {
-    "src": "{/tag}content",
-    "exp": "content",
-    "parts": [
-      { "type": "markup", "kind": "close", "name": "tag" },
-      { "type": "literal", "value": "content" }
-    ]
-  },
-  {
-    "src": "{#tag foo=bar}",
-    "exp": "",
-    "parts": [
-      {
-        "type": "markup",
-        "kind": "open",
-        "name": "tag",
-        "options": { "foo": "bar" }
-      }
-    ]
-  },
-  {
-    "src": "{#tag foo=bar/}",
-    "cleanSrc": "{#tag foo=bar /}",
-    "exp": "",
-    "parts": [
-      {
-        "type": "markup",
-        "kind": "standalone",
-        "name": "tag",
-        "options": { "foo": "bar" }
-      }
-    ]
-  },
-  {
-    "src": "{#tag a:foo=|foo| b:bar=$bar}",
-    "params": { "bar": "b a r" },
-    "exp": "",
-    "parts": [
-      {
-        "type": "markup",
-        "kind": "open",
-        "name": "tag",
-        "options": { "a:foo": "foo", "b:bar": "b a r" }
-      }
-    ]
-  },
-  {
-    "src": "{/tag foo=bar}",
-    "exp": "",
-    "parts": [
-      {
-        "type": "markup",
-        "kind": "close",
-        "name": "tag",
-        "options": { "foo": "bar" }
-      }
-    ]
-  },
-  {
-    "src": "{42 @foo @bar=13}",
-    "exp": "42",
-    "parts": [{ "type": "string", "value": "42" }]
-  },
-  {
-    "src": "{42 @foo=$bar}",
-    "exp": "42",
-    "parts": [{ "type": "string", "value": "42" }]
-  },
-  {
-    "src": "foo {+reserved}",
-    "exp": "foo {+}",
-    "parts": [
-      { "type": "literal", "value": "foo " },
-      { "type": "fallback", "source": "+" }
-    ],
-    "errors": [{ "type": "unsupported-annotation" }],
-    "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-  },
-  {
-    "src": "foo {&private}",
-    "exp": "foo {&}",
-    "parts": [
-      { "type": "literal", "value": "foo " },
-      { "type": "fallback", "source": "&" }
-    ],
-    "errors": [{ "type": "unsupported-annotation" }],
-    "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-  },
-  {
-    "src": "foo {?reserved @a @b=$c}",
-    "exp": "foo {?}",
-    "parts": [
-      { "type": "literal", "value": "foo " },
-      { "type": "fallback", "source": "?" }
-    ],
-    "errors": [{ "type": "unsupported-annotation" }],
-    "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-  },
-  {
-    "src": ".foo {42} {{bar}}",
-    "exp": "bar",
-    "parts": [{ "type": "literal", "value": "bar" }],
-    "errors": [{ "type": "unsupported-statement" }],
-    "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-  },
-  {
-    "src": ".foo{42}{{bar}}",
-    "cleanSrc": ".foo {42} {{bar}}",
-    "exp": "bar",
-    "parts": [{ "type": "literal", "value": "bar" }],
-    "errors": [{ "type": "unsupported-statement" }],
-    "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-  },
-  {
-    "src": ".foo |}lit{| {42}{{bar}}",
-    "cleanSrc": ".foo |}lit{| {42} {{bar}}",
-    "exp": "bar",
-    "parts": [{ "type": "literal", "value": "bar" }],
-    "errors": [{ "type": "unsupported-statement" }],
-    "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-  }
-]
diff --git a/testdata/message2/spec/test-functions.json b/testdata/message2/spec/test-functions.json
deleted file mode 100644
index a8979106947e..000000000000
--- a/testdata/message2/spec/test-functions.json
+++ /dev/null
@@ -1,343 +0,0 @@
-{
-  "date": [
-    { "src": "{:date}",
-      "exp": "{:date}",
-      "errors": [{ "type": "bad-input" }],
-      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-    },
-    {
-      "src": "{horse :date}",
-      "exp": "{|horse|}",
-      "errors": [{ "type": "bad-input" }],
-      "errorsJs": [{ "name": "RangeError" }],
-      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-    },
-    { "src": "{|2006-01-02| :date}", "exp": "1/2/06" },
-    { "src": "{|2006-01-02T15:04:06| :date}", "exp": "1/2/06" },
-    { "src": "{|2006-01-02| :date style=long}", "exp": "January 2, 2006" },
-    {
-      "src": ".local $d = {|2006-01-02| :date style=long} {{{$d :date}}}",
-      "exp": "January 2, 2006"
-    },
-    {
-      "ignoreJava": "Can't chain :time and :date, they are different types",
-      "src": ".local $t = {|2006-01-02T15:04:06| :time} {{{$t :date}}}",
-      "exp": "1/2/06"
-    }
-  ],
-  "time": [
-    { "src": "{:time}", "exp": "{:time}", "errors": [{ "type": "bad-input" }],
-      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-    },
-    {
-      "src": "{horse :time}",
-      "exp": "{|horse|}",
-      "errors": [{ "type": "bad-input" }],
-      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-    },
-    { "src": "{|2006-01-02T15:04:06| :time}", "expJs": "3:04 PM", "exp": "3:04 PM" },
-    {
-      "src": "{|2006-01-02T15:04:06| :time style=medium}",
-      "expJs": "3:04:06 PM",
-      "exp": "3:04:06 PM"
-    },
-    {
-      "src": ".local $t = {|2006-01-02T15:04:06| :time style=medium} {{{$t :time}}}",
-      "expJs": "3:04:06 PM",
-      "exp": "3:04:06 PM"
-    },
-    {
-      "ignoreJava": "Can't chain :time and :date, they are different types",
-      "src": ".local $d = {|2006-01-02T15:04:06| :date} {{{$d :time}}}",
-      "exp": "3:04 PM"
-    }
-  ],
-  "datetime": [
-    {
-      "src": "{:datetime}",
-      "exp": "{:datetime}",
-      "errors": [{ "type": "bad-input" }],
-      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-    },
-    {
-      "src": "{$x :datetime}",
-      "exp": "{$x}",
-      "params": { "x": true },
-      "errors": [{ "type": "bad-input" }]
-    },
-    {
-      "src": "{horse :datetime}",
-      "exp": "{|horse|}",
-      "errors": [{ "type": "bad-input" }],
-      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-    },
-    { "src": "{|2006-01-02T15:04:06| :datetime}", "expJs": "1/2/06, 3:04 PM", "exp": "1/2/06, 3:04 PM" },
-    {
-      "src": "{|2006-01-02T15:04:06| :datetime year=numeric month=|2-digit|}",
-      "exp": "01/2006"
-    },
-    {
-      "src": "{|2006-01-02T15:04:06| :datetime dateStyle=long}",
-      "exp": "January 2, 2006"
-    },
-    {
-      "src": "{|2006-01-02T15:04:06| :datetime timeStyle=medium}",
-      "expJs": "3:04:06 PM",
-      "exp": "3:04:06 PM"
-    },
-    {
-      "src": "{$dt :datetime}",
-      "params": { "dt": "2006-01-02T15:04:06" },
-      "expJs": "1/2/06, 3:04 PM",
-      "exp": "1/2/06, 3:04 PM"
-    },
-    {
-      "ignoreJava": "Can't chain :time and :date, they are different types",
-      "ignoreCpp": "Same reason as Java",
-      "src": ".input {$dt :time style=medium} {{{$dt :datetime dateStyle=long}}}",
-      "params": { "dt": "2006-01-02T15:04:06" },
-      "exp": "January 2, 2006 at 3:04:06 PM"
-    }
-  ],
-  "integer": [
-    { "src": "hello {4.2 :integer}", "exp": "hello 4" },
-    { "src": "hello {-4.20 :integer}", "exp": "hello -4" },
-    { "src": "hello {0.42e+1 :integer}", "exp": "hello 4" },
-    {
-      "src": ".match {$foo :integer} one {{one}} * {{other}}",
-      "params": { "foo": 1.2 },
-      "exp": "one"
-    }
-  ],
-  "number": [
-    { "src": "hello {4.2 :number}", "exp": "hello 4.2" },
-    { "src": "hello {-4.20 :number}", "exp": "hello -4.2" },
-    { "src": "hello {0.42e+1 :number}", "exp": "hello 4.2" },
-    {
-      "src": "hello {foo :number}",
-      "exp": "hello {|foo|}",
-      "errors": [{ "type": "bad-input" }],
-      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-    },
-    {
-      "src": "hello {:number}",
-      "exp": "hello {:number}",
-      "errors": [{ "type": "bad-input" }]
-    },
-    {
-      "src": "hello {4.2 :number minimumFractionDigits=2}",
-      "exp": "hello 4.20"
-    },
-    {
-      "src": "hello {|4.2| :number minimumFractionDigits=|2|}",
-      "exp": "hello 4.20"
-    },
-    {
-      "src": "hello {4.2 :number minimumFractionDigits=$foo}",
-      "params": { "foo": 2 },
-      "exp": "hello 4.20"
-    },
-    {
-      "src": "hello {|4.2| :number minimumFractionDigits=$foo}",
-      "params": { "foo": "2" },
-      "exp": "hello 4.20"
-    },
-    {
-      "src": ".local $foo = {$bar :number} {{bar {$foo}}}",
-      "params": { "bar": 4.2 },
-      "exp": "bar 4.2"
-    },
-    {
-      "src": ".local $foo = {$bar :number minimumFractionDigits=2} {{bar {$foo}}}",
-      "params": { "bar": 4.2 },
-      "exp": "bar 4.20"
-    },
-    {
-      "src": ".local $foo = {$bar :number minimumFractionDigits=foo} {{bar {$foo}}}",
-      "params": { "bar": 4.2 },
-      "comment": "I think it is fine to ignore invalid options",
-      "expJs": "bar {$bar}",
-      "exp": "bar 4.2",
-      "errorsJs": [{ "type": "bad-option" }]
-    },
-    {
-      "src": ".local $foo = {$bar :number} {{bar {$foo}}}",
-      "params": { "bar": "foo" },
-      "exp": "bar {$bar}",
-      "errors": [{ "type": "bad-input" }],
-      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-    },
-    {
-      "src": ".input {$foo :number} {{bar {$foo}}}",
-      "params": { "foo": 4.2 },
-      "exp": "bar 4.2"
-    },
-    {
-      "src": ".input {$foo :number minimumFractionDigits=2} {{bar {$foo}}}",
-      "params": { "foo": 4.2 },
-      "exp": "bar 4.20"
-    },
-    {
-      "src": ".input {$foo :number minimumFractionDigits=foo} {{bar {$foo}}}",
-      "params": { "foo": 4.2 },
-      "comment": "I think it is fine to ignore invalid options",
-      "exp": "bar 4.2",
-      "expJs": "bar {$foo}",
-      "errorsJs": [{ "type": "bad-option" }]
-    },
-    {
-      "src": ".input {$foo :number} {{bar {$foo}}}",
-      "params": { "foo": "foo" },
-      "exp": "bar {$foo}",
-      "errors": [{ "type": "bad-input" }],
-      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-    },
-    {
-      "src": ".match {$foo :number} one {{one}} * {{other}}",
-      "params": { "foo": 1 },
-      "exp": "one"
-    },
-    {
-      "src": ".match {$foo :number} 1 {{=1}} one {{one}} * {{other}}",
-      "params": { "foo": 1 },
-      "exp": "=1"
-    },
-    {
-      "src": ".match {$foo :number} one {{one}} 1 {{=1}} * {{other}}",
-      "params": { "foo": 1 },
-      "exp": "=1"
-    },
-    {
-      "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}",
-      "params": { "foo": 1, "bar": 1 },
-      "exp": "one one"
-    },
-    {
-      "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}",
-      "params": { "foo": 1, "bar": 2 },
-      "exp": "one other"
-    },
-    {
-      "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}",
-      "params": { "foo": 2, "bar": 2 },
-      "exp": "other"
-    },
-    {
-      "src": ".input {$foo :number} .match {$foo} one {{one}} * {{other}}",
-      "params": { "foo": 1 },
-      "exp": "one"
-    },
-    {
-      "src": ".local $foo = {$bar :number} .match {$foo} one {{one}} * {{other}}",
-      "params": { "bar": 1 },
-      "exp": "one"
-    },
-    {
-      "src": ".input {$foo :number} .local $bar = {$foo} .match {$bar} one {{one}} * {{other}}",
-      "params": { "foo": 1 },
-      "exp": "one"
-    },
-    {
-      "src": ".input {$bar :number} .match {$bar} one {{one}} * {{other}}",
-      "params": { "bar": 2 },
-      "exp": "other"
-    },
-    {
-      "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}",
-      "params": { "bar": 1 },
-      "exp": "one"
-    },
-    {
-      "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}",
-      "params": { "bar": 2 },
-      "exp": "other"
-    },
-    {
-      "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}",
-      "params": { "bar": 1 },
-      "exp": "one"
-    },
-    {
-      "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}",
-      "params": { "bar": 2 },
-      "exp": "other"
-    },
-    {
-      "src": ".input {$none} .match {$foo :number} one {{one}} * {{{$none}}}",
-      "params": { "foo": 1 },
-      "exp": "one"
-    },
-    {
-      "src": ".local $bar = {$none} .match {$foo :number} one {{one}} * {{{$bar}}}",
-      "params": { "foo": 1 },
-      "exp": "one"
-    },
-    {
-      "src": ".local $bar = {$none} .match {$foo :number} one {{one}} * {{{$bar}}}",
-      "params": { "foo": 2 },
-      "exp": "{$none}",
-      "errors": [{ "type": "unresolved-var" }]
-    },
-    {
-      "src": "{42 :number @foo @bar=13}",
-      "exp": "42",
-      "parts": [
-        { "type": "number", "parts": [{ "type": "integer", "value": "42" }] }
-      ]
-    }
-  ],
-  "ordinal": [
-    {
-      "src": ".match {$foo :ordinal} one {{st}} two {{nd}} few {{rd}} * {{th}}",
-      "params": { "foo": 1 },
-      "exp": "th",
-      "errors": [{ "type": "missing-func" }, { "type": "not-selectable" }],
-      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-    },
-    {
-      "src": "hello {42 :ordinal}",
-      "exp": "hello {|42|}",
-      "errors": [{ "type": "missing-func" }],
-      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-    }
-  ],
-  "plural": [
-    {
-      "src": ".match {$foo :plural} one {{one}} * {{other}}",
-      "params": { "foo": 1 },
-      "exp": "other",
-      "errors": [{ "type": "missing-func" }, { "type": "not-selectable" }],
-      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-    },
-    {
-      "src": "hello {42 :plural}",
-      "exp": "hello {|42|}",
-      "errors": [{ "type": "missing-func" }],
-      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-    }
-  ],
-  "string": [
-    {
-      "src": ".match {$foo :string} |1| {{one}} * {{other}}",
-      "params": { "foo": "1" },
-      "exp": "one"
-    },
-    {
-      "src": ".match {$foo :string} 1 {{one}} * {{other}}",
-      "params": { "foo": 1 },
-      "exp": "one",
-      "ignoreJava": "See https://unicode-org.atlassian.net/browse/ICU-22754?focusedCommentId=175933"
-    },
-    {
-      "src": ".match {$foo :string} 1 {{one}} * {{other}}",
-      "params": { "foo": null },
-      "exp": "other"
-    },
-    {
-      "src": ".match {$foo :string} 1 {{one}} * {{other}}",
-      "exp": "other",
-      "errors": [{ "type": "unresolved-var" }],
-      "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"
-    }
-  ]
-}
diff --git a/testdata/message2/spec/unsupported-expressions.json b/testdata/message2/spec/unsupported-expressions.json
new file mode 100644
index 000000000000..f7d611509d23
--- /dev/null
+++ b/testdata/message2/spec/unsupported-expressions.json
@@ -0,0 +1,53 @@
+{
+  "scenario": "Reserved and private annotations",
+  "description": "Tests for unsupported expressions (reserved/private)",
+  "defaultTestProperties": {
+    "locale": "en-US",
+    "expErrors": [
+      {
+        "type": "unsupported-expression"
+      }
+    ]
+  },
+  "tests": [
+    { "src": "hello {|4.2| %number}" },
+    { "src": "hello {|4.2| %n|um|ber}"  },
+    { "src": "{+42}"  },
+    { "src": "hello {|4.2| &num|be|r}"  },
+    { "src": "hello {|4.2| ^num|be|r}"  },
+    { "src": "hello {|4.2| +num|be|r}"  },
+    { "src": "hello {|4.2| ?num|be||r|s}"  },
+    { "src": "hello {|foo| !number}"  },
+    { "src": "hello {|foo| *number}"  },
+    { "src": "hello {?number}"  },
+    { "src": "{<tag}"  },
+    { "src": ".local $bar = {$none ~plural} .match {foo :string}  * {{{$bar}}}"  },
+    { "src": "hello {|4.2| %num\\\\ber}"  },
+    { "src": "hello {|4.2| %num\\{be\\|r}"  },
+    { "src": "hello {|4.2| %num\\\\\\}ber}"  },
+    { "src": "hello {|4.2| !}"  },
+    { "src": "hello {|4.2| %}"  },
+    { "src": "hello {|4.2| *}"  },
+    { "src": "hello {|4.2| ^abc|123||5|\\\\}"  },
+    { "src": "hello {|4.2| ^ abc|123||5|\\\\}"  },
+    { "src": "hello {|4.2| ^ abc|123||5|\\\\ \\|def |3.14||2|}"  },
+    { "src": "hello {|4.2| ? }"  },
+    { "src": "hello {|4.2| %xyzz }"  },
+    { "src": "hello {|4.2| >xyzz   }"  },
+    { "src": "hello {$foo ~xyzz }"  },
+    { "src": "hello {$x   <xyzz   }"  },
+    { "src": "{>xyzz }"  },
+    { "src": "{  !xyzz   }"  },
+    { "src": "{~xyzz }"  },
+    { "src": "{ <xyzz   }"  },
+    { "src": "hello {|4.2| !xy z z }"  },
+    { "src": "hello {|4.2| *num \\\\ b er}"  },
+    { "src": "hello {|4.2| %num \\\\ b |3.14| r    }"  },
+    { "src": "hello {|4.2|    +num xx \\\\ b |3.14| r  }"  },
+    { "src": "hello {$foo    +num x \\\\ abcde |3.14| r  }"  },
+    { "src": "hello {$foo    >num x \\\\ abcde |aaa||3.14||42| r  }"  },
+    { "src": "hello {$foo    >num x \\\\ abcde |aaa||3.14| |42| r  }"  },
+    { "src" : ".input{ $n ~ }{{{$n}}}" }
+  ]
+}
+
diff --git a/testdata/message2/spec/unsupported-statements.json b/testdata/message2/spec/unsupported-statements.json
new file mode 100644
index 000000000000..d944aa0f786d
--- /dev/null
+++ b/testdata/message2/spec/unsupported-statements.json
@@ -0,0 +1,18 @@
+{
+  "scenario": "Reserved statements",
+  "description": "Tests for unsupported statements",
+  "defaultTestProperties": {
+    "locale": "en-US",
+    "expErrors": [
+      {
+        "type": "unsupported-statement"
+      }
+    ]
+  },
+  "tests": [
+    { "src" : ".i {1} {{}}" },
+    { "src" : ".l $y = {|bar|} {{}}" },
+    { "src" : ".l $x.y = {|bar|} {{}}" }
+  ]
+}
+
diff --git a/testdata/message2/syntax-errors-diagnostics-multiline.json b/testdata/message2/syntax-errors-diagnostics-multiline.json
index b7b87036f20a..facfadebab47 100644
--- a/testdata/message2/syntax-errors-diagnostics-multiline.json
+++ b/testdata/message2/syntax-errors-diagnostics-multiline.json
@@ -1,4 +1,15 @@
-[
+{
+  "scenario": "Syntax errors with character and line offsets",
+  "description": "Syntax errors; for ICU4C, the character and line offsets in the parse error are checked",
+  "defaultTestProperties": {
+    "locale": "en-US",
+    "expErrors": [
+      {
+        "type": "syntax-error"
+      }
+    ]
+  },
+  "tests": [
     {
         "src": "{{hello wo\nrld",
         "char": 3,
@@ -35,4 +46,5 @@
         "line": 3,
         "comment": "Offset for end-of-input should be 0 here because the line begins after the '\n', but there is no character after the '\n'"
     }
-]
+  ]
+}
diff --git a/testdata/message2/syntax-errors-diagnostics.json b/testdata/message2/syntax-errors-diagnostics.json
index 2b0188f6b557..5b68ae80f31b 100644
--- a/testdata/message2/syntax-errors-diagnostics.json
+++ b/testdata/message2/syntax-errors-diagnostics.json
@@ -1,402 +1,348 @@
-[
-    { "src": "}{|xyz|", "char": 0, "errors": [{"type": "parse-error"}] },
-    { "src": "}", "char": 0, "errors": [{"type": "parse-error"}] },
+{
+  "scenario": "Syntax errors with character offsets",
+  "description": "Syntax errors; for ICU4C, the character offset in the parse error is checked",
+  "defaultTestProperties": {
+    "locale": "en-US",
+    "expErrors": [
+      {
+        "type": "syntax-error"
+      }
+    ]
+  },
+  "tests": [
+    { "src": "}{|xyz|", "char": 0 },
+    { "src": "}", "char": 0 },
     {
         "src": "{{{%\\y{}}",
         "char": 5,
-        "comment": "Backslash followed by non-backslash followed by a '{' -- this should be an error immediately after the first backslash",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Backslash followed by non-backslash followed by a '{' -- this should be an error immediately after the first backslash"
     },
     {
         "src": "{%abc|\\z}}",
         "char": 7,
-        "comment": "Reserved chars followed by a '|' that doesn't begin a valid literal -- this should be an error at the first invalid char in the literal",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Reserved chars followed by a '|' that doesn't begin a valid literal -- this should be an error at the first invalid char in the literal"
     },
     {
         "src": "{%\\y{p}}",
         "char": 3,
-        "comment": "Same pattern, but with a valid reserved-char following the erroneous reserved-escape -- the offset should be the same as with the previous one",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Same pattern, but with a valid reserved-char following the erroneous reserved-escape -- the offset should be the same as with the previous one"
     },
     {
         "src": "{{{%ab|\\z|cd}}",
         "char": 8,
-        "comment": "Erroneous literal inside a reserved string -- the error should be at the first erroneous literal char",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Erroneous literal inside a reserved string -- the error should be at the first erroneous literal char"
     },
     {
         "src": "hello {|4.2| %num\\ber}}",
         "char": 18,
-        "comment": "Single backslash not allowed",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Single backslash not allowed"
     },
     {
         "src": "hello {|4.2| %num{be\\|r}}",
         "char": 17,
-        "comment": "Unescaped '{' not allowed",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Unescaped '{' not allowed"
     },
     {
         "src": "hello {|4.2| %num}be\\|r}}",
-        "char": 21,
-        "comment": "Unescaped '}' -- will be interpreted as the end of the reserved string, and the error will be reported at the index of '|', which is when the parser determines that the escaped '|' isn't a valid text-escape",
-        "errors": [{"type": "parse-error"}]
-    },
+        "char": 23,
+        "comment": "Unescaped '}' -- will be interpreted as the end of the reserved string, and the error will be reported at the index of the next '}'"    },
     {
         "src": "hello {|4.2| %num\\{be|r}}",
         "char": 25,
-        "comment": "Unescaped '|' -- will be interpreted as the beginning of a literal. Error at end of input",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Unescaped '|' -- will be interpreted as the beginning of a literal. Error at end of input"
     },
     {
         "src": ".match{|y|}|y|{{{|||}}}",
         "char": 19,
-        "comment": "No spaces are required here. The error should be in the pattern, not before",
-        "errors": [{"type": "parse-error"}]
+        "comment": "No spaces are required here. The error should be in the pattern, not before"
     },
     {
         "src": ".match {|y|}|foo|bar {{{a}}}",
         "char": 17,
-        "comment": "Missing spaces between keys",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Missing spaces between keys"
     },
     {
         "src": ".match {|y|} |quux| |foo|bar {{{a}}}",
         "char": 25,
-        "comment": "Missing spaces between keys",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Missing spaces between keys"
     },
     {
         "src": ".match {|y|}  |quux| |foo||bar| {{{a}}}",
         "char": 26,
-        "comment": "Missing spaces between keys",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Missing spaces between keys"
     },
     {
         "src": ".match {|y|}  |\\q| * %{! {z}",
         "char": 16,
-        "comment": "Error parsing the first key -- the error should be there, not in the also-erroneous third key",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Error parsing the first key -- the error should be there, not in the also-erroneous third key"
     },
     {
         "src":  ".match {|y|}  * %{! {z} |\\q|",
         "char": 16,
-        "comment": "Error parsing the second key -- the error should be there, not in the also-erroneous third key",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Error parsing the second key -- the error should be there, not in the also-erroneous third key"
     },
     {
         "src": ".match {|y|}  * |\\q| {\\z}",
         "char": 18,
-        "comment": "Error parsing the last key -- the error should be there, not in the erroneous pattern",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Error parsing the last key -- the error should be there, not in the erroneous pattern"
     },
     {
         "src": ".match {|y|} {\\|} {@}  * * * {{a}}",
         "char": 14,
-        "comment": "Non-expression as scrutinee in pattern -- error should be at the first non-expression, not the later non-expression",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Non-expression as scrutinee in pattern -- error should be at the first non-expression, not the later non-expression"
     },
     {
         "src": ".match {|y|}  $foo * {{a}} when * :bar {{b}}",
         "char": 14,
-        "comment": "Non-key in variant -- error should be there, not in the next erroneous variant",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Non-key in variant -- error should be there, not in the next erroneous variant"
     },
     {
         "src": "{{ foo {|bar|} \\q baz  ",
         "char": 16,
-        "comment": "Error should be within the first erroneous `text` or expression",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Error should be within the first erroneous `text` or expression"
     },
     {
         "src": "{{{:    }}}",
         "char": 4,
-        "comment": "':' has to be followed by a function name -- the error should be at the first whitespace character",
-        "errors": [{"type": "parse-error"}]
+        "comment": "':' has to be followed by a function name -- the error should be at the first whitespace character"
     },
     {
         "src": ".local $x = }|foo|}",
         "char": 12,
-        "comment": "Expression not starting with a '{'",
-        "errors": [{"type": "parse-error"}]
-    },
-    {
-        "src": ".local $x = {|foo|} .l $y = {|bar|} .local $z {|quux|}",
-        "char": 22,
-        "comment": "Error should be at the first declaration not starting with a `.local`",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Expression not starting with a '{'"
     },
     {
         "src": ".local $bar {|foo|} {{$bar}}",
         "char": 12,
-        "comment": "Missing '=' in `.local` declaration",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Missing '=' in `.local` declaration"
     },
     {
         "src": ".local bar = {|foo|} {{$bar}}",
         "char": 7,
-        "comment": "LHS of declaration doesn't start with a '$'",
-        "errors": [{"type": "parse-error"}]
+        "comment": "LHS of declaration doesn't start with a '$'"
     },
     {
         "src": ".local $bar = |foo| {{$bar}}",
         "char": 14,
-        "comment": "`.local` RHS isn't an expression",
-        "errors": [{"type": "parse-error"}]
+        "comment": "`.local` RHS isn't an expression"
     },
     {
         "src": "{{extra}}content",
         "char": 9,
-        "comment": "Trailing characters that are not whitespace",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Trailing characters that are not whitespace"
     },
     {
         "src": ".match {|x|}  * {{foo}}extra",
         "char": 28,
-        "comment": "Trailing characters that are not whitespace",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Trailing characters that are not whitespace"
     },
     {
         "src": ".match {$foo :string} {$bar :string}  one * {{one}}  * * {{other}}   ",
         "char": 66,
-        "comment": "Trailing whitespace at end of message should not be accepted either",
-        "errors": [{"type": "parse-error"}]
-    },
-    {
-        "src": "{{hi}} ",
-        "char": 6,
-        "comment": "Trailing whitespace at end of message should not be accepted either",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Trailing whitespace at end of message should not be accepted either"
     },
     {
         "src": "empty { }",
         "char": 8,
-        "comment": "Empty expression",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Empty expression"
     },
     {
         "src": ".match {}  * {{foo}}",
         "char": 8,
-        "comment": "Empty expression",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Empty expression"
     },
     {
         "src": "bad {:}",
         "char": 6,
-        "comment": "':' not preceding a function name",
-        "errors": [{"type": "parse-error"}]
+        "comment": "':' not preceding a function name"
     },
     {
         "src": "{{no-equal {|42| :number m }}}",
         "char":  27,
-        "comment": "Missing '=' after option name",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Missing '=' after option name"
     },
     {
         "src": "{{no-equal {|42| :number minimumFractionDigits 2}}}",
         "char":  47,
-        "comment": "Missing '=' after option name",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Missing '=' after option name"
     },
     {
         "src": "bad {:placeholder option value}",
         "char":  25,
-        "comment": "Missing '=' after option name",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Missing '=' after option name"
     },
     {
         "src": "hello {|4.2| :number min=2=3}",
         "char":  26,
-        "comment": "Extra '=' after option name",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Extra '=' after option name"
     },
     {
         "src": "hello {|4.2| :number min=2max=3}",
         "char":  26,
-        "comment": "Missing space between options",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Missing space between options"
     },
     {
         "src": "hello {|4.2| :number min=|a|max=3}",
         "char": 28,
-        "comment": "Missing whitespace between valid options",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Missing whitespace between valid options"
     },
     {
         "src": "hello {|4.2| :number min=|\\a|}",
         "char": 27,
-        "comment": "Ill-formed RHS of option -- the error should be within the RHS, not after parsing options",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Ill-formed RHS of option -- the error should be within the RHS, not after parsing options"
     },
     {
         "src": "no-equal {|42| :number   {}",
         "char": 25,
-        "comment": "Junk after annotation",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Junk after annotation"
     },
     {
         "src": "bad {:placeholder option=}",
         "char": 25,
-        "comment": "Missing RHS of option",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Missing RHS of option"
     },
     {
         "src": "bad {:placeholder option}",
         "char": 24,
-        "comment": "Missing RHS of option",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Missing RHS of option"
     },
     {
         "src": "bad {$placeholder option}",
         "char": 18,
-        "comment": "Annotation is not a function or reserved text",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Annotation is not a function or reserved text"
     },
     {
         "src": "no {$placeholder end",
         "char": 17,
-        "comment": "Annotation is not a function or reserved text",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Annotation is not a function or reserved text"
     },
     {
         "src": ".match  * {{foo}}",
         "char": 8,
-        "comment": "Missing expression in selectors",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Missing expression in selectors"
     },
     {
         "src": ".match |x|  * {{foo}}",
         "char": 7,
-        "comment": "Non-expression in selectors",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Non-expression in selectors"
     },
     {
         "src": ".match {|x|}  * foo",
         "char": 19,
-        "comment": "Missing RHS in variant",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Missing RHS in variant"
     },
     {
         "src": "{$:abc}",
         "char": 2,
-        "comment": "Variable names can't start with a : or -",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Variable names can't start with a : or -"
     },
     {
         "src": "{$-abc}",
         "char": 2,
-        "comment": "Variable names can't start with a : or -",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Variable names can't start with a : or -"
     },
     {
         "src": "{$bar+foo}",
         "char": 5,
-        "comment": "Missing space before annotation. Note that {{$bar:foo}} and {{$bar-foo}} are valid, because variable names can contain a ':' or a '-'",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Missing space before annotation. Note that {{$bar:foo}} and {{$bar-foo}} are valid, because variable names can contain a ':' or a '-'"
     },
     {
         "src": "{|3.14|:foo}",
         "char": 7,
-        "comment": "Missing space before annotation.",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Missing space before annotation."
     },
     {
         "src": "{|3.14|-foo}",
         "char": 7,
-        "comment": "Missing space before annotation.",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Missing space before annotation."
     },
     {
         "src": "{|3.14|+foo}",
         "char": 7,
-        "comment": "Missing space before annotation.",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Missing space before annotation."
     },
     {
         "src": ".local $foo = {$bar} .match {$foo}  :one {one} * {other}",
         "char": 36,
-        "comment": "Unquoted literals can't begin with a ':'",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Unquoted literals can't begin with a ':'"
     },
     {
         "src": ".local $foo = {$bar :fun option=:a} {{bar {$foo}}}",
         "char": 32,
-        "comment": "Unquoted literals can't begin with a ':'",
-        "errors": [{"type": "parse-error"}]
-    },
-    { "src": "{|foo| {#markup}}", "char": 7, "comment": "Markup in wrong place", "errors": [{"type": "parse-error"}] },
-    { "src": "{|foo| #markup}", "char": 7, "comment": "Markup in wrong place", "errors": [{"type": "parse-error"}] },
-    { "src": "{|foo| {#markup/}}", "char": 7, "comment": "Markup in wrong place", "errors": [{"type": "parse-error"}] },
-    { "src": "{|foo| {/markup}}", "char": 7, "comment": "Markup in wrong place", "errors": [{"type": "parse-error"}] },
-    { "src": ".input $x = {|1|} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression", "errors": [{"type": "parse-error"}] },
-    { "src": ".input $x = {:number} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression", "errors": [{"type": "parse-error"}] },
-    { "src": ".input {|1| :number} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression", "errors": [{"type": "parse-error"}] },
-    { "src": ".input {:number} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression", "errors": [{"type": "parse-error"}] },
-    { "src": ".input {|1|} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression", "errors": [{"type": "parse-error"}] },
-    { "src": ".", "char": 1, "errors": [{"type": "parse-error"}]},
-    { "src":  "{", "char": 1, "errors": [{"type": "parse-error"}]},
-    { "src":  "}", "char": 0, "errors": [{"type": "parse-error"}]},
-    { "src":  "{}", "char": 1, "errors": [{"type": "parse-error"}]},
-    { "src":  "{{", "char": 2, "errors": [{"type": "parse-error"}]},
-    { "src":  "{{}", "char": 3, "errors": [{"type": "parse-error"}]},
-    { "src":  "{{}}}", "char": 4, "errors": [{"type": "parse-error"}]},
-    { "src":  "{|foo| #markup}", "char": 7, "errors": [{"type": "parse-error"}]},
-    { "src":  "{{missing end brace}", "char": 20, "errors": [{"type": "parse-error"}]},
-    { "src":  "{{missing end braces", "char": 20, "errors": [{"type": "parse-error"}]},
-    { "src":  "{{missing end {$braces", "char": 22, "errors": [{"type": "parse-error"}]},
-    { "src":  "{{extra}} content", "char": 9, "errors": [{"type": "parse-error"}]},
-    { "src":  "empty { } placeholder", "char": 8, "errors": [{"type": "parse-error"}]},
-    { "src":  "missing space {42:func}", "char": 17, "errors": [{"type": "parse-error"}]},
-    { "src":  "missing space {|foo|:func}", "char": 20, "errors": [{"type": "parse-error"}]},
-    { "src":  "missing space {|foo|@bar}", "char": 20, "errors": [{"type": "parse-error"}]},
-    { "src":  "missing space {:func@bar}", "char": 20, "errors": [{"type": "parse-error"}]},
-    { "src":  "{:func @bar@baz}", "char": 11, "errors": [{"type": "parse-error"}]},
-    { "src":  "{:func @bar=42@baz}", "char": 14, "errors": [{"type": "parse-error"}]},
-    { "src":  "{+reserved@bar}", "char": 10, "errors": [{"type": "parse-error"}]},
-    { "src":  "{&private@bar}", "char": 9, "errors": [{"type": "parse-error"}]},
-    { "src":  "bad {:} placeholder", "char": 6, "errors": [{"type": "parse-error"}]},
-    { "src":  "bad {\\u0000placeholder}", "char": 5, "errors": [{"type": "parse-error"}]},
-    { "src":  "no-equal {|42| :number minimumFractionDigits 2}", "char": 45, "errors": [{"type": "parse-error"}]},
-    { "src":  "bad {:placeholder option=}", "char": 25, "errors": [{"type": "parse-error"}]},
-    { "src":  "bad {:placeholder option value}", "char": 25, "errors": [{"type": "parse-error"}]},
-    { "src":  "bad {:placeholder option:value}", "char": 30, "errors": [{"type": "parse-error"}]},
-    { "src":  "bad {:placeholder option}", "char": 24, "errors": [{"type": "parse-error"}]},
-    { "src":  "bad {:placeholder:}", "char": 18, "errors": [{"type": "parse-error"}]},
-    { "src":  "bad {::placeholder}", "char": 6, "errors": [{"type": "parse-error"}]},
-    { "src":  "bad {:placeholder::foo}", "char": 18, "errors": [{"type": "parse-error"}]},
-    { "src":  "bad {:placeholder option:=x}", "char": 25, "errors": [{"type": "parse-error"}]},
-    { "src":  "bad {:placeholder :option=x}", "char": 18, "errors": [{"type": "parse-error"}]},
-    { "src":  "bad {:placeholder option::x=y}", "char": 25, "errors": [{"type": "parse-error"}]},
-    { "src":  "bad {$placeholder option}", "char": 18, "errors": [{"type": "parse-error"}]},
-    { "src":  "bad {:placeholder @attribute=}", "char": 29, "errors": [{"type": "parse-error"}]},
-    { "src":  "bad {:placeholder @attribute=@foo}", "char": 29, "errors": [{"type": "parse-error"}]},
-    { "src":  "no {placeholder end", "char": 16, "errors": [{"type": "parse-error"}]},
-    { "src":  "no {$placeholder end", "char": 17, "errors": [{"type": "parse-error"}]},
-    { "src":  "no {:placeholder end", "char": 20, "errors": [{"type": "parse-error"}]},
-    { "src":  "no {|placeholder| end", "char": 18, "errors": [{"type": "parse-error"}]},
-    { "src":  "no {|literal} end", "char": 17, "errors": [{"type": "parse-error"}]},
-    { "src":  "no {|literal or placeholder end", "char": 31, "errors": [{"type": "parse-error"}]},
-    { "src":  ".local bar = {|foo|} {{_}}", "char": 7, "errors": [{"type": "parse-error"}]},
-    { "src":  ".local #bar = {|foo|} {{_}}", "char": 7, "errors": [{"type": "parse-error"}]},
-    { "src":  ".local $bar {|foo|} {{_}}", "char": 12, "errors": [{"type": "parse-error"}]},
-    { "src":  ".local $bar = |foo| {{_}}", "char": 14, "errors": [{"type": "parse-error"}]},
-    { "src":  ".match {#foo} * {{foo}}", "char": 8, "errors": [{"type": "parse-error"}]},
-    { "src":  ".match {} * {{foo}}", "char": 8, "errors": [{"type": "parse-error"}]},
-    { "src":  ".match {|foo| :x} {|bar| :x} ** {{foo}}", "char": 30, "errors": [{"type": "parse-error"}]},
-    { "src":  ".match * {{foo}}", "char": 7, "errors": [{"type": "parse-error"}]},
-    { "src":  ".match {|x| :x} * foo", "char": 21, "errors": [{"type": "parse-error"}]},
-    { "src":  ".match {|x| :x} * {{foo}} extra", "char": 31, "errors": [{"type": "parse-error"}]},
-    { "src":  ".match |x| * {{foo}}", "char": 7, "errors": [{"type": "parse-error"}]},
-    { "src": ".match {|foo| :string} o:ne {{one}}  * {{other}}", "char": 24, "comment" : "tests for ':' in unquoted literals (not allowed)" , "errors": [{"type": "parse-error"}]},
-    { "src": ".match {|foo| :string} one: {{one}}  * {{other}}", "char": 26, "comment" : "tests for ':' in unquoted literals (not allowed)" , "errors": [{"type": "parse-error"}]},
-    { "src": ".local $foo = {|42| :number option=a:b} {{bar {$foo}}}", "char": 36, "comment" : "tests for ':' in unquoted literals (not allowed)" , "errors": [{"type": "parse-error"}]},
-    { "src": ".local $foo = {|42| :number option=a:b:c} {{bar {$foo}}}", "char": 36, "comment" : "tests for ':' in unquoted literals (not allowed)" , "errors": [{"type": "parse-error"}]},
-    { "src": "{$bar:foo}", "char": 5, "comment" : "tests for ':' in unquoted literals (not allowed)", "errors": [{"type": "parse-error"}]},
+        "comment": "Unquoted literals can't begin with a ':'"
+    },
+    { "src": "{|foo| {#markup}}", "char": 7, "comment": "Markup in wrong place" },
+    { "src": "{|foo| #markup}", "char": 7, "comment": "Markup in wrong place" },
+    { "src": "{|foo| {#markup/}}", "char": 7, "comment": "Markup in wrong place" },
+    { "src": "{|foo| {/markup}}", "char": 7, "comment": "Markup in wrong place" },
+    { "src": ".input $x = {|1|} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression" },
+    { "src": ".input $x = {:number} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression" },
+    { "src": ".input {|1| :number} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression" },
+    { "src": ".input {:number} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression" },
+    { "src": ".input {|1|} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression" },
+    { "src": ".", "char": 1},
+    { "src":  "{", "char": 1},
+    { "src":  "}", "char": 0},
+    { "src":  "{}", "char": 1},
+    { "src":  "{{", "char": 2},
+    { "src":  "{{}", "char": 3},
+    { "src":  "{{}}}", "char": 4},
+    { "src":  "{|foo| #markup}", "char": 7},
+    { "src":  "{{missing end brace}", "char": 20},
+    { "src":  "{{missing end braces", "char": 20},
+    { "src":  "{{missing end {$braces", "char": 22},
+    { "src":  "{{extra}} content", "char": 10},
+    { "src":  "empty { } placeholder", "char": 8},
+    { "src":  "missing space {42:func}", "char": 17},
+    { "src":  "missing space {|foo|:func}", "char": 20},
+    { "src":  "missing space {|foo|@bar}", "char": 20},
+    { "src":  "missing space {:func@bar}", "char": 20},
+    { "src":  "{:func @bar@baz}", "char": 11},
+    { "src":  "{:func @bar=42@baz}", "char": 14},
+    { "src":  "{+reserved@bar}", "char": 10},
+    { "src":  "{&private@bar}", "char": 9},
+    { "src":  "bad {:} placeholder", "char": 6},
+    { "src":  "bad {\\u0000placeholder}", "char": 5},
+    { "src":  "no-equal {|42| :number minimumFractionDigits 2}", "char": 45},
+    { "src":  "bad {:placeholder option=}", "char": 25},
+    { "src":  "bad {:placeholder option value}", "char": 25},
+    { "src":  "bad {:placeholder option:value}", "char": 30},
+    { "src":  "bad {:placeholder option}", "char": 24},
+    { "src":  "bad {:placeholder:}", "char": 18},
+    { "src":  "bad {::placeholder}", "char": 6},
+    { "src":  "bad {:placeholder::foo}", "char": 18},
+    { "src":  "bad {:placeholder option:=x}", "char": 25},
+    { "src":  "bad {:placeholder :option=x}", "char": 18},
+    { "src":  "bad {:placeholder option::x=y}", "char": 25},
+    { "src":  "bad {$placeholder option}", "char": 18},
+    { "src":  "bad {:placeholder @attribute=}", "char": 29},
+    { "src":  "bad {:placeholder @attribute=@foo}", "char": 29},
+    { "src":  "no {placeholder end", "char": 16},
+    { "src":  "no {$placeholder end", "char": 17},
+    { "src":  "no {:placeholder end", "char": 20},
+    { "src":  "no {|placeholder| end", "char": 18},
+    { "src":  "no {|literal} end", "char": 17},
+    { "src":  "no {|literal or placeholder end", "char": 31},
+    { "src":  ".local bar = {|foo|} {{_}}", "char": 7},
+    { "src":  ".local #bar = {|foo|} {{_}}", "char": 7},
+    { "src":  ".local $bar {|foo|} {{_}}", "char": 12},
+    { "src":  ".local $bar = |foo| {{_}}", "char": 14},
+    { "src":  ".match {#foo} * {{foo}}", "char": 8},
+    { "src":  ".match {} * {{foo}}", "char": 8},
+    { "src":  ".match {|foo| :x} {|bar| :x} ** {{foo}}", "char": 30},
+    { "src":  ".match * {{foo}}", "char": 7},
+    { "src":  ".match {|x| :x} * foo", "char": 21},
+    { "src":  ".match {|x| :x} * {{foo}} extra", "char": 31},
+    { "src":  ".match |x| * {{foo}}", "char": 7},
+    { "src": ".match {|foo| :string} o:ne {{one}}  * {{other}}", "char": 24, "comment" : "tests for ':' in unquoted literals (not allowed)" },
+    { "src": ".match {|foo| :string} one: {{one}}  * {{other}}", "char": 26, "comment" : "tests for ':' in unquoted literals (not allowed)" },
+    { "src": ".local $foo = {|42| :number option=a:b} {{bar {$foo}}}", "char": 36, "comment" : "tests for ':' in unquoted literals (not allowed)" },
+    { "src": ".local $foo = {|42| :number option=a:b:c} {{bar {$foo}}}", "char": 36, "comment" : "tests for ':' in unquoted literals (not allowed)" },
+    { "src": "{$bar:foo}", "char": 5, "comment" : "tests for ':' in unquoted literals (not allowed)"},
     {
         "src":  ".match {1} {{_}}",
         "char": 12,
-        "comment": "Disambiguating a wrong .match from an unsupported statement",
-        "errors": [{"type": "parse-error"}]
+        "comment": "Disambiguating a wrong .match from an unsupported statement"
     }
-]
+  ]
+}
diff --git a/testdata/message2/syntax-errors-end-of-input.json b/testdata/message2/syntax-errors-end-of-input.json
index 69fcc04d0cf3..d97dcb24ac82 100644
--- a/testdata/message2/syntax-errors-end-of-input.json
+++ b/testdata/message2/syntax-errors-end-of-input.json
@@ -1,4 +1,15 @@
-[
+{
+  "scenario": "Syntax errors for unexpected end-of-input",
+  "description": "Syntax errors; for ICU4C, the character offset is expected to be the last character",
+  "defaultTestProperties": {
+    "locale": "en-US",
+    "expErrors": [
+      {
+        "type": "syntax-error"
+      }
+    ]
+  },
+  "tests": [
     { "src": ".local   ", "char": 9 },
     { "src": ".lo", "char": 3 },
     { "src": ".local $foo", "char": 11 },
@@ -13,4 +24,6 @@
     { "src": "{{missing end {$brace}", "char": 22 },
     { "src": "{{missing end {$brace}}", "char": 23 },
     { "src": "{{%xyz", "char": 6 }
-]
+  ]
+}
+
diff --git a/testdata/message2/tricky-declarations.json b/testdata/message2/tricky-declarations.json
index b340c46e90bf..3fded666e633 100644
--- a/testdata/message2/tricky-declarations.json
+++ b/testdata/message2/tricky-declarations.json
@@ -1,17 +1,19 @@
-[
+{
+  "scenario": "Declarations tests",
+  "description": "Tests for interesting combinations of .local and .input",
+  "defaultTestProperties": {
+    "locale": "en-US"
+  },
+  "tests": [
     { "src": ".input {$var :number minimumFractionDigits=$var2} .input {$var2 :number minimumFractionDigits=5} {{{$var} {$var2}}}",
       "exp": "1.000 3.00000",
-      "params": { "var": 1, "var2": 3 },
-      "errors": [{ "type": "duplicate-declaration" }]
+      "params": [{ "name": "var", "value": 1}, {"name": "var2", "value": 3 }],
+      "expErrors": [{ "type": "duplicate-declaration" }]
     },
     { "src": ".local $var = {$var2} .local $var2 = {1} {{{$var} {$var2}}}",
       "exp": "5 1",
-      "params": { "var2": 5 },
-      "errors": [{ "type": "duplicate-declaration" }]
-    },
-    {
-        "src": ".local $var2 = {1} {{{$var2}}}",
-        "exp": "1",
-        "params": { "var2": 5 }
+      "params": [{ "name": "var2", "value": 5 }],
+      "expErrors": [{ "type": "duplicate-declaration" }]
     }
-]
+  ]
+}
diff --git a/testdata/message2/valid-tests.json b/testdata/message2/valid-tests.json
index f037c49d4b48..5f1292e00896 100644
--- a/testdata/message2/valid-tests.json
+++ b/testdata/message2/valid-tests.json
@@ -1,5 +1,10 @@
-[
-    { "src": "hello {|4.2| :number}", "exp": "hello 4.2"},
+{
+  "scenario": "Valid tests",
+  "description": "Additional valid tests",
+  "defaultTestProperties": {
+    "locale": "en-US"
+  },
+  "tests": [
     { "src": "hello {|4.2| :number minimumFractionDigits=2}", "exp": "hello 4.20"},
     { "src": "hello {|4.2| :number minimumFractionDigits = 2}", "exp": "hello 4.20" },
     { "src": "hello {|4.2| :number minimumFractionDigits= 2}", "exp": "hello 4.20" },
@@ -63,63 +68,61 @@
     { "src": "{42e369}", "exp": "42e369",
       "ignoreJava": "See ICU-22810"},
     { "src": "hello {|3| :number   }", "exp": "hello 3" },
-    { "src": "{:foo}", "errors": [{ "type": "missing-func" }],
+    { "src": "{:foo}", "expErrors": [{ "type": "unknown-function" }],
       "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" },
-    { "src": "{:foo }", "errors": [{ "type": "missing-func" }],
+    { "src": "{:foo }", "expErrors": [{ "type": "unknown-function" }],
       "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" },
-    { "src": "{:foo   }", "errors": [{ "type": "missing-func" }],
+    { "src": "{:foo   }", "expErrors": [{ "type": "unknown-function" }],
       "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" },
-    { "src": "{:foo k=v}", "errors": [{ "type": "missing-func" }],
+    { "src": "{:foo k=v}", "expErrors": [{ "type": "unknown-function" }],
       "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" },
-    { "src": "{:foo k=v   }", "errors": [{ "type": "missing-func" }],
+    { "src": "{:foo k=v   }", "expErrors": [{ "type": "unknown-function" }],
       "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" },
-    { "src": "{:foo k1=v1   k2=v2}", "errors": [{ "type": "missing-func" }],
+    { "src": "{:foo k1=v1   k2=v2}", "expErrors": [{ "type": "unknown-function" }],
       "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" },
-    { "src": "{:foo k1=v1   k2=v2   }", "errors": [{ "type": "missing-func" }],
+    { "src": "{:foo k1=v1   k2=v2   }", "expErrors": [{ "type": "unknown-function" }],
       "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" },
     { "src": "{|3.14| }", "exp": "3.14" },
     { "src": "{|3.14|    }", "exp": "3.14" },
     { "src": "{|3.14|    :number}", "exp": "3.14" },
     { "src": "{|3.14|    :number   }", "exp": "3.14" },
-    { "src": "{$bar }", "errors": [{ "type": "unresolved-var" }],
+    { "src": "{$bar }", "expErrors": [{ "type": "unresolved-variable" }],
       "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" },
-    { "src": "{$bar    }", "errors": [{ "type": "unresolved-var" }],
+    { "src": "{$bar    }", "expErrors": [{ "type": "unresolved-variable" }],
       "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" },
-    { "src": "{$bar    :foo}", "errors": [{ "type": "unresolved-var" }],
+    { "src": "{$bar    :foo}", "expErrors": [{ "type": "unresolved-variable" }],
       "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" },
-    { "src": "{$bar    :foo   }", "errors": [{ "type": "unresolved-var" }],
+    { "src": "{$bar    :foo   }", "expErrors": [{ "type": "unresolved-variable" }],
       "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" },
-    { "src": "{$bar-foo}", "errors": [{ "type": "unresolved-var" }],
+    { "src": "{$bar-foo}", "expErrors": [{ "type": "unresolved-variable" }],
       "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" },
     { "src": ".local $foo = {|hello|} .local $foo = {$foo} {{{$foo}}}",
-      "errors": [{ "type": "duplicate-declaration" }] },
+      "expErrors": [{ "type": "duplicate-declaration" }] },
     { "src": "good {placeholder}", "exp": "good placeholder" },
     {
         "src": "a\\\\qbc",
         "exp": "a\\qbc",
         "comment": "pattern -> escaped-char -> backslash backslash"
     },
-
     {
-        "src": "{$one} and {$two}",
-        "params": { "one": "1.3", "two": "4.2" },
-        "exp": "1.3 and 4.2"
+      "comment": "message -> simple-message -> simple-start pattern -> escaped-char",
+      "src": "\\\\",
+      "exp": "\\"
     },
     {
-        "src": ".local $foo = {$baz} .local $bar = {$foo} {{bar {$bar}}}",
-        "params": { "baz": "foo" },
-        "exp": "bar foo"
+      "comment": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" s \"#\" identifier \"}\"", 
+      "src": "{ #a}",
+      "exp": ""
     },
     {
-        "src": ".local $foo = {$foo} {{bar {$foo}}}",
-        "params": { "foo": "foo" },
-        "exp": "bar foo",
-        "errors": [{ "type": "duplicate-declaration" }]
+      "comment": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" s \"/\" identifier \"}\"",
+      "src": "{ /a}",
+      "exp": ""
     },
     {
-        "src": ".local $foo = {$bar} .local $bar = {$baz} {{bar {$foo}}}",
-        "params": { "baz": "foo" },
-        "exp": "bar {$bar}",
-        "errors": [{ "type": "duplicate-declaration" }]
+      "comment": "message -> complex-message -> *(declaration [s]) complex-body -> declaration complex-body -> input-declaration complex-body -> input variable-expression complex-body",
+      "src": ".input{$x}{{}}",
+      "exp": ""
     }
-]
+  ]
+}