From 1bbfdb64778d476c3aa574fd99898dc19861687a Mon Sep 17 00:00:00 2001
From: Tsanie Lily <tsorgy@gmail.com>
Date: Tue, 5 May 2020 13:36:57 +0800
Subject: [PATCH] feature: multi-language supported

---
 Pixiview.iOS/GlobalSuppressions.cs            |   8 ++
 Pixiview.iOS/Pixiview.iOS.csproj              |   1 +
 Pixiview.iOS/Renderers/AdaptedPageRenderer.cs |   1 -
 Pixiview.iOS/Renderers/CardViewRenderer.cs    |   4 +-
 Pixiview.iOS/Services/EnvironmentService.cs   | 131 ++++++++++++++++--
 Pixiview/App.cs                               |  21 ++-
 Pixiview/MainPage.xaml                        |   3 +-
 Pixiview/Pixiview.csproj                      |   8 ++
 Pixiview/Resources/Languages/zh-CN.xml        |   4 +
 Pixiview/Resources/PlatformCulture.cs         |  43 ++++++
 Pixiview/Resources/ResourceHelper.cs          |  92 ++++++++++++
 Pixiview/Utils/IEnvironmentService.cs         |   7 +-
 12 files changed, 298 insertions(+), 25 deletions(-)
 create mode 100644 Pixiview.iOS/GlobalSuppressions.cs
 create mode 100644 Pixiview/Resources/Languages/zh-CN.xml
 create mode 100644 Pixiview/Resources/PlatformCulture.cs
 create mode 100644 Pixiview/Resources/ResourceHelper.cs

diff --git a/Pixiview.iOS/GlobalSuppressions.cs b/Pixiview.iOS/GlobalSuppressions.cs
new file mode 100644
index 0000000..4285a53
--- /dev/null
+++ b/Pixiview.iOS/GlobalSuppressions.cs
@@ -0,0 +1,8 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "<Pending>")]
diff --git a/Pixiview.iOS/Pixiview.iOS.csproj b/Pixiview.iOS/Pixiview.iOS.csproj
index e7a89a4..eb8b40c 100644
--- a/Pixiview.iOS/Pixiview.iOS.csproj
+++ b/Pixiview.iOS/Pixiview.iOS.csproj
@@ -74,6 +74,7 @@
     <Compile Include="Renderers\RoundLabelRenderer.cs" />
     <Compile Include="Renderers\CardViewRenderer.cs" />
     <Compile Include="Renderers\RoundImageRenderer.cs" />
+    <Compile Include="GlobalSuppressions.cs" />
   </ItemGroup>
   <ItemGroup>
     <InterfaceDefinition Include="Resources\LaunchScreen.storyboard" />
diff --git a/Pixiview.iOS/Renderers/AdaptedPageRenderer.cs b/Pixiview.iOS/Renderers/AdaptedPageRenderer.cs
index c06fbd2..9a4d45c 100644
--- a/Pixiview.iOS/Renderers/AdaptedPageRenderer.cs
+++ b/Pixiview.iOS/Renderers/AdaptedPageRenderer.cs
@@ -66,7 +66,6 @@ namespace Pixiview.iOS.Renderers
         }
 
         [SuppressMessage("Code Notifications", "XI0002:Notifies you from using newer Apple APIs when targeting an older OS version", Justification = "<Pending>")]
-        [SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "<Pending>")]
         private UIStatusBarStyle ConvertStyle(StatusBarStyles style)
         {
             switch (style)
diff --git a/Pixiview.iOS/Renderers/CardViewRenderer.cs b/Pixiview.iOS/Renderers/CardViewRenderer.cs
index 6f2e7d9..8971cf5 100644
--- a/Pixiview.iOS/Renderers/CardViewRenderer.cs
+++ b/Pixiview.iOS/Renderers/CardViewRenderer.cs
@@ -1,7 +1,5 @@
-using CoreGraphics;
-using Pixiview.iOS.Renderers;
+using Pixiview.iOS.Renderers;
 using Pixiview.UI;
-using UIKit;
 using Xamarin.Forms;
 using Xamarin.Forms.Platform.iOS;
 
diff --git a/Pixiview.iOS/Services/EnvironmentService.cs b/Pixiview.iOS/Services/EnvironmentService.cs
index dca6429..c060999 100644
--- a/Pixiview.iOS/Services/EnvironmentService.cs
+++ b/Pixiview.iOS/Services/EnvironmentService.cs
@@ -1,5 +1,9 @@
 using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Threading;
+using Foundation;
 using Pixiview.iOS.Services;
+using Pixiview.Resources;
 using Pixiview.Utils;
 using UIKit;
 using Xamarin.Essentials;
@@ -10,6 +14,20 @@ namespace Pixiview.iOS.Services
 {
     public class EnvironmentService : IEnvironmentService
     {
+        public EnvironmentParameter GetEnvironment()
+        {
+            return new EnvironmentParameter
+            {
+                IconLightFontFamily = "FontAwesome5Pro-Light",
+                IconRegularFontFamily = "FontAwesome5Pro-Regular",
+                IconSolidFontFamily = "FontAwesome5Pro-Solid",
+
+                IconLeft = "\uf104"     // for android, it's "\uf060"
+            };
+        }
+
+        #region - Theme -
+
         [SuppressMessage("Code Notifications", "XI0002:Notifies you from using newer Apple APIs when targeting an older OS version", Justification = "<Pending>")]
         public Theme GetApplicationTheme()
         {
@@ -36,21 +54,110 @@ namespace Pixiview.iOS.Services
             }
         }
 
-        public EnvironmentParameter GetEnvironment()
-        {
-            return new EnvironmentParameter
-            {
-                IconLightFontFamily = "FontAwesome5Pro-Light",
-                IconRegularFontFamily = "FontAwesome5Pro-Regular",
-                IconSolidFontFamily = "FontAwesome5Pro-Solid",
-
-                IconLeft = "\uf104"     // for android, it's "\uf060"
-            };
-        }
-
         public void SetStatusBarColor(Color color)
         {
             // nothing need to do
         }
+
+        #endregion
+
+        #region - Culture Info -
+
+        public CultureInfo GetCurrentCultureInfo()
+        {
+            string lang;
+            if (NSLocale.PreferredLanguages.Length > 0)
+            {
+                var pref = NSLocale.PreferredLanguages[0];
+                lang = iOSToDotnetLanguage(pref);
+            }
+            else
+            {
+                lang = "zh-CN";
+            }
+
+            CultureInfo ci;
+            var platform = new PlatformCulture(lang);
+            try
+            {
+                ci = new CultureInfo(platform.Language);
+            }
+            catch (CultureNotFoundException e)
+            {
+                try
+                {
+                    var fallback = ToDotnetFallbackLanguage(platform);
+                    App.DebugPrint($"{lang} failed, trying {fallback} ({e.Message})");
+                    ci = new CultureInfo(fallback);
+                }
+                catch (CultureNotFoundException e1)
+                {
+                    App.DebugError("culture.get", $"{lang} couldn't be set, using 'zh-CN' ({e1.Message})");
+                    ci = new CultureInfo("zh-CN");
+                }
+            }
+
+            return ci;
+        }
+
+        [SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
+        string iOSToDotnetLanguage(string iOSLanguage)
+        {
+            string netLanguage;
+
+            //certain languages need to be converted to CultureInfo equivalent
+            switch (iOSLanguage)
+            {
+                case "ms-MY":   // "Malaysian (Malaysia)" not supported .NET culture
+                case "ms-SG":   // "Malaysian (Singapore)" not supported .NET culture
+                    netLanguage = "ms"; // closest supported
+                    break;
+                case "gsw-CH":  // "Schwiizertüütsch (Swiss German)" not supported .NET culture
+                    netLanguage = "de-CH"; // closest supported
+                    break;
+                // add more application-specific cases here (if required)
+                // ONLY use cultures that have been tested and known to work
+                default:
+                    netLanguage = iOSLanguage;
+                    break;
+            }
+
+            App.DebugPrint($"iOS Language: {iOSLanguage}, .NET Language/Locale: {netLanguage}");
+            return netLanguage;
+        }
+
+        string ToDotnetFallbackLanguage(PlatformCulture platCulture)
+        {
+            string netLanguage;
+
+            switch (platCulture.LanguageCode)
+            {
+                // 
+                case "pt":
+                    netLanguage = "pt-PT"; // fallback to Portuguese (Portugal)
+                    break;
+                case "gsw":
+                    netLanguage = "de-CH"; // equivalent to German (Switzerland) for this app
+                    break;
+                // add more application-specific cases here (if required)
+                // ONLY use cultures that have been tested and known to work
+                default:
+                    netLanguage = platCulture.LanguageCode; // use the first part of the identifier (two chars, usually);
+                    break;
+            }
+
+            App.DebugPrint($".NET Fallback Language/Locale: {platCulture.LanguageCode} to {netLanguage} (application-specific)");
+            return netLanguage;
+        }
+
+        public void SetCultureInfo(CultureInfo ci)
+        {
+            Thread.CurrentThread.CurrentCulture = ci;
+            Thread.CurrentThread.CurrentUICulture = ci;
+
+            App.DebugPrint($"CurrentCulture set: {ci.Name}");
+        }
+
+        #endregion
     }
 }
diff --git a/Pixiview/App.cs b/Pixiview/App.cs
index 77180ec..686436b 100644
--- a/Pixiview/App.cs
+++ b/Pixiview/App.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
+using Pixiview.Resources;
 using Pixiview.UI.Theme;
 using Pixiview.Utils;
 using Xamarin.Forms;
@@ -11,16 +12,11 @@ namespace Pixiview
     {
         // public properties
         public static Theme CurrentTheme { get; private set; }
+        public static PlatformCulture CurrentCulture { get; private set; }
         public static Dictionary<string, object> ExtraResources { get; private set; }
 
-        public App()
+        private void InitResources(IEnvironmentService service)
         {
-            InitResources();
-        }
-
-        private void InitResources()
-        {
-            var service = DependencyService.Get<IEnvironmentService>();
             var p = service.GetEnvironment();
 
             ExtraResources = new Dictionary<string, object>
@@ -36,6 +32,13 @@ namespace Pixiview
             SetTheme(theme, true);
         }
 
+        private void InitLanguage(IEnvironmentService service)
+        {
+            var ci = service.GetCurrentCultureInfo();
+            service.SetCultureInfo(ci);
+            CurrentCulture = new PlatformCulture(ci.Name.ToLower());
+        }
+
         private void SetTheme(Theme theme, bool force = false)
         {
             if (force || theme != CurrentTheme)
@@ -62,6 +65,10 @@ namespace Pixiview
 
         protected override void OnStart()
         {
+            var service = DependencyService.Get<IEnvironmentService>();
+            InitResources(service);
+            InitLanguage(service);
+
             MainPage = UIFactory.CreateNavigationPage(new MainPage());
         }
 
diff --git a/Pixiview/MainPage.xaml b/Pixiview/MainPage.xaml
index 50cc0bf..caf028f 100644
--- a/Pixiview/MainPage.xaml
+++ b/Pixiview/MainPage.xaml
@@ -5,13 +5,14 @@
                xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                xmlns:u="clr-namespace:Pixiview.UI"
                xmlns:util="clr-namespace:Pixiview.Utils"
+               xmlns:r="clr-namespace:Pixiview.Resources"
                mc:Ignorable="d"
                x:Class="Pixiview.MainPage"
                BackgroundColor="{DynamicResource WindowColor}"
                OrientationChanged="Page_OrientationChanged"
                util:StatusBar.StatusBarStyle="{DynamicResource StatusBarStyle}">
     <NavigationPage.TitleView>
-        <u:NavigationTitle Title="Follow"
+        <u:NavigationTitle Title="{r:Text Follow}"
                            IsLeftButtonVisible="True"
                            LeftButtonClicked="NavigationTitle_LeftButtonClicked"
                            RightButtonClicked="NavigationTitle_RightButtonClicked"/>
diff --git a/Pixiview/Pixiview.csproj b/Pixiview/Pixiview.csproj
index 7ae65c0..87eb65b 100644
--- a/Pixiview/Pixiview.csproj
+++ b/Pixiview/Pixiview.csproj
@@ -19,5 +19,13 @@
     <Folder Include="UI\" />
     <Folder Include="Utils\" />
     <Folder Include="UI\Theme\" />
+    <Folder Include="Resources\" />
+    <Folder Include="Resources\Languages\" />
+  </ItemGroup>
+  <ItemGroup>
+    <None Remove="Resources\Languages\zh-CN.xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <EmbeddedResource Include="Resources\Languages\zh-CN.xml" />
   </ItemGroup>
 </Project>
\ No newline at end of file
diff --git a/Pixiview/Resources/Languages/zh-CN.xml b/Pixiview/Resources/Languages/zh-CN.xml
new file mode 100644
index 0000000..d12a3ae
--- /dev/null
+++ b/Pixiview/Resources/Languages/zh-CN.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<root>
+    <Follow>已关注</Follow>
+</root>
\ No newline at end of file
diff --git a/Pixiview/Resources/PlatformCulture.cs b/Pixiview/Resources/PlatformCulture.cs
new file mode 100644
index 0000000..cf986bf
--- /dev/null
+++ b/Pixiview/Resources/PlatformCulture.cs
@@ -0,0 +1,43 @@
+namespace Pixiview.Resources
+{
+    public class PlatformCulture
+    {
+        public string PlatformString { get; private set; }
+        public string LanguageCode { get; private set; }
+        public string LocaleCode { get; private set; }
+
+        public string Language
+        {
+            get { return string.IsNullOrEmpty(LocaleCode) ? LanguageCode : LanguageCode + "-" + LocaleCode; }
+        }
+
+        public PlatformCulture() : this(null) { }
+        public PlatformCulture(string cultureString)
+        {
+            if (string.IsNullOrEmpty(cultureString))
+            {
+                //throw new ArgumentNullException(nameof(cultureString), "Expected culture identieifer");
+                cultureString = "en";
+            }
+
+            PlatformString = cultureString.Replace('_', '-');
+            var index = PlatformString.IndexOf('-');
+            if (index > 0)
+            {
+                var parts = PlatformString.Split('-');
+                LanguageCode = parts[0];
+                LocaleCode = parts[parts.Length - 1];
+            }
+            else
+            {
+                LanguageCode = PlatformString;
+                LocaleCode = "";
+            }
+        }
+
+        public override string ToString()
+        {
+            return PlatformString;
+        }
+    }
+}
diff --git a/Pixiview/Resources/ResourceHelper.cs b/Pixiview/Resources/ResourceHelper.cs
new file mode 100644
index 0000000..46d31e3
--- /dev/null
+++ b/Pixiview/Resources/ResourceHelper.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Xml.Linq;
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+
+namespace Pixiview.Resources
+{
+    public class ResourceHelper
+    {
+        static readonly Dictionary<string, LanguageResource> dict = new Dictionary<string, LanguageResource>();
+
+        public static string GetResource(string name, params object[] args)
+        {
+            if (!dict.TryGetValue(App.CurrentCulture.PlatformString, out LanguageResource lang))
+            {
+                lang = new LanguageResource(App.CurrentCulture);
+                dict.Add(App.CurrentCulture.PlatformString, lang);
+            }
+
+            if (args == null || args.Length == 0)
+            {
+                return lang[name];
+            }
+            return string.Format(lang[name], args);
+        }
+
+        private class LanguageResource
+        {
+            private readonly Dictionary<string, string> strings;
+
+            public string this[string key]
+            {
+                get
+                {
+                    if (strings != null && strings.TryGetValue(key, out string val))
+                    {
+                        return val;
+                    }
+                    return key;
+                }
+            }
+
+            public LanguageResource(PlatformCulture lang)
+            {
+                try
+                {
+                    var assembly = IntrospectionExtensions.GetTypeInfo(typeof(LanguageResource)).Assembly;
+                    var names = assembly.GetManifestResourceNames();
+                    var name = names.FirstOrDefault(n
+                        => string.Equals(n, $"Pixiview.Resources.Languages.{lang.Language}.xml", StringComparison.OrdinalIgnoreCase)
+                        || string.Equals(n, $"Pixiview.Resources.Languages.{lang.LanguageCode}.xml", StringComparison.OrdinalIgnoreCase));
+                    if (name == null)
+                    {
+                        name = "Pixiview.Resources.Languages.zh-CN.xml";
+                    }
+                    XDocument xml;
+                    using (var stream = assembly.GetManifestResourceStream(name))
+                    {
+                        xml = XDocument.Load(stream);
+                    }
+                    strings = new Dictionary<string, string>();
+                    foreach (var ele in xml.Root.Elements())
+                    {
+                        strings[ele.Name.LocalName] = ele.Value;
+                    }
+                }
+                catch
+                {
+                    // load failed
+                }
+            }
+        }
+    }
+
+    [ContentProperty(nameof(Text))]
+    public class TextExtension : IMarkupExtension
+    {
+        public string Text { get; set; }
+
+        public object ProvideValue(IServiceProvider serviceProvider)
+        {
+            if (Text == null)
+            {
+                return string.Empty;
+            }
+            return ResourceHelper.GetResource(Text);
+        }
+    }
+}
diff --git a/Pixiview/Utils/IEnvironmentService.cs b/Pixiview/Utils/IEnvironmentService.cs
index fc37ea5..53f5645 100644
--- a/Pixiview/Utils/IEnvironmentService.cs
+++ b/Pixiview/Utils/IEnvironmentService.cs
@@ -1,12 +1,17 @@
-using Xamarin.Forms;
+using System.Globalization;
+using Xamarin.Forms;
 
 namespace Pixiview.Utils
 {
     public interface IEnvironmentService
     {
         EnvironmentParameter GetEnvironment();
+
         Theme GetApplicationTheme();
         void SetStatusBarColor(Color color);
+
+        CultureInfo GetCurrentCultureInfo();
+        void SetCultureInfo(CultureInfo ci);
     }
 
     public class EnvironmentParameter