Migrate more `package:intl` to null safety: migrate most of `src/intl`, leaving `number_formatter` and `compact_number_formatter`.

PiperOrigin-RevId: 328329336
diff --git a/lib/intl.dart b/lib/intl.dart
index cb83005..880dbcc 100644
--- a/lib/intl.dart
+++ b/lib/intl.dart
@@ -202,43 +202,12 @@
   /// Note that null is interpreted as meaning the default locale, so if
   /// [newLocale] is null the default locale will be returned.
   static String verifiedLocale(
-      String newLocale, bool Function(String) localeExists,
-      {String Function(String) onFailure = _throwLocaleError}) {
-    // TODO(alanknight): Previously we kept a single verified locale on the Intl
-    // object, but with different verification for different uses, that's more
-    // difficult. As a result, we call this more often. Consider keeping
-    // verified locales for each purpose if it turns out to be a performance
-    // issue.
-    if (newLocale == null) {
-      return verifiedLocale(getCurrentLocale(), localeExists,
-          onFailure: onFailure);
-    }
-    if (localeExists(newLocale)) {
-      return newLocale;
-    }
-    for (var each in [
-      canonicalizedLocale(newLocale),
-      shortLocale(newLocale),
-      'fallback'
-    ]) {
-      if (localeExists(each)) {
-        return each;
-      }
-    }
-    return onFailure(newLocale);
-  }
-
-  /// The default action if a locale isn't found in verifiedLocale. Throw
-  /// an exception indicating the locale isn't correct.
-  static String _throwLocaleError(String localeName) {
-    throw ArgumentError('Invalid locale "$localeName"');
-  }
+          String newLocale, bool Function(String) localeExists,
+          {String Function(String) onFailure}) =>
+      helpers.verifiedLocale(newLocale, localeExists, onFailure);
 
   /// Return the short version of a locale name, e.g. 'en_US' => 'en'
-  static String shortLocale(String aLocale) {
-    if (aLocale.length < 2) return aLocale;
-    return aLocale.substring(0, 2).toLowerCase();
-  }
+  static String shortLocale(String aLocale) => helpers.shortLocale(aLocale);
 
   /// Return the name [aLocale] turned into xx_YY where it might possibly be
   /// in the wrong case or with a hyphen instead of an underscore. If
diff --git a/lib/src/date_format_internal.dart b/lib/src/date_format_internal.dart
index 08c9129..63df2d0 100644
--- a/lib/src/date_format_internal.dart
+++ b/lib/src/date_format_internal.dart
@@ -1,7 +1,6 @@
 // Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-// @dart=2.9
 
 /// This contains internal implementation details of the date formatting code
 /// which are exposed as public functions because they must be called by other
@@ -43,10 +42,10 @@
     UninitializedLocaleData('initializeDateFormatting(<locale>)', en_USSymbols);
 
 /// Cache the last used symbols to reduce repeated lookups.
-DateSymbols cachedDateSymbols;
+DateSymbols? cachedDateSymbols;
 
 /// Which locale was last used for symbol lookup.
-String lastDateSymbolLocale;
+String? lastDateSymbolLocale;
 
 /// This holds the patterns used for date/time formatting, indexed
 /// by locale. Note that it will be set differently during initialization,
diff --git a/lib/src/intl/bidi.dart b/lib/src/intl/bidi.dart
index ba028c4..07d18a2 100644
--- a/lib/src/intl/bidi.dart
+++ b/lib/src/intl/bidi.dart
@@ -1,7 +1,6 @@
 // Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-// @dart=2.9
 
 /// Bidi stands for Bi-directional text.  According to
 /// http://en.wikipedia.org/wiki/Bi-directional_text: Bi-directional text is
@@ -113,8 +112,8 @@
       r'($|-|_)',
       caseSensitive: false);
 
-  static String _lastLocaleCheckedForRtl;
-  static bool _lastRtlCheck;
+  static String? _lastLocaleCheckedForRtl;
+  static bool? _lastRtlCheck;
 
   /// Check if a BCP 47 / III [languageString] indicates an RTL language.
   ///
@@ -137,13 +136,13 @@
   /// http://www.iana.org/assignments/language-subtag-registry, as well as
   /// Sindhi (sd) and Uyghur (ug).  The presence of other subtags of the
   /// language code, e.g. regions like EG (Egypt), is ignored.
-  static bool isRtlLanguage([String languageString]) {
+  static bool isRtlLanguage([String? languageString]) {
     var language = languageString ?? global_state.getCurrentLocale();
     if (_lastLocaleCheckedForRtl != language) {
       _lastLocaleCheckedForRtl = language;
       _lastRtlCheck = _rtlLocaleRegex.hasMatch(language);
     }
-    return _lastRtlCheck;
+    return _lastRtlCheck!;
   }
 
   /// Enforce the [html] snippet in RTL directionality regardless of overall
@@ -185,7 +184,7 @@
     if (html.startsWith('<')) {
       var buffer = StringBuffer();
       var startIndex = 0;
-      Match match = RegExp('<\\w+').firstMatch(html);
+      var match = RegExp('<\\w+').firstMatch(html);
       if (match != null) {
         buffer
           ..write(html.substring(startIndex, match.end))
@@ -202,7 +201,7 @@
   /// problem of messy bracket display that frequently happens in RTL layout.
   /// If [isRtlContext] is true, then we explicitly want to wrap in a span of
   /// RTL directionality, regardless of the estimated directionality.
-  static String guardBracketInHtml(String str, [bool isRtlContext]) {
+  static String guardBracketInHtml(String str, [bool? isRtlContext]) {
     var useRtl = isRtlContext == null ? hasAnyRtl(str) : isRtlContext;
     var matchingBrackets =
         RegExp(r'(\(.*?\)+)|(\[.*?\]+)|(\{.*?\}+)|(&lt;.*?(&gt;)+)');
@@ -216,7 +215,7 @@
   /// as good as guardBracketInHtml. If [isRtlContext] is true, then we
   /// explicitly want to wrap in a span of RTL directionality, regardless of the
   /// estimated directionality.
-  static String guardBracketInText(String str, [bool isRtlContext]) {
+  static String guardBracketInText(String str, [bool? isRtlContext]) {
     var useRtl = isRtlContext == null ? hasAnyRtl(str) : isRtlContext;
     var mark = useRtl ? RLM : LRM;
     return _guardBracketHelper(
@@ -230,7 +229,7 @@
   /// would return 'firehydrant!'.  // TODO(efortuna): Get rid of this once this
   /// is implemented in Dart.  // See Issue 2979.
   static String _guardBracketHelper(String str, RegExp regexp,
-      [String before, String after]) {
+      [String? before, String? after]) {
     var buffer = StringBuffer();
     var startIndex = 0;
     for (var match in regexp.allMatches(str)) {
diff --git a/lib/src/intl/bidi_formatter.dart b/lib/src/intl/bidi_formatter.dart
index f84cd14..2e7331a 100644
--- a/lib/src/intl/bidi_formatter.dart
+++ b/lib/src/intl/bidi_formatter.dart
@@ -1,7 +1,6 @@
 // Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-// @dart=2.9
 
 import 'dart:convert';
 
@@ -71,13 +70,13 @@
   /// should always use a `span` tag, even when the input directionality is
   /// neutral or matches the context, so that the DOM structure of the output
   /// does not depend on the combination of directionalities.
-  BidiFormatter.LTR([alwaysSpan = false])
+  BidiFormatter.LTR([bool alwaysSpan = false])
       : contextDirection = TextDirection.LTR,
         _alwaysSpan = alwaysSpan;
-  BidiFormatter.RTL([alwaysSpan = false])
+  BidiFormatter.RTL([bool alwaysSpan = false])
       : contextDirection = TextDirection.RTL,
         _alwaysSpan = alwaysSpan;
-  BidiFormatter.UNKNOWN([alwaysSpan = false])
+  BidiFormatter.UNKNOWN([bool alwaysSpan = false])
       : contextDirection = TextDirection.UNKNOWN,
         _alwaysSpan = alwaysSpan;
 
@@ -100,7 +99,7 @@
   /// a trailing unicode BiDi mark matching the context directionality is
   /// appended (LRM or RLM). If [isHtml] is false, we HTML-escape the [text].
   String wrapWithSpan(String text,
-      {bool isHtml = false, bool resetDir = true, TextDirection direction}) {
+      {bool isHtml = false, bool resetDir = true, TextDirection? direction}) {
     direction ??= estimateDirection(text, isHtml: isHtml);
     String result;
     if (!isHtml) text = const HtmlEscape().convert(text);
@@ -135,7 +134,7 @@
   /// [isHtml]. [isHtml] is used to designate if the text contains HTML (escaped
   /// or unescaped).
   String wrapWithUnicode(String text,
-      {bool isHtml = false, bool resetDir = true, TextDirection direction}) {
+      {bool isHtml = false, bool resetDir = true, TextDirection? direction}) {
     direction ??= estimateDirection(text, isHtml: isHtml);
     var result = text;
     if (contextDirection.isDirectionChange(direction)) {
diff --git a/lib/src/intl/date_builder.dart b/lib/src/intl/date_builder.dart
index 7281fb3..f0eaddd 100644
--- a/lib/src/intl/date_builder.dart
+++ b/lib/src/intl/date_builder.dart
@@ -1,7 +1,6 @@
 // Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-// @dart=2.9
 
 import 'package:clock/clock.dart';
 
@@ -35,7 +34,7 @@
   ///
   /// Kept as a field to cache the result and to reduce the possibility of error
   /// after we've verified.
-  DateTime _date;
+  DateTime? _date;
 
   /// The number of times we've retried, for error reporting.
   int _retried = 0;
@@ -146,7 +145,7 @@
   }
 
   void _verify(int value, int min, int max, String desc, String originalInput,
-      [DateTime parsed]) {
+      [DateTime? parsed]) {
     if (value < min || value > max) {
       var parsedDescription = parsed == null ? '' : ' Date parsed as $parsed.';
       var errorDescription =
@@ -182,7 +181,7 @@
   DateTime asDate({int retries = 3}) {
     // TODO(alanknight): Validate the date, especially for things which
     // can crash the VM, e.g. large month values.
-    if (_date != null) return _date;
+    if (_date != null) return _date!;
 
     DateTime preliminaryResult;
     final hasCentury = !_hasAmbiguousCentury || year < 0 || year >= 100;
@@ -227,7 +226,7 @@
     } else {
       _date = _correctForErrors(preliminaryResult, retries);
     }
-    return _date;
+    return _date!;
   }
 
   /// Given a local DateTime, check for errors and try to compensate for them if
diff --git a/lib/src/intl/date_format.dart b/lib/src/intl/date_format.dart
index 03d868c..9251ee2 100644
--- a/lib/src/intl/date_format.dart
+++ b/lib/src/intl/date_format.dart
@@ -1,11 +1,10 @@
 // Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-// @dart=2.9
 
 import 'package:intl/date_symbols.dart';
-import 'package:intl/intl.dart';
 import 'package:intl/src/date_format_internal.dart';
+import 'package:intl/src/intl_helpers.dart' as helpers;
 
 import 'constants.dart' as constants;
 import 'date_builder.dart';
@@ -266,13 +265,13 @@
   ///
   /// If [locale] does not exist in our set of supported locales then an
   /// [ArgumentError] is thrown.
-  DateFormat([String newPattern, String locale]) {
+  DateFormat([String? newPattern, String? locale])
+      : _locale = helpers.verifiedLocale(locale, localeExists, null) {
     // TODO(alanknight): It should be possible to specify multiple skeletons eg
     // date, time, timezone all separately. Adding many or named parameters to
     // the constructor seems awkward, especially with the possibility of
     // confusion with the locale. A 'fluent' interface with cascading on an
     // instance might work better? A list of patterns is also possible.
-    _locale = Intl.verifiedLocale(locale, localeExists);
     addPattern(newPattern);
   }
 
@@ -362,8 +361,7 @@
   }
 
   DateTime _parseLoose(String inputString, bool utc) {
-    var dateFields =
-        DateBuilder(locale ?? Intl.defaultLocale, dateTimeConstructor);
+    var dateFields = DateBuilder(locale, dateTimeConstructor);
     if (utc) dateFields.utc = true;
     var stream = IntlStream(inputString);
     for (var field in _formatFields) {
@@ -391,8 +389,7 @@
   DateTime _parse(String inputString, {bool utc = false, bool strict = false}) {
     // TODO(alanknight): The Closure code refers to special parsing of numeric
     // values with no delimiters, which we currently don't do. Should we?
-    var dateFields =
-        DateBuilder(locale ?? Intl.defaultLocale, dateTimeConstructor);
+    var dateFields = DateBuilder(locale, dateTimeConstructor);
     if (utc) dateFields.utc = true;
     dateFields.dateOnly = dateOnly;
     var stream = IntlStream(inputString);
@@ -411,7 +408,7 @@
   ///
   /// For example, 'yyyy-MM-dd' would be true, but 'dd hh:mm' would be false.
   bool get dateOnly => _dateOnly ??= _checkDateOnly;
-  bool _dateOnly;
+  bool? _dateOnly;
   bool get _checkDateOnly => _formatFields.every((each) => each.forDate);
 
   /// Given user input, attempt to parse the [inputString] into the anticipated
@@ -641,20 +638,20 @@
   /// The full template string. This may have been specified directly, or
   /// it may have been derived from a skeleton and the locale information
   /// on how to interpret that skeleton.
-  String _pattern;
+  String? _pattern;
 
   /// We parse the format string into individual [_DateFormatField] objects
   /// that are used to do the actual formatting and parsing. Do not use
   /// this variable directly, use the getter [_formatFields].
-  List<_DateFormatField> _formatFieldsPrivate;
+  List<_DateFormatField>? _formatFieldsPrivate;
 
   /// Getter for [_formatFieldsPrivate] that lazily initializes it.
   List<_DateFormatField> get _formatFields {
     if (_formatFieldsPrivate == null) {
       if (_pattern == null) _useDefaultPattern();
-      _formatFieldsPrivate = parsePattern(_pattern);
+      _formatFieldsPrivate = parsePattern(_pattern!);
     }
-    return _formatFieldsPrivate;
+    return _formatFieldsPrivate!;
   }
 
   /// We are being asked to do formatting without having set any pattern.
@@ -694,7 +691,7 @@
   /// known skeletons.  If it's found there, then use the corresponding pattern
   /// for this locale.  If it's not, then treat [inputPattern] as an explicit
   /// pattern.
-  DateFormat addPattern(String inputPattern, [String separator = ' ']) {
+  DateFormat addPattern(String? inputPattern, [String separator = ' ']) {
     // TODO(alanknight): This is an expensive operation. Caching recently used
     // formats, or possibly introducing an entire 'locale' object that would
     // cache patterns for that locale could be a good optimization.
@@ -710,7 +707,7 @@
   }
 
   /// Return the pattern that we use to format dates.
-  String get pattern => _pattern;
+  String? get pattern => _pattern;
 
   /// Return the skeletons for our current locale.
   Map<dynamic, dynamic> get _availableSkeletons => dateTimePatterns[locale];
@@ -719,14 +716,15 @@
   ///
   /// This can be useful to find lists like the names of weekdays or months in a
   /// locale, but the structure of this data may change, and it's generally
-  /// better to go through the [format] and [parse] APIs. If the locale isn't
-  /// present, or is uninitialized, returns null.
+  /// better to go through the [format] and [parse] APIs.
+  ///
+  /// If the locale isn't present, or is uninitialized, throws.
   DateSymbols get dateSymbols {
     if (_locale != lastDateSymbolLocale) {
       lastDateSymbolLocale = _locale;
       cachedDateSymbols = dateTimeSymbols[_locale];
     }
-    return cachedDateSymbols;
+    return cachedDateSymbols!;
   }
 
   static final Map<String, bool> _useNativeDigitsByDefault = {};
@@ -753,14 +751,14 @@
     _useNativeDigitsByDefault[locale] = value;
   }
 
-  bool _useNativeDigits;
+  bool? _useNativeDigits;
 
   /// Should we use native digits for printing DateTime, or ASCII.
   ///
   /// The default for this can be set using [useNativeDigitsByDefaultFor].
   bool get useNativeDigits => _useNativeDigits == null
       ? _useNativeDigits = shouldUseNativeDigitsByDefaultFor(locale)
-      : _useNativeDigits;
+      : _useNativeDigits!;
 
   /// Should we use native digits for printing DateTime, or ASCII.
   set useNativeDigits(bool native) {
@@ -778,28 +776,28 @@
   /// locale.
   static final Map<String, RegExp> _digitMatchers = {};
 
-  RegExp _digitMatcher;
+  RegExp? _digitMatcher;
 
   /// A regular expression which matches against digits for this locale.
   RegExp get digitMatcher {
-    if (_digitMatcher != null) return _digitMatcher;
+    if (_digitMatcher != null) return _digitMatcher!;
     _digitMatcher = _digitMatchers.putIfAbsent(localeZero, _initDigitMatcher);
-    return _digitMatcher;
+    return _digitMatcher!;
   }
 
-  int _localeZeroCodeUnit;
+  int? _localeZeroCodeUnit;
 
   /// For performance, keep the code unit of the zero digit available.
   int get localeZeroCodeUnit => _localeZeroCodeUnit == null
       ? _localeZeroCodeUnit = localeZero.codeUnitAt(0)
-      : _localeZeroCodeUnit;
+      : _localeZeroCodeUnit!;
 
-  String _localeZero;
+  String? _localeZero;
 
   /// For performance, keep the zero digit available.
   String get localeZero => _localeZero == null
       ? _localeZero = useNativeDigits ? dateSymbols.ZERODIGIT ?? '0' : '0'
-      : _localeZero;
+      : _localeZero!;
 
   // Does this use non-ASCII digits, e.g. Eastern Arabic.
   bool get usesNativeDigits =>
@@ -812,7 +810,7 @@
   /// locale digits.
   String _localizeDigits(String numberString) {
     if (usesAsciiDigits) return numberString;
-    var newDigits = List<int>(numberString.length);
+    var newDigits = List<int>.filled(numberString.length, 0);
     var oldDigits = numberString.codeUnits;
     for (var i = 0; i < numberString.length; i++) {
       newDigits[i] =
@@ -848,7 +846,6 @@
 
   /// Parse the template pattern and return a list of field objects.
   List<_DateFormatField> parsePattern(String pattern) {
-    if (pattern == null) return null;
     return _parsePatternHelper(pattern).reversed.toList();
   }
 
@@ -866,12 +863,12 @@
   }
 
   /// Find elements in a string that are patterns for specific fields.
-  _DateFormatField _match(String pattern) {
+  _DateFormatField? _match(String pattern) {
     for (var i = 0; i < _matchers.length; i++) {
       var regex = _matchers[i];
       var match = regex.firstMatch(pattern);
       if (match != null) {
-        return _fieldConstructors[i](match.group(0), this);
+        return _fieldConstructors[i](match.group(0)!, this);
       }
     }
     return null;
diff --git a/lib/src/intl/date_format_field.dart b/lib/src/intl/date_format_field.dart
index 0bb0159..b6aa50e 100644
--- a/lib/src/intl/date_format_field.dart
+++ b/lib/src/intl/date_format_field.dart
@@ -1,7 +1,6 @@
 // Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-// @dart=2.9
 
 part of 'date_format.dart';
 
@@ -17,11 +16,10 @@
   DateFormat parent;
 
   /// Trimmed version of [pattern].
-  String _trimmedPattern;
+  final String _trimmedPattern;
 
-  _DateFormatField(this.pattern, this.parent) {
-    _trimmedPattern = pattern.trim();
-  }
+  _DateFormatField(this.pattern, this.parent)
+      : _trimmedPattern = pattern.trim();
 
   /// Does this field potentially represent part of a Date, i.e. is not
   /// time-specific.
@@ -84,7 +82,7 @@
   }
 
   /// Throw a format exception with an error message indicating the position.
-  void throwFormatException(IntlStream stream) {
+  Never throwFormatException(IntlStream stream) {
     throw FormatException('Trying to read $this from ${stream.contents} '
         'at position ${stream.index}');
   }
@@ -94,7 +92,8 @@
 /// change according to the date's data. As such, the implementation
 /// is extremely simple.
 class _DateFormatLiteralField extends _DateFormatField {
-  _DateFormatLiteralField(pattern, parent) : super(pattern, parent);
+  _DateFormatLiteralField(String pattern, DateFormat parent)
+      : super(pattern, parent);
 
   void parse(IntlStream input, DateBuilder dateFields) {
     parseLiteral(input);
@@ -107,14 +106,13 @@
 /// Represents a literal field with quoted characters in it. This is
 /// only slightly more complex than a _DateFormatLiteralField.
 class _DateFormatQuotedField extends _DateFormatField {
-  String _fullPattern;
+  final String _fullPattern;
 
   String fullPattern() => _fullPattern;
 
-  _DateFormatQuotedField(pattern, parent)
-      : super(_patchQuotes(pattern), parent) {
-    _fullPattern = pattern;
-  }
+  _DateFormatQuotedField(String pattern, DateFormat parent)
+      : _fullPattern = pattern,
+        super(_patchQuotes(pattern), parent);
 
   void parse(IntlStream input, DateBuilder dateFields) {
     parseLiteral(input);
@@ -258,7 +256,7 @@
     _LoosePatternField(pattern, parent).parse(input, dateFields);
   }
 
-  bool _forDate;
+  bool? _forDate;
 
   /// Is this field involved in computing the date portion, as opposed to the
   /// time.
diff --git a/lib/src/intl/intl_stream.dart b/lib/src/intl/intl_stream.dart
index 3149a53..888ed9d 100644
--- a/lib/src/intl/intl_stream.dart
+++ b/lib/src/intl/intl_stream.dart
@@ -1,7 +1,6 @@
 // Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-// @dart=2.9
 
 import 'dart:math';
 
@@ -60,7 +59,7 @@
 
   /// Find the index of the first element for which [f] returns true.
   /// Advances the stream to that position.
-  int findIndex(bool Function(dynamic) f) {
+  int? findIndex(bool Function(dynamic) f) {
     while (!atEnd()) {
       if (f(next())) return index - 1;
     }
@@ -83,14 +82,14 @@
   /// For non-ascii digits, the optional arguments are a regular expression
   /// [digitMatcher] to find the next integer, and the codeUnit of the local
   /// zero [zeroDigit].
-  int nextInteger({RegExp digitMatcher, int zeroDigit}) {
+  int? nextInteger({RegExp? digitMatcher, int? zeroDigit}) {
     var string = (digitMatcher ?? regexp.asciiDigitMatcher).stringMatch(rest());
     if (string == null || string.isEmpty) return null;
     read(string.length);
     if (zeroDigit != null && zeroDigit != constants.asciiZeroCodeUnit) {
       // Trying to optimize this, as it might get called a lot.
       var oldDigits = string.codeUnits;
-      var newDigits = List<int>(string.length);
+      var newDigits = List<int>.filled(string.length, 0);
       for (var i = 0; i < string.length; i++) {
         newDigits[i] = oldDigits[i] - zeroDigit + constants.asciiZeroCodeUnit;
       }
diff --git a/lib/src/intl/text_direction.dart b/lib/src/intl/text_direction.dart
index 7bcb7e4..e9b5649 100644
--- a/lib/src/intl/text_direction.dart
+++ b/lib/src/intl/text_direction.dart
@@ -1,7 +1,6 @@
 // Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-// @dart=2.9
 
 /// Represents directionality of text.
 ///
diff --git a/lib/src/intl_helpers.dart b/lib/src/intl_helpers.dart
index 46c57eb..6b3d992 100644
--- a/lib/src/intl_helpers.dart
+++ b/lib/src/intl_helpers.dart
@@ -10,6 +10,7 @@
 import 'dart:async';
 
 import 'global_state.dart' as global_state;
+import 'intl_helpers.dart' as helpers;
 
 /// Type for the callback action when a message translation is not found.
 typedef MessageIfAbsent = String Function(
@@ -145,3 +146,41 @@
   if (region.length <= 3) region = region.toUpperCase();
   return '${aLocale[0]}${aLocale[1]}_$region';
 }
+
+String verifiedLocale(String? newLocale, bool Function(String) localeExists,
+    String Function(String)? onFailure) {
+// TODO(alanknight): Previously we kept a single verified locale on the Intl
+// object, but with different verification for different uses, that's more
+// difficult. As a result, we call this more often. Consider keeping
+// verified locales for each purpose if it turns out to be a performance
+// issue.
+  if (newLocale == null) {
+    return verifiedLocale(
+        global_state.getCurrentLocale(), localeExists, onFailure);
+  }
+  if (localeExists(newLocale)) {
+    return newLocale;
+  }
+  for (var each in [
+    helpers.canonicalizedLocale(newLocale),
+    helpers.shortLocale(newLocale),
+    'fallback'
+  ]) {
+    if (localeExists(each)) {
+      return each;
+    }
+  }
+  return (onFailure ?? _throwLocaleError)(newLocale);
+}
+
+/// The default action if a locale isn't found in verifiedLocale. Throw
+/// an exception indicating the locale isn't correct.
+String _throwLocaleError(String localeName) {
+  throw ArgumentError('Invalid locale "$localeName"');
+}
+
+/// Return the short version of a locale name, e.g. 'en_US' => 'en'
+String shortLocale(String aLocale) {
+  if (aLocale.length < 2) return aLocale;
+  return aLocale.substring(0, 2).toLowerCase();
+}