Add locale parameter to EditableText (#18222)


diff --git a/bin/internal/goldens.version b/bin/internal/goldens.version
index 6e25562..120095b 100644
--- a/bin/internal/goldens.version
+++ b/bin/internal/goldens.version
@@ -1 +1 @@
-760320e3fc8ccd12deb4066bd4033a98d078359f
+568342373b14fab81fd665d397c8c00db3d46fca
diff --git a/packages/flutter/lib/src/painting/text_painter.dart b/packages/flutter/lib/src/painting/text_painter.dart
index 4704b34..3da44f1 100644
--- a/packages/flutter/lib/src/painting/text_painter.dart
+++ b/packages/flutter/lib/src/painting/text_painter.dart
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphConstraints, ParagraphStyle, Locale;
+import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphConstraints, ParagraphStyle;
 
 import 'package:flutter/foundation.dart';
 import 'package:flutter/gestures.dart';
@@ -46,7 +46,7 @@
     double textScaleFactor = 1.0,
     int maxLines,
     String ellipsis,
-    ui.Locale locale,
+    Locale locale,
   }) : assert(text == null || text.debugAssertIsValid()),
        assert(textAlign != null),
        assert(textScaleFactor != null),
@@ -168,9 +168,9 @@
   }
 
   /// The locale used to select region-specific glyphs.
-  ui.Locale get locale => _locale;
-  ui.Locale _locale;
-  set locale(ui.Locale value) {
+  Locale get locale => _locale;
+  Locale _locale;
+  set locale(Locale value) {
     if (_locale == value)
       return;
     _locale = value;
diff --git a/packages/flutter/lib/src/painting/text_style.dart b/packages/flutter/lib/src/painting/text_style.dart
index 36b7f12..36b4a6d 100644
--- a/packages/flutter/lib/src/painting/text_style.dart
+++ b/packages/flutter/lib/src/painting/text_style.dart
@@ -299,6 +299,13 @@
   final double height;
 
   /// The locale used to select region-specific glyphs.
+  ///
+  /// This property is rarely set. Typically the locale used to select
+  /// region-specific glyphs is defined by the text widget's [BuildContext]
+  /// using `Localizations.localeOf(context)`. For example [RichText] defines
+  /// its locale this way. However, a rich text widget's [TextSpan]s could specify
+  /// text styles with different explicit locales in order to select different
+  /// region-specifc glyphs for each text span.
   final Locale locale;
 
   /// The paint drawn as a background for the text.
diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart
index 6052fed..4090340 100644
--- a/packages/flutter/lib/src/rendering/editable.dart
+++ b/packages/flutter/lib/src/rendering/editable.dart
@@ -133,6 +133,7 @@
     this.onCaretChanged,
     this.ignorePointer = false,
     bool obscureText = false,
+    Locale locale,
   }) : assert(textAlign != null),
        assert(textDirection != null, 'RenderEditable created without a textDirection.'),
        assert(maxLines == null || maxLines > 0),
@@ -145,6 +146,7 @@
          textAlign: textAlign,
          textDirection: textDirection,
          textScaleFactor: textScaleFactor,
+         locale: locale,
        ),
        _cursorColor = cursorColor,
        _showCursor = showCursor ?? new ValueNotifier<bool>(false),
@@ -249,6 +251,24 @@
     markNeedsSemanticsUpdate();
   }
 
+  /// Used by this renderer's internal [TextPainter] to select a locale-specific
+  /// font.
+  ///
+  /// In some cases the same Unicode character may be rendered differently depending
+  /// on the locale. For example the '骨' character is rendered differently in
+  /// the Chinese and Japanese locales. In these cases the [locale] may be used
+  /// to select a locale-specific font.
+  ///
+  /// If this value is null, a system-dependent algorithm is used to select
+  /// the font.
+  Locale get locale => _textPainter.locale;
+  set locale(Locale value) {
+    if (_textPainter.locale == value)
+      return;
+    _textPainter.locale = value;
+    markNeedsTextLayout();
+  }
+
   /// The color to use when painting the cursor.
   Color get cursorColor => _cursorColor;
   Color _cursorColor;
@@ -749,6 +769,7 @@
     properties.add(new IntProperty('maxLines', maxLines));
     properties.add(new DiagnosticsProperty<Color>('selectionColor', selectionColor));
     properties.add(new DoubleProperty('textScaleFactor', textScaleFactor));
+    properties.add(new DiagnosticsProperty<Locale>('locale', locale, defaultValue: null));
     properties.add(new DiagnosticsProperty<TextSelection>('selection', selection));
     properties.add(new DiagnosticsProperty<ViewportOffset>('offset', offset));
   }
diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart
index 7f813a6..31f7b8b 100644
--- a/packages/flutter/lib/src/rendering/paragraph.dart
+++ b/packages/flutter/lib/src/rendering/paragraph.dart
@@ -458,6 +458,7 @@
     properties.add(new FlagProperty('softWrap', value: softWrap, ifTrue: 'wrapping at box width', ifFalse: 'no wrapping except at line break characters', showName: true));
     properties.add(new EnumProperty<TextOverflow>('overflow', overflow));
     properties.add(new DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: 1.0));
+    properties.add(new DiagnosticsProperty<Locale>('locale', locale, defaultValue: null));
     properties.add(new IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
   }
 }
diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart
index e9ff2c7..b7664dc 100644
--- a/packages/flutter/lib/src/widgets/editable_text.dart
+++ b/packages/flutter/lib/src/widgets/editable_text.dart
@@ -12,6 +12,7 @@
 import 'focus_manager.dart';
 import 'focus_scope.dart';
 import 'framework.dart';
+import 'localizations.dart';
 import 'media_query.dart';
 import 'scroll_controller.dart';
 import 'scroll_physics.dart';
@@ -160,6 +161,7 @@
     @required this.cursorColor,
     this.textAlign = TextAlign.start,
     this.textDirection,
+    this.locale,
     this.textScaleFactor,
     this.maxLines = 1,
     this.autofocus = false,
@@ -229,6 +231,15 @@
   /// Defaults to the ambient [Directionality], if any.
   final TextDirection textDirection;
 
+  /// Used to select a font when the same Unicode character can
+  /// be rendered differently, depending on the locale.
+  ///
+  /// It's rarely necessary to set this property. By default its value
+  /// is inherited from the enclosing app with `Localizations.localeOf(context)`.
+  ///
+  /// See [RenderEditable.locale] for more information.
+  final Locale locale;
+
   /// The number of font pixels for each logical pixel.
   ///
   /// For example, if the text scale factor is 1.5, text will be 50% larger than
@@ -299,6 +310,7 @@
     style?.debugFillProperties(properties);
     properties.add(new EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null));
     properties.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
+    properties.add(new DiagnosticsProperty<Locale>('locale', locale, defaultValue: null));
     properties.add(new DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null));
     properties.add(new IntProperty('maxLines', maxLines, defaultValue: 1));
     properties.add(new DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
@@ -687,6 +699,7 @@
               textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context),
               textAlign: widget.textAlign,
               textDirection: _textDirection,
+              locale: widget.locale,
               obscureText: widget.obscureText,
               autocorrect: widget.autocorrect,
               offset: offset,
@@ -747,6 +760,7 @@
     this.textScaleFactor,
     this.textAlign,
     @required this.textDirection,
+    this.locale,
     this.obscureText,
     this.autocorrect,
     this.offset,
@@ -767,6 +781,7 @@
   final double textScaleFactor;
   final TextAlign textAlign;
   final TextDirection textDirection;
+  final Locale locale;
   final bool obscureText;
   final bool autocorrect;
   final ViewportOffset offset;
@@ -786,6 +801,7 @@
       textScaleFactor: textScaleFactor,
       textAlign: textAlign,
       textDirection: textDirection,
+      locale: locale ?? Localizations.localeOf(context, nullOk: true),
       selection: value.selection,
       offset: offset,
       onSelectionChanged: onSelectionChanged,
@@ -807,6 +823,7 @@
       ..textScaleFactor = textScaleFactor
       ..textAlign = textAlign
       ..textDirection = textDirection
+      ..locale = locale ?? Localizations.localeOf(context, nullOk: true)
       ..selection = value.selection
       ..offset = offset
       ..onSelectionChanged = onSelectionChanged
diff --git a/packages/flutter/lib/src/widgets/text.dart b/packages/flutter/lib/src/widgets/text.dart
index bb7e85b..c88e8be 100644
--- a/packages/flutter/lib/src/widgets/text.dart
+++ b/packages/flutter/lib/src/widgets/text.dart
@@ -206,6 +206,7 @@
     this.style,
     this.textAlign,
     this.textDirection,
+    this.locale,
     this.softWrap,
     this.overflow,
     this.textScaleFactor,
@@ -220,6 +221,7 @@
     this.style,
     this.textAlign,
     this.textDirection,
+    this.locale,
     this.softWrap,
     this.overflow,
     this.textScaleFactor,
@@ -263,6 +265,15 @@
   /// Defaults to the ambient [Directionality], if any.
   final TextDirection textDirection;
 
+  /// Used to select a font when the same Unicode character can
+  /// be rendered differently, depending on the locale.
+  ///
+  /// It's rarely necessary to set this property. By default its value
+  /// is inherited from the enclosing app with `Localizations.localeOf(context)`.
+  ///
+  /// See [RenderParagraph.locale] for more information.
+  final Locale locale;
+
   /// Whether the text should break at soft line breaks.
   ///
   /// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space.
@@ -303,6 +314,7 @@
     return new RichText(
       textAlign: textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,
       textDirection: textDirection, // RichText uses Directionality.of to obtain a default if this is null.
+      locale: locale, // RichText uses Localizations.localeOf to obtain a default if this is null
       softWrap: softWrap ?? defaultTextStyle.softWrap,
       overflow: overflow ?? defaultTextStyle.overflow,
       textScaleFactor: textScaleFactor ?? MediaQuery.textScaleFactorOf(context),
@@ -325,6 +337,7 @@
     style?.debugFillProperties(properties);
     properties.add(new EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null));
     properties.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
+    properties.add(new DiagnosticsProperty<Locale>('locale', locale, defaultValue: null));
     properties.add(new FlagProperty('softWrap', value: softWrap, ifTrue: 'wrapping at box width', ifFalse: 'no wrapping except at line break characters', showName: true));
     properties.add(new EnumProperty<TextOverflow>('overflow', overflow, defaultValue: null));
     properties.add(new DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null));
diff --git a/packages/flutter/test/material/theme_test.dart b/packages/flutter/test/material/theme_test.dart
index ec02914..e8db419 100644
--- a/packages/flutter/test/material/theme_test.dart
+++ b/packages/flutter/test/material/theme_test.dart
@@ -451,7 +451,7 @@
   @override FontStyle get fontStyle => _delegate.fontStyle;
   @override FontWeight get fontWeight => _delegate.fontWeight;
   @override double get height => _delegate.height;
-  @override ui.Locale get locale => _delegate.locale;
+  @override Locale get locale => _delegate.locale;
   @override ui.Paint get background => _delegate.background;
   @override bool get inherit => _delegate.inherit;
   @override double get letterSpacing => _delegate.letterSpacing;
@@ -479,7 +479,7 @@
   }
 
   @override
-  TextStyle copyWith({Color color, String fontFamily, double fontSize, FontWeight fontWeight, FontStyle fontStyle, double letterSpacing, double wordSpacing, TextBaseline textBaseline, double height, ui.Locale locale, ui.Paint background, TextDecoration decoration, Color decorationColor, TextDecorationStyle decorationStyle, String debugLabel}) {
+  TextStyle copyWith({Color color, String fontFamily, double fontSize, FontWeight fontWeight, FontStyle fontStyle, double letterSpacing, double wordSpacing, TextBaseline textBaseline, double height, Locale locale, ui.Paint background, TextDecoration decoration, Color decorationColor, TextDecorationStyle decorationStyle, String debugLabel}) {
     throw new UnimplementedError();
   }
 
@@ -489,7 +489,7 @@
   }
 
   @override
-  ui.ParagraphStyle getParagraphStyle({TextAlign textAlign, TextDirection textDirection, double textScaleFactor = 1.0, String ellipsis, int maxLines, ui.Locale locale}) {
+  ui.ParagraphStyle getParagraphStyle({TextAlign textAlign, TextDirection textDirection, double textScaleFactor = 1.0, String ellipsis, int maxLines, Locale locale}) {
     throw new UnimplementedError();
   }
 
diff --git a/packages/flutter/test/rendering/editable_test.dart b/packages/flutter/test/rendering/editable_test.dart
index 5f0d8ca..47c8ab3 100644
--- a/packages/flutter/test/rendering/editable_test.dart
+++ b/packages/flutter/test/rendering/editable_test.dart
@@ -14,6 +14,7 @@
       ),
       textAlign: TextAlign.start,
       textDirection: TextDirection.ltr,
+      locale: const Locale('ja', 'JP'),
       offset: new ViewportOffset.zero(),
     );
     expect(editable.getMinIntrinsicWidth(double.infinity), 50.0);
@@ -33,6 +34,7 @@
         ' │ maxLines: 1\n'
         ' │ selectionColor: null\n'
         ' │ textScaleFactor: 1.0\n'
+        ' │ locale: ja_JP\n'
         ' │ selection: null\n'
         ' │ offset: _FixedViewportOffset#00000(offset: 0.0)\n'
         ' ╘═╦══ text ═══\n'
@@ -46,4 +48,4 @@
       ),
     );
   });
-}
\ No newline at end of file
+}
diff --git a/packages/flutter/test/rendering/localized_fonts_test.dart b/packages/flutter/test/rendering/localized_fonts_test.dart
new file mode 100644
index 0000000..ed18ab2
--- /dev/null
+++ b/packages/flutter/test/rendering/localized_fonts_test.dart
@@ -0,0 +1,159 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io' show Platform;
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+// TODO(hansmuller): when https://github.com/flutter/flutter/issues/17700
+// is fixed, these tests should be updated to use a real font (not Ahem).
+
+void main() {
+  testWidgets(
+    'RichText TextSpan styles with different locales',
+    (WidgetTester tester) async {
+
+      await tester.pumpWidget(
+        new MaterialApp(
+          supportedLocales: const <Locale>[
+            const Locale('en', 'US'),
+            const Locale('ja'),
+            const Locale('zh'),
+          ],
+          home: new Builder(
+            builder: (BuildContext context) {
+              const String character = '骨';
+              final TextStyle style = Theme.of(context).textTheme.display3;
+              return new Scaffold(
+                body: new Container(
+                  padding: const EdgeInsets.all(48.0),
+                  alignment: Alignment.center,
+                  child: new RepaintBoundary(
+                    // Expected result can be seen here:
+                    // https://user-images.githubusercontent.com/1377460/40503473-faad6f34-5f42-11e8-972b-d83b727c9d0e.png
+                    child: new RichText(
+                      text: new TextSpan(
+                        children: <TextSpan>[
+                          new TextSpan(text: character, style: style.copyWith(locale: const Locale('ja'))),
+                          new TextSpan(text: character, style: style.copyWith(locale: const Locale('zh'))),
+                        ],
+                      ),
+                    ),
+                  ),
+                ),
+              );
+            },
+          ),
+        )
+      );
+
+      await expectLater(
+        find.byType(RichText),
+        matchesGoldenFile('localized_fonts.rich_text.styled_text_span.png'),
+        skip: !Platform.isLinux,
+      );
+    },
+    skip: !Platform.isLinux,
+  );
+
+  testWidgets(
+    'Text with locale-specific glyphs, ambient locale',
+    (WidgetTester tester) async {
+      await tester.pumpWidget(
+        new MaterialApp(
+          supportedLocales: const <Locale>[
+            const Locale('en', 'US'),
+            const Locale('ja'),
+            const Locale('zh'),
+          ],
+          home: new Builder(
+            builder: (BuildContext context) {
+              const String character = '骨';
+              final TextStyle style = Theme.of(context).textTheme.display3;
+              return new Scaffold(
+                body: new Container(
+                  padding: const EdgeInsets.all(48.0),
+                  alignment: Alignment.center,
+                  child: new RepaintBoundary(
+                    // Expected result can be seen here:
+                    // https://user-images.githubusercontent.com/1377460/40503473-faad6f34-5f42-11e8-972b-d83b727c9d0e.png
+                    child: new Row(
+                      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+                      children: <Widget>[
+                        new Localizations.override(
+                          context: context,
+                          locale: const Locale('ja'),
+                          child: new Text(character, style: style),
+                        ),
+                        new Localizations.override(
+                          context: context,
+                          locale: const Locale('zh'),
+                          child: new Text(character, style: style),
+                        ),
+                      ],
+                    ),
+                  ),
+                ),
+              );
+            },
+          ),
+        )
+      );
+
+      await expectLater(
+        find.byType(Row),
+        matchesGoldenFile('localized_fonts.text_ambient_locale.chars.png'),
+        skip: !Platform.isLinux,
+      );
+    },
+    skip: !Platform.isLinux,
+  );
+
+  testWidgets(
+    'Text with locale-specific glyphs, explicit locale',
+    (WidgetTester tester) async {
+      await tester.pumpWidget(
+        new MaterialApp(
+          supportedLocales: const <Locale>[
+            const Locale('en', 'US'),
+            const Locale('ja'),
+            const Locale('zh'),
+          ],
+          home: new Builder(
+            builder: (BuildContext context) {
+              const String character = '骨';
+              final TextStyle style = Theme.of(context).textTheme.display3;
+              return new Scaffold(
+                body: new Container(
+                  padding: const EdgeInsets.all(48.0),
+                  alignment: Alignment.center,
+                  child: new RepaintBoundary(
+                    // Expected result can be seen here:
+                    // https://user-images.githubusercontent.com/1377460/40503473-faad6f34-5f42-11e8-972b-d83b727c9d0e.png
+                    child: new Row(
+                      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+                      children: <Widget>[
+                        new Text(character, style: style, locale: const Locale('ja')),
+	                new Text(character, style: style, locale: const Locale('zh')),
+                      ],
+                    ),
+                  ),
+                ),
+              );
+            },
+          ),
+        )
+      );
+
+      await expectLater(
+        find.byType(Row),
+        matchesGoldenFile('localized_fonts.text_explicit_locale.chars.png'),
+        skip: !Platform.isLinux,
+      );
+    },
+    skip: !Platform.isLinux,
+  );
+
+}
diff --git a/packages/flutter/test/rendering/paragraph_test.dart b/packages/flutter/test/rendering/paragraph_test.dart
index ca8bd3f..7130ef3 100644
--- a/packages/flutter/test/rendering/paragraph_test.dart
+++ b/packages/flutter/test/rendering/paragraph_test.dart
@@ -288,6 +288,7 @@
     final RenderParagraph paragraph = new RenderParagraph(
       const TextSpan(text: _kText),
       textDirection: TextDirection.ltr,
+      locale: const Locale('ja', 'JP'),
     );
     expect(paragraph, hasAGoodToStringDeep);
     expect(
@@ -301,6 +302,7 @@
         ' │ textDirection: ltr\n'
         ' │ softWrap: wrapping at box width\n'
         ' │ overflow: clip\n'
+        ' │ locale: ja_JP\n'
         ' │ maxLines: unlimited\n'
         ' ╘═╦══ text ═══\n'
         '   ║ TextSpan:\n'