feat(i18n): Task #7 — Hebrew/English localization infrastructure
- Strings.resx (Hebrew, default) + Strings.en-US.resx (English override)
cover every dialog/sidebar/ribbon string, plus filing summary format
templates (`Msg_FilingSummary_*`)
- LocalizationManager singleton wraps ResourceManager with Get() and
Format() helpers; SetCulture honors AppSettings.Locale
- LocExtension XAML markup ({l:Loc Key}) for bindings
- Themes/RtlFonts.xaml — global RightToLeft FlowDirection and Hebrew
font stack (Segoe UI / David / Frank Ruehl / Arial)
- AddInHost calls LocalizationManager.SetCulture(Settings.Locale) before
any UI thread reads a resource
Existing dialogs keep their hardcoded Hebrew for v1 — those are already
in the correct (he-IL) language. Future iterations can switch to
{l:Loc …} bindings; the resource files already enumerate every string so
the migration is mechanical.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Windows.Markup;
|
||||
using MarcusLaw.OutlookAddin.UI.Services;
|
||||
|
||||
namespace MarcusLaw.OutlookAddin.UI.Markup
|
||||
{
|
||||
/// <summary>
|
||||
/// XAML markup extension that resolves a resource key against
|
||||
/// <see cref="LocalizationManager.Instance"/>. Usage:
|
||||
/// <code><TextBlock Text="{l:Loc Settings_Title}" /></code>
|
||||
///
|
||||
/// Resolution is one-shot — culture changes require a restart, matching
|
||||
/// the design in PROJECT-BRIEFING.md ("Override via SettingsManager…
|
||||
/// requires restart").
|
||||
/// </summary>
|
||||
[MarkupExtensionReturnType(typeof(string))]
|
||||
public sealed class LocExtension : MarkupExtension
|
||||
{
|
||||
public string? Key { get; set; }
|
||||
|
||||
public LocExtension() { }
|
||||
|
||||
public LocExtension(string key) { Key = key; }
|
||||
|
||||
public override object ProvideValue(IServiceProvider serviceProvider)
|
||||
{
|
||||
return string.IsNullOrEmpty(Key) ? string.Empty : LocalizationManager.Instance.Get(Key!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<resheader name="resmimetype"><value>text/microsoft-resx</value></resheader>
|
||||
<resheader name="version"><value>2.0</value></resheader>
|
||||
<resheader name="reader"><value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></resheader>
|
||||
<resheader name="writer"><value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></resheader>
|
||||
|
||||
<data name="Btn_Save" xml:space="preserve"><value>Save</value></data>
|
||||
<data name="Btn_Cancel" xml:space="preserve"><value>Cancel</value></data>
|
||||
<data name="Btn_File" xml:space="preserve"><value>File</value></data>
|
||||
<data name="Btn_Continue" xml:space="preserve"><value>Continue</value></data>
|
||||
<data name="Btn_TestConnection" xml:space="preserve"><value>Test connection</value></data>
|
||||
|
||||
<data name="FileToCase_Title" xml:space="preserve"><value>File mail to EspoCRM</value></data>
|
||||
<data name="FileToCase_SearchPlaceholder" xml:space="preserve"><value>Search case, account, contact…</value></data>
|
||||
<data name="FileToCase_RecentLabel" xml:space="preserve"><value>Recent</value></data>
|
||||
<data name="FileToCase_CreateContact" xml:space="preserve"><value>Create new contact from sender</value></data>
|
||||
|
||||
<data name="ComposeFromCase_Title" xml:space="preserve"><value>Compose mail from case</value></data>
|
||||
<data name="ComposeFromCase_SearchLabel" xml:space="preserve"><value>Search case:</value></data>
|
||||
|
||||
<data name="Settings_Title" xml:space="preserve"><value>EspoCRM Add-in settings</value></data>
|
||||
<data name="Settings_TabGeneral" xml:space="preserve"><value>General</value></data>
|
||||
<data name="Settings_TabFolders" xml:space="preserve"><value>Folders</value></data>
|
||||
<data name="Settings_TabAbout" xml:space="preserve"><value>About</value></data>
|
||||
<data name="Settings_ServerUrl" xml:space="preserve"><value>EspoCRM server URL</value></data>
|
||||
<data name="Settings_Username" xml:space="preserve"><value>Username</value></data>
|
||||
<data name="Settings_ApiKey" xml:space="preserve"><value>API key</value></data>
|
||||
<data name="Settings_Language" xml:space="preserve"><value>Interface language</value></data>
|
||||
<data name="Settings_FoldersHint" xml:space="preserve"><value>Pick folders to watch. In Auto-file mode, the add-in files mail it can match with high confidence; otherwise it tags "Needs filing".</value></data>
|
||||
<data name="Settings_ColWatched" xml:space="preserve"><value>Watch</value></data>
|
||||
<data name="Settings_ColFolder" xml:space="preserve"><value>Folder</value></data>
|
||||
<data name="Settings_ColAccount" xml:space="preserve"><value>Account</value></data>
|
||||
<data name="Settings_ColMode" xml:space="preserve"><value>Mode</value></data>
|
||||
<data name="Settings_ModeNotifyOnly" xml:space="preserve"><value>Notify only</value></data>
|
||||
<data name="Settings_ModeAutoFile" xml:space="preserve"><value>Auto-file</value></data>
|
||||
|
||||
<data name="Sidebar_Title" xml:space="preserve"><value>Case info</value></data>
|
||||
<data name="Sidebar_EmptyHint" xml:space="preserve"><value>Select a mail to see case details</value></data>
|
||||
<data name="Sidebar_NoMatch" xml:space="preserve"><value>No contact match for</value></data>
|
||||
<data name="Sidebar_OpenManualSearch" xml:space="preserve"><value>Search manually in EspoCRM</value></data>
|
||||
<data name="Sidebar_Loading" xml:space="preserve"><value>Loading…</value></data>
|
||||
<data name="Sidebar_ContactLabel" xml:space="preserve"><value>Contact</value></data>
|
||||
<data name="Sidebar_AccountLabel" xml:space="preserve"><value>Account</value></data>
|
||||
<data name="Sidebar_RecentCasesLabel" xml:space="preserve"><value>Recent cases</value></data>
|
||||
<data name="Sidebar_NoCases" xml:space="preserve"><value>(no open cases)</value></data>
|
||||
<data name="Sidebar_FileToThis" xml:space="preserve"><value>File to this case</value></data>
|
||||
<data name="Sidebar_FileElsewhere" xml:space="preserve"><value>File elsewhere…</value></data>
|
||||
<data name="Sidebar_OpenInCrm" xml:space="preserve"><value>Open in EspoCRM</value></data>
|
||||
|
||||
<data name="Ribbon_FileToEspoCrm" xml:space="preserve"><value>File to EspoCRM</value></data>
|
||||
<data name="Ribbon_FileToEspoCrm_Screentip" xml:space="preserve"><value>File the selected mail to EspoCRM</value></data>
|
||||
<data name="Ribbon_ComposeFromCase" xml:space="preserve"><value>Compose from case</value></data>
|
||||
<data name="Ribbon_Settings" xml:space="preserve"><value>Settings</value></data>
|
||||
|
||||
<data name="Msg_NotConfigured_OpenSettings" xml:space="preserve"><value>Add-in is not configured yet. Open settings now?</value></data>
|
||||
<data name="Msg_NoMailSelected" xml:space="preserve"><value>No mail selected. Pick one or more messages and try again.</value></data>
|
||||
<data name="Msg_AuthRejected" xml:space="preserve"><value>API key was rejected. Update settings and retry.</value></data>
|
||||
<data name="Msg_FilingSummary_Filed" xml:space="preserve"><value>Filed {0} message(s) to "{1}"</value></data>
|
||||
<data name="Msg_FilingSummary_Queued" xml:space="preserve"><value>{0} queued for retry</value></data>
|
||||
<data name="Msg_FilingSummary_Failed" xml:space="preserve"><value>{0} failed</value></data>
|
||||
<data name="Msg_FilingSummary_None" xml:space="preserve"><value>Nothing was filed.</value></data>
|
||||
</root>
|
||||
@@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<resheader name="resmimetype"><value>text/microsoft-resx</value></resheader>
|
||||
<resheader name="version"><value>2.0</value></resheader>
|
||||
<resheader name="reader"><value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></resheader>
|
||||
<resheader name="writer"><value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></resheader>
|
||||
|
||||
<!-- Common buttons -->
|
||||
<data name="Btn_Save" xml:space="preserve"><value>שמור</value></data>
|
||||
<data name="Btn_Cancel" xml:space="preserve"><value>ביטול</value></data>
|
||||
<data name="Btn_File" xml:space="preserve"><value>תייק</value></data>
|
||||
<data name="Btn_Continue" xml:space="preserve"><value>המשך</value></data>
|
||||
<data name="Btn_TestConnection" xml:space="preserve"><value>בדוק חיבור</value></data>
|
||||
|
||||
<!-- File-to-Case dialog -->
|
||||
<data name="FileToCase_Title" xml:space="preserve"><value>תיוק מייל ל-EspoCRM</value></data>
|
||||
<data name="FileToCase_SearchPlaceholder" xml:space="preserve"><value>חפש תיק, לקוח, איש קשר…</value></data>
|
||||
<data name="FileToCase_RecentLabel" xml:space="preserve"><value>אחרונים</value></data>
|
||||
<data name="FileToCase_CreateContact" xml:space="preserve"><value>צור איש קשר חדש מהשולח</value></data>
|
||||
|
||||
<!-- Compose-from-Case dialog -->
|
||||
<data name="ComposeFromCase_Title" xml:space="preserve"><value>כתוב מייל מתיק</value></data>
|
||||
<data name="ComposeFromCase_SearchLabel" xml:space="preserve"><value>חפש תיק:</value></data>
|
||||
|
||||
<!-- Settings dialog -->
|
||||
<data name="Settings_Title" xml:space="preserve"><value>הגדרות תוסף EspoCRM</value></data>
|
||||
<data name="Settings_TabGeneral" xml:space="preserve"><value>כללי</value></data>
|
||||
<data name="Settings_TabFolders" xml:space="preserve"><value>תיקיות</value></data>
|
||||
<data name="Settings_TabAbout" xml:space="preserve"><value>אודות</value></data>
|
||||
<data name="Settings_ServerUrl" xml:space="preserve"><value>כתובת שרת EspoCRM</value></data>
|
||||
<data name="Settings_Username" xml:space="preserve"><value>שם משתמש</value></data>
|
||||
<data name="Settings_ApiKey" xml:space="preserve"><value>מפתח API</value></data>
|
||||
<data name="Settings_Language" xml:space="preserve"><value>שפת ממשק</value></data>
|
||||
<data name="Settings_FoldersHint" xml:space="preserve"><value>סמן תיקיות לניטור. במצב 'אוטומטי' התוסף יתייק לבד מיילים שזוהו בוודאות, אחרת רק יסמן 'נדרש תיוק'.</value></data>
|
||||
<data name="Settings_ColWatched" xml:space="preserve"><value>נטור</value></data>
|
||||
<data name="Settings_ColFolder" xml:space="preserve"><value>תיקייה</value></data>
|
||||
<data name="Settings_ColAccount" xml:space="preserve"><value>חשבון</value></data>
|
||||
<data name="Settings_ColMode" xml:space="preserve"><value>מצב</value></data>
|
||||
<data name="Settings_ModeNotifyOnly" xml:space="preserve"><value>התרעה בלבד</value></data>
|
||||
<data name="Settings_ModeAutoFile" xml:space="preserve"><value>תיוק אוטומטי</value></data>
|
||||
|
||||
<!-- Reading-pane sidebar -->
|
||||
<data name="Sidebar_Title" xml:space="preserve"><value>פרטי תיק</value></data>
|
||||
<data name="Sidebar_EmptyHint" xml:space="preserve"><value>סמן מייל כדי לראות את פרטי התיק</value></data>
|
||||
<data name="Sidebar_NoMatch" xml:space="preserve"><value>לא נמצא איש קשר ל-</value></data>
|
||||
<data name="Sidebar_OpenManualSearch" xml:space="preserve"><value>חפש ידנית ב-EspoCRM</value></data>
|
||||
<data name="Sidebar_Loading" xml:space="preserve"><value>טוען…</value></data>
|
||||
<data name="Sidebar_ContactLabel" xml:space="preserve"><value>איש קשר</value></data>
|
||||
<data name="Sidebar_AccountLabel" xml:space="preserve"><value>לקוח/חברה</value></data>
|
||||
<data name="Sidebar_RecentCasesLabel" xml:space="preserve"><value>תיקים אחרונים</value></data>
|
||||
<data name="Sidebar_NoCases" xml:space="preserve"><value>(אין תיקים פתוחים)</value></data>
|
||||
<data name="Sidebar_FileToThis" xml:space="preserve"><value>תייק לתיק הזה</value></data>
|
||||
<data name="Sidebar_FileElsewhere" xml:space="preserve"><value>תייק למקום אחר…</value></data>
|
||||
<data name="Sidebar_OpenInCrm" xml:space="preserve"><value>פתח ב-EspoCRM</value></data>
|
||||
|
||||
<!-- Ribbon labels -->
|
||||
<data name="Ribbon_FileToEspoCrm" xml:space="preserve"><value>תייק ל-EspoCRM</value></data>
|
||||
<data name="Ribbon_FileToEspoCrm_Screentip" xml:space="preserve"><value>תייק את המייל הנבחר ל-EspoCRM</value></data>
|
||||
<data name="Ribbon_ComposeFromCase" xml:space="preserve"><value>כתוב מתיק</value></data>
|
||||
<data name="Ribbon_Settings" xml:space="preserve"><value>הגדרות</value></data>
|
||||
|
||||
<!-- Toasts / messages -->
|
||||
<data name="Msg_NotConfigured_OpenSettings" xml:space="preserve"><value>התוסף עדיין לא מוגדר. לפתוח את חלון ההגדרות עכשיו?</value></data>
|
||||
<data name="Msg_NoMailSelected" xml:space="preserve"><value>לא נבחר מייל לתיוק. סמן מייל אחד או יותר ונסה שוב.</value></data>
|
||||
<data name="Msg_AuthRejected" xml:space="preserve"><value>מפתח ה-API נדחה. יש לעדכן את ההגדרות ולנסות שוב.</value></data>
|
||||
<data name="Msg_FilingSummary_Filed" xml:space="preserve"><value>תויקו {0} מיילים אל "{1}"</value></data>
|
||||
<data name="Msg_FilingSummary_Queued" xml:space="preserve"><value>{0} בתור לניסיון חוזר</value></data>
|
||||
<data name="Msg_FilingSummary_Failed" xml:space="preserve"><value>{0} נכשלו</value></data>
|
||||
<data name="Msg_FilingSummary_None" xml:space="preserve"><value>לא נשלחו פריטים.</value></data>
|
||||
</root>
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Resources;
|
||||
using System.Threading;
|
||||
|
||||
namespace MarcusLaw.OutlookAddin.UI.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Strongly-typed wrapper around <see cref="ResourceManager"/>. The
|
||||
/// default culture is Hebrew (he-IL) — that's the user base. Switch by
|
||||
/// calling <see cref="SetCulture"/> from SettingsViewModel.
|
||||
/// </summary>
|
||||
public sealed class LocalizationManager
|
||||
{
|
||||
private const string BaseName = "MarcusLaw.OutlookAddin.UI.Resources.Strings";
|
||||
|
||||
public static LocalizationManager Instance { get; } = new LocalizationManager();
|
||||
|
||||
public ResourceManager ResourceManager { get; }
|
||||
|
||||
private LocalizationManager()
|
||||
{
|
||||
ResourceManager = new ResourceManager(BaseName, typeof(LocalizationManager).Assembly);
|
||||
}
|
||||
|
||||
public string Get(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ResourceManager.GetString(key, CultureInfo.CurrentUICulture) ?? key;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
public string Format(string key, params object[] args)
|
||||
{
|
||||
var template = Get(key);
|
||||
try { return string.Format(CultureInfo.CurrentUICulture, template, args); }
|
||||
catch { return template; }
|
||||
}
|
||||
|
||||
public static void SetCulture(string cultureName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cultureName)) return;
|
||||
try
|
||||
{
|
||||
var ci = CultureInfo.GetCultureInfo(cultureName);
|
||||
Thread.CurrentThread.CurrentUICulture = ci;
|
||||
CultureInfo.DefaultThreadCurrentUICulture = ci;
|
||||
}
|
||||
catch (CultureNotFoundException) { /* leave as-is */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<!-- Apply this dictionary at App level (or merge into a Window's Resources)
|
||||
to set a consistent RTL layout and a Hebrew-capable font stack across
|
||||
every WPF window and user control. -->
|
||||
<Style TargetType="Window">
|
||||
<Setter Property="FlowDirection" Value="RightToLeft" />
|
||||
<Setter Property="FontFamily" Value="Segoe UI, David, Frank Ruehl, Arial" />
|
||||
<Setter Property="UseLayoutRounding" Value="True" />
|
||||
</Style>
|
||||
<Style TargetType="UserControl">
|
||||
<Setter Property="FlowDirection" Value="RightToLeft" />
|
||||
<Setter Property="FontFamily" Value="Segoe UI, David, Frank Ruehl, Arial" />
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -57,6 +57,10 @@ namespace OutlookAddin.Services
|
||||
|
||||
SettingsManager = new SettingsManager();
|
||||
Settings = SettingsManager.Load();
|
||||
|
||||
// Apply the chosen UI locale before any view-model touches resource strings.
|
||||
MarcusLaw.OutlookAddin.UI.Services.LocalizationManager.SetCulture(Settings.Locale);
|
||||
|
||||
CredentialStore = new DpapiCredentialStore();
|
||||
RetryQueue = new DiskRetryQueue(Logger);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user