מדריך: איך לייצר לוקליזציה ב-wpf בצורה פשוטה וקלילה
-
@קומפיונט כתב במדריך: איך לייצר לוקליזציה ב-wpf בצורה פשוטה וקלילה:
תנסה ליישם משהו שמאפשר לשנות את השפה בצורה דינאמית, מבלי להפעיל מחדש
אם בשביל הספורט, אדרבא, שינסה או תנסה אתה או כל מי שרוצה לבדוק את כישוריו.
אבל אם בשביל טובת המוצר, צריך לזכור שאין שכלולים (פיצ'רים בלעז) בחינם.
אם אנחנו לא נעצרים לחשוב כמה השכלול נדרש וישר רוצים אותו כי זה פשוט כולו טוב - "שכלול",
אז עלולים לשלם מחירים יקרים להחריד בגישה הזו.
לכל שכלול יש מחיר. השכלול נשלף מהשרוול, והמחיר הוא לכל מה שכבר קיים, לעיתים יקר.
זה דרגה גבוהה של הביטוי "האוייב של הטוב הוא העוד יותר טוב".נ.ב. לא התייחסתי לדוגמה הזאת, ייתכן שזה שני מילות בקוד.
זה פשוט כלל שלמדתי בשנים האחרונות בצורה קשוחה בבית הספר של החיים.
ודוקא בגלל זה, שיש לשים לב מראש בשלב התכנון, האם השכלול הזה יהיה בעתיד הקרוב מאוד נדרש (במקרה של עתיד לא קרוב מאוד צריך "לקחת בחשבון" אבל לא להשקיע תכנונים של ממש), כדי שהשינוי לא יהיה בעל מחיר גבוה. -
@OdedDvir אני גם רצתי להסתכל על המנגנון המובנה שזכרתי שישנו,
(יתרה מזאת, אני עצמי עשיתי אי פעם תוכנה בWPF עם תמיכה בשני שפות +דו כיווניות, והכל עם המנגנון הרשמי לפי הספר).
טוב אחרי עיון של כמה דקות בתיעוד לא לגמרי הצלחתי להבין איך עושים, לעומת את של @pcinfogmach שכן הבנתי תוך דקה,
ואני חושד שבאמת יש מורכבות יתר בפתרון המובנה ואולי לכן @pcinfogmach לא השתמש בו. -
גם לו יצוייר שיהיה אי פעם תור כדי לטפוח למייקרוסופט על הגב - אני בטח לא אהיה מהראשונים
אין לי בעיה שמישהו ממציא משהו קיים אם יש לזה ערך מוסף, אני בעצמי המצאתי כמה גלגלים מחדש (למרות שחלקם יצאו מרובעים...)
אני מסכים שיש למנגנון התרגום המובנה חסרונות:- קבצי ה-resource לא ניתנים לעריכה בעורך טקסט, בניגוד ל-json. אבל לענ"ד זה חיסרון שבא לידי ביטוי רק בכמות גדולה של מחרוזות, כשרוצים לסקור כמה שפות במקביל, ובמקרה הנ"ל, אף @pcinfogmach ציין שהפתרון שלו לוקה בחסר.
- לא בדיוק חסרון במנגנון, אבל סתם מעצבן, שבגרסת ה-preview של VS הם ניסו ליצור כלי יותר נוח לתחזוקה של כל השפות במקביל, רק שלא ידעתי שאין תאימות אחורה לגרסה היציבה של VS, וה-resources לא ייפתחו שם אח"כ...
למרות האמור לעיל, אחרי עקומת הלמידה של ה-setup הראשוני, היישום בקוד ממש נוח לדעתי, ואני משתמש בזה תדיר.
-
@OdedDvir
json הוא פורמט עבודה נוח וגמיש יותר בשבילי, את שאר הנימוקים כבר כתבתי וכתבו אחרים למעלה. כל אחד לפי טעמו כמובן. אני לא יכול להכחיש שיש איזושהי מעלה בפיתרון המובנה אישית זה היה מריטת עצבים עד שנמאס לי והלכתי על כיוון אחר. כשאתה בונה כמה פרוייקטים במקביל הפעולות המכניות הזוטרות האלה שחוזרים על עצמם שוב ושוב מתחילים להציק לך מאוד הם מפריעם לזרימה ולכיף שבתכנות. אז בחרתי בדרך קצת פחות מציקה.
נקוט האי כללא בידך (בעירבון מוגבל): כל דבר שאפשר לעשות עליו העתק הדבק ולמחזר אותו עבור הפרוייקט הבא שלך שווה זהב. תרגומים של פקדים שהרבה פעמים חוזרים על עצמם שווים זהב ב-json. -
@pcinfogmach כתב במדריך: איך לייצר לוקליזציה ב-wpf בצורה פשוטה וקלילה:
כשאתה בונה כמה פרוייקטים במקביל הפעולות המכניות הזוטרות האלה שחוזרים על עצמם שוב ושוב מתחילים להציק לך מאוד
הפתרון הוא לכתוב ספרייה משותפת שמאגדת את כל הבסיס לפרויקטים שלך, לדוגמא: classים בסיסיים של ViewModel (או reference לספריית MVVM כלשהיא), Localization, Theme, DI וכל מה שאתה משתמש
בכל פרויקט חדש אתה פשוט מוסיף reference לספרייה המשותפת (ואולי עוד כמה שורות קוד), וככה חסכת הרבה זמן בהקמה של הפרויקט
-
@קומפיונט כתב במדריך: איך לייצר לוקליזציה ב-wpf בצורה פשוטה וקלילה:
תנסה ליישם משהו שמאפשר לשנות את השפה בצורה דינאמית, מבלי להפעיל מחדש
אחרי תקופה הייתי צריך משהו כזה מצו"ב הקוד:
את קבצי השפה של ה-json יש להכניס לתוך תיקייה בשם "Locale".
קישור לפרוייקט דוגמאאשמח לקבל משוב
using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text.Json; using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace Localization { public static class LocalizationExtension { private static string _locale = "en"; private static readonly string _localeFolder = "Locale"; private static ConcurrentDictionary<string, string> translations; private static readonly List<WeakReference<DependencyObject>> RegisteredElements = new List<WeakReference<DependencyObject>>(); public static string Locale { get => _locale; set { if (value != _locale) { _locale = value; UpdateAllRegisteredElements(); } } } public static readonly DependencyProperty KeyProperty = DependencyProperty.RegisterAttached( "Key", typeof(string), typeof(LocalizationExtension), new PropertyMetadata(null, OnKeyChanged)); public static void SetKey(DependencyObject element, string value) => element.SetValue(KeyProperty, value); public static string GetKey(DependencyObject element) => (string)element.GetValue(KeyProperty); private static void OnKeyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d == null) return; SetText(d, (string)e.NewValue); CleanupRegisteredElements(); RegisteredElements.Add(new WeakReference<DependencyObject>(d)); } public static RelayCommand<string> ChangeLocaleCommand = new RelayCommand<string>((value) => { Locale = value; } ); public static RelayCommand NextLocaleCommand = new RelayCommand(() => { NextLocale(); }); public static void NextLocale() { string localeFolder = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), _localeFolder); if (Directory.Exists(localeFolder)) { var locales = Directory.GetFiles(localeFolder, "*.json") .Select(Path.GetFileNameWithoutExtension) .ToList(); if (locales.Count == 0) return; int currentIndex = locales.IndexOf(Locale); if (currentIndex == -1 || currentIndex == locales.Count - 1) Locale = locales[0]; else Locale = locales[currentIndex + 1]; } } private static void UpdateAllRegisteredElements() { LoadTranslations(); CleanupRegisteredElements(); foreach (var weakReference in RegisteredElements) { if (weakReference.TryGetTarget(out var target)) { var key = GetKey(target); SetText(target, key); } } } private static void CleanupRegisteredElements() => RegisteredElements.RemoveAll(wr => !wr.TryGetTarget(out _)); public static void LoadTranslations() { string filePath = GetLocaleFilePath(); try { string json = File.ReadAllText(filePath); translations = JsonSerializer.Deserialize<ConcurrentDictionary<string, string>>(json) ?? new ConcurrentDictionary<string, string>(); } catch { translations = new ConcurrentDictionary<string, string>(); } } static string GetLocaleFilePath() { string localeFolder = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), _localeFolder); string filePath = Path.Combine(localeFolder, _locale + ".json"); if (!File.Exists(filePath)) { if (Directory.Exists(localeFolder)) { string[] files = Directory.GetFiles(localeFolder, "*.json"); if (files.Length > 0) filePath = filePath = files[0]; else filePath = null; } else filePath = null; } return filePath; } public static void SetText(DependencyObject d, string key) { if (d == null || string.IsNullOrEmpty(key)) return; if (translations == null) LoadTranslations(); if (translations.TryGetValue(key, out var translation)) { switch (d) { case ContentControl contentControl: contentControl.Content = translation; break; default: var textProperty = d.GetType().GetProperty("Text"); textProperty?.SetValue(d, translation); break; } } } }
הקוד משתמש ב-class שנקרא RelayCommand
public class RelayCommand : ICommand { private readonly Action _execute; private readonly Func<bool> _canExecute; public RelayCommand(Action execute, Func<bool> canExecute = null) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute = canExecute; } public bool CanExecute(object parameter) => _canExecute?.Invoke() ?? true; public void Execute(object parameter) => _execute(); public event EventHandler CanExecuteChanged { add => CommandManager.RequerySuggested += value; remove => CommandManager.RequerySuggested -= value; } } public class RelayCommand<T> : ICommand { private readonly Action<T> _execute; private readonly Func<T, bool> _canExecute; public RelayCommand(Action<T> execute, Func<T, bool> canExecute = null) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute = canExecute; } public bool CanExecute(object parameter) => _canExecute?.Invoke((T)parameter) ?? true; public void Execute(object parameter) => _execute((T)parameter); public event EventHandler CanExecuteChanged { add => CommandManager.RequerySuggested += value; remove => CommandManager.RequerySuggested -= value; } } }
דוגמת שימוש:
<Button Width="200" Height="50" locale:LocalizationExtension.Key="LoginButton" Command="{x:Static locale:LocalizationExtension.NextLocaleCommand}"
-
@pcinfogmach מהקוד זה נראה שאתה שומר reference לכל הפקדים שצריכים להיות עם טקסט דינאמי, ומשנה אותם בהתאם מתי שצריך.
אתה יכול במקום זה להשתמש עם DynamicResource ובכל פעם שמוחלפת השפה לשנות את ה - Resrouce של השפה ב - Application.Current.Resources.
-
@קומפיונט כתב במדריך: איך לייצר לוקליזציה ב-wpf בצורה פשוטה וקלילה:
@pcinfogmach מהקוד זה נראה שאתה שומר reference לכל הפקדים שצריכים להיות עם טקסט דינאמי, ומשנה אותם בהתאם מתי שצריך.
אתה יכול במקום זה להשתמש עם DynamicResource ובכל פעם שמוחלפת השפה לשנות את ה - Resrouce של השפה ב - Application.Current.Resources.
0
אכן זה עובד יפה ומייצר קוד הרבה יותר טוב. תודה.
אשמח אם תוכל לענות לי על כמה שאלות:
אני לא מצליח לטעון את זה ב-DesignTime זה מאוד מקשה על הפיתוח, מבחינת שגיאות לא אמיתיות ועוד ועוד. אלא אם כן אני ייצר rsourcedictionary ב-xaml כמו שמתארים פה ואז אני מאבד את כל הרעיון כי הרעיון היה לייצר קוד אחד שמתאים להרבה אפליקציות עם טעינה מקובץ json
האם Application.Current.Resources. זמין ב-Vsto (תוספים לאופיס) שזה בעצם אפליקצייה של winforms שתומכת ב-wpf.
להלן הקוד שיצרתי עם זה.
using Localization; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text.Json; using System.Windows; namespace localization { public static class LocaleResouceManager { private static string _locale = "en"; private static readonly string _localeFolder = "Locale"; public static string Locale { get => _locale; set { if (value != _locale) { _locale = value; UpdateResources(); } } } public static void UpdateResources() { string filePath = GetLocaleFilePath(); if (filePath == null) return; try { string json = File.ReadAllText(filePath); var translations = JsonSerializer.Deserialize<Dictionary<string, string>>(json); if (translations != null) { var applicationResources = Application.Current.Resources; foreach (var key in translations.Keys) { if (applicationResources.Contains(key)) applicationResources[key] = translations[key]; else applicationResources.Add(key, translations[key]); } } } catch { // Handle errors (e.g., log or display error) } } public static void NextLocale() { string localeFolder = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), _localeFolder); if (Directory.Exists(localeFolder)) { var locales = Directory.GetFiles(localeFolder, "*.json") .Select(Path.GetFileNameWithoutExtension) .ToList(); if (locales.Count == 0) return; int currentIndex = locales.IndexOf(Locale); if (currentIndex == -1 || currentIndex == locales.Count - 1) Locale = locales[0]; else Locale = locales[currentIndex + 1]; } } private static string GetLocaleFilePath() { string localeFolder = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), _localeFolder); string filePath = Path.Combine(localeFolder, _locale + ".json"); if (!File.Exists(filePath)) { if (Directory.Exists(localeFolder)) { string[] files = Directory.GetFiles(localeFolder, "*.json"); filePath = files.Length > 0 ? files[0] : null; } else { filePath = null; } } return filePath; } } }
-
@pcinfogmach כתב במדריך: איך לייצר לוקליזציה ב-wpf בצורה פשוטה וקלילה:
האם Application.Current.Resources. זמין ב-Vsto (תוספים לאופיס) שזה בעצם אפליקצייה של winforms שתומכת ב-wpf.
כאן יש כמה רעיונות:
https://drwpf.com/blog/2007/10/05/managing-application-resources-when-wpf-is-hosted/רק נשאר הבעיה המצבנת של designtime
-
סקיצה סופית על ידי markupExtension ו-יצירת binding דינאמי. עובד גם על ב-designtime.
LocaleDictionary.cs
LocaleExtension.cs
RelayCommand.csזמין גם בגיטהאב
https://github.com/pcinfogmach/Wpf.Localizationדוגמת שימוש
<Window x:Class="localizationTestApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:localizationTestApp" xmlns:loc="clr-namespace:localization;assembly=localization" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center"> <Button Width="200" Height="50" Content="{loc:LocaleExtension Text=Login}" Command="{x:Static loc:LocaleDictionary.NextLocaleCommand}"/> <Button Width="200" Height="50" Content="Load Hebrew" Command="{x:Static loc:LocaleDictionary.ChangeLocaleCommand}" CommandParameter="he"/> <TextBlock Margin="10" Text="{loc:LocaleExtension Text=Welcome}"/> <TextBox Width="200" Text="{loc:LocaleExtension Text=Placeholder}"/> <ComboBox Width="200" IsEditable="True" ItemsSource="{x:Static loc:LocaleDictionary.LocaleList}" Text="{Binding Path=(loc:LocaleDictionary.Locale)}"/> </StackPanel> </Window>
-
@pcinfogmach אתה יכול לקצר את
loc:LocaleExtension
ל -loc:Locale
, הסיומת Extension לא נצרכת.
ואגב למה אתה משתמש ב - ConcurrentDictionary ולא ב - Dictionary רגיל? -
@קומפיונט
שוב תודה על העזרה המדהימה
מה ההסבר למה בעצם אני יכול לקצר את זה? הרי הוא נמצא בתוך תיקייה וnamespace שנקראת locale זה לא יגרום בעיות?השתמשתי ב-oncurrent dictionary כי לא ידעתי אם יכול להיות בעיות עם Threding אזמח לשמוע אם אתה בטוח שזה לא נצרך.
-
@pcinfogmach אתה יכול לקצר את זה כמו שאתה כותב
StaticResource
,Binding
וכו, והשם של הקלאס הואStaticResourceExtension
,BindingExtension
ה - ConcurrentDictionary מיותר כי ממילא יש ל UI רק thread חוקי אחד, וכל האירועים שאתה מקבל כגון לחיצה על כפתור רצים על אותו thread ראשי.