[simple_browser][i18n] Adds i18n for simple_browser

Adds L10N support for simple_browser.  Adds sample languages and
translations.

Screenshot with Dutch locale:
https://photos.app.goo.gl/cwwhvAcYZy8xoonB6

Bug: 35579
Bug: 39359
Change-Id: I613b8e5bf71b0e83c7ad57319a099fd8300b2d8a
diff --git a/bin/simple_browser/BUILD.gn b/bin/simple_browser/BUILD.gn
index 4030bd7..fcad778 100644
--- a/bin/simple_browser/BUILD.gn
+++ b/bin/simple_browser/BUILD.gn
@@ -21,18 +21,23 @@
   manifest = "pubspec.yaml"
 
   sources = [
+    "main.dart",
     "src/blocs/tabs_bloc.dart",
     "src/models/tabs_action.dart",
   ]
 
   deps = [
+    "//sdk/fidl/fuchsia.intl",
     "//sdk/fidl/fuchsia.web",
+    "//src/experiences/bin/simple_browser_internationalization:internationalization",
     "//third_party/dart-pkg/git/flutter/packages/flutter",
     "//third_party/dart-pkg/pub/html_unescape",
     "//third_party/dart-pkg/pub/http",
+    "//third_party/dart/third_party/pkg/intl",
     "//topaz/public/dart/fuchsia_logger",
     "//topaz/public/dart/fuchsia_modular",
     "//topaz/public/dart/fuchsia_scenic_flutter",
+    "//topaz/public/dart/fuchsia_services",
     "//topaz/public/dart/widgets:lib.widgets",
     "//topaz/public/lib/webview",
   ]
diff --git a/bin/simple_browser/lib/app.dart b/bin/simple_browser/lib/app.dart
index bf756f6..275ba13 100644
--- a/bin/simple_browser/lib/app.dart
+++ b/bin/simple_browser/lib/app.dart
@@ -4,6 +4,7 @@
 
 import 'package:flutter/material.dart';
 import 'package:fuchsia_scenic_flutter/child_view.dart' show ChildView;
+import 'package:internationalization/strings.dart';
 import 'package:simple_browser/src/blocs/webpage_bloc.dart';
 import 'src/blocs/tabs_bloc.dart';
 import 'src/models/tabs_action.dart';
@@ -19,39 +20,56 @@
 class App extends StatelessWidget {
   final TabsBloc<WebPageBloc> tabsBloc;
 
-  const App({@required this.tabsBloc});
+  final Locale locale;
+
+  final Iterable<LocalizationsDelegate<dynamic>> localizationsDelegates;
+
+  final Iterable<Locale> supportedLocales;
+
+  /// The [locale], [localizationsDelegates] and [supportedLocales] parameters
+  /// are the same as in [MaterialApp].
+  const App(
+      {@required this.tabsBloc,
+      this.locale,
+      this.localizationsDelegates,
+      this.supportedLocales});
 
   @override
-  Widget build(BuildContext context) => MaterialApp(
-        title: 'Browser',
-        theme: ThemeData(
-          fontFamily: 'RobotoMono',
-          textSelectionColor: _kSelectionColor,
-          textSelectionHandleColor: _kForegroundColor,
-          hintColor: _kForegroundColor,
-          cursorColor: _kForegroundColor,
-          primaryColor: _kBackgroundColor,
-          canvasColor: _kBackgroundColor,
-          accentColor: _kForegroundColor,
-          textTheme: TextTheme(
-            body1: _kTextStyle,
-            subhead: _kTextStyle,
-          ),
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      title: Strings.browser,
+      theme: ThemeData(
+        fontFamily: 'RobotoMono',
+        textSelectionColor: _kSelectionColor,
+        textSelectionHandleColor: _kForegroundColor,
+        hintColor: _kForegroundColor,
+        cursorColor: _kForegroundColor,
+        primaryColor: _kBackgroundColor,
+        canvasColor: _kBackgroundColor,
+        accentColor: _kForegroundColor,
+        textTheme: TextTheme(
+          body1: _kTextStyle,
+          subhead: _kTextStyle,
         ),
-        home: Scaffold(
-          body: Column(
-            children: <Widget>[
-              AnimatedBuilder(
-                animation: tabsBloc.currentTabNotifier,
-                builder: (_, __) =>
-                    NavigationBar(bloc: tabsBloc.currentTab, newTab: newTab),
-              ),
-              TabsWidget(bloc: tabsBloc),
-              Expanded(child: _buildContent()),
-            ],
-          ),
+      ),
+      locale: locale,
+      localizationsDelegates: localizationsDelegates,
+      supportedLocales: supportedLocales,
+      home: Scaffold(
+        body: Column(
+          children: <Widget>[
+            AnimatedBuilder(
+              animation: tabsBloc.currentTabNotifier,
+              builder: (_, __) =>
+                  NavigationBar(bloc: tabsBloc.currentTab, newTab: newTab),
+            ),
+            TabsWidget(bloc: tabsBloc),
+            Expanded(child: _buildContent()),
+          ],
         ),
-      );
+      ),
+    );
+  }
 
   Widget _buildContent() => AnimatedBuilder(
         animation: tabsBloc.currentTabNotifier,
diff --git a/bin/simple_browser/lib/main.dart b/bin/simple_browser/lib/main.dart
index 21ecc0f..47558d7 100644
--- a/bin/simple_browser/lib/main.dart
+++ b/bin/simple_browser/lib/main.dart
@@ -3,12 +3,24 @@
 // found in the LICENSE file.
 
 import 'dart:convert';
+
 import 'package:flutter/material.dart';
+import 'package:flutter_localizations/flutter_localizations.dart';
+
+import 'package:fidl_fuchsia_intl/fidl_async.dart';
+import 'package:fuchsia_internationalization_flutter/internationalization.dart';
 import 'package:fuchsia_logger/logger.dart';
 import 'package:fuchsia_modular/module.dart' as modular;
+import 'package:fuchsia_services/services.dart';
+import 'package:internationalization/localizations_delegate.dart'
+    as localizations;
+import 'package:internationalization/supported_locales.dart'
+    as supported_locales;
+import 'package:intl/intl.dart';
 import 'package:simple_browser/src/models/tabs_action.dart';
 import 'package:simple_browser/src/models/webpage_action.dart';
 import 'package:webview/webview.dart';
+
 import 'app.dart';
 import 'src/blocs/tabs_bloc.dart';
 import 'src/blocs/webpage_bloc.dart';
@@ -50,5 +62,45 @@
     },
   );
   modular.Module().registerIntentHandler(RootIntentHandler(tabsBloc));
-  runApp(App(tabsBloc: tabsBloc));
+
+  final _intl = PropertyProviderProxy();
+  StartupContext.fromStartupInfo().incoming.connectToService(_intl);
+
+  final locales = LocaleSource(_intl);
+
+  runApp(Localized(tabsBloc, locales.stream()));
+}
+
+/// This is a localized version of the browser app.  It is the same as the
+/// original App, but it also has the current locale injected.
+class Localized extends StatelessWidget {
+  // The tabs bloc to use for the underlying widget.
+  final TabsBloc _tabsBloc;
+
+  // The stream of locale updates.
+  final Stream<Locale> _localeStream;
+
+  const Localized(this._tabsBloc, this._localeStream);
+
+  @override
+  Widget build(BuildContext context) {
+    return StreamBuilder<Locale>(
+        stream: _localeStream,
+        builder: (BuildContext context, AsyncSnapshot<Locale> snapshot) {
+          final Locale locale = snapshot.data;
+          // This is required so app parts which don't depend on the flutter
+          // locale have access to it.
+          Intl.defaultLocale = locale.toString();
+          return App(
+            tabsBloc: _tabsBloc,
+            locale: locale,
+            localizationsDelegates: [
+              localizations.delegate(),
+              GlobalMaterialLocalizations.delegate,
+              GlobalWidgetsLocalizations.delegate,
+            ],
+            supportedLocales: supported_locales.locales,
+          );
+        });
+  }
 }
diff --git a/bin/simple_browser/lib/src/widgets/history_buttons.dart b/bin/simple_browser/lib/src/widgets/history_buttons.dart
index 24fe462..82b08f1 100644
--- a/bin/simple_browser/lib/src/widgets/history_buttons.dart
+++ b/bin/simple_browser/lib/src/widgets/history_buttons.dart
@@ -3,6 +3,8 @@
 // found in the LICENSE file.
 
 import 'package:flutter/material.dart';
+import 'package:internationalization/strings.dart';
+
 import '../blocs/webpage_bloc.dart';
 import '../models/webpage_action.dart';
 
@@ -16,31 +18,33 @@
   final WebPageBloc bloc;
 
   @override
-  Widget build(BuildContext context) => Row(
-        crossAxisAlignment: CrossAxisAlignment.stretch,
-        children: <Widget>[
-          AnimatedBuilder(
-              animation: bloc.backStateNotifier,
-              builder: (_, __) => _HistoryButton(
-                  title: 'BCK',
-                  onTap: () => bloc.request.add(GoBackAction()),
-                  isEnabled: bloc.backState)),
-          SizedBox(width: 8.0),
-          AnimatedBuilder(
-              animation: bloc.forwardStateNotifier,
-              builder: (_, __) => _HistoryButton(
-                  title: 'FWD',
-                  onTap: () => bloc.request.add(GoForwardAction()),
-                  isEnabled: bloc.forwardState)),
-          SizedBox(width: 8.0),
-          AnimatedBuilder(
-              animation: bloc.urlNotifier,
-              builder: (_, __) => _HistoryButton(
-                  title: 'RFRSH',
-                  onTap: () => bloc.request.add(RefreshAction()),
-                  isEnabled: bloc.pageType == PageType.normal)),
-        ],
-      );
+  Widget build(BuildContext context) {
+    return Row(
+      crossAxisAlignment: CrossAxisAlignment.stretch,
+      children: <Widget>[
+        AnimatedBuilder(
+            animation: bloc.backStateNotifier,
+            builder: (_, __) => _HistoryButton(
+                title: Strings.back.toUpperCase(),
+                onTap: () => bloc.request.add(GoBackAction()),
+                isEnabled: bloc.backState)),
+        SizedBox(width: 8.0),
+        AnimatedBuilder(
+            animation: bloc.forwardStateNotifier,
+            builder: (_, __) => _HistoryButton(
+                title: Strings.forward.toUpperCase(),
+                onTap: () => bloc.request.add(GoForwardAction()),
+                isEnabled: bloc.forwardState)),
+        SizedBox(width: 8.0),
+        AnimatedBuilder(
+            animation: bloc.urlNotifier,
+            builder: (_, __) => _HistoryButton(
+                title: Strings.refresh.toUpperCase(),
+                onTap: () => bloc.request.add(RefreshAction()),
+                isEnabled: bloc.pageType == PageType.normal)),
+      ],
+    );
+  }
 }
 
 class _HistoryButton extends StatelessWidget {
diff --git a/bin/simple_browser/lib/src/widgets/navigation_field.dart b/bin/simple_browser/lib/src/widgets/navigation_field.dart
index 014eda2..92497a5 100644
--- a/bin/simple_browser/lib/src/widgets/navigation_field.dart
+++ b/bin/simple_browser/lib/src/widgets/navigation_field.dart
@@ -3,6 +3,8 @@
 // found in the LICENSE file.
 
 import 'package:flutter/material.dart';
+import 'package:internationalization/strings.dart';
+
 import '../blocs/webpage_bloc.dart';
 import '../models/webpage_action.dart';
 
@@ -82,10 +84,9 @@
         keyboardType: TextInputType.url,
         decoration: InputDecoration(
           contentPadding: EdgeInsets.zero,
-          // Extra spaces before SEARCH are meant to visually center the S,
-          // so the cursor's position will sit in the beginning of the hint,
-          // rather than halfway between the A and the R.
-          hintText: '     SEARCH',
+          // In general, do not use space characters to move graphical elements
+          // around, or you are going to have a bad time. :)
+          hintText: '     ${Strings.search.toUpperCase()}',
           border: InputBorder.none,
           isDense: true,
         ),
diff --git a/bin/simple_browser/meta/simple_browser.cmx b/bin/simple_browser/meta/simple_browser.cmx
index 93d3f6c..b87a486 100644
--- a/bin/simple_browser/meta/simple_browser.cmx
+++ b/bin/simple_browser/meta/simple_browser.cmx
@@ -24,23 +24,24 @@
             "fuchsia.cobalt.LoggerFactory",
             "fuchsia.deprecatedtimezone.Timezone",
             "fuchsia.fonts.Provider",
+            "fuchsia.intl.PropertyProvider",
             "fuchsia.logger.LogSink",
             "fuchsia.media.Audio",
             "fuchsia.mediacodec.CodecFactory",
             "fuchsia.modular.Clipboard",
+            "fuchsia.modular.ComponentContext",
             "fuchsia.modular.ModuleContext",
-            "fuchsia.posix.socket.Provider",
             "fuchsia.net.NameLookup",
             "fuchsia.netstack.Netstack",
+            "fuchsia.posix.socket.Provider",
             "fuchsia.process.Launcher",
             "fuchsia.sys.Environment",
             "fuchsia.sys.Launcher",
             "fuchsia.sysmem.Allocator",
             "fuchsia.ui.input.ImeService",
             "fuchsia.ui.input.ImeVisibilityService",
-            "fuchsia.ui.scenic.Scenic",
             "fuchsia.ui.policy.Presenter",
-            "fuchsia.modular.ComponentContext",
+            "fuchsia.ui.scenic.Scenic",
             "fuchsia.vulkan.loader.Loader",
             "fuchsia.web.ContextProvider"
         ]
diff --git a/bin/simple_browser/test/simple_browser_test.dart b/bin/simple_browser/test/simple_browser_test.dart
index 6f19acc..f2ba8f8 100644
--- a/bin/simple_browser/test/simple_browser_test.dart
+++ b/bin/simple_browser/test/simple_browser_test.dart
@@ -4,13 +4,19 @@
 
 import 'dart:async';
 import 'package:flutter/foundation.dart';
-import 'package:test/test.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:fuchsia_logger/logger.dart';
 
 // ignore_for_file: implementation_imports
+import 'package:simple_browser/main.dart' show Localized;
 import 'package:simple_browser/src/blocs/tabs_bloc.dart';
+import 'package:simple_browser/src/blocs/webpage_bloc.dart';
 import 'package:simple_browser/src/models/tabs_action.dart';
 
 void main() {
+  setupLogger(name: 'simple_browser_test');
+
   // Add a tab, and see that there is 1 tab
   test('add 1 tab', () async {
     final tb = TabsBloc(
@@ -58,6 +64,28 @@
     );
     expect(tb.currentTab, 'tab 0', reason: 'unexpected tab content');
   });
+
+  testWidgets('localized text is displayed in the widgets',
+      (WidgetTester tester) async {
+    var locales = Stream.fromIterable(
+        [Locale.fromSubtags(languageCode: 'sr', countryCode: 'RS')]);
+    TabsBloc<WebPageBloc> tabsBloc;
+    tabsBloc = TabsBloc(
+      tabFactory: () => WebPageBloc(
+          popupHandler: (tab) =>
+              tabsBloc.request.add(AddTabAction<WebPageBloc>(tab: tab))),
+      disposeTab: (tab) {
+        tab.dispose();
+      },
+    );
+    await tester.pumpWidget(Localized(tabsBloc, locales));
+    await tester.pump();
+    expect(
+        find.byWidgetPredicate(
+            (Widget widget) => widget is Title && widget.title == 'Прегледач',
+            description: 'A widget with a localized title was displayed'),
+        findsOneWidget);
+  });
 }
 
 /// awaits for a single callback from a [Listenable]
diff --git a/bin/simple_browser_internationalization/BUILD.gn b/bin/simple_browser_internationalization/BUILD.gn
new file mode 100644
index 0000000..7f3f54c
--- /dev/null
+++ b/bin/simple_browser_internationalization/BUILD.gn
@@ -0,0 +1,35 @@
+# Copyright 2019 The Fuchsia 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("//build/dart/dart_library.gni")
+import("//src/modular/build/modular_config/modular_config.gni")
+import("//topaz/runtime/dart/flutter_test.gni")
+import("//topaz/runtime/flutter_runner/flutter_app.gni")
+
+dart_library("internationalization") {
+  package_name = "internationalization"
+
+  # Disabling analysis for the time being, this is mostly generated code to begin
+  # with, and there are issues like this one that make things harder than necessary:
+  # https://github.com/dart-lang/sdk/issues/38598.
+  disable_analysis = true
+
+  sources = [
+    "localization/messages_all.dart",
+    "localization/messages_messages.dart",
+    "localization/messages_nl.dart",
+    "localization/messages_sr.dart",
+    "localizations_delegate.dart",
+    "strings.dart",
+    "supported_locales.dart",
+  ]
+
+  deps = [
+    "//third_party/dart-pkg/git/flutter/packages/flutter",
+    "//third_party/dart-pkg/git/flutter/packages/flutter_localizations",
+    "//third_party/dart/third_party/pkg/intl",
+    "//topaz/public/dart/fuchsia_internationalization_flutter",
+    "//topaz/public/dart/widgets:lib.widgets",
+  ]
+}
diff --git a/bin/simple_browser_internationalization/README.md b/bin/simple_browser_internationalization/README.md
new file mode 100644
index 0000000..f30004e
--- /dev/null
+++ b/bin/simple_browser_internationalization/README.md
@@ -0,0 +1,42 @@
+# `simple_browser` internationalization and localization
+
+This Dart package contains the support for `simple_browser` localization and
+internationalization.
+
+At its current state the support is in its very early phase.  There is a lot of
+manual scaffolding that could be replaced by auto-generating the code of
+interest.  Doing so will be the topic of followup work.
+
+# How to use this package
+
+This package is purpose-built for the `simple_browser`.  The central part is
+the file `lib/strings.dart` which by design contains all the strings that the
+simple browser needs to vary based on the system locale.
+
+Add a function to this file whenever you need to introduce a new text message.
+
+## Translating
+
+The translation process amounts to making ARB files for each of the supported
+locales of interest and providing the translations for the messages and
+strings.  The basic tool for doing so would be a simple text editor. While
+there also exists a wealth of tools that help software translators, it is out
+of scope of this file to go into specific details.  So the choice of the
+translation environment is left to the developer and translator.
+
+## Generating Dart runtime format for the translations
+
+Once translated you can use the file `./scripts/run_generate_from_arb.sh` from
+`fuchsia_internationalization` library to generate the Dart code that wires up
+the translation.  You will need to rebuild the entire project to take the new
+translations in.
+
+# Further reading
+
+Internationalization and localization are topic unto themselves, and it is out
+of scope of this file to go into all the details.  Please see the section
+on [Internationalizing Flutter apps][1] for many more details that are directly
+relevant to Dart and Flutter app localization.
+
+[1]: https://flutter.dev/docs/development/accessibility-and-localization/internationalization
+[arb]: https://github.com/google/app-resource-bundle
diff --git a/bin/simple_browser_internationalization/analysis_options.yaml b/bin/simple_browser_internationalization/analysis_options.yaml
new file mode 100644
index 0000000..86fb3da
--- /dev/null
+++ b/bin/simple_browser_internationalization/analysis_options.yaml
@@ -0,0 +1,6 @@
+# Copyright 2019 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+include: ../../../analysis_options.yaml
+
diff --git a/bin/simple_browser_internationalization/lib/localization/messages_all.dart b/bin/simple_browser_internationalization/lib/localization/messages_all.dart
new file mode 100644
index 0000000..1bc2700
--- /dev/null
+++ b/bin/simple_browser_internationalization/lib/localization/messages_all.dart
@@ -0,0 +1,70 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that looks up messages for specific locales by
+// delegating to the appropriate library.
+
+// Ignore issues from commonly used lints in this file.
+// ignore_for_file:implementation_imports, file_names, unnecessary_new
+// ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering
+// ignore_for_file:argument_type_not_assignable, invalid_assignment
+// ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases
+// ignore_for_file:comment_references
+
+import 'dart:async';
+
+import 'package:intl/intl.dart';
+import 'package:intl/message_lookup_by_library.dart';
+import 'package:intl/src/intl_helpers.dart';
+
+import 'messages_messages.dart' deferred as messages_messages;
+import 'messages_nl.dart' deferred as messages_nl;
+import 'messages_sr.dart' deferred as messages_sr;
+
+typedef Future<dynamic> LibraryLoader();
+Map<String, LibraryLoader> _deferredLibraries = {
+  'messages': messages_messages.loadLibrary,
+  'nl': messages_nl.loadLibrary,
+  'sr': messages_sr.loadLibrary,
+};
+
+MessageLookupByLibrary _findExact(String localeName) {
+  switch (localeName) {
+    case 'messages':
+      return messages_messages.messages;
+    case 'nl':
+      return messages_nl.messages;
+    case 'sr':
+      return messages_sr.messages;
+    default:
+      return null;
+  }
+}
+
+/// User programs should call this before using [localeName] for messages.
+Future<bool> initializeMessages(String localeName) async {
+  var availableLocale = Intl.verifiedLocale(
+      localeName, (locale) => _deferredLibraries[locale] != null,
+      onFailure: (_) => null);
+  if (availableLocale == null) {
+    return new Future.value(false);
+  }
+  var lib = _deferredLibraries[availableLocale];
+  await (lib == null ? new Future.value(false) : lib());
+  initializeInternalMessageLookup(() => new CompositeMessageLookup());
+  messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
+  return new Future.value(true);
+}
+
+bool _messagesExistFor(String locale) {
+  try {
+    return _findExact(locale) != null;
+  } catch (e) {
+    return false;
+  }
+}
+
+MessageLookupByLibrary _findGeneratedMessagesFor(String locale) {
+  var actualLocale =
+      Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null);
+  if (actualLocale == null) return null;
+  return _findExact(actualLocale);
+}
diff --git a/bin/simple_browser_internationalization/lib/localization/messages_messages.dart b/bin/simple_browser_internationalization/lib/localization/messages_messages.dart
new file mode 100644
index 0000000..c60187c
--- /dev/null
+++ b/bin/simple_browser_internationalization/lib/localization/messages_messages.dart
@@ -0,0 +1,30 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a messages locale. All the
+// messages from the main program should be duplicated here with the same
+// function name.
+
+// Ignore issues from commonly used lints in this file.
+// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
+// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
+// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
+// ignore_for_file:unused_import, file_names
+
+import 'package:intl/intl.dart';
+import 'package:intl/message_lookup_by_library.dart';
+
+final messages = new MessageLookup();
+
+typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'messages';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static _notInlinedMessages(_) => <String, Function>{
+        "back": MessageLookupByLibrary.simpleMessage("Bck"),
+        "browser": MessageLookupByLibrary.simpleMessage("Browser"),
+        "forward": MessageLookupByLibrary.simpleMessage("Fwd"),
+        "refresh": MessageLookupByLibrary.simpleMessage("Rfrsh"),
+        "search": MessageLookupByLibrary.simpleMessage("Search")
+      };
+}
diff --git a/bin/simple_browser_internationalization/lib/localization/messages_nl.dart b/bin/simple_browser_internationalization/lib/localization/messages_nl.dart
new file mode 100644
index 0000000..f458900
--- /dev/null
+++ b/bin/simple_browser_internationalization/lib/localization/messages_nl.dart
@@ -0,0 +1,30 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a nl locale. All the
+// messages from the main program should be duplicated here with the same
+// function name.
+
+// Ignore issues from commonly used lints in this file.
+// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
+// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
+// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
+// ignore_for_file:unused_import, file_names
+
+import 'package:intl/intl.dart';
+import 'package:intl/message_lookup_by_library.dart';
+
+final messages = new MessageLookup();
+
+typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'nl';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static _notInlinedMessages(_) => <String, Function>{
+        "back": MessageLookupByLibrary.simpleMessage("Trg"),
+        "browser": MessageLookupByLibrary.simpleMessage("Browser"),
+        "forward": MessageLookupByLibrary.simpleMessage("Vor"),
+        "refresh": MessageLookupByLibrary.simpleMessage("Vrvrs"),
+        "search": MessageLookupByLibrary.simpleMessage("Zoek")
+      };
+}
diff --git a/bin/simple_browser_internationalization/lib/localization/messages_sr.dart b/bin/simple_browser_internationalization/lib/localization/messages_sr.dart
new file mode 100644
index 0000000..2c1e744
--- /dev/null
+++ b/bin/simple_browser_internationalization/lib/localization/messages_sr.dart
@@ -0,0 +1,30 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a sr locale. All the
+// messages from the main program should be duplicated here with the same
+// function name.
+
+// Ignore issues from commonly used lints in this file.
+// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
+// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
+// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
+// ignore_for_file:unused_import, file_names
+
+import 'package:intl/intl.dart';
+import 'package:intl/message_lookup_by_library.dart';
+
+final messages = new MessageLookup();
+
+typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'sr';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static _notInlinedMessages(_) => <String, Function>{
+        "back": MessageLookupByLibrary.simpleMessage("Наз"),
+        "browser": MessageLookupByLibrary.simpleMessage("Прегледач"),
+        "forward": MessageLookupByLibrary.simpleMessage("Нпр"),
+        "refresh": MessageLookupByLibrary.simpleMessage("Освж"),
+        "search": MessageLookupByLibrary.simpleMessage("Претрага")
+      };
+}
diff --git a/bin/simple_browser_internationalization/lib/localizations_delegate.dart b/bin/simple_browser_internationalization/lib/localizations_delegate.dart
new file mode 100644
index 0000000..f8d782d
--- /dev/null
+++ b/bin/simple_browser_internationalization/lib/localizations_delegate.dart
@@ -0,0 +1,47 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Should the localizations delegate be generated instead?
+
+import 'dart:async';
+import 'dart:ui' show Locale;
+
+import 'package:flutter/material.dart';
+import 'package:intl/intl.dart';
+import 'package:fuchsia_logger/logger.dart';
+
+import 'localization/messages_all.dart' as messages_all;
+import 'supported_locales.dart' as supported_locales;
+
+LocalizationsDelegate<void> delegate() => _LocalizationsDelegate();
+
+class _LocalizationsDelegate extends LocalizationsDelegate<void> {
+  static Future<void> loadLocale(Locale locale) async {
+    final String name =
+        (locale.countryCode == null || locale.countryCode.isEmpty)
+            ? locale.languageCode
+            : locale.toString();
+    final String localeName = Intl.canonicalizedLocale(name);
+    await messages_all.initializeMessages(localeName);
+    log.info(
+        '_LocalizationsDelegate: current default locale: ${Intl.defaultLocale}');
+  }
+
+  const _LocalizationsDelegate();
+
+  @override
+  Future<void> load(Locale locale) => loadLocale(locale);
+
+  // For the time being, never reload.
+  @override
+  bool shouldReload(_LocalizationsDelegate __) => false;
+
+  @override
+  bool isSupported(Locale locale) {
+    bool supported = supported_locales.locales.contains(locale);
+    log.finer(
+        '_LocalizationsDelegate: locale: ${locale.toString()}; isSupported: ${supported.toString()}');
+    return supported;
+  }
+}
diff --git a/bin/simple_browser_internationalization/lib/strings.dart b/bin/simple_browser_internationalization/lib/strings.dart
new file mode 100644
index 0000000..7c4b6ba
--- /dev/null
+++ b/bin/simple_browser_internationalization/lib/strings.dart
@@ -0,0 +1,48 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// This file is an L10N developer experience study fragment.
+// Let's see how it works when you offload all strings to a file.
+
+// The Intl.message texts have been offloaded to a separate file so as to
+// minimize the amount of code needed to be exported for localization.
+// While the individual static String get messages have been left in their original form,
+// the case-ness of the text is part of the presentation so should probably be
+// handled in the view code.
+
+import 'package:intl/intl.dart';
+
+/// Provides access to localized strings used in Ermine code.
+class Strings {
+  static final Strings instance = Strings._internal();
+  factory Strings() => instance;
+  Strings._internal();
+
+  static String get back => Intl.message(
+        'Bck',
+        name: 'back',
+        desc: 'A very short label meaning "Go back (to previous web page)"',
+      );
+  static String get forward => Intl.message(
+        'Fwd',
+        name: 'forward',
+        desc: 'A very short label meaning "Go forward (to the next web page)"',
+      );
+  static String get refresh => Intl.message(
+        'Rfrsh',
+        name: 'refresh',
+        desc: 'A very short label meaning "Refresh the web page"',
+      );
+  static String get search => Intl.message(
+        'Search',
+        name: 'search',
+        desc:
+            'A regular length string label appearing in the browser search bar',
+      );
+  static String get browser => Intl.message(
+        'Browser',
+        name: 'browser',
+        desc: 'As in: web browser',
+      );
+}
diff --git a/bin/simple_browser_internationalization/lib/supported_locales.dart b/bin/simple_browser_internationalization/lib/supported_locales.dart
new file mode 100644
index 0000000..84d978f
--- /dev/null
+++ b/bin/simple_browser_internationalization/lib/supported_locales.dart
@@ -0,0 +1,10 @@
+// Copyright 2019 The Fuchsia 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:ui' show Locale;
+
+List<Locale> locales = [
+  const Locale.fromSubtags(languageCode: 'sr'),
+  const Locale.fromSubtags(languageCode: 'nl'),
+];
diff --git a/bin/simple_browser_internationalization/pubspec.yaml b/bin/simple_browser_internationalization/pubspec.yaml
new file mode 100644
index 0000000..4c61966
--- /dev/null
+++ b/bin/simple_browser_internationalization/pubspec.yaml
@@ -0,0 +1,12 @@
+# Copyright 2019 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+name: internationalization
+description: The internationalization library for Ermine.
+
+flutter:
+
+dependencies:
+  # Needed for the ARB extraction scripts until this is fully automated.
+  intl_translation: ^0.17.2
+
diff --git a/bin/simple_browser_internationalization/resources/intl_messages.arb b/bin/simple_browser_internationalization/resources/intl_messages.arb
new file mode 100644
index 0000000..9556f92
--- /dev/null
+++ b/bin/simple_browser_internationalization/resources/intl_messages.arb
@@ -0,0 +1,33 @@
+{
+  "@@last_modified": "2019-11-11T17:42:01.328995",
+  "back": "Bck",
+  "@back": {
+    "description": "A very short label meaning \"Go back (to previous web page)\"",
+    "type": "text",
+    "placeholders": {}
+  },
+  "forward": "Fwd",
+  "@forward": {
+    "description": "A very short label meaning \"Go forward (to the next web page)\"",
+    "type": "text",
+    "placeholders": {}
+  },
+  "refresh": "Rfrsh",
+  "@refresh": {
+    "description": "A very short label meaning \"Refresh the web page\"",
+    "type": "text",
+    "placeholders": {}
+  },
+  "search": "Search",
+  "@search": {
+    "description": "A regular length string label appearing in the browser search bar",
+    "type": "text",
+    "placeholders": {}
+  },
+  "browser": "Browser",
+  "@browser": {
+    "description": "As in: web browser",
+    "type": "text",
+    "placeholders": {}
+  }
+}
diff --git a/bin/simple_browser_internationalization/resources/intl_nl.arb b/bin/simple_browser_internationalization/resources/intl_nl.arb
new file mode 100644
index 0000000..9728252
--- /dev/null
+++ b/bin/simple_browser_internationalization/resources/intl_nl.arb
@@ -0,0 +1,33 @@
+{
+  "@@last_modified": "2019-11-06T17:31:05.454814",
+  "back": "Trg",
+  "@back": {
+    "description": "A very short label meaning \"Go back (to previous web page)\"",
+    "type": "text",
+    "placeholders": {}
+  },
+  "forward": "Vor",
+  "@forward": {
+    "description": "A very short label meaning \"Go forward (to the next web page)\"",
+    "type": "text",
+    "placeholders": {}
+  },
+  "refresh": "Vrvrs",
+  "@refresh": {
+    "description": "A very short label meaning \"Refresh the web page\"",
+    "type": "text",
+    "placeholders": {}
+  },
+  "search": "Zoek",
+  "@search": {
+    "description": "A regular length string label appearing in the browser search bar",
+    "type": "text",
+    "placeholders": {}
+  },
+  "browser": "Browser",
+  "@browser": {
+    "description": "As in: web browser",
+    "type": "text",
+    "placeholders": {}
+  }
+}
diff --git a/bin/simple_browser_internationalization/resources/intl_sr.arb b/bin/simple_browser_internationalization/resources/intl_sr.arb
new file mode 100644
index 0000000..7aca5cd
--- /dev/null
+++ b/bin/simple_browser_internationalization/resources/intl_sr.arb
@@ -0,0 +1,33 @@
+{
+  "@@last_modified": "2019-11-06T17:31:05.454814",
+  "back": "Наз",
+  "@back": {
+    "description": "A very short label meaning \"Go back (to previous web page)\"",
+    "type": "text",
+    "placeholders": {}
+  },
+  "forward": "Нпр",
+  "@forward": {
+    "description": "A very short label meaning \"Go forward (to the next web page)\"",
+    "type": "text",
+    "placeholders": {}
+  },
+  "refresh": "Освж",
+  "@refresh": {
+    "description": "A very short label meaning \"Refresh the web page\"",
+    "type": "text",
+    "placeholders": {}
+  },
+  "search": "Претрага",
+  "@search": {
+    "description": "A regular length string label appearing in the browser search bar",
+    "type": "text",
+    "placeholders": {}
+  },
+  "browser": "Прегледач",
+  "@browser": {
+    "description": "As in: web browser",
+    "type": "text",
+    "placeholders": {}
+  }
+}
diff --git a/bin/simple_browser_internationalization/scripts/run_extract_to_arb.sh b/bin/simple_browser_internationalization/scripts/run_extract_to_arb.sh
new file mode 100755
index 0000000..d847f84
--- /dev/null
+++ b/bin/simple_browser_internationalization/scripts/run_extract_to_arb.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+# Copyright 2019 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+set -x
+
+$FUCHSIA_DIR/third_party/dart-pkg/git/flutter/bin/flutter \
+  packages pub get intl_translation:extract_to_arb
+
+$FUCHSIA_DIR/third_party/dart-pkg/git/flutter/bin/flutter \
+  packages pub run intl_translation:extract_to_arb \
+  --output-dir=resources lib/*strings.dart
+
+rm .packages pubspec.lock
+rm -fr .dart_tool
diff --git a/bin/simple_browser_internationalization/scripts/run_generate_from_arb.sh b/bin/simple_browser_internationalization/scripts/run_generate_from_arb.sh
new file mode 100755
index 0000000..93f7b81
--- /dev/null
+++ b/bin/simple_browser_internationalization/scripts/run_generate_from_arb.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+# Copyright 2019 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+set -x
+
+$FUCHSIA_DIR/third_party/dart-pkg/git/flutter/bin/flutter \
+  packages pub get intl_translation:generate_from_arb
+
+$FUCHSIA_DIR/third_party/dart-pkg/git/flutter/bin/flutter \
+  packages pub run intl_translation:generate_from_arb \
+  --output-dir=lib/localization \
+  lib/*strings.dart \
+  resources/*.arb
+
+rm .packages pubspec.lock
+rm -fr .dart_tool