Add optional custom localization for TimePicker, the same way DatePicker has it (#84566)

diff --git a/packages/flutter/lib/src/material/time_picker.dart b/packages/flutter/lib/src/material/time_picker.dart
index 0f7068b..e36cdfc 100644
--- a/packages/flutter/lib/src/material/time_picker.dart
+++ b/packages/flutter/lib/src/material/time_picker.dart
@@ -1290,6 +1290,9 @@
     Key? key,
     required this.initialSelectedTime,
     required this.helpText,
+    required this.errorInvalidText,
+    required this.hourLabelText,
+    required this.minuteLabelText,
     required this.autofocusHour,
     required this.autofocusMinute,
     required this.onChanged,
@@ -1304,6 +1307,15 @@
   /// Optionally provide your own help text to the time picker.
   final String? helpText;
 
+  /// Optionally provide your own validation error text.
+  final String? errorInvalidText;
+
+  /// Optionally provide your own hour label text.
+  final String? hourLabelText;
+
+  /// Optionally provide your own minute label text.
+  final String? minuteLabelText;
+
   final bool? autofocusHour;
 
   final bool? autofocusMinute;
@@ -1480,12 +1492,13 @@
                             validator: _validateHour,
                             onSavedSubmitted: _handleHourSavedSubmitted,
                             onChanged: _handleHourChanged,
+                            hourLabelText: widget.hourLabelText,
                           ),
                           const SizedBox(height: 8.0),
                           if (!hourHasError.value && !minuteHasError.value)
                             ExcludeSemantics(
                               child: Text(
-                                MaterialLocalizations.of(context).timePickerHourLabel,
+                                widget.hourLabelText ?? MaterialLocalizations.of(context).timePickerHourLabel,
                                 style: theme.textTheme.caption,
                                 maxLines: 1,
                                 overflow: TextOverflow.ellipsis,
@@ -1511,12 +1524,13 @@
                             autofocus: widget.autofocusMinute,
                             validator: _validateMinute,
                             onSavedSubmitted: _handleMinuteSavedSubmitted,
+                            minuteLabelText: widget.minuteLabelText,
                           ),
                           const SizedBox(height: 8.0),
                           if (!hourHasError.value && !minuteHasError.value)
                             ExcludeSemantics(
                               child: Text(
-                                MaterialLocalizations.of(context).timePickerMinuteLabel,
+                                widget.minuteLabelText ?? MaterialLocalizations.of(context).timePickerMinuteLabel,
                                 style: theme.textTheme.caption,
                                 maxLines: 1,
                                 overflow: TextOverflow.ellipsis,
@@ -1540,7 +1554,7 @@
           ),
           if (hourHasError.value || minuteHasError.value)
             Text(
-              MaterialLocalizations.of(context).invalidTimeLabel,
+              widget.errorInvalidText ?? MaterialLocalizations.of(context).invalidTimeLabel,
               style: theme.textTheme.bodyText2!.copyWith(color: theme.colorScheme.error),
             )
           else
@@ -1560,6 +1574,7 @@
     required this.validator,
     required this.onSavedSubmitted,
     required this.onChanged,
+    required this.hourLabelText,
     this.restorationId,
   }) : super(key: key);
 
@@ -1569,6 +1584,7 @@
   final FormFieldValidator<String> validator;
   final ValueChanged<String?> onSavedSubmitted;
   final ValueChanged<String> onChanged;
+  final String? hourLabelText;
   final String? restorationId;
 
   @override
@@ -1579,7 +1595,7 @@
       isHour: true,
       autofocus: autofocus,
       style: style,
-      semanticHintText: MaterialLocalizations.of(context).timePickerHourLabel,
+      semanticHintText: hourLabelText ??  MaterialLocalizations.of(context).timePickerHourLabel,
       validator: validator,
       onSavedSubmitted: onSavedSubmitted,
       onChanged: onChanged,
@@ -1595,6 +1611,7 @@
     required this.autofocus,
     required this.validator,
     required this.onSavedSubmitted,
+    required this.minuteLabelText,
     this.restorationId,
   }) : super(key: key);
 
@@ -1603,6 +1620,7 @@
   final bool? autofocus;
   final FormFieldValidator<String> validator;
   final ValueChanged<String?> onSavedSubmitted;
+  final String? minuteLabelText;
   final String? restorationId;
 
   @override
@@ -1613,7 +1631,7 @@
       isHour: false,
       autofocus: autofocus,
       style: style,
-      semanticHintText: MaterialLocalizations.of(context).timePickerMinuteLabel,
+      semanticHintText: minuteLabelText ?? MaterialLocalizations.of(context).timePickerMinuteLabel,
       validator: validator,
       onSavedSubmitted: onSavedSubmitted,
     );
@@ -1784,6 +1802,9 @@
     this.cancelText,
     this.confirmText,
     this.helpText,
+    this.errorInvalidText,
+    this.hourLabelText,
+    this.minuteLabelText,
     this.restorationId,
     this.initialEntryMode = TimePickerEntryMode.dial,
   }) : assert(initialTime != null),
@@ -1808,6 +1829,15 @@
   /// Optionally provide your own help text to the header of the time picker.
   final String? helpText;
 
+  /// Optionally provide your own validation error text.
+  final String? errorInvalidText;
+
+  /// Optionally provide your own hour label text.
+  final String? hourLabelText;
+
+  /// Optionally provide your own minute label text.
+  final String? minuteLabelText;
+
   /// Restoration ID to save and restore the state of the [TimePickerDialog].
   ///
   /// If it is non-null, the time picker will persist and restore the
@@ -2220,6 +2250,9 @@
                 _TimePickerInput(
                   initialSelectedTime: _selectedTime.value,
                   helpText: widget.helpText,
+                  errorInvalidText: widget.errorInvalidText,
+                  hourLabelText: widget.hourLabelText,
+                  minuteLabelText: widget.minuteLabelText,
                   autofocusHour: _autofocusHour.value,
                   autofocusMinute: _autofocusMinute.value,
                   onChanged: _handleTimeChanged,
@@ -2286,8 +2319,9 @@
 /// determine the initial time entry selection of the picker (either a clock
 /// dial or text input).
 ///
-/// Optional strings for the [helpText], [cancelText], and [confirmText] can be
-/// provided to override the default values.
+/// Optional strings for the [helpText], [cancelText], [errorInvalidText],
+/// [hourLabelText], [minuteLabelText] and [confirmText] can be provided to
+/// override the default values.
 ///
 /// By default, the time picker gets its colors from the overall theme's
 /// [ColorScheme]. The time picker can be further customized by providing a
@@ -2342,6 +2376,9 @@
   String? cancelText,
   String? confirmText,
   String? helpText,
+  String? errorInvalidText,
+  String? hourLabelText,
+  String? minuteLabelText,
   RouteSettings? routeSettings,
 }) async {
   assert(context != null);
@@ -2356,6 +2393,9 @@
     cancelText: cancelText,
     confirmText: confirmText,
     helpText: helpText,
+    errorInvalidText: errorInvalidText,
+    hourLabelText: hourLabelText,
+    minuteLabelText: minuteLabelText,
   );
   return showDialog<TimeOfDay>(
     context: context,
diff --git a/packages/flutter/test/material/time_picker_test.dart b/packages/flutter/test/material/time_picker_test.dart
index 3858085..a100b08 100644
--- a/packages/flutter/test/material/time_picker_test.dart
+++ b/packages/flutter/test/material/time_picker_test.dart
@@ -861,6 +861,31 @@
     expect(find.text(helpText), findsOneWidget);
   });
 
+  testWidgets('Hour label text is used - Input', (WidgetTester tester) async {
+    const String hourLabelText = 'Custom hour label';
+    await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input, hourLabelText: hourLabelText);
+    expect(find.text(hourLabelText), findsOneWidget);
+  });
+
+
+  testWidgets('Minute label text is used - Input', (WidgetTester tester) async {
+    const String minuteLabelText = 'Custom minute label';
+    await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input, minuteLabelText: minuteLabelText);
+    expect(find.text(minuteLabelText), findsOneWidget);
+  });
+
+  testWidgets('Invalid error text is used - Input', (WidgetTester tester) async {
+    const String errorInvalidText = 'Custom validation error';
+    await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input, errorInvalidText: errorInvalidText);
+    // Input invalid time (hour) to force validation error
+    await tester.enterText(find.byType(TextField).first, '88');
+    final MaterialLocalizations materialLocalizations = MaterialLocalizations.of(tester.element(find.byType(TextButton).first));
+    // Tap the ok button to trigger the validation error with custom translation
+    await tester.tap(find.text(materialLocalizations.okButtonLabel));
+    await tester.pumpAndSettle(const Duration(seconds: 1));
+    expect(find.text(errorInvalidText), findsOneWidget);
+  });
+
   testWidgets('Can toggle to dial entry mode', (WidgetTester tester) async {
     await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input);
     await tester.tap(find.byIcon(Icons.access_time));
@@ -1124,6 +1149,9 @@
   double textScaleFactor = 1.0,
   TimePickerEntryMode entryMode = TimePickerEntryMode.dial,
   String? helpText,
+  String? hourLabelText,
+  String? minuteLabelText,
+  String? errorInvalidText,
   bool accessibleNavigation = false,
 }) async {
   await tester.pumpWidget(
@@ -1152,6 +1180,9 @@
                         initialTime: initialTime,
                         initialEntryMode: entryMode,
                         helpText: helpText,
+                        hourLabelText: hourLabelText,
+                        minuteLabelText: minuteLabelText,
+                        errorInvalidText: errorInvalidText
                       );
                     },
                     child: const Text('X'),