diff --git a/GenHub/Directory.Packages.props b/GenHub/Directory.Packages.props
index e0138d49c..0a4d1f5bd 100644
--- a/GenHub/Directory.Packages.props
+++ b/GenHub/Directory.Packages.props
@@ -27,6 +27,7 @@
+
diff --git a/GenHub/GenHub.Core/Extensions/Localization/LocalizationExtensions.cs b/GenHub/GenHub.Core/Extensions/Localization/LocalizationExtensions.cs
new file mode 100644
index 000000000..0ea44ff4d
--- /dev/null
+++ b/GenHub/GenHub.Core/Extensions/Localization/LocalizationExtensions.cs
@@ -0,0 +1,300 @@
+using System.Globalization;
+using GenHub.Core.Interfaces.Localization;
+using GenHub.Core.Resources.Strings;
+
+namespace GenHub.Core.Extensions.Localization;
+
+///
+/// Extension methods for localization services and culture-aware formatting.
+///
+public static class LocalizationExtensions
+{
+ ///
+ /// Tries to get a localized string, returning a success indicator.
+ ///
+ /// The localization service.
+ /// The resource key.
+ /// The localized string if found; otherwise, null.
+ /// True if the string was found; otherwise, false.
+ public static bool TryGetString(
+ this ILocalizationService service,
+ string key,
+ out string? value)
+ {
+ ArgumentNullException.ThrowIfNull(service, nameof(service));
+ ArgumentException.ThrowIfNullOrWhiteSpace(key, nameof(key));
+
+ try
+ {
+ var result = service.GetString(StringResources.UiCommon, key);
+
+ // Check if we got a missing translation marker
+ if (result.StartsWith('[') && result.EndsWith(']'))
+ {
+ value = null;
+ return false;
+ }
+
+ value = result;
+ return true;
+ }
+ catch
+ {
+ value = null;
+ return false;
+ }
+ }
+
+ ///
+ /// Formats a date using the current culture's date format.
+ ///
+ /// The localization service.
+ /// The date to format.
+ /// Optional format string (defaults to short date pattern).
+ /// The formatted date string.
+ public static string FormatDate(
+ this ILocalizationService service,
+ DateTime date,
+ string? format = null)
+ {
+ ArgumentNullException.ThrowIfNull(service, nameof(service));
+
+ var culture = service.CurrentCulture;
+ return string.IsNullOrWhiteSpace(format)
+ ? date.ToString("d", culture) // Short date pattern
+ : date.ToString(format, culture);
+ }
+
+ ///
+ /// Formats a date and time using the current culture's format.
+ ///
+ /// The localization service.
+ /// The date and time to format.
+ /// Optional format string (defaults to short date + time pattern).
+ /// The formatted date and time string.
+ public static string FormatDateTime(
+ this ILocalizationService service,
+ DateTime dateTime,
+ string? format = null)
+ {
+ ArgumentNullException.ThrowIfNull(service, nameof(service));
+
+ var culture = service.CurrentCulture;
+ return string.IsNullOrWhiteSpace(format)
+ ? dateTime.ToString("g", culture) // Short date + time pattern
+ : dateTime.ToString(format, culture);
+ }
+
+ ///
+ /// Formats a time using the current culture's time format.
+ ///
+ /// The localization service.
+ /// The time to format.
+ /// Optional format string (defaults to short time pattern).
+ /// The formatted time string.
+ public static string FormatTime(
+ this ILocalizationService service,
+ DateTime time,
+ string? format = null)
+ {
+ ArgumentNullException.ThrowIfNull(service, nameof(service));
+
+ var culture = service.CurrentCulture;
+ return string.IsNullOrWhiteSpace(format)
+ ? time.ToString("t", culture) // Short time pattern
+ : time.ToString(format, culture);
+ }
+
+ ///
+ /// Formats a number using the current culture's number format.
+ ///
+ /// The localization service.
+ /// The number to format.
+ /// Optional number of decimal places.
+ /// The formatted number string.
+ public static string FormatNumber(
+ this ILocalizationService service,
+ double number,
+ int? decimals = null)
+ {
+ ArgumentNullException.ThrowIfNull(service, nameof(service));
+
+ var culture = service.CurrentCulture;
+ return decimals.HasValue
+ ? number.ToString($"N{decimals.Value}", culture)
+ : number.ToString("N", culture);
+ }
+
+ ///
+ /// Formats a number using the current culture's number format.
+ ///
+ /// The localization service.
+ /// The number to format.
+ /// Optional number of decimal places.
+ /// The formatted number string.
+ public static string FormatNumber(
+ this ILocalizationService service,
+ decimal number,
+ int? decimals = null)
+ {
+ ArgumentNullException.ThrowIfNull(service, nameof(service));
+
+ var culture = service.CurrentCulture;
+ return decimals.HasValue
+ ? number.ToString($"N{decimals.Value}", culture)
+ : number.ToString("N", culture);
+ }
+
+ ///
+ /// Formats a currency value using the current culture's currency format.
+ ///
+ /// The localization service.
+ /// The amount to format.
+ /// Optional ISO currency code (e.g., "USD", "EUR"). If null, uses culture's default.
+ /// The formatted currency string.
+ public static string FormatCurrency(
+ this ILocalizationService service,
+ decimal amount,
+ string? currencyCode = null)
+ {
+ ArgumentNullException.ThrowIfNull(service, nameof(service));
+
+ var culture = service.CurrentCulture;
+
+ if (!string.IsNullOrWhiteSpace(currencyCode))
+ {
+ // Create a region info to get the currency symbol
+ try
+ {
+ var regions = CultureInfo.GetCultures(CultureTypes.SpecificCultures)
+ .Select(c => new RegionInfo(c.Name))
+ .FirstOrDefault(r => r.ISOCurrencySymbol.Equals(currencyCode, StringComparison.OrdinalIgnoreCase));
+
+ if (regions != null)
+ {
+ return amount.ToString("C", culture);
+ }
+ }
+ catch
+ {
+ // Fall through to default formatting
+ }
+ }
+
+ return amount.ToString("C", culture);
+ }
+
+ ///
+ /// Formats a percentage using the current culture's percentage format.
+ ///
+ /// The localization service.
+ /// The value to format (e.g., 0.85 for 85%).
+ /// Optional number of decimal places.
+ /// The formatted percentage string.
+ public static string FormatPercentage(
+ this ILocalizationService service,
+ double value,
+ int? decimals = null)
+ {
+ ArgumentNullException.ThrowIfNull(service, nameof(service));
+
+ var culture = service.CurrentCulture;
+ return decimals.HasValue
+ ? value.ToString($"P{decimals.Value}", culture)
+ : value.ToString("P", culture);
+ }
+
+ ///
+ /// Formats a file size in a human-readable format using current culture.
+ ///
+ /// The localization service.
+ /// The size in bytes.
+ /// The formatted file size string (e.g., "1.5 MB").
+ public static string FormatFileSize(
+ this ILocalizationService service,
+ long bytes)
+ {
+ ArgumentNullException.ThrowIfNull(service, nameof(service));
+
+ string[] sizes = { "B", "KB", "MB", "GB", "TB" };
+ double size = bytes;
+ int order = 0;
+
+ while (size >= 1024 && order < sizes.Length - 1)
+ {
+ order++;
+ size /= 1024;
+ }
+
+ var culture = service.CurrentCulture;
+ return $"{size.ToString("0.##", culture)} {sizes[order]}";
+ }
+
+ ///
+ /// Gets a formatted string for plural forms based on count.
+ ///
+ /// The localization service.
+ /// The count to determine plurality.
+ /// The resource key for zero items.
+ /// The resource key for one item.
+ /// The resource key for many items.
+ /// The appropriate pluralized string.
+ public static string GetPluralString(
+ this ILocalizationService service,
+ int count,
+ string zeroKey,
+ string oneKey,
+ string manyKey)
+ {
+ ArgumentNullException.ThrowIfNull(service, nameof(service));
+ ArgumentException.ThrowIfNullOrWhiteSpace(zeroKey, nameof(zeroKey));
+ ArgumentException.ThrowIfNullOrWhiteSpace(oneKey, nameof(oneKey));
+ ArgumentException.ThrowIfNullOrWhiteSpace(manyKey, nameof(manyKey));
+
+ var key = count switch
+ {
+ 0 => zeroKey,
+ 1 => oneKey,
+ _ => manyKey
+ };
+
+ return service.GetString(StringResources.UiCommon, key, count);
+ }
+
+ ///
+ /// Checks if a culture is Right-to-Left (RTL).
+ ///
+ /// The localization service.
+ /// True if the current culture is RTL; otherwise, false.
+ public static bool IsRightToLeft(this ILocalizationService service)
+ {
+ ArgumentNullException.ThrowIfNull(service, nameof(service));
+
+ var culture = service.CurrentCulture;
+ return culture.TextInfo.IsRightToLeft;
+ }
+
+ ///
+ /// Gets the display name of the current culture in its native language.
+ ///
+ /// The localization service.
+ /// The native name of the current culture.
+ public static string GetCurrentCultureNativeName(this ILocalizationService service)
+ {
+ ArgumentNullException.ThrowIfNull(service, nameof(service));
+
+ return service.CurrentCulture.NativeName;
+ }
+
+ ///
+ /// Gets the display name of the current culture in English.
+ ///
+ /// The localization service.
+ /// The English name of the current culture.
+ public static string GetCurrentCultureEnglishName(this ILocalizationService service)
+ {
+ ArgumentNullException.ThrowIfNull(service, nameof(service));
+
+ return service.CurrentCulture.EnglishName;
+ }
+}
\ No newline at end of file
diff --git a/GenHub/GenHub.Core/GenHub.Core.csproj b/GenHub/GenHub.Core/GenHub.Core.csproj
index fb5901a66..24dbe6915 100644
--- a/GenHub/GenHub.Core/GenHub.Core.csproj
+++ b/GenHub/GenHub.Core/GenHub.Core.csproj
@@ -4,6 +4,7 @@
enable
enable
true
+ true
@@ -12,5 +13,12 @@
+
+
+
+
+ ResXFileCodeGenerator
+ GenHub.Core.Resources.Strings
+
diff --git a/GenHub/GenHub.Core/Interfaces/Localization/ILanguageProvider.cs b/GenHub/GenHub.Core/Interfaces/Localization/ILanguageProvider.cs
new file mode 100644
index 000000000..6b62aa1a7
--- /dev/null
+++ b/GenHub/GenHub.Core/Interfaces/Localization/ILanguageProvider.cs
@@ -0,0 +1,30 @@
+using System.Globalization;
+using System.Resources;
+
+namespace GenHub.Core.Interfaces.Localization;
+
+///
+/// Manages resource discovery and resource manager provisioning for localization.
+///
+public interface ILanguageProvider
+{
+ ///
+ /// Discovers all available satellite assembly cultures.
+ ///
+ /// A task that yields a list of available cultures.
+ Task> DiscoverAvailableLanguages();
+
+ ///
+ /// Gets a ResourceManager for the specified base name.
+ ///
+ /// The base name of the resource (e.g., "GenHub.Core.Resources.Strings").
+ /// The ResourceManager instance.
+ ResourceManager GetResourceManager(string baseName);
+
+ ///
+ /// Validates that a culture is available in the application.
+ ///
+ /// The culture to validate.
+ /// True if the culture is available; otherwise, false.
+ bool ValidateCulture(CultureInfo culture);
+}
\ No newline at end of file
diff --git a/GenHub/GenHub.Core/Interfaces/Localization/ILocalizationService.cs b/GenHub/GenHub.Core/Interfaces/Localization/ILocalizationService.cs
new file mode 100644
index 000000000..1be36aa65
--- /dev/null
+++ b/GenHub/GenHub.Core/Interfaces/Localization/ILocalizationService.cs
@@ -0,0 +1,62 @@
+using System.Globalization;
+
+namespace GenHub.Core.Interfaces.Localization;
+
+///
+/// Provides localization services for retrieving translated strings with runtime language switching.
+///
+public interface ILocalizationService
+{
+ ///
+ /// Gets or sets the currently active culture.
+ ///
+ CultureInfo CurrentCulture { get; set; }
+
+ ///
+ /// Gets all available cultures (languages) in the application.
+ ///
+ IReadOnlyList AvailableCultures { get; }
+
+ ///
+ /// Gets an observable that emits whenever the language/culture changes.
+ /// Subscribe to this for reactive UI updates.
+ ///
+ IObservable CultureChanged { get; }
+
+ ///
+ /// Gets a localized string from the specified resource set.
+ ///
+ /// The resource set base name (e.g., from StringResources constants).
+ /// The resource key.
+ /// The localized string, or a fallback if not found.
+ string GetString(string resourceSet, string key);
+
+ ///
+ /// Gets a formatted localized string from the specified resource set.
+ ///
+ /// The resource set base name (e.g., from StringResources constants).
+ /// The resource key.
+ /// Format arguments.
+ /// The formatted localized string, or a fallback if not found.
+ string GetString(string resourceSet, string key, params object[] args);
+
+ ///
+ /// Changes the active culture/language asynchronously.
+ ///
+ /// The culture to activate.
+ /// A task representing the asynchronous operation.
+ Task SetCulture(CultureInfo culture);
+
+ ///
+ /// Changes the active culture by language code (e.g., "en", "de") asynchronously.
+ ///
+ /// The language code.
+ /// A task representing the asynchronous operation.
+ Task SetCulture(string cultureName);
+
+ ///
+ /// Refreshes the list of available cultures by rescanning for languages.
+ ///
+ /// A task representing the asynchronous operation.
+ Task RefreshAvailableCultures();
+}
\ No newline at end of file
diff --git a/GenHub/GenHub.Core/Models/Localization/LocalizationOptions.cs b/GenHub/GenHub.Core/Models/Localization/LocalizationOptions.cs
new file mode 100644
index 000000000..3bf5756ed
--- /dev/null
+++ b/GenHub/GenHub.Core/Models/Localization/LocalizationOptions.cs
@@ -0,0 +1,45 @@
+namespace GenHub.Core.Models.Localization;
+
+///
+/// Configuration options for the localization system.
+///
+public class LocalizationOptions
+{
+ ///
+ /// Gets or sets the default culture code (e.g., "en", "en-US").
+ ///
+ ///
+ /// This is the culture that will be used when the application starts
+ /// if no user preference is set.
+ ///
+ public string DefaultCulture { get; set; } = "en";
+
+ ///
+ /// Gets or sets the fallback culture code used when a translation is missing.
+ ///
+ ///
+ /// When a translation key is not found in the current culture,
+ /// the system will attempt to use this culture's translation.
+ ///
+ public string FallbackCulture { get; set; } = "en";
+
+ ///
+ /// Gets or sets a value indicating whether to log missing translations.
+ ///
+ ///
+ /// When enabled, missing translation keys will be logged as warnings.
+ /// This is useful during development to identify untranslated content.
+ /// Default is true to help developers identify missing translations.
+ ///
+ public bool LogMissingTranslations { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether to throw an exception when a translation is missing.
+ ///
+ ///
+ /// When enabled, requesting a missing translation key will throw an exception.
+ /// This is typically disabled in production to allow graceful degradation.
+ /// Default is false to prevent application crashes.
+ ///
+ public bool ThrowOnMissingTranslation { get; set; } = false;
+}
\ No newline at end of file
diff --git a/GenHub/GenHub.Core/Resources/Strings/Errors.Operations.resx b/GenHub/GenHub.Core/Resources/Strings/Errors.Operations.resx
new file mode 100644
index 000000000..a60005b86
--- /dev/null
+++ b/GenHub/GenHub.Core/Resources/Strings/Errors.Operations.resx
@@ -0,0 +1,241 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+
+ Operation failed
+ Generic operation failure message
+
+
+ An unexpected error occurred
+ Generic unexpected error message
+
+
+ An unexpected error occurred: {0}
+ Unexpected error with details. {0} is the error message
+
+
+
+
+ Failed to load settings
+ Error message when settings fail to load
+
+
+ Failed to save settings
+ Error message when settings fail to save
+
+
+ Failed to reset settings to defaults
+ Error message when settings reset fails
+
+
+ Failed to update selected tab setting
+ Error message when updating selected tab fails
+
+
+
+
+ Error occurred while browsing for game path
+ Error message when browsing for game path fails
+
+
+ Error occurred while browsing for settings file path
+ Error message when browsing for settings file path fails
+
+
+ Error occurred while browsing for CAS root path
+ Error message when browsing for CAS root path fails
+
+
+ Failed to get memory usage
+ Error message when retrieving memory usage fails
+
+
+
+
+ Error initializing profiles
+ Error message when profile initialization fails
+
+
+ Error scanning for games
+ Error message when game scanning fails
+
+
+ Error occurred during automatic profile creation
+ Error message when automatic profile creation fails
+
+
+ Error creating profile for installation {0}
+ Error message when creating a specific profile fails. {0} is installation ID
+
+
+
+
+ Failed to load initial settings
+ Error message when loading initial settings fails
+
+
+ Could not find main window to show update dialog
+ Error message when main window cannot be found
+
+
+ Failed to show update notification window
+ Error message when showing update window fails
+
+
+
+
+ Update check failed
+ Error message when update check fails
+
+
+ Failed to install update
+ Error message when update installation fails
+
+
+ Failed to open browser for release notes.
+ Error message when opening browser for release notes fails
+
+
+
+
+ An unexpected error occurred: {0}
+ Error message when content search fails. {0} is the error message
+
+
+
+
+ Error retrieving resource string for key: {0}
+ Error message when retrieving a resource string fails. {0} is the key
+
+
+ Format error for key '{0}' with {1} arguments
+ Error message for format errors. {0} is key, {1} is argument count
+
+
+ Error formatting resource string for key: {0}
+ Error message when formatting a resource string fails. {0} is the key
+
+
+ Error setting culture to: {0}
+ Error message when setting culture fails. {0} is the culture name
+
+
+ Missing translation for key '{0}' in culture '{1}'
+ Error/warning for missing translations. {0} is key, {1} is culture
+
+
+ Missing translation for key '{0}' in culture '{1}'
+ Exception message for missing translations. {0} is key, {1} is culture
+
+
+
+
+ Error discovering available languages
+ Error message when discovering available languages fails
+
+
+ Error creating ResourceManager for base name: {0}
+ Error message when creating ResourceManager fails. {0} is the base name
+
+
+ Error validating culture: {0}
+ Error message when culture validation fails. {0} is the culture name
+
+
+ Unexpected error validating culture: {0}
+ Unexpected error during culture validation. {0} is the culture name
+
+
+ Error during satellite assembly discovery
+ Error message when satellite assembly discovery fails
+
+
+
+
+ GameProfileManager service is not available.
+ Error when GameProfileManager service is unavailable
+
+
+ Profile content loader service not available
+ Error when ProfileContentLoader service is unavailable
+
+
+ Game settings service not available
+ Error when GameSettingsService is unavailable
+
+
+
+
+ Options file directory not found
+ Error when options file directory cannot be found
+
+
+
+
+ Failed to load tools: {0}
+ Error when loading tools fails. {0} = error details
+
+
+ Error loading tool '{0}': {1}
+ Error when a specific tool fails to load. {0} = tool name, {1} = error
+
+
\ No newline at end of file
diff --git a/GenHub/GenHub.Core/Resources/Strings/Errors.Validation.resx b/GenHub/GenHub.Core/Resources/Strings/Errors.Validation.resx
new file mode 100644
index 000000000..b433fbab4
--- /dev/null
+++ b/GenHub/GenHub.Core/Resources/Strings/Errors.Validation.resx
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+
+ Invalid MaxConcurrentDownloads value: {0}. Resetting to 3.
+ Validation error for max concurrent downloads. {0} is the invalid value
+
+
+ Invalid DownloadBufferSizeKB value: {0}. Resetting to 80KB.
+ Validation error for download buffer size. {0} is the invalid value
+
+
+ Preferred game install path does not exist: {0}
+ Validation warning when specified path doesn't exist. {0} is the path
+
+
+ Settings file directory does not exist: {0}
+ Validation warning when settings directory doesn't exist. {0} is the directory
+
+
+
+
+ Concurrent downloads must be between {0} and {1}
+ Validation error for concurrent downloads range. {0} is min, {1} is max
+
+
+ Download timeout must be between {0} and {1} seconds
+ Validation error for download timeout range. {0} is min seconds, {1} is max seconds
+
+
+ Buffer size must be between {0} and {1} KB
+ Validation error for buffer size range. {0} is min KB, {1} is max KB
+
+
+
+
+ This field is required
+ Generic required field validation message
+
+
+ Invalid format
+ Generic invalid format validation message
+
+
+ Invalid path
+ Generic invalid path validation message
+
+
+ Invalid URL
+ Generic invalid URL validation message
+
+
+ Value is too low (minimum: {0})
+ Validation error when value is below minimum. {0} is the minimum value
+
+
+ Value is too high (maximum: {0})
+ Validation error when value is above maximum. {0} is the maximum value
+
+
+
+
+ Invalid culture name: {0}
+ Error when culture name is invalid. {0} is the culture name
+
+
+ Culture '{0}' is not available. Falling back to '{1}'
+ Warning when culture is not available. {0} is requested culture, {1} is fallback
+
+
+ Culture '{0}' not found. Using invariant culture
+ Warning when culture is not found. {0} is the culture name
+
+
+
+
+ Please select a game installation
+ Validation error when no game installation is selected
+
+
+ Please enter a profile name
+ Validation error when profile name is empty
+
+
+ A Game Installation must be enabled for the profile to be launchable.
+ Validation error when no game installation is enabled
+
+
\ No newline at end of file
diff --git a/GenHub/GenHub.Core/Resources/Strings/Messages.Confirmations.resx b/GenHub/GenHub.Core/Resources/Strings/Messages.Confirmations.resx
new file mode 100644
index 000000000..3f75e8328
--- /dev/null
+++ b/GenHub/GenHub.Core/Resources/Strings/Messages.Confirmations.resx
@@ -0,0 +1,167 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+
+ Are you sure you want to delete this profile?
+ Confirmation message for deleting a profile
+
+
+ Are you sure you want to delete the profile '{0}'?
+ Confirmation message for deleting a specific profile. {0} is profile name
+
+
+ Are you sure you want to delete this file?
+ Confirmation message for deleting a file
+
+
+ Are you sure you want to delete {0} files?
+ Confirmation message for deleting multiple files. {0} is file count
+
+
+ Are you sure you want to delete this folder and all its contents?
+ Confirmation message for deleting a folder
+
+
+
+
+ Are you sure you want to reset all settings to defaults?
+ Confirmation message for resetting settings
+
+
+ Are you sure you want to reset this profile to default settings?
+ Confirmation message for resetting a profile
+
+
+ Are you sure you want to clear the cache? This cannot be undone.
+ Confirmation message for clearing cache
+
+
+
+
+ A file with this name already exists. Do you want to overwrite it?
+ Confirmation message for overwriting a file
+
+
+ A profile with this name already exists. Do you want to overwrite it?
+ Confirmation message for overwriting a profile
+
+
+ Replace existing {0}?
+ Confirmation message for replacing existing item. {0} is item type
+
+
+
+
+ Are you sure you want to exit the application?
+ Confirmation message for exiting the application
+
+
+ You have unsaved changes. Do you want to save before closing?
+ Confirmation message when closing with unsaved changes
+
+
+ Are you sure you want to cancel this operation?
+ Confirmation message for canceling an operation
+
+
+ Are you sure you want to stop this download?
+ Confirmation message for stopping a download
+
+
+
+
+ Do you want to apply these changes?
+ Confirmation message for applying changes
+
+
+ Are you sure you want to discard all changes?
+ Confirmation message for discarding changes
+
+
+ Do you want to proceed with this action?
+ Generic confirmation message for proceeding with an action
+
+
+ There are warnings. Do you want to continue anyway?
+ Confirmation message for continuing despite warnings
+
+
+
+
+ An update is available. Do you want to install it now?
+ Confirmation message for installing an update
+
+
+ The application needs to restart to apply the update. Restart now?
+ Confirmation message for restarting to apply update
+
+
+
+
+ Are you sure you want to import this data? Existing data may be overwritten.
+ Confirmation message for importing data
+
+
+ Do you want to export this data?
+ Confirmation message for exporting data
+
+
\ No newline at end of file
diff --git a/GenHub/GenHub.Core/Resources/Strings/Messages.Success.resx b/GenHub/GenHub.Core/Resources/Strings/Messages.Success.resx
new file mode 100644
index 000000000..79de90664
--- /dev/null
+++ b/GenHub/GenHub.Core/Resources/Strings/Messages.Success.resx
@@ -0,0 +1,167 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+
+ Settings saved successfully
+ Success message when settings are saved
+
+
+ Settings reset to defaults
+ Success message when settings are reset to defaults
+
+
+ Settings loaded successfully
+ Success message when settings are loaded
+
+
+
+
+ Profile created successfully
+ Success message when a profile is created
+
+
+ Profile updated successfully
+ Success message when a profile is updated
+
+
+ Profile deleted successfully
+ Success message when a profile is deleted
+
+
+ Profiles saved successfully
+ Success message when profiles are saved
+
+
+
+
+ MainViewModel initialized
+ Log message when MainViewModel is initialized
+
+
+ Game profile launcher initialized
+ Log message when game profile launcher is initialized
+
+
+ UpdateNotificationViewModel initialized
+ Log message when UpdateNotificationViewModel is initialized
+
+
+
+
+ LocalizationService initialized with culture: {0}
+ Log message when LocalizationService is initialized. {0} is culture name
+
+
+ Culture changed to: {0}
+ Log message when culture is changed. {0} is the new culture name
+
+
+ Available cultures refreshed. Found {0} cultures
+ Log message when available cultures are refreshed. {0} is culture count
+
+
+ Added default culture: {0}
+ Log message when default culture is added. {0} is culture name
+
+
+ Discovered {0} available languages: {1}
+ Log message for discovered languages. {0} is count, {1} is comma-separated list
+
+
+ Found satellite assembly for culture: {0}
+ Log message when satellite assembly is found. {0} is culture name
+
+
+ Created ResourceManager for base name: {0}
+ Log message when ResourceManager is created. {0} is base name
+
+
+
+
+ Update check completed. Update available: {0}
+ Log message when update check completes. {0} is true/false
+
+
+
+
+ Game scan completed successfully. Found {0} installations
+ Success message when game scan completes. {0} is installation count
+
+
+
+
+ Operation completed successfully
+ Generic operation completion message
+
+
+ Data saved successfully
+ Generic data save success message
+
+
+ Import completed successfully
+ Success message for import operations
+
+
+ Export completed successfully
+ Success message for export operations
+
+
\ No newline at end of file
diff --git a/GenHub/GenHub.Core/Resources/Strings/StringResources.cs b/GenHub/GenHub.Core/Resources/Strings/StringResources.cs
new file mode 100644
index 000000000..8b51f68f5
--- /dev/null
+++ b/GenHub/GenHub.Core/Resources/Strings/StringResources.cs
@@ -0,0 +1,68 @@
+namespace GenHub.Core.Resources.Strings;
+
+///
+/// Provides strongly-typed access to string resource namespaces.
+/// This class contains constants for all resource file base names used in the localization system.
+///
+public static class StringResources
+{
+ ///
+ /// Base name for common UI resources (buttons, status messages, actions).
+ ///
+ public const string UiCommon = "GenHub.Core.Resources.Strings.UI.Common";
+
+ ///
+ /// Base name for navigation-related UI resources (tab names, section headers).
+ ///
+ public const string UiNavigation = "GenHub.Core.Resources.Strings.UI.Navigation";
+
+ ///
+ /// Base name for game profile management UI resources.
+ ///
+ public const string UiGameProfiles = "GenHub.Core.Resources.Strings.UI.GameProfiles";
+
+ ///
+ /// Base name for settings page UI resources.
+ ///
+ public const string UiSettings = "GenHub.Core.Resources.Strings.UI.Settings";
+
+ ///
+ /// Base name for update functionality UI resources.
+ ///
+ public const string UiUpdates = "GenHub.Core.Resources.Strings.UI.Updates";
+
+ ///
+ /// Base name for tools management UI resources.
+ ///
+ public const string UiTools = "GenHub.Core.Resources.Strings.UI.Tools";
+
+ ///
+ /// Base name for downloads UI resources.
+ ///
+ public const string UiDownloads = "GenHub.Core.Resources.Strings.UI.Downloads";
+
+ ///
+ /// Base name for validation error messages.
+ ///
+ public const string ErrorsValidation = "GenHub.Core.Resources.Strings.Errors.Validation";
+
+ ///
+ /// Base name for operation error messages.
+ ///
+ public const string ErrorsOperations = "GenHub.Core.Resources.Strings.Errors.Operations";
+
+ ///
+ /// Base name for success messages.
+ ///
+ public const string MessagesSuccess = "GenHub.Core.Resources.Strings.Messages.Success";
+
+ ///
+ /// Base name for confirmation dialog messages.
+ ///
+ public const string MessagesConfirmations = "GenHub.Core.Resources.Strings.Messages.Confirmations";
+
+ ///
+ /// Base name for UI tooltip resources.
+ ///
+ public const string Tooltips = "GenHub.Core.Resources.Strings.Tooltips";
+}
\ No newline at end of file
diff --git a/GenHub/GenHub.Core/Resources/Strings/Tooltips.resx b/GenHub/GenHub.Core/Resources/Strings/Tooltips.resx
new file mode 100644
index 000000000..7ad7f5b65
--- /dev/null
+++ b/GenHub/GenHub.Core/Resources/Strings/Tooltips.resx
@@ -0,0 +1,247 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+
+ Save changes
+ Tooltip for save button
+
+
+ Cancel and discard changes
+ Tooltip for cancel button
+
+
+ Refresh data
+ Tooltip for refresh button
+
+
+ Delete selected item
+ Tooltip for delete button
+
+
+ Edit selected item
+ Tooltip for edit button
+
+
+ Add new item
+ Tooltip for add button
+
+
+ Browse for file or folder
+ Tooltip for browse button
+
+
+ Reset to default values
+ Tooltip for reset button
+
+
+
+
+ Choose between dark and light theme
+ Tooltip for theme setting
+
+
+ Default workspace directory for game profiles
+ Tooltip for workspace path setting
+
+
+ Maximum number of simultaneous downloads (1-10)
+ Tooltip for max concurrent downloads setting
+
+
+ Buffer size for downloads in kilobytes
+ Tooltip for download buffer size setting
+
+
+ Timeout duration for downloads in seconds
+ Tooltip for download timeout setting
+
+
+ Automatically check for application updates when starting
+ Tooltip for auto check updates setting
+
+
+ Allow downloads to continue in the background
+ Tooltip for allow background downloads setting
+
+
+ Enable detailed logging for troubleshooting
+ Tooltip for enable detailed logging setting
+
+
+ Default strategy for creating game workspaces
+ Tooltip for default workspace strategy setting
+
+
+ Root directory for Content-Addressable Storage
+ Tooltip for CAS root path setting
+
+
+ Automatically clean up unused cache files
+ Tooltip for enable automatic GC setting
+
+
+ Maximum cache size in gigabytes
+ Tooltip for max cache size setting
+
+
+ Verify file integrity during cache operations
+ Tooltip for verify integrity setting
+
+
+
+
+ Create a new game profile
+ Tooltip for create new profile button
+
+
+ Launch game with this profile
+ Tooltip for launch profile button
+
+
+ Edit profile settings
+ Tooltip for edit profile button
+
+
+ Delete this profile
+ Tooltip for delete profile button
+
+
+ Create desktop shortcut for this profile
+ Tooltip for create shortcut button
+
+
+ Scan system for installed games
+ Tooltip for scan for games button
+
+
+
+
+ Check for application updates
+ Tooltip for check for updates button
+
+
+ View release notes in browser
+ Tooltip for view release notes button
+
+
+ Download and install the update
+ Tooltip for install update button
+
+
+ Dismiss update notification
+ Tooltip for dismiss update button
+
+
+
+
+ Manage and launch game profiles
+ Tooltip for game profiles navigation tab
+
+
+ View and manage downloads
+ Tooltip for downloads navigation tab
+
+
+ Configure application settings
+ Tooltip for settings navigation tab
+
+
+ Browse and search for content
+ Tooltip for content browser navigation tab
+
+
+
+
+ Search for content
+ Tooltip for content search button
+
+
+ Filter search results
+ Tooltip for content filter options
+
+
+ Sort search results
+ Tooltip for content sort options
+
+
+
+
+ Show help information
+ Tooltip for help icon
+
+
+ Show additional information
+ Tooltip for info icon
+
+
+ Warning - attention required
+ Tooltip for warning icon
+
+
+ Error - action failed
+ Tooltip for error icon
+
+
+ Success - action completed
+ Tooltip for success icon
+
+
\ No newline at end of file
diff --git a/GenHub/GenHub.Core/Resources/Strings/UI.Common.resx b/GenHub/GenHub.Core/Resources/Strings/UI.Common.resx
new file mode 100644
index 000000000..6acf2b9f7
--- /dev/null
+++ b/GenHub/GenHub.Core/Resources/Strings/UI.Common.resx
@@ -0,0 +1,249 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+
+ Save
+ Generic save button text
+
+
+ Cancel
+ Generic cancel button text
+
+
+ OK
+ Generic OK button text
+
+
+ Apply
+ Generic apply button text
+
+
+ Close
+ Generic close button text
+
+
+ Back
+ Generic back navigation button
+
+
+ Next
+ Generic next navigation button
+
+
+ Browse
+ Generic browse button text
+
+
+ Delete
+ Generic delete button text
+
+
+ Edit
+ Generic edit button text
+
+
+ Add
+ Generic add button text
+
+
+ Remove
+ Generic remove button text
+
+
+ Refresh
+ Generic refresh button text
+
+
+ Search
+ Generic search button text
+
+
+
+
+ Loading...
+ Generic loading status message
+
+
+ Please wait...
+ Generic please wait message
+
+
+ Success
+ Generic success status
+
+
+ Failed
+ Generic failed status
+
+
+ Error
+ Generic error status
+
+
+ Warning
+ Generic warning status
+
+
+ Info
+ Generic info status
+
+
+
+
+ Copy
+ Copy action text
+
+
+ Paste
+ Paste action text
+
+
+ Cut
+ Cut action text
+
+
+ Select
+ Select action text
+
+
+ Clear
+ Clear action text
+
+
+ Reset
+ Reset action text
+
+
+ Import
+ Import action text
+
+
+ Export
+ Export action text
+
+
+
+
+ Launch
+ Button to launch a game or application
+
+
+ Stop
+ Button to stop a running process
+
+
+ Prepare
+ Button to prepare workspace
+
+
+ New
+ Button to create a new item
+
+
+ Scan
+ Button to scan for items
+
+
+ ✕ Remove
+ Button to remove an item from a list
+
+
+ + Add
+ Button to add an item to a list
+
+
+
+
+ version
+ Version label (lowercase)
+
+
+ by
+ Author attribution label (lowercase)
+
+
+
+
+ Loading content...
+ Status shown while loading content
+
+
+ Saving...
+ Status shown while saving
+
+
+ Processing...
+ Status shown while processing
+
+
+ Initializing...
+ Status shown during initialization
+
+
+ Ready
+ Status when system is ready
+
+
+ Complete
+ Status when operation is complete
+
+
\ No newline at end of file
diff --git a/GenHub/GenHub.Core/Resources/Strings/UI.Downloads.resx b/GenHub/GenHub.Core/Resources/Strings/UI.Downloads.resx
new file mode 100644
index 000000000..a858dd0c8
--- /dev/null
+++ b/GenHub/GenHub.Core/Resources/Strings/UI.Downloads.resx
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+
+ Downloads
+ Page title for the Downloads tab
+
+
+ Manage your downloads and installations
+ Page subtitle for the Downloads tab
+
+
+
+
+ Primary Downloads
+ Section header for primary downloads area
+
+
+ Additional Content
+ Section header for additional content area
+
+
+
+
+ GitHub Builds
+ Download category for GitHub builds
+
+
+ Official Releases
+ Download category for official releases
+
+
+ Mod Downloads
+ Download category for mods
+
+
+ Maps
+ Download category for maps
+
+
+ Tools & Utilities
+ Download category for tools and utilities
+
+
+ Patches & Fixes
+ Download category for patches and fixes
+
+
+
+
+ (Coming Soon)
+ Label indicating a feature is coming soon, with parentheses
+
+
+ Coming Soon
+ Status text indicating a feature is coming soon
+
+
+
+
+ Downloading...
+ Status shown while downloading
+
+
+ Download Complete
+ Status shown when download is complete
+
+
+ Download Failed
+ Status shown when download fails
+
+
+ Paused
+ Status shown when download is paused
+
+
+ Queued
+ Status shown when download is queued
+
+
+ Installing...
+ Status shown while installing
+
+
\ No newline at end of file
diff --git a/GenHub/GenHub.Core/Resources/Strings/UI.GameProfiles.resx b/GenHub/GenHub.Core/Resources/Strings/UI.GameProfiles.resx
new file mode 100644
index 000000000..ff0b8ca81
--- /dev/null
+++ b/GenHub/GenHub.Core/Resources/Strings/UI.GameProfiles.resx
@@ -0,0 +1,361 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+
+ Loading profiles...
+ Status message shown when loading game profiles
+
+
+ Profiles loaded
+ Status message shown when profiles are successfully loaded
+
+
+ Error loading profiles
+ Status message shown when profile loading fails
+
+
+
+
+ Scanning for games...
+ Status message shown when scanning for game installations
+
+
+ Scan complete. Found {0} game installations
+ Status message shown when scan completes. {0} is the count of installations found
+
+
+ Scan failed: {0}
+ Status message shown when scan fails. {0} is the error message
+
+
+ Error during scan
+ Status message shown when an unexpected error occurs during scan
+
+
+
+
+ Game installation service not available
+ Error message shown when required service is not available
+
+
+ Logger not available - scan may proceed without detailed logging
+ Warning message shown when logger is not available
+
+
+
+
+ {0} Profile
+ Auto-generated profile name. {0} is the installation type
+
+
+ Auto-created profile for {0} installation at {1}
+ Auto-generated profile description. {0} is installation type, {1} is path
+
+
+ Found {0} game installations, creating profiles
+ Status message during profile creation. {0} is installation count
+
+
+ Created profile '{0}' for installation {1}
+ Log message for successful profile creation. {0} is profile name, {1} is installation ID
+
+
+ Failed to create profile for installation {0}: {1}
+ Log message for failed profile creation. {0} is installation ID, {1} is error message
+
+
+ Profile creation complete: {0} created, {1} failed
+ Summary message after profile creation. {0} is success count, {1} is failure count
+
+
+ No game installations found
+ Message shown when no game installations are detected
+
+
+ Game installation scan failed: {0}
+ Warning message when installation scan fails. {0} is error details
+
+
+
+
+ Game Profiles
+ Page title for the Game Profiles section
+
+
+ Select a profile to launch or create a new one
+ Page subtitle for the Game Profiles section
+
+
+
+
+ Add New Profile
+ Button to create a new game profile
+
+
+
+
+ Scanning...
+ Status shown during game scanning
+
+
+ Loaded {0} profiles
+ Status showing number of loaded profiles. {0} = count
+
+
+ Launching {0}...
+ Status when launching a profile. {0} = profile name
+
+
+ {0} launched successfully (Process ID: {1})
+ Success message after launching. {0} = profile name, {1} = process ID
+
+
+ Stopping {0}...
+ Status when stopping a profile. {0} = profile name
+
+
+ {0} stopped successfully
+ Success message after stopping. {0} = profile name
+
+
+ Edit mode enabled
+ Status when edit mode is turned on
+
+
+ Edit mode disabled
+ Status when edit mode is turned off
+
+
+ Saving profiles...
+ Status while saving profiles
+
+
+ Profiles saved successfully
+ Success message after saving profiles
+
+
+ Deleting {0}...
+ Status when deleting a profile. {0} = profile name
+
+
+ {0} deleted successfully
+ Success message after deleting. {0} = profile name
+
+
+ Profile updated successfully
+ Success message after updating a profile
+
+
+ New profile window closed
+ Status when new profile window closes
+
+
+ Preparing workspace for {0}...
+ Status when preparing workspace. {0} = profile name
+
+
+ Workspace prepared for {0} at {1}
+ Success message after workspace preparation. {0} = profile name, {1} = path
+
+
+
+
+ Profile manager not available
+ Error when profile manager service is unavailable
+
+
+ Game Profile Manager service is not initialized
+ Error when profile manager is not initialized
+
+
+ Failed to load profiles: {0}
+ Error loading profiles. {0} = error details
+
+
+ Failed to launch {0}: {1}
+ Error launching profile. {0} = profile name, {1} = error
+
+
+ Error launching {0}
+ Generic error launching profile. {0} = profile name
+
+
+ Failed to stop {0}: {1}
+ Error stopping profile. {0} = profile name, {1} = error
+
+
+ Error saving profiles
+ Generic error saving profiles
+
+
+ Failed to delete {0}: {1}
+ Error deleting profile. {0} = profile name, {1} = error
+
+
+ Error deleting {0}
+ Generic error deleting profile. {0} = profile name
+
+
+ Profile settings not available
+ Error when profile settings are unavailable
+
+
+ Profile editor not available
+ Error when profile editor is unavailable
+
+
+ Failed to load profile: {0}
+ Error loading a specific profile. {0} = error details
+
+
+ Could not find main window to open settings
+ Error when main window cannot be found
+
+
+ Error creating new profile
+ Generic error creating a new profile
+
+
+ Profile launcher not available
+ Error when profile launcher is unavailable
+
+
+ Failed to prepare workspace for {0}: {1}
+ Error preparing workspace. {0} = profile name, {1} = error
+
+
+ Error preparing workspace for {0}
+ Generic error preparing workspace. {0} = profile name
+
+
+
+
+ Not Prepared
+ Workspace status when not yet prepared
+
+
+ Symlinked
+ Workspace status when using symlinks
+
+
+ Copied
+ Workspace status when files are copied
+
+
+ Hybrid
+ Workspace status when using hybrid strategy
+
+
+ Hard Linked
+ Workspace status when using hard links
+
+
+ Prepared
+ Generic workspace status when prepared
+
+
+
+
+ Steam
+ Steam installation platform name
+
+
+ EA App
+ EA App installation platform name
+
+
+ The First Decade
+ The First Decade installation type name
+
+
+ Wine
+ Wine/Linux installation platform name
+
+
+ Retail
+ Retail installation type name
+
+
+ CD/ISO
+ CD/ISO installation type name
+
+
+ PC Game
+ Default PC game installation type name
+
+
+
+
+ C&C Generals
+ Command and Conquer Generals game name
+
+
+ C&C Generals: Zero Hour
+ Command and Conquer Generals Zero Hour game name
+
+
+
+
+ ADMIN
+ Badge indicating admin privileges required
+
+
\ No newline at end of file
diff --git a/GenHub/GenHub.Core/Resources/Strings/UI.Navigation.resx b/GenHub/GenHub.Core/Resources/Strings/UI.Navigation.resx
new file mode 100644
index 000000000..e74e9a882
--- /dev/null
+++ b/GenHub/GenHub.Core/Resources/Strings/UI.Navigation.resx
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+
+ Game Profiles
+ Navigation tab name for game profiles section
+
+
+ Downloads
+ Navigation tab name for downloads section
+
+
+ Settings
+ Navigation tab name for settings section
+
+
+ Content
+ Navigation tab name for content browser section
+
+
+ About
+ Navigation tab name for about section
+
+
+ Updates
+ Navigation tab name for updates section
+
+
+ Home
+ Navigation tab name for home section
+
+
+ Tools
+ Navigation tab name for tools section
+
+
+
+
+ C&C Generals Launcher
+ Application tagline shown in navigation
+
+
\ No newline at end of file
diff --git a/GenHub/GenHub.Core/Resources/Strings/UI.Settings.resx b/GenHub/GenHub.Core/Resources/Strings/UI.Settings.resx
new file mode 100644
index 000000000..389e9df1a
--- /dev/null
+++ b/GenHub/GenHub.Core/Resources/Strings/UI.Settings.resx
@@ -0,0 +1,407 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+
+ Save Settings
+ Button text for saving settings
+
+
+ Saving...
+ Button text shown while saving is in progress
+
+
+
+
+ Theme
+ Label for theme selection setting
+
+
+ Dark
+ Dark theme option
+
+
+ Light
+ Light theme option
+
+
+
+
+ Workspace Path
+ Label for workspace path setting
+
+
+ Settings File Path
+ Label for settings file path setting
+
+
+ Cache Path
+ Label for cache path setting
+
+
+ Content Storage Path
+ Label for content storage path setting
+
+
+ CAS Root Path
+ Label for Content-Addressable Storage root path setting
+
+
+
+
+ Select Game Installation Directory
+ Title for folder picker dialog when selecting game install directory
+
+
+ Select Settings File Location
+ Title for file picker dialog when selecting settings file location
+
+
+ Select Content-Addressable Storage Root Directory
+ Title for folder picker dialog when selecting CAS root directory
+
+
+
+
+ Max Concurrent Downloads
+ Label for maximum concurrent downloads setting
+
+
+ Download Buffer Size (KB)
+ Label for download buffer size setting
+
+
+ Download Timeout (Seconds)
+ Label for download timeout setting
+
+
+ Download User Agent
+ Label for download user agent setting
+
+
+ Allow Background Downloads
+ Label for allow background downloads checkbox
+
+
+
+
+ Auto Check for Updates on Startup
+ Label for auto check for updates checkbox
+
+
+ Enable Detailed Logging
+ Label for enable detailed logging checkbox
+
+
+ Default Workspace Strategy
+ Label for default workspace strategy setting
+
+
+
+
+ Enable Automatic Garbage Collection
+ Label for enable automatic garbage collection checkbox
+
+
+ Max Cache Size (GB)
+ Label for maximum cache size setting
+
+
+ Max Concurrent Operations
+ Label for maximum concurrent CAS operations
+
+
+ Verify Integrity
+ Label for verify integrity checkbox
+
+
+ Garbage Collection Grace Period (Days)
+ Label for garbage collection grace period setting
+
+
+ Auto GC Interval (Days)
+ Label for automatic garbage collection interval setting
+
+
+
+
+ Content Directories
+ Label for content directories setting
+
+
+ GitHub Discovery Repositories
+ Label for GitHub discovery repositories setting
+
+
+
+
+ Current Memory Usage (MB)
+ Label for current memory usage display
+
+
+
+
+ Settings
+ Page title for the Settings section
+
+
+ Configure your GenHub preferences and application behavior
+ Page subtitle for the Settings section
+
+
+
+
+ ✓ Settings saved successfully!
+ Notification shown when settings are saved
+
+
+
+
+ 🎮 Game Configuration
+ Section header for game configuration settings
+
+
+ 📥 Downloads
+ Section header for download settings
+
+
+ 🎨 Appearance
+ Section header for appearance settings
+
+
+ ⚡ Performance
+ Section header for performance settings
+
+
+ 🔗 Content-Addressable Storage (CAS)
+ Section header for CAS settings
+
+
+ 🗄️ Storage
+ Section header for storage settings
+
+
+ 📂 Local Content Directories
+ Section header for content directories settings
+
+
+ 🌐 GitHub Discovery
+ Section header for GitHub discovery settings
+
+
+
+
+ Preferred Game Installation Path
+ Label for preferred game path setting
+
+
+ Cache Directory
+ Label for cache directory setting
+
+
+ Content Storage Directory
+ Label for content storage directory setting
+
+
+ Content Directories (one per line)
+ Label for content directories setting
+
+
+ GitHub Repositories (one per line, owner/repo)
+ Label for GitHub repositories setting
+
+
+ Memory Usage
+ Label for memory usage display
+
+
+
+
+ Auto-detect game installation path...
+ Placeholder for game path input
+
+
+ Default (platform-dependent)
+ Placeholder for platform-dependent default paths
+
+
+ Default (%AppData%/GenHub/cas-pool)
+ Placeholder for CAS root path
+
+
+ Default (%AppData%/GenHub/Content)
+ Placeholder for content storage path
+
+
+ e.g. C:\Games\GenHub\Mods
+ Example placeholder for mods directory
+
+
+ e.g. some-author/some-mod
+ Example placeholder for GitHub repository
+
+
+
+
+ Specify where you prefer games to be installed. Leave empty for auto-detection.
+ Description for game path setting
+
+
+ Choose how files are managed in game workspaces
+ Description for workspace strategy setting
+
+
+ Limit the number of simultaneous downloads to manage bandwidth usage (1-10)
+ Description for max concurrent downloads setting
+
+
+ Buffer size used for file downloads (4KB - 1MB, default: 80KB)
+ Description for buffer size setting
+
+
+ Timeout for each download operation (10-3600 seconds, default: 600 seconds)
+ Description for timeout setting
+
+
+ HTTP User-Agent string sent with download requests
+ Description for user agent setting
+
+
+ Continue downloads when the application is minimized or in the background
+ Description for background downloads setting
+
+
+ Choose between light and dark themes
+ Description for theme setting
+
+
+ Enable verbose logging for troubleshooting. May impact performance.
+ Description for detailed logging setting
+
+
+ Automatically check for GenHub updates when the application starts
+ Description for auto check updates setting
+
+
+ Specify a custom location for your settings file. Leave empty to use the default location.
+ Description for settings file path setting
+
+
+ Monitor application memory consumption
+ Description for memory usage display
+
+
+ Directory where deduplicated content is stored. All workspaces reference files from here.
+ Description for CAS root path setting
+
+
+ Maximum size for the CAS pool before garbage collection runs (1-1000 GB)
+ Description for max cache size setting
+
+
+ Number of simultaneous CAS operations (1-16, default: 4)
+ Description for CAS concurrent operations setting
+
+
+ Automatically clean up unreferenced content to free disk space
+ Description for automatic GC setting
+
+
+ How long to keep unreferenced content before deletion (1-365 days)
+ Description for GC grace period setting
+
+
+ How often to run automatic garbage collection (1-30 days)
+ Description for auto GC interval setting
+
+
+ Check file hashes to ensure data integrity (recommended, slight performance impact)
+ Description for verify integrity setting
+
+
+ Override the default cache directory for downloads and temp files.
+ Description for cache path setting
+
+
+ Override the default directory where downloaded content is stored permanently.
+ Description for content storage path setting
+
+
+ Specify directories to scan for local content. One path per line.
+ Description for content directories setting
+
+
+ Specify GitHub repositories to scan for releases. One owner/repo per line.
+ Description for GitHub repositories setting
+
+
+
+
+ Settings are automatically saved and will take effect immediately or on next restart as needed.
+ Hint about settings auto-save behavior
+
+
+
+
+ Reset to Defaults
+ Button to reset settings to default values
+
+
\ No newline at end of file
diff --git a/GenHub/GenHub.Core/Resources/Strings/UI.Tools.resx b/GenHub/GenHub.Core/Resources/Strings/UI.Tools.resx
new file mode 100644
index 000000000..963a4716e
--- /dev/null
+++ b/GenHub/GenHub.Core/Resources/Strings/UI.Tools.resx
@@ -0,0 +1,217 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+
+ Tools
+ Page title for the Tools tab
+
+
+ Manage and use tool plugins to extend GenHub functionality
+ Page subtitle for the Tools tab
+
+
+
+
+ Installed Tools
+ Section label for installed tools list
+
+
+ Name
+ Field label for tool name
+
+
+ Version
+ Field label for tool version
+
+
+ Author
+ Field label for tool author
+
+
+ Description
+ Field label for tool description
+
+
+ Tool ID
+ Field label for tool identifier
+
+
+
+
+ ➕ Add Tool
+ Button to add a new tool plugin
+
+
+ 🔄 Refresh
+ Button to refresh the tools list
+
+
+ Close
+ Button to close a dialog
+
+
+
+
+ View Details
+ Context menu item to view tool details
+
+
+ Remove Tool
+ Context menu item to remove a tool
+
+
+
+
+ Tool Details
+ Dialog title for viewing tool details
+
+
+ Select Tool Plugin Assembly
+ File picker dialog title
+
+
+
+
+ Tool Plugin Assembly
+ File type description for DLL files
+
+
+
+
+ Loading...
+ Loading status message
+
+
+ No tools installed. Click 'Add Tool' to install a tool plugin.
+ Status when no tools are installed
+
+
+ Installing tool...
+ Status during tool installation
+
+
+ ✓ Tool '{0}' v{1} installed successfully.
+ Success message after installing a tool. {0} = tool name, {1} = version
+
+
+ Removing tool '{0}'...
+ Status during tool removal. {0} = tool name
+
+
+ ✓ Tool '{0}' removed successfully.
+ Success message after removing a tool. {0} = tool name
+
+
+ Refreshing tools...
+ Status during tools refresh
+
+
+ ✓ Refreshed {0} tool(s) successfully.
+ Success message after refreshing tools. {0} = tool count
+
+
+
+
+ No tools installed yet
+ Empty state title when no tools are installed
+
+
+ Click 'Add Tool' to install your first tool plugin
+ Empty state description when no tools are installed
+
+
+ Select a tool from the list
+ Prompt to select a tool from the sidebar
+
+
+
+
+ Expand Sidebar
+ Tooltip for expand sidebar button
+
+
+ Collapse Sidebar
+ Tooltip for collapse sidebar button
+
+
+
+
+ ✗ Failed to install tool: {0}
+ Error message when tool installation fails. {0} = error details
+
+
+ ✗ Failed to remove tool: {0}
+ Error message when tool removal fails. {0} = error details
+
+
+ ⚠ Failed to refresh tools: {0}
+ Error message when tools refresh fails. {0} = error details
+
+
+ ⚠ Failed to load tools: {0}
+ Error message when loading tools fails. {0} = error details
+
+
+ ✗ Error loading tool '{0}': {1}
+ Error message when a specific tool fails to load. {0} = tool name, {1} = error
+
+
\ No newline at end of file
diff --git a/GenHub/GenHub.Core/Resources/Strings/UI.Updates.resx b/GenHub/GenHub.Core/Resources/Strings/UI.Updates.resx
new file mode 100644
index 000000000..d0d00318f
--- /dev/null
+++ b/GenHub/GenHub.Core/Resources/Strings/UI.Updates.resx
@@ -0,0 +1,181 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+
+ Ready to check for updates
+ Initial status message before checking for updates
+
+
+ Checking for updates...
+ Status message while checking for updates
+
+
+ Update available: {0}
+ Status message when update is available. {0} is the version number
+
+
+ No updates available
+ Status message when no updates are available
+
+
+ Update check failed
+ Status message when update check fails
+
+
+
+
+ Preparing to download update...
+ Status message when preparing to download update
+
+
+ Preparing download...
+ Progress message when preparing download
+
+
+ Update prepared successfully! The application will restart automatically.
+ Status message when update is successfully prepared
+
+
+ Update prepared successfully! The application will restart automatically.
+ Progress message when update is successfully prepared
+
+
+ Update installation failed.
+ Status message when update installation fails
+
+
+ Installation failed
+ Progress message when installation fails
+
+
+ Update failed
+ Status message when update process fails
+
+
+ Ready
+ Initial progress message
+
+
+
+
+ Install Update
+ Button text for installing update
+
+
+ Installing...
+ Button text shown while installing
+
+
+
+
+ No compatible update package found for your platform.
+ Error message when no compatible update package is found
+
+
+ Failed to check for updates: {0}
+ Error message when update check fails. {0} is the error details
+
+
+ Update failed: {0}
+ Error message when update fails. {0} is the error details
+
+
+
+
+ Current Version
+ Label for current application version
+
+
+ Latest Version
+ Label for latest available version
+
+
+ v0.0.0
+ Default version display when no version is available
+
+
+
+
+ Update Available!
+ Heading when an update is available (with exclamation)
+
+
+ What's new:
+ Label for release notes section
+
+
+ This update includes bug fixes, performance improvements, and new features.
+ Default text for update description when no specific notes
+
+
+ You're up to date!
+ Status message when app is already on latest version
+
+
+ is ready to install
+ Partial text indicating version is ready
+
+
+ % complete
+ Partial text for percentage completion
+
+
\ No newline at end of file
diff --git a/GenHub/GenHub.Core/Services/Localization/LanguageProvider.cs b/GenHub/GenHub.Core/Services/Localization/LanguageProvider.cs
new file mode 100644
index 000000000..1d5b4c986
--- /dev/null
+++ b/GenHub/GenHub.Core/Services/Localization/LanguageProvider.cs
@@ -0,0 +1,198 @@
+using System.Globalization;
+using System.Reflection;
+using System.Resources;
+using GenHub.Core.Interfaces.Localization;
+using Microsoft.Extensions.Logging;
+
+namespace GenHub.Core.Services.Localization;
+
+///
+/// Provides language discovery and resource manager provisioning for localization.
+///
+public class LanguageProvider : ILanguageProvider
+{
+ private readonly ILogger _logger;
+ private readonly Dictionary _resourceManagers;
+ private readonly object _lock = new();
+ private IReadOnlyList? _cachedCultures;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger instance.
+ public LanguageProvider(ILogger logger)
+ {
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _resourceManagers = new Dictionary();
+ }
+
+ ///
+ public async Task> DiscoverAvailableLanguages()
+ {
+ return await Task.Run(() =>
+ {
+ lock (_lock)
+ {
+ if (_cachedCultures != null)
+ {
+ _logger.LogDebug("Returning cached cultures: {Count} languages", _cachedCultures.Count);
+ return _cachedCultures;
+ }
+
+ var cultures = new List();
+ var assembly = Assembly.GetExecutingAssembly();
+
+ try
+ {
+ // Add default/invariant culture (English)
+ var defaultCulture = CultureInfo.GetCultureInfo("en");
+ cultures.Add(defaultCulture);
+ _logger.LogDebug("Added default culture: {Culture}", defaultCulture.Name);
+
+ // Discover satellite assemblies
+ var satelliteCultures = DiscoverSatelliteAssemblyCultures(assembly);
+ cultures.AddRange(satelliteCultures);
+
+ // Remove duplicates and sort by English name
+ _cachedCultures = cultures
+ .DistinctBy(c => c.TwoLetterISOLanguageName)
+ .OrderBy(c => c.EnglishName)
+ .ToList()
+ .AsReadOnly();
+
+ _logger.LogInformation(
+ "Discovered {Count} available languages: {Languages}",
+ _cachedCultures.Count,
+ string.Join(", ", _cachedCultures.Select(c => c.Name)));
+
+ return _cachedCultures;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error discovering available languages");
+
+ // Return at least the default culture
+ _cachedCultures = new List { CultureInfo.GetCultureInfo("en") }.AsReadOnly();
+ return _cachedCultures;
+ }
+ }
+ });
+ }
+
+ ///
+ public ResourceManager GetResourceManager(string baseName)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(baseName, nameof(baseName));
+
+ lock (_lock)
+ {
+ if (_resourceManagers.TryGetValue(baseName, out var existingManager))
+ {
+ return existingManager;
+ }
+
+ try
+ {
+ var assembly = Assembly.GetExecutingAssembly();
+ var resourceManager = new ResourceManager(baseName, assembly);
+
+ _resourceManagers[baseName] = resourceManager;
+ _logger.LogDebug("Created ResourceManager for base name: {BaseName}", baseName);
+
+ return resourceManager;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error creating ResourceManager for base name: {BaseName}", baseName);
+ throw;
+ }
+ }
+ }
+
+ ///
+ public bool ValidateCulture(CultureInfo culture)
+ {
+ ArgumentNullException.ThrowIfNull(culture, nameof(culture));
+
+ try
+ {
+ var assembly = Assembly.GetExecutingAssembly();
+
+ // The invariant/default culture is always valid
+ if (culture.Equals(CultureInfo.InvariantCulture) ||
+ culture.TwoLetterISOLanguageName.Equals("en", StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ // Try to get the satellite assembly for this culture
+ try
+ {
+ var satelliteAssembly = assembly.GetSatelliteAssembly(culture);
+ return satelliteAssembly != null;
+ }
+ catch (FileNotFoundException)
+ {
+ // Satellite assembly doesn't exist for this culture
+ return false;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Error validating culture: {Culture}", culture.Name);
+ return false;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Unexpected error validating culture: {Culture}", culture.Name);
+ return false;
+ }
+ }
+
+ ///
+ /// Discovers cultures that have satellite assemblies.
+ ///
+ /// The assembly to scan.
+ /// A list of cultures with satellite assemblies.
+ private List DiscoverSatelliteAssemblyCultures(Assembly assembly)
+ {
+ var satelliteCultures = new List();
+
+ try
+ {
+ // Get all cultures and check if satellite assembly exists
+ var allCultures = CultureInfo.GetCultures(CultureTypes.AllCultures)
+ .Where(c => !string.IsNullOrEmpty(c.Name)); // Skip invariant culture
+
+ foreach (var culture in allCultures)
+ {
+ try
+ {
+ // Attempt to get satellite assembly
+ var satelliteAssembly = assembly.GetSatelliteAssembly(culture);
+ if (satelliteAssembly != null)
+ {
+ satelliteCultures.Add(culture);
+ _logger.LogDebug("Found satellite assembly for culture: {Culture}", culture.Name);
+ }
+ }
+ catch (FileNotFoundException)
+ {
+ // This is expected for cultures without satellite assemblies
+ continue;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogTrace(ex, "Error checking satellite assembly for culture: {Culture}", culture.Name);
+ continue;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Error during satellite assembly discovery");
+ }
+
+ return satelliteCultures;
+ }
+}
\ No newline at end of file
diff --git a/GenHub/GenHub.Core/Services/Localization/LocalizationService.cs b/GenHub/GenHub.Core/Services/Localization/LocalizationService.cs
new file mode 100644
index 000000000..26ced4fc4
--- /dev/null
+++ b/GenHub/GenHub.Core/Services/Localization/LocalizationService.cs
@@ -0,0 +1,313 @@
+using System.Globalization;
+using System.Reactive.Subjects;
+using GenHub.Core.Interfaces.Localization;
+using GenHub.Core.Models.Localization;
+using Microsoft.Extensions.Logging;
+
+namespace GenHub.Core.Services.Localization;
+
+///
+/// Provides localization services with runtime language switching and reactive updates.
+///
+public class LocalizationService : ILocalizationService, IDisposable
+{
+ private readonly ILanguageProvider _languageProvider;
+ private readonly ILogger _logger;
+ private readonly LocalizationOptions _options;
+ private readonly Subject _cultureChangedSubject;
+ private readonly object _lock = new();
+ private CultureInfo _currentCulture;
+ private IReadOnlyList? _availableCultures;
+ private bool _disposed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The language provider for resource management.
+ /// The logger instance.
+ /// The localization options.
+ public LocalizationService(
+ ILanguageProvider languageProvider,
+ ILogger logger,
+ LocalizationOptions? options = null)
+ {
+ _languageProvider = languageProvider ?? throw new ArgumentNullException(nameof(languageProvider));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _options = options ?? new LocalizationOptions();
+ _cultureChangedSubject = new Subject();
+
+ // Initialize with default culture
+ _currentCulture = GetCultureFromString(_options.DefaultCulture);
+ SetThreadCulture(_currentCulture);
+
+ _logger.LogInformation("LocalizationService initialized with culture: {Culture}", _currentCulture.Name);
+ }
+
+ ///
+ public CultureInfo CurrentCulture
+ {
+ get
+ {
+ lock (_lock)
+ {
+ return _currentCulture;
+ }
+ }
+
+ set
+ {
+ SetCulture(value).GetAwaiter().GetResult();
+ }
+ }
+
+ ///
+ public IReadOnlyList AvailableCultures
+ {
+ get
+ {
+ if (_availableCultures != null)
+ {
+ return _availableCultures;
+ }
+
+ // Synchronously get cached or discover
+ _availableCultures = _languageProvider.DiscoverAvailableLanguages().GetAwaiter().GetResult();
+ return _availableCultures;
+ }
+ }
+
+ ///
+ public IObservable CultureChanged => _cultureChangedSubject;
+
+ ///
+ /// Gets a localized string from the specified resource set.
+ ///
+ /// The resource set base name (e.g., from StringResources constants).
+ /// The resource key.
+ /// The localized string, or a fallback if not found.
+ public string GetString(string resourceSet, string key)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(resourceSet, nameof(resourceSet));
+ ArgumentException.ThrowIfNullOrWhiteSpace(key, nameof(key));
+
+ try
+ {
+ var resourceManager = _languageProvider.GetResourceManager(resourceSet);
+
+ lock (_lock)
+ {
+ var value = resourceManager.GetString(key, _currentCulture);
+
+ if (string.IsNullOrEmpty(value))
+ {
+ return HandleMissingTranslation(key);
+ }
+
+ return value;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error retrieving resource string for key: {Key} from resource set: {ResourceSet}", key, resourceSet);
+ return HandleMissingTranslation(key);
+ }
+ }
+
+ ///
+ /// Gets a formatted localized string from the specified resource set.
+ ///
+ /// The resource set base name (e.g., from StringResources constants).
+ /// The resource key.
+ /// Format arguments.
+ /// The formatted localized string, or a fallback if not found.
+ public string GetString(string resourceSet, string key, params object[] args)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(resourceSet, nameof(resourceSet));
+ ArgumentException.ThrowIfNullOrWhiteSpace(key, nameof(key));
+
+ try
+ {
+ var format = GetString(resourceSet, key);
+
+ if (args == null || args.Length == 0)
+ {
+ return format;
+ }
+
+ return string.Format(_currentCulture, format, args);
+ }
+ catch (FormatException ex)
+ {
+ _logger.LogError(
+ ex,
+ "Format error for key '{Key}' from resource set '{ResourceSet}' with {ArgCount} arguments",
+ key,
+ resourceSet,
+ args?.Length ?? 0);
+
+ return HandleMissingTranslation(key);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error formatting resource string for key: {Key} from resource set: {ResourceSet}", key, resourceSet);
+ return HandleMissingTranslation(key);
+ }
+ }
+
+ ///
+ public async Task SetCulture(CultureInfo culture)
+ {
+ ArgumentNullException.ThrowIfNull(culture, nameof(culture));
+
+ await Task.Run(() =>
+ {
+ lock (_lock)
+ {
+ try
+ {
+ // Validate the culture is available
+ if (!_languageProvider.ValidateCulture(culture))
+ {
+ _logger.LogWarning(
+ "Culture '{Culture}' is not available. Falling back to '{Fallback}'",
+ culture.Name,
+ _options.FallbackCulture);
+
+ culture = GetCultureFromString(_options.FallbackCulture);
+ }
+
+ // Set the culture
+ _currentCulture = culture;
+ SetThreadCulture(culture);
+
+ _logger.LogInformation("Culture changed to: {Culture}", culture.Name);
+
+ // Notify observers
+ _cultureChangedSubject.OnNext(culture);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error setting culture to: {Culture}", culture.Name);
+ throw;
+ }
+ }
+ });
+ }
+
+ ///
+ public async Task SetCulture(string cultureName)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(cultureName, nameof(cultureName));
+
+ try
+ {
+ var culture = GetCultureFromString(cultureName);
+ await SetCulture(culture);
+ }
+ catch (CultureNotFoundException ex)
+ {
+ _logger.LogError(ex, "Invalid culture name: {CultureName}", cultureName);
+ throw new ArgumentException($"Invalid culture name: {cultureName}", nameof(cultureName), ex);
+ }
+ }
+
+ ///
+ public async Task RefreshAvailableCultures()
+ {
+ _logger.LogDebug("Refreshing available cultures");
+
+ _availableCultures = await _languageProvider.DiscoverAvailableLanguages();
+
+ _logger.LogInformation(
+ "Available cultures refreshed. Found {Count} cultures",
+ _availableCultures.Count);
+ }
+
+ ///
+ /// Disposes the service and releases resources.
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Disposes the service and releases resources.
+ ///
+ /// True if disposing managed resources.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ _cultureChangedSubject?.OnCompleted();
+ _cultureChangedSubject?.Dispose();
+ }
+
+ _disposed = true;
+ }
+
+ ///
+ /// Sets the thread culture for both CurrentCulture and CurrentUICulture.
+ ///
+ /// The culture to set.
+ private void SetThreadCulture(CultureInfo culture)
+ {
+ CultureInfo.CurrentCulture = culture;
+ CultureInfo.CurrentUICulture = culture;
+ Thread.CurrentThread.CurrentCulture = culture;
+ Thread.CurrentThread.CurrentUICulture = culture;
+ }
+
+ ///
+ /// Gets a CultureInfo from a culture string, with error handling.
+ ///
+ /// The culture name.
+ /// The CultureInfo instance.
+ private CultureInfo GetCultureFromString(string cultureName)
+ {
+ try
+ {
+ return CultureInfo.GetCultureInfo(cultureName);
+ }
+ catch (CultureNotFoundException ex)
+ {
+ _logger.LogWarning(
+ ex,
+ "Culture '{Culture}' not found. Using invariant culture",
+ cultureName);
+
+ return CultureInfo.InvariantCulture;
+ }
+ }
+
+ ///
+ /// Handles missing translation keys according to configuration.
+ ///
+ /// The missing translation key.
+ /// A placeholder string for the missing translation.
+ private string HandleMissingTranslation(string key)
+ {
+ if (_options.LogMissingTranslations)
+ {
+ _logger.LogWarning(
+ "Missing translation for key '{Key}' in culture '{Culture}'",
+ key,
+ _currentCulture.Name);
+ }
+
+ if (_options.ThrowOnMissingTranslation)
+ {
+ throw new InvalidOperationException(
+ $"Missing translation for key '{key}' in culture '{_currentCulture.Name}'");
+ }
+
+ // In development, return key with markers; in production, return clean key
+ return _options.LogMissingTranslations ? $"[{key}]" : key;
+ }
+}
\ No newline at end of file
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Resources/StringResourcesTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Resources/StringResourcesTests.cs
new file mode 100644
index 000000000..e0bb23b50
--- /dev/null
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Resources/StringResourcesTests.cs
@@ -0,0 +1,263 @@
+using GenHub.Core.Interfaces.Localization;
+using GenHub.Core.Resources.Strings;
+using GenHub.Core.Services.Localization;
+using Microsoft.Extensions.Logging;
+using Moq;
+using System.Globalization;
+using System.Resources;
+using Xunit;
+
+namespace GenHub.Tests.Core.Resources;
+
+///
+/// Integration tests for string resource loading and localization infrastructure.
+/// Tests that .resx files are properly embedded, ResourceManagers can load them,
+/// and the LocalizationService can retrieve strings.
+///
+public class StringResourcesTests
+{
+ private readonly ILanguageProvider _languageProvider;
+ private readonly ILocalizationService _localizationService;
+
+ public StringResourcesTests()
+ {
+ var loggerLanguageProvider = new Mock>();
+ var loggerLocalizationService = new Mock>();
+
+ _languageProvider = new LanguageProvider(loggerLanguageProvider.Object);
+ _localizationService = new LocalizationService(
+ _languageProvider,
+ loggerLocalizationService.Object);
+ }
+
+ [Fact]
+ public void AllResourceSetsAreEmbedded()
+ {
+ // Arrange & Act - Try to create ResourceManager for each resource set
+ var resourceSets = new[]
+ {
+ StringResources.UiCommon,
+ StringResources.UiNavigation,
+ StringResources.UiGameProfiles,
+ StringResources.UiSettings,
+ StringResources.UiUpdates,
+ StringResources.ErrorsValidation,
+ StringResources.ErrorsOperations,
+ StringResources.MessagesSuccess,
+ StringResources.MessagesConfirmations,
+ StringResources.Tooltips
+ };
+
+ foreach (var resourceSet in resourceSets)
+ {
+ // Assert - Should not throw exception
+ var resourceManager = _languageProvider.GetResourceManager(resourceSet);
+ Assert.NotNull(resourceManager);
+ }
+ }
+
+ [Fact]
+ public void CanLoadStringFromUiCommon()
+ {
+ // Arrange
+ var resourceManager = _languageProvider.GetResourceManager(StringResources.UiCommon);
+
+ // Act
+ var saveButton = resourceManager.GetString("Button.Save", CultureInfo.GetCultureInfo("en"));
+
+ // Assert
+ Assert.NotNull(saveButton);
+ Assert.Equal("Save", saveButton);
+ }
+
+ [Fact]
+ public void CanLoadStringFromUiNavigation()
+ {
+ // Arrange
+ var resourceManager = _languageProvider.GetResourceManager(StringResources.UiNavigation);
+
+ // Act
+ var gameProfilesTab = resourceManager.GetString("Tab.GameProfiles", CultureInfo.GetCultureInfo("en"));
+
+ // Assert
+ Assert.NotNull(gameProfilesTab);
+ Assert.Equal("Game Profiles", gameProfilesTab);
+ }
+
+ [Fact]
+ public void CanLoadStringFromErrorsValidation()
+ {
+ // Arrange
+ var resourceManager = _languageProvider.GetResourceManager(StringResources.ErrorsValidation);
+
+ // Act
+ var requiredField = resourceManager.GetString("RequiredField", CultureInfo.GetCultureInfo("en"));
+
+ // Assert
+ Assert.NotNull(requiredField);
+ Assert.Equal("This field is required", requiredField);
+ }
+
+ [Fact]
+ public void LocalizationService_CanRetrieveStringWithResourceSet()
+ {
+ // Act
+ var saveButton = _localizationService.GetString(StringResources.UiCommon, "Button.Save");
+
+ // Assert
+ Assert.NotNull(saveButton);
+ Assert.Equal("Save", saveButton);
+ }
+
+ [Fact]
+ public void LocalizationService_CanFormatStringWithArguments()
+ {
+ // Act
+ var scanComplete = _localizationService.GetString(
+ StringResources.UiGameProfiles,
+ "Status.ScanComplete",
+ 5);
+
+ // Assert
+ Assert.NotNull(scanComplete);
+ Assert.Contains("5", scanComplete);
+ Assert.Contains("game installations", scanComplete);
+ }
+
+ [Fact]
+ public void LocalizationService_ReturnsPlaceholderForMissingKey()
+ {
+ // Act
+ var result = _localizationService.GetString(StringResources.UiCommon, "NonExistentKey");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Contains("NonExistentKey", result);
+ }
+
+ [Fact]
+ public void LanguageProvider_CanDiscoverEnglishCulture()
+ {
+ // Act
+ var cultures = _languageProvider.DiscoverAvailableLanguages().GetAwaiter().GetResult();
+
+ // Assert
+ Assert.NotNull(cultures);
+ Assert.NotEmpty(cultures);
+ Assert.Contains(cultures, c => c.TwoLetterISOLanguageName == "en");
+ }
+
+ [Fact]
+ public void LanguageProvider_ValidatesEnglishCulture()
+ {
+ // Arrange
+ var englishCulture = CultureInfo.GetCultureInfo("en");
+
+ // Act
+ var isValid = _languageProvider.ValidateCulture(englishCulture);
+
+ // Assert
+ Assert.True(isValid);
+ }
+
+ [Theory]
+ [InlineData(StringResources.UiCommon, "Button.Cancel", "Cancel")]
+ [InlineData(StringResources.UiCommon, "Button.OK", "OK")]
+ [InlineData(StringResources.UiCommon, "Status.Loading", "Loading...")]
+ [InlineData(StringResources.UiNavigation, "Tab.Downloads", "Downloads")]
+ [InlineData(StringResources.UiNavigation, "Tab.Settings", "Settings")]
+ [InlineData(StringResources.UiSettings, "Button.SaveSettings", "Save Settings")]
+ [InlineData(StringResources.UiUpdates, "Status.ReadyToCheck", "Ready to check for updates")]
+ [InlineData(StringResources.ErrorsValidation, "InvalidFormat", "Invalid format")]
+ [InlineData(StringResources.ErrorsOperations, "OperationFailed", "Operation failed")]
+ [InlineData(StringResources.MessagesSuccess, "OperationCompleted", "Operation completed successfully")]
+ [InlineData(StringResources.MessagesConfirmations, "DeleteProfile", "Are you sure you want to delete this profile?")]
+ [InlineData(StringResources.Tooltips, "Button.Save", "Save changes")]
+ public void CanLoadSpecificStringsFromAllResourceSets(string resourceSet, string key, string expected)
+ {
+ // Act
+ var result = _localizationService.GetString(resourceSet, key);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void ResourceManager_IsCachedByLanguageProvider()
+ {
+ // Act
+ var rm1 = _languageProvider.GetResourceManager(StringResources.UiCommon);
+ var rm2 = _languageProvider.GetResourceManager(StringResources.UiCommon);
+
+ // Assert - Should return the same instance (cached)
+ Assert.Same(rm1, rm2);
+ }
+
+ [Fact]
+ public void LocalizationService_CurrentCultureIsEnglishByDefault()
+ {
+ // Act
+ var currentCulture = _localizationService.CurrentCulture;
+
+ // Assert
+ Assert.NotNull(currentCulture);
+ Assert.Equal("en", currentCulture.TwoLetterISOLanguageName);
+ }
+
+ [Fact]
+ public void AllResourceSetsContainMultipleStrings()
+ {
+ // Arrange
+ var resourceSets = new[]
+ {
+ StringResources.UiCommon,
+ StringResources.UiNavigation,
+ StringResources.UiGameProfiles,
+ StringResources.UiSettings,
+ StringResources.UiUpdates,
+ StringResources.ErrorsValidation,
+ StringResources.ErrorsOperations,
+ StringResources.MessagesSuccess,
+ StringResources.MessagesConfirmations,
+ StringResources.Tooltips
+ };
+
+ foreach (var resourceSetName in resourceSets)
+ {
+ // Act
+ var resourceManager = _languageProvider.GetResourceManager(resourceSetName);
+ var resourceSet = resourceManager.GetResourceSet(CultureInfo.GetCultureInfo("en"), true, true);
+
+ // Assert
+ Assert.NotNull(resourceSet);
+
+ var enumerator = resourceSet.GetEnumerator();
+ var count = 0;
+ while (enumerator.MoveNext())
+ {
+ count++;
+ }
+
+ // Each resource file should have at least 5 strings
+ // Note: Some resource sets like UI.Navigation naturally have fewer entries
+ Assert.True(count >= 5, $"{resourceSetName} should have at least 5 strings, but has {count}");
+ }
+ }
+
+ [Fact]
+ public void FormatStrings_WorkWithMultipleParameters()
+ {
+ // Act
+ var profileDescription = _localizationService.GetString(
+ StringResources.UiGameProfiles,
+ "Profile.AutoCreatedDescription",
+ "Steam",
+ "C:\\Games\\MyGame");
+
+ // Assert
+ Assert.NotNull(profileDescription);
+ Assert.Contains("Steam", profileDescription);
+ Assert.Contains("C:\\Games\\MyGame", profileDescription);
+ }
+}
\ No newline at end of file
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Services/Localization/LanguageProviderTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Services/Localization/LanguageProviderTests.cs
new file mode 100644
index 000000000..e6743baff
--- /dev/null
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Services/Localization/LanguageProviderTests.cs
@@ -0,0 +1,185 @@
+using System.Globalization;
+using GenHub.Core.Services.Localization;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+
+namespace GenHub.Tests.Core.Services.Localization;
+
+///
+/// Tests for .
+///
+public class LanguageProviderTests
+{
+ private readonly Mock> _mockLogger;
+ private readonly LanguageProvider _provider;
+
+ public LanguageProviderTests()
+ {
+ _mockLogger = new Mock>();
+ _provider = new LanguageProvider(_mockLogger.Object);
+ }
+
+ [Fact]
+ public void Constructor_WithNullLogger_ThrowsArgumentNullException()
+ {
+ // Act & Assert
+ Assert.Throws(() => new LanguageProvider(null!));
+ }
+
+ [Fact]
+ public async Task DiscoverAvailableLanguages_ReturnsAtLeastEnglish()
+ {
+ // Act
+ var result = await _provider.DiscoverAvailableLanguages();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.NotEmpty(result);
+ Assert.Contains(result, c => c.TwoLetterISOLanguageName == "en");
+ }
+
+ [Fact]
+ public async Task DiscoverAvailableLanguages_CachesResults()
+ {
+ // Act
+ var result1 = await _provider.DiscoverAvailableLanguages();
+ var result2 = await _provider.DiscoverAvailableLanguages();
+
+ // Assert
+ Assert.Same(result1, result2);
+ }
+
+ [Fact]
+ public async Task DiscoverAvailableLanguages_ReturnsDistinctCultures()
+ {
+ // Act
+ var result = await _provider.DiscoverAvailableLanguages();
+
+ // Assert
+ var languageCodes = result.Select(c => c.TwoLetterISOLanguageName).ToList();
+ var distinctCodes = languageCodes.Distinct().ToList();
+ Assert.Equal(distinctCodes.Count, languageCodes.Count);
+ }
+
+ [Fact]
+ public async Task DiscoverAvailableLanguages_SortsResultsByEnglishName()
+ {
+ // Act
+ var result = await _provider.DiscoverAvailableLanguages();
+
+ // Assert
+ var englishNames = result.Select(c => c.EnglishName).ToList();
+ var sortedNames = englishNames.OrderBy(n => n).ToList();
+ Assert.Equal(sortedNames, englishNames);
+ }
+
+ [Fact]
+ public void GetResourceManager_WithValidBaseName_ReturnsResourceManager()
+ {
+ // Arrange
+ var baseName = "GenHub.Core.Resources.Strings";
+
+ // Act
+ var result = _provider.GetResourceManager(baseName);
+
+ // Assert
+ Assert.NotNull(result);
+ }
+
+ [Fact]
+ public void GetResourceManager_WithSameBaseName_ReturnsSameInstance()
+ {
+ // Arrange
+ var baseName = "GenHub.Core.Resources.Strings";
+
+ // Act
+ var result1 = _provider.GetResourceManager(baseName);
+ var result2 = _provider.GetResourceManager(baseName);
+
+ // Assert
+ Assert.Same(result1, result2);
+ }
+
+ [Fact]
+ public void GetResourceManager_WithNullBaseName_ThrowsArgumentException()
+ {
+ // Act & Assert
+ Assert.Throws(() => _provider.GetResourceManager(null!));
+ }
+
+ [Fact]
+ public void GetResourceManager_WithEmptyBaseName_ThrowsArgumentException()
+ {
+ // Act & Assert
+ Assert.Throws(() => _provider.GetResourceManager(string.Empty));
+ }
+
+ [Fact]
+ public void GetResourceManager_WithWhitespaceBaseName_ThrowsArgumentException()
+ {
+ // Act & Assert
+ Assert.Throws(() => _provider.GetResourceManager(" "));
+ }
+
+ [Fact]
+ public void ValidateCulture_WithNullCulture_ThrowsArgumentNullException()
+ {
+ // Act & Assert
+ Assert.Throws(() => _provider.ValidateCulture(null!));
+ }
+
+ [Fact]
+ public void ValidateCulture_WithEnglishCulture_ReturnsTrue()
+ {
+ // Arrange
+ var culture = CultureInfo.GetCultureInfo("en");
+
+ // Act
+ var result = _provider.ValidateCulture(culture);
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ValidateCulture_WithEnglishUSCulture_ReturnsTrue()
+ {
+ // Arrange
+ var culture = CultureInfo.GetCultureInfo("en-US");
+
+ // Act
+ var result = _provider.ValidateCulture(culture);
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ValidateCulture_WithInvariantCulture_ReturnsTrue()
+ {
+ // Arrange
+ var culture = CultureInfo.InvariantCulture;
+
+ // Act
+ var result = _provider.ValidateCulture(culture);
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ValidateCulture_WithUnavailableCulture_ReturnsFalse()
+ {
+ // Arrange
+ // Use a culture that definitely won't have a satellite assembly
+ var culture = CultureInfo.GetCultureInfo("zu-ZA"); // Zulu
+
+ // Act
+ var result = _provider.ValidateCulture(culture);
+
+ // Assert
+ // This will be false unless satellite assembly exists
+ Assert.False(result);
+ }
+}
\ No newline at end of file
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Services/Localization/LocalizationServiceTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Services/Localization/LocalizationServiceTests.cs
new file mode 100644
index 000000000..4d8110dfc
--- /dev/null
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Services/Localization/LocalizationServiceTests.cs
@@ -0,0 +1,408 @@
+using System.Globalization;
+using System.Reactive.Linq;
+using GenHub.Core.Interfaces.Localization;
+using GenHub.Core.Models.Localization;
+using GenHub.Core.Resources.Strings;
+using GenHub.Core.Services.Localization;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+
+namespace GenHub.Tests.Core.Services.Localization;
+
+///
+/// Tests for .
+///
+public class LocalizationServiceTests : IDisposable
+{
+ private readonly Mock _mockLanguageProvider;
+ private readonly Mock> _mockLogger;
+ private readonly LocalizationService _service;
+ private readonly LocalizationOptions _options;
+
+ public LocalizationServiceTests()
+ {
+ _mockLanguageProvider = new Mock();
+ _mockLogger = new Mock>();
+ _options = new LocalizationOptions
+ {
+ DefaultCulture = "en",
+ FallbackCulture = "en",
+ LogMissingTranslations = true,
+ ThrowOnMissingTranslation = false
+ };
+
+ // Setup default behavior for language provider
+ var availableCultures = new List
+ {
+ CultureInfo.GetCultureInfo("en"),
+ CultureInfo.GetCultureInfo("de"),
+ CultureInfo.GetCultureInfo("fr")
+ };
+
+ _mockLanguageProvider
+ .Setup(p => p.DiscoverAvailableLanguages())
+ .ReturnsAsync(availableCultures.AsReadOnly());
+
+ _mockLanguageProvider
+ .Setup(p => p.ValidateCulture(It.IsAny()))
+ .Returns(c =>
+ c.TwoLetterISOLanguageName == "en" ||
+ c.TwoLetterISOLanguageName == "de" ||
+ c.TwoLetterISOLanguageName == "fr");
+
+ _service = new LocalizationService(_mockLanguageProvider.Object, _mockLogger.Object, _options);
+ }
+
+ public void Dispose()
+ {
+ _service?.Dispose();
+ GC.SuppressFinalize(this);
+ }
+
+ [Fact]
+ public void Constructor_WithNullLanguageProvider_ThrowsArgumentNullException()
+ {
+ // Act & Assert
+ Assert.Throws(() =>
+ new LocalizationService(null!, _mockLogger.Object, _options));
+ }
+
+ [Fact]
+ public void Constructor_WithNullLogger_ThrowsArgumentNullException()
+ {
+ // Act & Assert
+ Assert.Throws(() =>
+ new LocalizationService(_mockLanguageProvider.Object, null!, _options));
+ }
+
+ [Fact]
+ public void Constructor_WithNullOptions_UsesDefaultOptions()
+ {
+ // Act
+ using var service = new LocalizationService(_mockLanguageProvider.Object, _mockLogger.Object, null);
+
+ // Assert
+ Assert.NotNull(service);
+ Assert.NotNull(service.CurrentCulture);
+ }
+
+ [Fact]
+ public void Constructor_SetsDefaultCulture()
+ {
+ // Assert
+ Assert.Equal("en", _service.CurrentCulture.TwoLetterISOLanguageName);
+ }
+
+ [Fact]
+ public void CurrentCulture_Get_ReturnsCurrentCulture()
+ {
+ // Act
+ var culture = _service.CurrentCulture;
+
+ // Assert
+ Assert.NotNull(culture);
+ Assert.Equal("en", culture.TwoLetterISOLanguageName);
+ }
+
+ [Fact]
+ public async Task CurrentCulture_Set_ChangesCurrentCulture()
+ {
+ // Arrange
+ var germanCulture = CultureInfo.GetCultureInfo("de");
+
+ // Act
+ _service.CurrentCulture = germanCulture;
+ await Task.Delay(100); // Give async operation time to complete
+
+ // Assert
+ Assert.Equal("de", _service.CurrentCulture.TwoLetterISOLanguageName);
+ }
+
+ [Fact]
+ public void AvailableCultures_ReturnsDiscoveredCultures()
+ {
+ // Act
+ var cultures = _service.AvailableCultures;
+
+ // Assert
+ Assert.NotNull(cultures);
+ Assert.Contains(cultures, c => c.TwoLetterISOLanguageName == "en");
+ Assert.Contains(cultures, c => c.TwoLetterISOLanguageName == "de");
+ Assert.Contains(cultures, c => c.TwoLetterISOLanguageName == "fr");
+ }
+
+ [Fact]
+ public void CultureChanged_IsObservable()
+ {
+ // Act & Assert
+ Assert.NotNull(_service.CultureChanged);
+ }
+
+ [Fact]
+ public async Task SetCulture_WithValidCulture_UpdatesCurrentCulture()
+ {
+ // Arrange
+ var germanCulture = CultureInfo.GetCultureInfo("de");
+
+ // Act
+ await _service.SetCulture(germanCulture);
+
+ // Assert
+ Assert.Equal("de", _service.CurrentCulture.TwoLetterISOLanguageName);
+ }
+
+ [Fact]
+ public async Task SetCulture_WithValidCulture_NotifiesObservers()
+ {
+ // Arrange
+ var germanCulture = CultureInfo.GetCultureInfo("de");
+ CultureInfo? notifiedCulture = null;
+ using var subscription = _service.CultureChanged.Subscribe(c => notifiedCulture = c);
+
+ // Act
+ await _service.SetCulture(germanCulture);
+
+ // Assert
+ Assert.NotNull(notifiedCulture);
+ Assert.Equal("de", notifiedCulture.TwoLetterISOLanguageName);
+ }
+
+ [Fact]
+ public async Task SetCulture_WithInvalidCulture_FallsBackToDefaultCulture()
+ {
+ // Arrange
+ var invalidCulture = CultureInfo.GetCultureInfo("zu-ZA"); // Zulu
+
+ // Act
+ await _service.SetCulture(invalidCulture);
+
+ // Assert - should fall back to English
+ Assert.Equal("en", _service.CurrentCulture.TwoLetterISOLanguageName);
+ }
+
+ [Fact]
+ public async Task SetCulture_WithNullCulture_ThrowsArgumentNullException()
+ {
+ // Act & Assert
+ await Assert.ThrowsAsync(() => _service.SetCulture((CultureInfo)null!));
+ }
+
+ [Fact]
+ public async Task SetCulture_ByCultureName_UpdatesCurrentCulture()
+ {
+ // Act
+ await _service.SetCulture("de");
+
+ // Assert
+ Assert.Equal("de", _service.CurrentCulture.TwoLetterISOLanguageName);
+ }
+
+ [Fact]
+ public async Task SetCulture_WithNullCultureName_ThrowsArgumentException()
+ {
+ // Act & Assert
+ await Assert.ThrowsAsync(() => _service.SetCulture((string)null!));
+ }
+
+ [Fact]
+ public async Task SetCulture_WithEmptyCultureName_ThrowsArgumentException()
+ {
+ // Act & Assert
+ await Assert.ThrowsAsync(() => _service.SetCulture(string.Empty));
+ }
+
+ [Fact]
+ public async Task SetCulture_WithWhitespaceCultureName_ThrowsArgumentException()
+ {
+ // Act & Assert
+ await Assert.ThrowsAsync(() => _service.SetCulture(" "));
+ }
+
+ [Fact]
+ public async Task SetCulture_WithInvalidCultureName_FallsBackToDefaultCulture()
+ {
+ // Act
+ await _service.SetCulture("invalid-culture");
+
+ // Assert - should fall back to English since invalid culture
+ Assert.Equal("en", _service.CurrentCulture.TwoLetterISOLanguageName);
+ }
+
+ [Fact]
+ public void GetString_WithNullKey_ThrowsArgumentException()
+ {
+ // Act & Assert
+ Assert.Throws(() => _service.GetString(StringResources.UiCommon, null!));
+ }
+
+ [Fact]
+ public void GetString_WithEmptyKey_ThrowsArgumentException()
+ {
+ // Act & Assert
+ Assert.Throws(() => _service.GetString(StringResources.UiCommon, string.Empty));
+ }
+
+ [Fact]
+ public void GetString_WithWhitespaceKey_ThrowsArgumentException()
+ {
+ // Act & Assert
+ Assert.Throws(() => _service.GetString(StringResources.UiCommon, " "));
+ }
+
+ [Fact]
+ public void GetString_WithMissingKey_ReturnsKeyWithMarker()
+ {
+ // Arrange
+ var key = "NonExistentKey";
+
+ // Act
+ var result = _service.GetString(StringResources.UiCommon, key);
+
+ // Assert - when LogMissingTranslations is true, returns [key]
+ Assert.Contains(key, result);
+ }
+
+ [Fact]
+ public void GetString_WithParameters_FormatsString()
+ {
+ // Arrange
+ var key = "TestKey";
+ var args = new object[] { "test", 123 };
+
+ // Act
+ var result = _service.GetString(StringResources.UiCommon, key, args);
+
+ // Assert
+ Assert.NotNull(result);
+ }
+
+ [Fact]
+ public void GetString_WithNullParameters_ReturnsUnformattedString()
+ {
+ // Arrange
+ var key = "TestKey";
+
+ // Act
+ var result = _service.GetString(StringResources.UiCommon, key, (object[])null!);
+
+ // Assert
+ Assert.NotNull(result);
+ }
+
+ [Fact]
+ public async Task RefreshAvailableCultures_UpdatesAvailableCultures()
+ {
+ // Arrange
+ var newCultures = new List
+ {
+ CultureInfo.GetCultureInfo("en"),
+ CultureInfo.GetCultureInfo("es")
+ };
+
+ _mockLanguageProvider
+ .Setup(p => p.DiscoverAvailableLanguages())
+ .ReturnsAsync(newCultures.AsReadOnly());
+
+ // Act
+ await _service.RefreshAvailableCultures();
+
+ // Assert
+ var cultures = _service.AvailableCultures;
+ Assert.Contains(cultures, c => c.TwoLetterISOLanguageName == "es");
+ }
+
+ [Fact]
+ public async Task CultureChanged_EmitsMultipleNotifications()
+ {
+ // Arrange
+ var notifications = new List();
+ using var subscription = _service.CultureChanged.Subscribe(c => notifications.Add(c));
+
+ // Act
+ await _service.SetCulture("de");
+ await _service.SetCulture("fr");
+ await _service.SetCulture("en");
+
+ // Assert
+ Assert.Equal(3, notifications.Count);
+ Assert.Equal("de", notifications[0].TwoLetterISOLanguageName);
+ Assert.Equal("fr", notifications[1].TwoLetterISOLanguageName);
+ Assert.Equal("en", notifications[2].TwoLetterISOLanguageName);
+ }
+
+ [Fact]
+ public async Task SetCulture_ThreadSafe_HandlesMultipleConcurrentCalls()
+ {
+ // Arrange
+ var cultures = new[] { "en", "de", "fr" };
+ var tasks = new List();
+
+ // Act
+ for (int i = 0; i < 10; i++)
+ {
+ var culture = cultures[i % cultures.Length];
+ tasks.Add(_service.SetCulture(culture));
+ }
+
+ await Task.WhenAll(tasks);
+
+ // Assert
+ Assert.NotNull(_service.CurrentCulture);
+ Assert.Contains(_service.CurrentCulture.TwoLetterISOLanguageName, cultures);
+ }
+
+ [Fact]
+ public void Dispose_CompletesObservable()
+ {
+ // Arrange
+ bool completed = false;
+ using var subscription = _service.CultureChanged.Subscribe(
+ _ => { },
+ () => completed = true);
+
+ // Act
+ _service.Dispose();
+
+ // Assert
+ Assert.True(completed);
+ }
+
+ [Fact]
+ public void GetString_WithThrowOnMissingTranslationEnabled_ThrowsException()
+ {
+ // Arrange
+ var options = new LocalizationOptions
+ {
+ ThrowOnMissingTranslation = true
+ };
+
+ using var service = new LocalizationService(_mockLanguageProvider.Object, _mockLogger.Object, options);
+ var key = "NonExistentKey";
+
+ // Act & Assert
+ Assert.Throws(() => service.GetString(StringResources.UiCommon, key));
+ }
+
+ [Fact]
+ public void GetString_WithLogMissingTranslationsDisabled_ReturnsCleanKey()
+ {
+ // Arrange
+ var options = new LocalizationOptions
+ {
+ LogMissingTranslations = false,
+ ThrowOnMissingTranslation = false
+ };
+
+ using var service = new LocalizationService(_mockLanguageProvider.Object, _mockLogger.Object, options);
+ var key = "NonExistentKey";
+
+ // Act
+ var result = service.GetString(StringResources.UiCommon, key);
+
+ // Assert
+ Assert.Equal(key, result);
+ Assert.DoesNotContain("[", result);
+ Assert.DoesNotContain("]", result);
+ }
+}
\ No newline at end of file
diff --git a/GenHub/GenHub/Infrastructure/DependencyInjection/LocalizationServicesModule.cs b/GenHub/GenHub/Infrastructure/DependencyInjection/LocalizationServicesModule.cs
new file mode 100644
index 000000000..58eb843c8
--- /dev/null
+++ b/GenHub/GenHub/Infrastructure/DependencyInjection/LocalizationServicesModule.cs
@@ -0,0 +1,54 @@
+using System;
+using GenHub.Core.Interfaces.Localization;
+using GenHub.Core.Models.Localization;
+using GenHub.Core.Services.Localization;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace GenHub.Infrastructure.DependencyInjection;
+
+///
+/// Provides dependency injection registration for localization services.
+///
+public static class LocalizationServicesModule
+{
+ ///
+ /// Adds localization services to the service collection.
+ ///
+ /// The service collection.
+ /// Optional localization options. If null, default options will be used.
+ /// The updated service collection.
+ public static IServiceCollection AddLocalizationServices(
+ this IServiceCollection services,
+ LocalizationOptions? options = null)
+ {
+ // Register localization options as singleton
+ var localizationOptions = options ?? new LocalizationOptions();
+ services.AddSingleton(localizationOptions);
+
+ // Register language provider as singleton
+ services.AddSingleton();
+
+ // Register localization service as singleton
+ services.AddSingleton();
+
+ return services;
+ }
+
+ ///
+ /// Adds localization services to the service collection with configuration action.
+ ///
+ /// The service collection.
+ /// Action to configure localization options.
+ /// The updated service collection.
+ public static IServiceCollection AddLocalizationServices(
+ this IServiceCollection services,
+ Action configureOptions)
+ {
+ ArgumentNullException.ThrowIfNull(configureOptions, nameof(configureOptions));
+
+ var options = new LocalizationOptions();
+ configureOptions(options);
+
+ return services.AddLocalizationServices(options);
+ }
+}
\ No newline at end of file
diff --git a/docs/architecture/localization-system-design.md b/docs/architecture/localization-system-design.md
new file mode 100644
index 000000000..8a5dd0706
--- /dev/null
+++ b/docs/architecture/localization-system-design.md
@@ -0,0 +1,1808 @@
+# GenHub Localization System Architecture Design
+
+## Table of Contents
+
+1. [Technology Evaluation](#1-technology-evaluation)
+2. [System Architecture Design](#2-system-architecture-design)
+3. [Avalonia Integration Strategy](#3-avalonia-integration-strategy)
+4. [Developer Experience](#4-developer-experience)
+5. [Translator Experience](#5-translator-experience)
+6. [Implementation Phases](#6-implementation-phases)
+7. [Technical Specifications](#7-technical-specifications)
+8. [Testing Strategy](#8-testing-strategy)
+9. [Open Questions and Future Considerations](#9-open-questions-and-future-considerations)
+
+---
+
+## 1. Technology Evaluation
+
+### 1.1 Selected Approach: .NET Resource Files (.resx)
+
+**Overview**: Traditional .NET localization using ResX files compiled into satellite assemblies.
+
+**Strengths**:
+
+- ✅ **Officially recommended** by Avalonia documentation
+- ✅ **Native .NET framework** support with ResourceManager
+- ✅ **Strong tooling** in Visual Studio, Rider, and ResXManager
+- ✅ **Design-time XAML IntelliSense** for resource keys
+- ✅ **Type-safe generated classes** (e.g., `Resources.UI_Common_Save`)
+- ✅ **Built-in parameter substitution** via `string.Format`
+- ✅ **Satellite assemblies** for clean deployment and optional language packs
+- ✅ **Professional translation workflows** (XLIFF export/import)
+- ✅ **Cultural formatting** (dates, numbers, currency) automatic
+
+**Challenges and Mitigations**:
+
+| Challenge | Mitigation |
+|-----------|------------|
+| XML-based format for translators | Provide ResXManager GUI tool (cross-platform) |
+| Runtime language switching complexity | Custom `LocalizationService` wrapping `ResourceManager` |
+| Version control noise | Git attributes for .resx files, focus PRs on satellite assemblies |
+| Community contribution barrier | Detailed guide, ResXManager, validation tools |
+| Linux tooling | ResXManager runs on Linux via .NET, provide CLI alternative |
+
+**Decision**: The benefits of .resx (native support, tooling, type safety) outweigh the challenges, especially with proper mitigation strategies.
+
+### 1.2 Alternative: JSON-Based Localization
+
+**Why Not JSON**: While JSON is more accessible to non-technical users, .resx provides:
+
+- Native Avalonia support (less custom code)
+- Better tooling ecosystem
+- Industry-standard translation workflows
+- Type-safe resource access
+- Compiled resources (slightly better performance)
+
+**Verdict**: .resx is the better fit for a production-quality application, despite JSON's simplicity advantage.
+
+### 1.3 Alternative: XAML Resource Dictionaries
+
+**Why Not**: XAML dictionaries don't scale well, lack parameter substitution, and have poor validation.
+
+**Verdict**: Not suitable for comprehensive localization.
+
+---
+
+## 2. System Architecture Design
+
+### 2.1 Service Layer Architecture
+
+#### 2.1.1 Core Interfaces
+
+```csharp
+namespace GenHub.Core.Interfaces.Localization;
+
+///
+/// Provides localization services for retrieving translated strings with runtime language switching.
+///
+public interface ILocalizationService
+{
+ ///
+ /// Gets the currently active culture.
+ ///
+ CultureInfo CurrentCulture { get; }
+
+ ///
+ /// Gets an observable that emits whenever the language/culture changes.
+ /// Subscribe to this for reactive UI updates.
+ ///
+ IObservable CultureChanged { get; }
+
+ ///
+ /// Gets all available cultures (languages) in the application.
+ ///
+ IReadOnlyList AvailableLanguages { get; }
+
+ ///
+ /// Changes the active culture/language.
+ ///
+ /// The culture to activate.
+ /// Operation result indicating success or failure.
+ OperationResult SetCulture(CultureInfo culture);
+
+ ///
+ /// Changes the active culture by language code (e.g., "en", "de").
+ ///
+ /// The language code.
+ /// Operation result indicating success or failure.
+ OperationResult SetLanguage(string languageCode);
+
+ ///
+ /// Gets a localized string from the specified resource set.
+ ///
+ /// The resource set name (e.g., "UI.Common").
+ /// The resource key (e.g., "Save").
+ /// The localized string, or the key if not found.
+ string GetString(string resourceSet, string key);
+
+ ///
+ /// Gets a localized string with parameter substitution.
+ ///
+ /// The resource set name.
+ /// The resource key.
+ /// Arguments for string.Format.
+ /// The formatted localized string.
+ string GetString(string resourceSet, string key, params object[] args);
+
+ ///
+ /// Tries to get a localized string, returning success status.
+ ///
+ bool TryGetString(string resourceSet, string key, out string value);
+}
+
+///
+/// Represents information about an available language.
+///
+public class LanguageInfo
+{
+ /// Gets the culture info.
+ public required CultureInfo Culture { get; init; }
+
+ /// Gets the language code (e.g., "en", "de").
+ public string Code => Culture.TwoLetterISOLanguageName;
+
+ /// Gets the native language name (e.g., "English", "Deutsch").
+ public string NativeName => Culture.NativeName;
+
+ /// Gets the English language name (e.g., "English", "German").
+ public string EnglishName => Culture.EnglishName;
+
+ /// Gets whether this is the default fallback language.
+ public bool IsDefault { get; init; }
+
+ /// Gets whether a satellite assembly exists for this culture.
+ public bool HasSatelliteAssembly { get; init; }
+}
+```
+
+#### 2.1.2 Resource Manager Abstraction
+
+```csharp
+namespace GenHub.Core.Interfaces.Localization;
+
+///
+/// Manages resource managers for different resource sets.
+///
+public interface IResourceManagerProvider
+{
+ ///
+ /// Gets a ResourceManager for the specified resource set.
+ ///
+ /// The resource set name (e.g., "UI.Common").
+ /// The ResourceManager instance.
+ ResourceManager GetResourceManager(string resourceSet);
+
+ ///
+ /// Discovers all available satellite assembly cultures.
+ ///
+ /// List of available cultures.
+ IReadOnlyList DiscoverAvailableCultures();
+
+ ///
+ /// Refreshes all resource managers (useful after culture change).
+ ///
+ void RefreshResourceManagers();
+}
+```
+
+#### 2.1.3 Dependency Injection Integration
+
+**Service Lifetimes**:
+
+- `ILocalizationService`: **Singleton** - Single instance for app-wide culture management
+- `IResourceManagerProvider`: **Singleton** - Caches ResourceManager instances
+- Resource classes (e.g., `Resources`): **Static** - Auto-generated by .NET
+
+**Registration Pattern**:
+
+```csharp
+public static class LocalizationModule
+{
+ public static IServiceCollection AddLocalizationServices(
+ this IServiceCollection services,
+ IConfigurationProviderService configProvider)
+ {
+ // Register resource manager provider
+ services.AddSingleton();
+
+ // Register localization service
+ services.AddSingleton(provider =>
+ {
+ var resourceProvider = provider.GetRequiredService();
+ var logger = provider.GetRequiredService>();
+ var userSettings = provider.GetRequiredService();
+
+ // Get default culture from user settings or app config
+ var defaultCulture = GetCultureFromSettings(userSettings, configProvider);
+
+ return new LocalizationService(
+ resourceProvider,
+ defaultCulture,
+ logger);
+ });
+
+ return services;
+ }
+
+ private static CultureInfo GetCultureFromSettings(
+ IUserSettingsService userSettings,
+ IConfigurationProviderService configProvider)
+ {
+ var languageCode = userSettings.Get().PreferredLanguage
+ ?? configProvider.GetDefaultLanguage();
+
+ try
+ {
+ return CultureInfo.GetCultureInfo(languageCode);
+ }
+ catch
+ {
+ return CultureInfo.GetCultureInfo("en-US");
+ }
+ }
+}
+```
+
+### 2.2 Resource Organization
+
+#### 2.2.1 Project Structure
+
+```
+GenHub/
+├── GenHub.Core/
+│ ├── Resources/
+│ │ ├── Strings/ # All .resx files
+│ │ │ ├── UI.Common.resx # English (neutral/default)
+│ │ │ ├── UI.Common.de.resx # German
+│ │ │ ├── UI.Common.fr.resx # French
+│ │ │ ├── UI.Common.es.resx # Spanish
+│ │ │ ├── UI.GameProfiles.resx # English
+│ │ │ ├── UI.GameProfiles.de.resx # German
+│ │ │ ├── Errors.Validation.resx # English
+│ │ │ ├── Errors.Validation.de.resx
+│ │ │ └── ...
+│ │ └── Strings.Designer.cs # Auto-generated (if using custom)
+│ └── Localization/
+│ └── LocalizationKeys.cs # Helper class (optional)
+```
+
+**Build Configuration** (`GenHub.Core.csproj`):
+
+```xml
+
+
+ net8.0
+ enable
+
+ en;de;fr;es
+
+
+
+
+
+ ResXFileCodeGenerator
+ GenHub.Core.Resources
+
+
+
+```
+
+#### 2.2.2 Resource File Naming Convention
+
+**Pattern**: `..resx`
+
+**Examples**:
+
+- `UI.Common.resx` → Common UI strings (Save, Cancel, etc.)
+- `UI.GameProfiles.resx` → Game Profiles view strings
+- `UI.Settings.resx` → Settings view strings
+- `UI.Navigation.resx` → Navigation tab strings
+- `Errors.Validation.resx` → Validation error messages
+- `Errors.Launch.resx` → Game launch error messages
+- `Messages.Success.resx` → Success notification messages
+- `Tooltips.resx` → Tooltip texts
+
+**Culture-Specific Naming**:
+
+- `UI.Common.resx` → English (neutral culture, fallback)
+- `UI.Common.de.resx` → German
+- `UI.Common.fr.resx` → French
+- `UI.Common.es.resx` → Spanish
+
+#### 2.2.3 Resource File Content Structure
+
+**UI.Common.resx** (English - Neutral):
+
+```xml
+
+
+
+ Save
+ Button text for save action
+
+
+ Cancel
+ Button text for cancel action
+
+
+ Delete
+ Button text for delete action
+
+
+ Edit
+ Button text for edit action
+
+
+ Create
+ Button text for create action
+
+
+ Browse...
+ Button text for file browser
+
+
+ Refresh
+ Button text for refresh action
+
+
+```
+
+**UI.GameProfiles.resx** (English):
+
+```xml
+
+
+
+ Game Profiles
+ Title for Game Profiles view
+
+
+ No game profiles found. Create one to get started!
+ Message shown when no profiles exist
+
+
+ Create Profile
+ Button text to create a new profile
+
+
+ Launch Game
+ Button text to launch a game
+
+
+ Profile Name
+ Label for profile name field
+
+
+```
+
+**Errors.Launch.resx** (English with parameters):
+
+```xml
+
+
+
+ Game profile '{0}' not found
+ Error when profile doesn't exist. Parameter: {0} = profile name
+
+
+ Failed to prepare workspace: {0}
+ Error during workspace preparation. Parameter: {0} = error details
+
+
+ Failed to start game process
+ Error when game process won't start
+
+
+```
+
+#### 2.2.4 Auto-Generated Resource Classes
+
+**.NET automatically generates** strongly-typed resource classes:
+
+**Conceptual output** (actual is in satellite assemblies):
+
+```csharp
+namespace GenHub.Core.Resources
+{
+ internal class UI_Common
+ {
+ private static ResourceManager resourceMan;
+ private static CultureInfo resourceCulture;
+
+ internal static ResourceManager ResourceManager
+ {
+ get
+ {
+ if (resourceMan == null)
+ {
+ resourceMan = new ResourceManager(
+ "GenHub.Core.Resources.Strings.UI.Common",
+ typeof(UI_Common).Assembly);
+ }
+ return resourceMan;
+ }
+ }
+
+ internal static string Save
+ {
+ get { return ResourceManager.GetString("Save", resourceCulture); }
+ }
+
+ internal static string Cancel
+ {
+ get { return ResourceManager.GetString("Cancel", resourceCulture); }
+ }
+
+ // ... other properties
+ }
+}
+```
+
+### 2.3 Fallback Mechanism
+
+#### 2.3.1 Native .NET Resource Fallback
+
+**.NET provides built-in fallback**:
+
+1. **Requested Culture** (e.g., `de-DE`)
+2. **Neutral Culture** (e.g., `de`)
+3. **Neutral Resource** (e.g., English from `UI.Common.resx`)
+
+**Example**:
+
+- User selects German (`de-DE`)
+- ResourceManager looks for `UI.Common.de-DE.resx` (doesn't exist)
+- Falls back to `UI.Common.de.resx` (exists, uses this)
+- If `de.resx` is missing a key, falls back to `UI.Common.resx` (English)
+
+**No custom fallback logic needed** - .NET handles it automatically!
+
+#### 2.3.2 Missing Translation Handling
+
+**Development Mode**:
+
+```csharp
+public class LocalizationService : ILocalizationService
+{
+ public string GetString(string resourceSet, string key)
+ {
+ var resourceManager = _resourceProvider.GetResourceManager(resourceSet);
+
+ try
+ {
+ var value = resourceManager.GetString(key, CultureInfo.CurrentUICulture);
+
+ if (string.IsNullOrEmpty(value))
+ {
+ _logger.LogWarning(
+ "Missing resource: {ResourceSet}.{Key} in culture {Culture}",
+ resourceSet, key, CultureInfo.CurrentUICulture.Name);
+
+ return $"[{resourceSet}.{key}]"; // Visual indicator
+ }
+
+ return value;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex,
+ "Error getting resource: {ResourceSet}.{Key}",
+ resourceSet, key);
+
+ return $"[{resourceSet}.{key}]";
+ }
+ }
+}
+```
+
+### 2.4 Dynamic Language Discovery
+
+#### 2.4.1 Satellite Assembly Discovery
+
+**Automatic Discovery** via assembly scanning:
+
+```csharp
+public class ResourceManagerProvider : IResourceManagerProvider
+{
+ private readonly Dictionary _resourceManagers = new();
+ private IReadOnlyList? _availableCultures;
+
+ public IReadOnlyList DiscoverAvailableCultures()
+ {
+ if (_availableCultures != null)
+ return _availableCultures;
+
+ var cultures = new List();
+ var assembly = typeof(ResourceManagerProvider).Assembly;
+
+ // Get all satellite assemblies
+ var satelliteCultures = CultureInfo.GetCultures(CultureTypes.AllCultures)
+ .Where(culture =>
+ {
+ try
+ {
+ // Check if satellite assembly exists
+ var satelliteAssembly = assembly.GetSatelliteAssembly(culture);
+ return satelliteAssembly != null;
+ }
+ catch
+ {
+ return false;
+ }
+ })
+ .ToList();
+
+ // Add neutral/invariant culture (English)
+ cultures.Add(CultureInfo.InvariantCulture);
+ cultures.Add(CultureInfo.GetCultureInfo("en-US"));
+
+ // Add discovered satellite cultures
+ cultures.AddRange(satelliteCultures);
+
+ _availableCultures = cultures
+ .DistinctBy(c => c.TwoLetterISOLanguageName)
+ .OrderBy(c => c.EnglishName)
+ .ToList();
+
+ return _availableCultures;
+ }
+
+ public ResourceManager GetResourceManager(string resourceSet)
+ {
+ if (_resourceManagers.TryGetValue(resourceSet, out var manager))
+ return manager;
+
+ // Create ResourceManager for this set
+ var baseName = $"GenHub.Core.Resources.Strings.{resourceSet}";
+ var assembly = typeof(ResourceManagerProvider).Assembly;
+
+ manager = new ResourceManager(baseName, assembly);
+ _resourceManagers[resourceSet] = manager;
+
+ return manager;
+ }
+}
+```
+
+#### 2.4.2 Language Metadata
+
+**Create `LanguageInfo` from discovered cultures**:
+
+```csharp
+public IReadOnlyList AvailableLanguages
+{
+ get
+ {
+ var cultures = _resourceProvider.DiscoverAvailableCultures();
+
+ return cultures.Select(culture => new LanguageInfo
+ {
+ Culture = culture,
+ IsDefault = culture.TwoLetterISOLanguageName == "en",
+ HasSatelliteAssembly = IsSatelliteAssemblyAvailable(culture)
+ }).ToList();
+ }
+}
+
+private bool IsSatelliteAssemblyAvailable(CultureInfo culture)
+{
+ try
+ {
+ var assembly = typeof(LocalizationService).Assembly;
+ var satellite = assembly.GetSatelliteAssembly(culture);
+ return satellite != null;
+ }
+ catch
+ {
+ return false;
+ }
+}
+```
+
+### 2.5 Runtime Language Switching
+
+#### 2.5.1 Culture Switching Strategy
+
+**Thread-Wide Culture Change**:
+
+```csharp
+public class LocalizationService : ILocalizationService
+{
+ private readonly Subject _cultureChangedSubject = new();
+ private CultureInfo _currentCulture;
+
+ public CultureInfo CurrentCulture => _currentCulture;
+
+ public IObservable CultureChanged =>
+ _cultureChangedSubject.AsObservable();
+
+ public OperationResult SetCulture(CultureInfo culture)
+ {
+ try
+ {
+ // Validate culture is available
+ if (!IsLanguageAvailable(culture))
+ {
+ return OperationResult.CreateFailure(
+ $"Culture '{culture.Name}' is not available");
+ }
+
+ // Set thread cultures
+ CultureInfo.CurrentCulture = culture;
+ CultureInfo.CurrentUICulture = culture;
+ Thread.CurrentThread.CurrentCulture = culture;
+ Thread.CurrentThread.CurrentUICulture = culture;
+
+ _currentCulture = culture;
+
+ // Refresh all resource managers to pick up new culture
+ _resourceProvider.RefreshResourceManagers();
+
+ // Notify subscribers on UI thread
+ RxApp.MainThreadScheduler.Schedule(() =>
+ _cultureChangedSubject.OnNext(culture));
+
+ _logger.LogInformation(
+ "Culture changed to: {Culture}", culture.Name);
+
+ return OperationResult.CreateSuccess(true);
+ }
+ catch (Exception ex)
+ {
+ return OperationResult.CreateFailure(
+ $"Culture switch failed: {ex.Message}");
+ }
+ }
+
+ public OperationResult SetLanguage(string languageCode)
+ {
+ try
+ {
+ var culture = CultureInfo.GetCultureInfo(languageCode);
+ return SetCulture(culture);
+ }
+ catch (CultureNotFoundException ex)
+ {
+ return OperationResult.CreateFailure(
+ $"Invalid language code: {languageCode}");
+ }
+ }
+
+ private bool IsLanguageAvailable(CultureInfo culture)
+ {
+ return AvailableLanguages.Any(l =>
+ l.Culture.TwoLetterISOLanguageName == culture.TwoLetterISOLanguageName);
+ }
+}
+```
+
+#### 2.5.2 Resource Manager Refresh
+
+```csharp
+public class ResourceManagerProvider : IResourceManagerProvider
+{
+ public void RefreshResourceManagers()
+ {
+ // ResourceManager automatically uses current thread culture
+ // No explicit refresh needed, but we clear cache to be safe
+ foreach (var manager in _resourceManagers.Values)
+ {
+ manager.ReleaseAllResources();
+ }
+ }
+}
+```
+
+#### 2.5.3 ViewModel Integration Pattern
+
+**Reactive Property Updates**:
+
+```csharp
+public partial class GameProfilesViewModel : ViewModelBase
+{
+ private readonly ILocalizationService _localization;
+ private readonly IResourceManagerProvider _resourceProvider;
+
+ [ObservableProperty]
+ private string _title;
+
+ [ObservableProperty]
+ private string _createButtonText;
+
+ [ObservableProperty]
+ private string _noProfilesMessage;
+
+ public GameProfilesViewModel(ILocalizationService localization)
+ {
+ _localization = localization;
+
+ // Initialize with current culture
+ UpdateLocalizedStrings();
+
+ // Subscribe to culture changes
+ _localization.CultureChanged
+ .ObserveOn(RxApp.MainThreadScheduler)
+ .Subscribe(_ => UpdateLocalizedStrings());
+ }
+
+ private void UpdateLocalizedStrings()
+ {
+ Title = _localization.GetString("UI.GameProfiles", "Title");
+ CreateButtonText = _localization.GetString("UI.GameProfiles", "CreateProfile");
+ NoProfilesMessage = _localization.GetString("UI.GameProfiles", "NoProfiles");
+ }
+}
+```
+
+---
+
+## 3. Avalonia Integration Strategy
+
+### 3.1 XAML Binding with Resource Extensions
+
+#### 3.1.1 Static Resource Binding (Simple Approach)
+
+**Using x:Static** with generated resource classes:
+
+```xaml
+
+
+
+
+
+
+
+
+
+
+
+```
+
+**Limitation**: Static bindings don't update on culture change. Need reactive approach.
+
+#### 3.1.2 ViewModel Property Binding (Recommended for Dynamic)
+
+**For runtime language switching**, expose properties:
+
+```xaml
+
+
+
+
+
+
+
+
+
+
+
+```
+
+#### 3.1.3 Custom Markup Extension (Advanced)
+
+**Dynamic resource binding** with custom extension:
+
+```csharp
+namespace GenHub.Common.MarkupExtensions;
+
+///
+/// Markup extension for dynamic localized resources that update on culture change.
+///
+public class LocalizeExtension : MarkupExtension
+{
+ public string ResourceSet { get; set; }
+ public string Key { get; set; }
+
+ public LocalizeExtension(string resourceSet, string key)
+ {
+ ResourceSet = resourceSet;
+ Key = key;
+ }
+
+ public override object ProvideValue(IServiceProvider serviceProvider)
+ {
+ var localization = AppLocator.GetServiceOrDefault();
+
+ if (localization == null)
+ return $"[{ResourceSet}.{Key}]";
+
+ // Create reactive binding that updates on culture change
+ var binding = localization.CultureChanged
+ .Select(_ => localization.GetString(ResourceSet, Key))
+ .StartWith(localization.GetString(ResourceSet, Key));
+
+ return binding.ToBinding();
+ }
+}
+```
+
+**Usage**:
+
+```xaml
+
+
+
+```
+
+### 3.2 Design-Time Preview Support
+
+**Generated resource classes work in design-time**:
+
+```xaml
+
+
+
+
+
+```
+
+**ViewModel design-time support**:
+
+```csharp
+public class DesignTimeGameProfilesViewModel : GameProfilesViewModel
+{
+ public DesignTimeGameProfilesViewModel()
+ : base(new DesignTimeLocalizationService())
+ {
+ // Populated with design-time data
+ }
+}
+```
+
+### 3.3 Parameter Substitution in XAML
+
+**ViewModel handles formatting**:
+
+```csharp
+public string DownloadProgressText
+{
+ get
+ {
+ var template = _localization.GetString("Messages.Info", "Downloading");
+ return string.Format(template, CurrentFileName, DownloadPercentage);
+ }
+}
+
+// Alternative using service directly
+public string GetProfileNotFoundError(string profileName)
+{
+ return _localization.GetString("Errors.Launch", "ProfileNotFound", profileName);
+}
+```
+
+```xaml
+
+```
+
+---
+
+## 4. Developer Experience
+
+### 4.1 Adding New Translatable Strings
+
+**Step-by-Step Workflow**:
+
+1. **Open appropriate .resx file** in Visual Studio/Rider (e.g., `UI.GameProfiles.resx`)
+
+2. **Add new entry** in Resource Editor:
+ - Name: `NewFeatureTitle`
+ - Value: `New Feature`
+ - Comment: `Title for new feature section`
+
+3. **Build project** to generate updated resource class
+
+4. **Use in ViewModel**:
+
+```csharp
+private void UpdateLocalizedStrings()
+{
+ NewFeatureTitle = _localization.GetString("UI.GameProfiles", "NewFeatureTitle");
+}
+```
+
+5. **Bind in XAML**:
+
+```xaml
+
+```
+
+### 4.2 IntelliSense and Compile-Time Checking
+
+**Resource Editor** provides:
+
+- ✅ IntelliSense for resource keys
+- ✅ Compile-time checking (missing keys cause build errors)
+- ✅ Refactoring support (rename keys safely)
+- ✅ Preview of all translations side-by-side
+
+**Type-Safe Access**:
+
+```csharp
+// Using generated class directly (type-safe)
+var saveText = UI_Common.Save;
+
+// Using service (flexible, supports runtime switching)
+var saveText = _localization.GetString("UI.Common", "Save");
+```
+
+### 4.3 Visual Studio Resource Editor
+
+**Features**:
+
+- Side-by-side view of all cultures
+- Easy copy from neutral to culture-specific
+- Search and filter
+- Validation (missing translations highlighted)
+- String freeze warnings
+
+### 4.4 Migration Strategy for Existing Hardcoded Strings
+
+**Phase 1: Extraction Tool**
+
+```csharp
+// PowerShell script to find hardcoded strings
+Get-ChildItem -Path . -Filter *.cs -Recurse |
+ Select-String -Pattern '"[A-Z][a-z].*"' |
+ Where-Object { $_.Line -notmatch "namespace|using|//" } |
+ Export-Csv hardcoded-strings.csv
+```
+
+**Phase 2: Create .resx Entries**
+
+- Review CSV
+- Add entries to appropriate .resx files
+- Categorize by feature area
+
+**Phase 3: Replace in Code**
+
+- Replace literals with `_localization.GetString()` calls
+- Update ViewModels to expose localized properties
+- Test each replacement
+
+---
+
+## 5. Translator Experience
+
+### 5.1 ResXManager - The Translator's Tool
+
+**ResXManager** is a free, open-source tool for editing .resx files:
+
+**Features**:
+
+- ✅ Grid view of all resource files and cultures
+- ✅ Side-by-side comparison
+- ✅ Missing translation highlighting
+- ✅ Inline editing
+- ✅ Export to Excel for external translation
+- ✅ Import from Excel
+- ✅ Visual Studio extension + standalone app
+- ✅ Cross-platform (.NET-based)
+
+**Installation**:
+
+```bash
+# Visual Studio Extension
+Install from VS Marketplace: "ResXManager"
+
+# Standalone Application
+dotnet tool install -g ResXResourceManager
+```
+
+**Usage Workflow**:
+
+1. Open GenHub solution in ResXManager
+2. See all resource files in grid view
+3. Filter to see untranslated entries
+4. Edit inline or export to Excel
+5. Save changes (updates .resx files)
+6. Commit to git
+
+### 5.2 Community Contribution Workflow
+
+**Simplified Workflow for Contributors**:
+
+1. **Fork GenHub repository**
+
+2. **Install ResXManager**:
+
+ ```bash
+ dotnet tool install -g ResXResourceManager
+ ```
+
+3. **Open project in ResXManager**:
+
+ ```bash
+ resxmanager GenHub/GenHub.sln
+ ```
+
+4. **Select language to translate**:
+ - Choose from dropdown (e.g., "German - de")
+ - See all missing translations highlighted in red
+
+5. **Translate**:
+ - Edit cells directly in grid
+ - Copy English text as reference
+ - Save frequently
+
+6. **Export for external translation** (optional):
+ - File → Export to Excel
+ - Send to translator
+ - File → Import from Excel when done
+
+7. **Test locally**:
+
+ ```bash
+ dotnet build
+ dotnet run --project GenHub/GenHub.Windows
+ # Change language in Settings to verify
+ ```
+
+8. **Submit Pull Request**:
+ - Only changed `.de.resx` files (or your language)
+ - PR description: "Add German translation"
+ - CI validates completeness
+
+### 5.3 Translation Validation
+
+**Automated Validation** in CI:
+
+```csharp
+public class ResourceValidator
+{
+ public ValidationResult ValidateTranslations()
+ {
+ var issues = new List();
+
+ // Get all neutral .resx files
+ var neutralResources = Directory.GetFiles(
+ "Resources/Strings", "*.resx", SearchOption.AllDirectories)
+ .Where(f => !Regex.IsMatch(f, @"\.[a-z]{2}\.resx$"))
+ .ToList();
+
+ foreach (var neutralFile in neutralResources)
+ {
+ var neutralKeys = GetResourceKeys(neutralFile);
+
+ // Find all culture-specific files
+ var baseName = Path.GetFileNameWithoutExtension(neutralFile);
+ var directory = Path.GetDirectoryName(neutralFile);
+ var cultureFiles = Directory.GetFiles(directory!, $"{baseName}.*.resx");
+
+ foreach (var cultureFile in cultureFiles)
+ {
+ var cultureKeys = GetResourceKeys(cultureFile);
+ var culture = GetCultureFromFileName(cultureFile);
+
+ // Check for missing keys
+ var missingKeys = neutralKeys.Except(cultureKeys).ToList();
+ if (missingKeys.Any())
+ {
+ issues.Add(new ValidationIssue(
+ $"Missing {missingKeys.Count} translations in {culture}",
+ ValidationSeverity.Warning,
+ cultureFile));
+ }
+
+ // Check for extra keys (not in neutral)
+ var extraKeys = cultureKeys.Except(neutralKeys).ToList();
+ if (extraKeys.Any())
+ {
+ issues.Add(new ValidationIssue(
+ $"Extra {extraKeys.Count} keys in {culture} (not in neutral)",
+ ValidationSeverity.Info,
+ cultureFile));
+ }
+ }
+ }
+
+ return new ValidationResult(issues);
+ }
+
+ private HashSet GetResourceKeys(string resxFile)
+ {
+ using var reader = new ResXResourceReader(resxFile);
+ return reader.Cast()
+ .Select(e => e.Key.ToString())
+ .ToHashSet();
+ }
+}
+```
+
+### 5.4 Translation Guide Documentation
+
+**Create `docs/TRANSLATION_GUIDE.md`**:
+
+```markdown
+# GenHub Translation Guide
+
+## Getting Started
+
+### Prerequisites
+- Git and GitHub account
+- .NET 8 SDK
+- ResXManager tool
+
+### Setup
+1. Fork the GenHub repository
+2. Clone your fork locally
+3. Install ResXManager:
+ ```bash
+ dotnet tool install -g ResXResourceManager
+ ```
+
+## Creating a New Translation
+
+### Step 1: Check if language exists
+
+Look in `GenHub/GenHub.Core/Resources/Strings/` for your language code.
+
+### Step 2: Open in ResXManager
+
+```bash
+cd GenHub
+resxmanager GenHub.sln
+```
+
+### Step 3: Add your language
+
+- Click "Add Culture" button
+- Select your language (e.g., "de - German")
+- All resource files get `.de.resx` versions
+
+### Step 4: Translate
+
+- Red cells = missing translation
+- Click cell to edit
+- English text is in first column for reference
+- Save frequently (Ctrl+S)
+
+### Step 5: Test
+
+```bash
+dotnet build
+dotnet run --project GenHub/GenHub.Windows
+```
+
+- Go to Settings → Language
+- Select your language
+- Verify all text displays correctly
+
+### Step 6: Submit
+
+- Commit only `.XX.resx` files (your language)
+- Push to your fork
+- Open Pull Request
+- Title: "Add [Language] translation"
+
+## Tips
+
+### Context
+
+- Read comments in English resources
+- Check the application to see where text appears
+- Ask in PR if unsure about context
+
+### Parameters
+
+- Keep `{0}`, `{1}` placeholders in same positions
+- Example: `"Profile '{0}' created"` → `"Profil '{0}' erstellt"`
+
+### Special Characters
+
+- Preserve `&` for access keys: `"&Save"` → `"&Speichern"`
+- Keep `...` (ellipsis) in button text
+- Maintain line breaks `\n` if present
+
+### Consistency
+
+- Use ResXManager's search to find how terms were translated before
+- Be consistent with terminology (e.g., "Profile" always translates the same)
+
+## Questions?
+
+Open an issue or ask in Discord!
+
+---
+
+## 6. Implementation Phases
+
+### Phase 1: Core Infrastructure (PR #1)
+
+**Objective**: Establish .resx-based localization service layer.
+
+**Deliverables**:
+1. Create `ILocalizationService` interface
+2. Create `IResourceManagerProvider` interface
+3. Implement `LocalizationService` class
+4. Implement `ResourceManagerProvider` class
+5. Create `LanguageInfo` model
+6. Add `LocalizationModule` for DI registration
+7. Update `UserSettings` to include `PreferredLanguage`
+8. Unit tests for core service logic
+
+**Dependencies**: None
+
+**Files Changed**:
+- `GenHub.Core/Interfaces/Localization/ILocalizationService.cs` (new)
+- `GenHub.Core/Interfaces/Localization/IResourceManagerProvider.cs` (new)
+- `GenHub.Core/Services/Localization/LocalizationService.cs` (new)
+- `GenHub.Core/Services/Localization/ResourceManagerProvider.cs` (new)
+- `GenHub.Core/Models/Localization/LanguageInfo.cs` (new)
+- `GenHub/GenHub/Infrastructure/DependencyInjection/LocalizationModule.cs` (new)
+- `GenHub.Core/Models/Common/UserSettings.cs` (modified)
+- `GenHub.Tests.Core/Localization/LocalizationServiceTests.cs` (new)
+
+**Testing**:
+- Unit tests for service methods
+- Mock ResourceManager for testing
+- Test culture switching logic
+- Test satellite assembly discovery
+
+**Acceptance Criteria**:
+- ✅ Can discover available cultures
+- ✅ Can switch cultures programmatically
+- ✅ Culture change triggers observable event
+- ✅ All unit tests pass
+
+**Estimated Effort**: 2-3 days
+
+---
+
+### Phase 2: Resource Files Structure (PR #2)
+
+**Objective**: Create .resx file structure and English resources.
+
+**Deliverables**:
+1. Create resource file organization in `GenHub.Core/Resources/Strings/`
+2. Create `UI.Common.resx` with common UI strings
+3. Create `UI.Navigation.resx` with navigation strings
+4. Create `UI.GameProfiles.resx` with Game Profiles strings
+5. Create `UI.Settings.resx` with Settings strings
+6. Create `UI.Downloads.resx` with Downloads strings
+7. Create `Errors.Validation.resx` with validation errors
+8. Create `Errors.Launch.resx` with launch errors
+9. Create `Messages.Success.resx` with success messages
+10. Configure project to build satellite assemblies
+11. Extract existing hardcoded strings to resources
+
+**Dependencies**: Phase 1
+
+**Files Changed**:
+- `GenHub.Core/Resources/Strings/UI.Common.resx` (new)
+- `GenHub.Core/Resources/Strings/UI.Navigation.resx` (new)
+- `GenHub.Core/Resources/Strings/UI.GameProfiles.resx` (new)
+- `GenHub.Core/Resources/Strings/UI.Settings.resx` (new)
+- `GenHub.Core/Resources/Strings/Errors.Validation.resx` (new)
+- `GenHub.Core/Resources/Strings/Errors.Launch.resx` (new)
+- `GenHub.Core/Resources/Strings/Messages.Success.resx` (new)
+- `GenHub.Core/GenHub.Core.csproj` (modified - add resource configuration)
+
+**Testing**:
+- Verify resources compile correctly
+- Test resource access via ResourceManager
+- Validate all keys are accessible
+
+**Acceptance Criteria**:
+- ✅ All .resx files created with English text
+- ✅ Project builds satellite assemblies
+- ✅ Resources accessible via ResourceManager
+- ✅ All existing UI strings have resource entries
+
+**Estimated Effort**: 3-4 days (includes string extraction)
+
+---
+
+### Phase 3: ViewModel Integration (PR #3)
+
+**Objective**: Integrate localization service with ViewModels.
+
+**Deliverables**:
+1. Update `ViewModelBase` with localization helper methods
+2. Update `MainViewModel` to use localized strings
+3. Update `GameProfilesViewModel` to use localized strings
+4. Update `SettingsViewModel` to use localized strings
+5. Update `DownloadsViewModel` to use localized strings
+6. Implement reactive string updates on culture change
+7. Add design-time localization service
+
+**Dependencies**: Phase 2
+
+**Files Changed**:
+- `GenHub/GenHub/Common/ViewModels/ViewModelBase.cs` (modified)
+- `GenHub/GenHub/Common/ViewModels/MainViewModel.cs` (modified)
+- `GenHub/GenHub/Features/GameProfiles/ViewModels/GameProfilesViewModel.cs` (modified)
+- `GenHub/GenHub/Features/Settings/ViewModels/SettingsViewModel.cs` (modified)
+- `GenHub/GenHub/Features/Downloads/ViewModels/DownloadsViewModel.cs` (modified)
+
+**Testing**:
+- Manual testing of each ViewModel
+- Verify strings display correctly
+- Test reactive updates (not yet functional, but structure in place)
+
+**Acceptance Criteria**:
+- ✅ All ViewModels use `ILocalizationService`
+- ✅ Localized properties exposed for binding
+- ✅ Reactive subscription pattern implemented
+- ✅ Design-time services work
+
+**Estimated Effort**: 3-4 days
+
+---
+
+### Phase 4: XAML Binding (PR #4)
+
+**Objective**: Update XAML views to use localized bindings.
+
+**Deliverables**:
+1. Update all XAML files to bind to ViewModel localized properties
+2. Remove all hardcoded strings from XAML
+3. Implement `LocalizeExtension` markup extension (optional)
+4. Test design-time preview in all views
+
+**Dependencies**: Phase 3
+
+**Files Changed**:
+- `GenHub/GenHub/Common/Views/MainWindow.axaml` (modified)
+- `GenHub/GenHub/Features/GameProfiles/Views/*.axaml` (modified)
+- `GenHub/GenHub/Features/Settings/Views/*.axaml` (modified)
+- `GenHub/GenHub/Features/Downloads/Views/*.axaml` (modified)
+- `GenHub/GenHub/Common/MarkupExtensions/LocalizeExtension.cs` (new, optional)
+
+**Testing**:
+- Visual testing of all views
+- Verify no hardcoded strings in XAML
+- Test design-time preview
+- Verify compiled bindings work
+
+**Acceptance Criteria**:
+- ✅ All XAML uses bindings (no hardcoded text)
+- ✅ Design-time preview works
+- ✅ All views render correctly
+- ✅ Compiled bindings functional
+
+**Estimated Effort**: 2-3 days
+
+---
+
+### Phase 5: Settings UI for Language Selection (PR #5)
+
+**Objective**: Add language selector to Settings.
+
+**Deliverables**:
+1. Add language selection ComboBox to Settings view
+2. Implement language change command in SettingsViewModel
+3. Display language metadata (native name, completion %)
+4. Persist language selection to UserSettings
+5. Apply selected language on app startup
+
+**Dependencies**: Phase 4
+
+**Files Changed**:
+- `GenHub/GenHub/Features/Settings/ViewModels/SettingsViewModel.cs` (modified)
+- `GenHub/GenHub/Features/Settings/Views/SettingsView.axaml` (modified)
+- `GenHub/GenHub/App.axaml.cs` (modified - load language on startup)
+
+**Testing**:
+- Manual testing of language switching
+- Verify all UI updates immediately
+- Test persistence across restarts
+- Test invalid language handling
+
+**Acceptance Criteria**:
+- ✅ User can select language from Settings
+- ✅ All UI updates on language change
+- ✅ Selection persists across restarts
+- ✅ Language metadata displays correctly
+
+**Estimated Effort**: 2 days
+
+---
+
+### Phase 6: German Translation (PR #6)
+
+**Objective**: Add complete German translation.
+
+**Deliverables**:
+1. Create all `.de.resx` files
+2. Translate all strings to German
+3. Add validation tests for German completeness
+4. Update CI to validate translations
+5. Test German language in application
+
+**Dependencies**: Phase 5
+
+**Files Changed**:
+- `GenHub.Core/Resources/Strings/UI.Common.de.resx` (new)
+- `GenHub.Core/Resources/Strings/UI.Navigation.de.resx` (new)
+- `GenHub.Core/Resources/Strings/UI.GameProfiles.de.resx` (new)
+- `GenHub.Core/Resources/Strings/UI.Settings.de.resx` (new)
+- `GenHub.Core/Resources/Strings/Errors.Validation.de.resx` (new)
+- `GenHub.Core/Resources/Strings/Errors.Launch.de.resx` (new)
+- All other resource files with `.de.resx` versions
+
+**Testing**:
+- Native speaker review (if available)
+- Automated completeness validation
+- Visual testing with German selected
+- Test parameter substitution
+
+**Acceptance Criteria**:
+- ✅ All resource files have German versions
+- ✅ 100% translation completeness
+- ✅ No parameter mismatches
+- ✅ Visual review passed
+
+**Estimated Effort**: 3-4 days (with native speaker help)
+
+---
+
+### Phase 7: Additional Languages (PR #7)
+
+**Objective**: Add French and Spanish translations.
+
+**Deliverables**:
+1. Create all `.fr.resx` files (French)
+2. Create all `.es.resx` files (Spanish)
+3. Validate translations
+4. Update documentation for community contributions
+
+**Dependencies**: Phase 6
+
+**Files Changed**:
+- `GenHub.Core/Resources/Strings/*.fr.resx` (new)
+- `GenHub.Core/Resources/Strings/*.es.resx` (new)
+- `docs/TRANSLATION_GUIDE.md` (new)
+- `.github/workflows/validate-translations.yml` (new)
+
+**Testing**:
+- Automated validation
+- Visual testing
+- Community review
+
+**Acceptance Criteria**:
+- ✅ French and Spanish translations available
+- ✅ CI validates translations
+- ✅ Translation guide published
+- ✅ Community can contribute
+
+**Estimated Effort**: 4-5 days (assumes community help or translation service)
+
+---
+
+### Phase 8: Documentation (PR #8)
+
+**Objective**: Complete all documentation.
+
+**Deliverables**:
+1. Update architecture documentation
+2. Create developer localization guide
+3. Update coding style guide
+4. Add code examples
+5. Create troubleshooting guide
+
+**Dependencies**: Phase 7
+
+**Files Changed**:
+- `docs/architecture/localization-system-design.md` (this document)
+- `docs/dev/localization.md` (new)
+- `docs/TRANSLATION_GUIDE.md` (enhanced)
+- `CONTRIBUTING.md` (updated)
+
+**Acceptance Criteria**:
+- ✅ Comprehensive documentation exists
+- ✅ Examples are clear and work
+- ✅ Contributors can follow guides
+
+**Estimated Effort**: 2 days
+
+---
+
+## 7. Technical Specifications
+
+### 7.1 Resource Key Naming Conventions
+
+**Format**: Use descriptive PascalCase names within each resource file.
+
+**Examples**:
+```
+
+UI.Common.resx:
+
+- Save
+- Cancel
+- Delete
+- BrowseButton (if needed to distinguish from Browse command)
+
+UI.GameProfiles.resx:
+
+- Title
+- NoProfiles
+- CreateProfile
+- LaunchGame
+- ProfileCount
+
+Errors.Validation.resx:
+
+- Required
+- InvalidPath
+- ProfileNameExists
+
+```
+
+### 7.2 Parameter Substitution
+
+**Standard `string.Format` syntax**:
+
+```xml
+
+
+ Game profile '{0}' not found
+ Parameter: {0} = profile name
+
+
+
+ Downloading {0}... ({1}%)
+ Parameters: {0} = filename, {1} = percentage
+
+```
+
+**Usage**:
+
+```csharp
+var error = _localization.GetString("Errors.Launch", "ProfileNotFound", profileName);
+var progress = _localization.GetString("Messages.Info", "Downloading", fileName, percentage);
+```
+
+### 7.3 Pluralization
+
+**Separate keys approach**:
+
+```xml
+
+ No profiles
+
+
+ 1 profile
+
+
+ {0} profiles
+
+```
+
+**Helper method**:
+
+```csharp
+public string GetPlural(string resourceSet, string keyBase, int count, params object[] args)
+{
+ var key = count switch
+ {
+ 0 => $"{keyBase}None",
+ 1 => $"{keyBase}One",
+ _ => $"{keyBase}Many"
+ };
+
+ return GetString(resourceSet, key, args);
+}
+```
+
+### 7.4 Culture/Locale Handling
+
+**Culture Format**:
+
+- Use specific cultures where possible: `en-US`, `de-DE`, `fr-FR`
+- Fallback to neutral cultures: `en`, `de`, `fr`
+- Thread culture set globally on language change
+
+### 7.5 Thread Safety
+
+**Service is thread-safe**:
+
+- Culture change uses lock
+- ResourceManager is thread-safe by design
+- Observable notifications marshalled to UI thread
+
+### 7.6 Performance Targets
+
+| Operation | Target | Notes |
+|-----------|--------|-------|
+| `GetString()` | < 1 µs | ResourceManager dictionary lookup |
+| `GetString()` with formatting | < 10 µs | Includes `string.Format` |
+| Culture switch | < 100 ms | Includes ResourceManager refresh |
+| UI update after switch | < 300 ms | ViewModels update properties |
+| Satellite assembly load | < 50 ms | Lazy loaded on demand |
+
+---
+
+## 8. Testing Strategy
+
+### 8.1 Unit Tests
+
+**LocalizationServiceTests.cs**:
+
+```csharp
+public class LocalizationServiceTests
+{
+ [Fact]
+ public void GetString_ValidKey_ReturnsTranslation()
+ {
+ var service = CreateService();
+ var result = service.GetString("UI.Common", "Save");
+ Assert.Equal("Save", result);
+ }
+
+ [Fact]
+ public void SetCulture_ValidCulture_UpdatesCurrentCulture()
+ {
+ var service = CreateService();
+ var germanCulture = CultureInfo.GetCultureInfo("de-DE");
+
+ var result = service.SetCulture(germanCulture);
+
+ Assert.True(result.Success);
+ Assert.Equal("de", service.CurrentCulture.TwoLetterISOLanguageName);
+ }
+
+ [Fact]
+ public void CultureChanged_Observable_EmitsOnSwitch()
+ {
+ var service = CreateService();
+ CultureInfo? emittedCulture = null;
+ service.CultureChanged.Subscribe(c => emittedCulture = c);
+
+ service.SetLanguage("de");
+
+ Assert.NotNull(emittedCulture);
+ Assert.Equal("de", emittedCulture.TwoLetterISOLanguageName);
+ }
+
+ [Fact]
+ public void GetString_WithParameters_FormatsCorrectly()
+ {
+ var service = CreateService();
+ var result = service.GetString("Errors.Launch", "ProfileNotFound", "TestProfile");
+ Assert.Contains("TestProfile", result);
+ }
+}
+```
+
+### 8.2 Integration Tests
+
+**Translation Completeness Tests**:
+
+```csharp
+[Theory]
+[InlineData("de-DE")]
+[InlineData("fr-FR")]
+[InlineData("es-ES")]
+public void CultureResources_HaveAllRequiredKeys(string cultureName)
+{
+ var culture = CultureInfo.GetCultureInfo(cultureName);
+ var validator = new ResourceValidator();
+
+ var result = validator.ValidateCulture(culture);
+
+ Assert.True(result.IsValid,
+ $"Missing keys in {cultureName}: {string.Join(", ", result.MissingKeys)}");
+}
+```
+
+### 8.3 CI/CD Validation
+
+**GitHub Actions** (`.github/workflows/validate-translations.yml`):
+
+```yaml
+name: Validate Translations
+
+on:
+ pull_request:
+ paths:
+ - 'GenHub.Core/Resources/Strings/**/*.resx'
+
+jobs:
+ validate:
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Install ResXManager CLI
+ run: dotnet tool install -g ResXResourceManager
+
+ - name: Build Project
+ run: dotnet build GenHub/GenHub.Core/GenHub.Core.csproj
+
+ - name: Run Translation Tests
+ run: dotnet test GenHub/GenHub.Tests.Core --filter Category=Translation
+
+ - name: Check Translation Completeness
+ run: |
+ # Custom script to validate all cultures have all keys
+ pwsh -File scripts/validate-translations.ps1
+```
+
+---
+
+## 9. Open Questions and Future Considerations
+
+### 9.1 ResX vs ResJSON Tool
+
+**Question**: Should we create a custom tool to convert between .resx and JSON for easier community contribution?
+
+**Recommendation**: Start with ResXManager. If community requests simpler workflow, build converter tool in Phase 9.
+
+### 9.2 Right-to-Left (RTL) Languages
+
+**Question**: When to add RTL support (Arabic, Hebrew)?
+
+**Recommendation**: Design is RTL-ready (Avalonia supports it). Add when community contributes RTL translations.
+
+### 9.3 Automated Translation
+
+**Question**: Use automated translation (e.g., Azure Translator) for initial drafts?
+
+**Recommendation**: Yes for bootstrapping new languages, but always require native speaker review before merging.
+
+### 9.4 XLIFF Export/Import
+
+**Question**: Support XLIFF format for professional translation services?
+
+**Recommendation**: ResXManager supports XLIFF. Document the workflow in Phase 7-8.
+
+### 9.5 Localization for Content Metadata
+
+**Question**: How to handle user-generated content (mod descriptions, etc.)?
+
+**Recommendation**: Separate system. App localization covers UI only. Content metadata stays in source language.
+
+---
+
+## Conclusion
+
+This .resx-based localization architecture provides a **production-ready, officially-supported solution** for GenHub that:
+
+1. **Follows Avalonia Best Practices**: Uses recommended .resx approach
+2. **Provides Excellent Tooling**: Visual Studio, Rider, and ResXManager support
+3. **Enables Runtime Switching**: Custom service layer provides dynamic culture changes
+4. **Maintains Type Safety**: Strongly-typed resource classes
+5. **Supports Community**: Clear workflow with ResXManager tool
+6. **Scales Gracefully**: Satellite assemblies, lazy loading, efficient lookups
+
+**Key Advantages Over JSON**:
+
+- ✅ Native .NET framework integration
+- ✅ Industry-standard translation workflows
+- ✅ Better IDE support
+- ✅ Compiled resources (type safety + performance)
+
+**Mitigations for .resx Concerns**:
+
+- 📦 ResXManager provides excellent UX for translators
+- 🔄 Custom service enables runtime switching
+- 📝 Clear documentation lowers contribution barrier
+- ✅ Automated validation ensures quality
+
+**Next Steps**:
+
+1. Review and approve this architecture
+2. Create GitHub issues for each phase
+3. Begin Phase 1 implementation
+4. Install ResXManager and familiarize team
+
+---
diff --git a/docs/localization/localization-resources-guide.md b/docs/localization/localization-resources-guide.md
new file mode 100644
index 000000000..b855fe015
--- /dev/null
+++ b/docs/localization/localization-resources-guide.md
@@ -0,0 +1,468 @@
+# GenHub Localization Resources Guide
+
+This guide documents the localization resource structure for GenHub, including resource file organization, naming conventions, usage patterns, and best practices for translators and developers.
+
+## Overview
+
+GenHub uses the .NET resource system (`.resx` files) for localization, with satellite assemblies for different languages. The resource files are organized by functional area to make translation and maintenance easier.
+
+## Resource File Structure
+
+All resource files are located in `GenHub.Core/Resources/Strings/` and follow a hierarchical naming convention:
+
+### UI Resources
+
+**UI.Common.resx** - Common UI elements used throughout the application
+
+- Button labels (Save, Cancel, OK, Apply, Close, etc.)
+- Common status messages (Loading, Success, Error, etc.)
+- Common actions (Copy, Paste, Import, Export, etc.)
+- Example keys: `Button.Save`, `Status.Loading`, `Action.Import`
+
+**UI.Navigation.resx** - Navigation-specific strings
+
+- Tab names (Game Profiles, Downloads, Settings, etc.)
+- Section headers
+- Navigation labels
+- Example keys: `Tab.GameProfiles`, `Tab.Downloads`, `Tab.Settings`
+
+**UI.GameProfiles.resx** - Game profile management UI
+
+- Profile loading status messages
+- Game scanning messages
+- Profile creation/editing labels
+- Service availability messages
+- Example keys: `Status.LoadingProfiles`, `Status.ScanningForGames`, `Profile.AutoCreated`
+
+**UI.Settings.resx** - Settings page UI
+
+- Setting labels and categories
+- Theme options
+- Path settings labels
+- Dialog titles
+- Download, CAS, and content settings
+- Example keys: `Button.SaveSettings`, `Theme.Dark`, `Download.MaxConcurrentDownloads`
+
+**UI.Updates.resx** - Update functionality UI
+
+- Update check status messages
+- Installation progress messages
+- Version display labels
+- Update-related errors
+- Example keys: `Status.CheckingForUpdates`, `Button.InstallUpdate`, `Version.Current`
+
+**UI.Tools.resx** - Tools management UI
+
+- Tool plugin management strings
+- Tool status messages
+- Empty state messages
+- Tool installation/removal dialogs
+- Example keys: `Title.Tools`, `Button.AddTool`, `Status.ToolInstalledSuccess`
+
+**UI.Downloads.resx** - Downloads UI
+
+- Download section headers
+- Download category labels
+- Download status messages
+- Coming soon labels
+- Example keys: `Section.PrimaryDownloads`, `Category.GitHubBuilds`, `Status.Downloading`
+
+### Error Resources
+
+**Errors.Validation.resx** - Validation error messages
+
+- Field validation (required, format, range)
+- Settings validation
+- Path and URL validation
+- Culture validation
+- Example keys: `RequiredField`, `InvalidFormat`, `PathDoesNotExist`
+
+**Errors.Operations.resx** - Operation error messages
+
+- General operation failures
+- Settings operation errors
+- File/path errors
+- Profile and update errors
+- Localization errors
+- Example keys: `OperationFailed`, `FailedToSaveSettings`, `ErrorScanningForGames`
+
+### Message Resources
+
+**Messages.Success.resx** - Success messages
+
+- Operation completion messages
+- Save confirmations
+- Initialization success messages
+- Localization success messages
+- Example keys: `SettingsSaved`, `ProfileCreated`, `OperationCompleted`
+
+**Messages.Confirmations.resx** - Confirmation dialog messages
+
+- Delete confirmations
+- Reset confirmations
+- Overwrite confirmations
+- Exit/close confirmations
+- Action confirmations
+- Example keys: `DeleteProfile`, `ResetSettings`, `ExitApplication`
+
+### Tooltip Resources
+
+**Tooltips.resx** - UI tooltips and help text
+
+- Button tooltips
+- Settings tooltips
+- Profile tooltips
+- Navigation tooltips
+- Icon tooltips
+- Example keys: `Button.Save`, `Settings.Theme`, `Profile.CreateNew`
+
+## Naming Conventions
+
+### Resource Keys
+
+Resource keys use hierarchical dot notation for organization:
+
+```
+Category.Subcategory.Identifier
+```
+
+Examples:
+
+- `Button.Save` - Save button in common UI
+- `UI.GameProfiles.List.Header.Name` - Name column header in game profiles list
+- `Errors.Validation.RequiredField` - Required field validation error
+- `Messages.Confirmations.DeleteProfile` - Delete profile confirmation
+
+### Categories
+
+Common categories used in keys:
+
+- `Button` - Button labels
+- `Status` - Status messages
+- `Action` - Action labels
+- `Tab` - Navigation tab names
+- `Dialog` - Dialog titles
+- `Error` - Error messages
+- `Warning` - Warning messages
+- `Icon` - Icon tooltips
+- `Nav` - Navigation items
+- `Settings` - Setting labels
+- `Profile` - Profile-related strings
+
+### Format Strings
+
+Format strings use standard .NET string formatting with numbered placeholders:
+
+```xml
+
+ Scan complete. Found {0} game installations
+ Status message shown when scan completes. {0} is the count of installations found
+
+```
+
+Usage in code:
+
+```csharp
+var message = _localizationService.GetString(
+ StringResources.UiGameProfiles,
+ "Status.ScanComplete",
+ 5); // Result: "Scan complete. Found 5 game installations"
+```
+
+## Using Resources in Code
+
+### Basic String Retrieval
+
+```csharp
+using GenHub.Core.Interfaces.Localization;
+using GenHub.Core.Resources.Strings;
+
+public class MyViewModel
+{
+ private readonly ILocalizationService _localizationService;
+
+ public MyViewModel(ILocalizationService localizationService)
+ {
+ _localizationService = localizationService;
+ }
+
+ public void SaveData()
+ {
+ // Get simple string
+ var buttonText = _localizationService.GetString(
+ StringResources.UiCommon,
+ "Button.Save");
+
+ // Get formatted string
+ var statusMessage = _localizationService.GetString(
+ StringResources.UiGameProfiles,
+ "Status.ScanComplete",
+ installationCount);
+ }
+}
+```
+
+### Available Resource Sets
+
+Use the `StringResources` class constants for resource set names:
+
+```csharp
+StringResources.UiCommon // UI.Common.resx
+StringResources.UiNavigation // UI.Navigation.resx
+StringResources.UiGameProfiles // UI.GameProfiles.resx
+StringResources.UiSettings // UI.Settings.resx
+StringResources.UiUpdates // UI.Updates.resx
+StringResources.UiTools // UI.Tools.resx
+StringResources.UiDownloads // UI.Downloads.resx
+StringResources.ErrorsValidation // Errors.Validation.resx
+StringResources.ErrorsOperations // Errors.Operations.resx
+StringResources.MessagesSuccess // Messages.Success.resx
+StringResources.MessagesConfirmations // Messages.Confirmations.resx
+StringResources.Tooltips // Tooltips.resx
+```
+
+### Reactive Culture Changes
+
+```csharp
+public class MyViewModel : IDisposable
+{
+ private readonly ILocalizationService _localizationService;
+ private IDisposable? _cultureSubscription;
+
+ public MyViewModel(ILocalizationService localizationService)
+ {
+ _localizationService = localizationService;
+
+ // Subscribe to culture changes
+ _cultureSubscription = _localizationService.CultureChanged.Subscribe(culture =>
+ {
+ // Refresh UI when culture changes
+ RefreshLocalizedStrings();
+ });
+ }
+
+ private void RefreshLocalizedStrings()
+ {
+ // Re-fetch localized strings
+ Title = _localizationService.GetString(StringResources.UiCommon, "Title");
+ }
+
+ public void Dispose()
+ {
+ _cultureSubscription?.Dispose();
+ }
+}
+```
+
+## Adding New Strings
+
+### Step 1: Identify the Appropriate Resource File
+
+Choose the resource file that best matches the string's purpose:
+
+- UI elements → UI.*.resx
+- Errors → Errors.*.resx
+- Success messages → Messages.Success.resx
+- Confirmations → Messages.Confirmations.resx
+- Tooltips → Tooltips.resx
+
+### Step 2: Add the Resource Entry
+
+Add a new `` entry to the `.resx` file:
+
+```xml
+
+ Your localized string here
+ Description for translators explaining context and parameters
+
+```
+
+### Step 3: Use in Code
+
+```csharp
+var text = _localizationService.GetString(
+ StringResources.UiCommon,
+ "YourKey");
+```
+
+## Best Practices for Developers
+
+### DO
+
+1. **Use descriptive key names** - `Button.SaveSettings` not `Btn1`
+2. **Group related strings** - All buttons together, all errors together
+3. **Add translator comments** - Explain context and parameters
+4. **Use format parameters** - `"Hello {0}"` instead of string concatenation
+5. **Keep format strings intact** - `"{0} of {1} items"` not `$"{current} of {total} items"`
+6. **Use appropriate resource sets** - Don't put all strings in one file
+
+### DON'T
+
+1. **Don't extract debug/log messages** - Keep developer messages in English
+2. **Don't extract exception messages** - Technical errors stay in English
+3. **Don't extract technical identifiers** - Variable names, file paths, etc.
+4. **Don't hardcode strings in UI** - Always use localization service
+5. **Don't concatenate translated strings** - Use format strings instead
+
+## Best Practices for Translators
+
+### Context is Important
+
+Always read the `` element to understand:
+
+- Where the string is used
+- What parameters represent
+- Any character limits or constraints
+
+### Format Strings
+
+**Preserve placeholders** - Don't translate `{0}`, `{1}`, etc.
+
+```xml
+
+Found {0} items in {1} seconds
+
+
+Trouvé {0} éléments en {1} secondes
+
+
+Trouvé des éléments en secondes
+```
+
+### Preserve Special Characters
+
+- Keep `...` (ellipsis) in status messages
+- Preserve `:` (colons) in labels
+- Maintain capitalization patterns for buttons (Title Case vs Sentence case)
+
+### UI Constraints
+
+- Button labels should be concise
+- Tooltips can be more descriptive
+- Error messages should be clear and actionable
+
+## Pluralization Guidelines
+
+For counts, provide context in comments:
+
+```xml
+
+ {0} item(s)
+ Count of items. For languages with complex plural rules, consider creating separate keys for singular/plural/other
+
+```
+
+For languages with complex plural rules (e.g., Russian, Arabic), consider creating separate keys:
+
+- `ItemCount.Zero`
+- `ItemCount.One`
+- `ItemCount.Two`
+- `ItemCount.Few`
+- `ItemCount.Many`
+
+## Testing Localization
+
+Run the integration tests to verify resources are properly loaded:
+
+```bash
+dotnet test GenHub.Tests/GenHub.Tests.Core --filter StringResourcesTests
+```
+
+Key tests verify:
+
+- All .resx files are embedded
+- ResourceManagers can load from each namespace
+- Strings are retrievable through LocalizationService
+- Format strings work with parameters
+- Satellite assemblies are generated
+
+## Satellite Assembly Generation
+
+Satellite assemblies are automatically generated during build. They are located in:
+
+```
+GenHub.Core/bin/Debug/net8.0/{culture}/GenHub.Core.resources.dll
+```
+
+For example, for French translations:
+
+```
+GenHub.Core/bin/Debug/net8.0/fr/GenHub.Core.resources.dll
+```
+
+## Adding a New Language (Phase 8-9)
+
+1. Create language-specific `.resx` files:
+ - `UI.Common.fr.resx` (French)
+ - `UI.Navigation.fr.resx`
+ - etc.
+
+2. Translate all strings while preserving format placeholders
+
+3. Build the project to generate satellite assemblies
+
+4. Test with:
+
+```csharp
+await _localizationService.SetCulture("fr");
+```
+
+## Current Phase Status
+
+**Phase 2 Complete** ✅
+
+- 10 .resx files created with 278 English strings
+- Resource configuration in GenHub.Core.csproj
+- StringResources helper class
+- LocalizationService updated with resource namespace support
+- Integration tests for resource loading
+- Documentation complete
+
+**Phase 2.5 Complete** ✅ (String Audit)
+
+- Comprehensive audit of all ViewModels and XAML views
+- 2 new .resx files created (UI.Tools, UI.Downloads)
+- 159 additional strings added across all resource files
+- StringResources.cs updated with new resource namespaces
+
+**Next Phase: Phase 3** - ViewModel Integration
+
+- Update ViewModels to use LocalizationService
+- Replace hardcoded strings with resource keys
+- Implement reactive UI updates on culture change
+
+## Resource Statistics
+
+Total resource files: **12**
+Total English strings: **437**
+
+By resource file:
+
+| Resource File | String Count |
+|---------------|-------------|
+| UI.Common.resx | 47 |
+| UI.Navigation.resx | 9 |
+| UI.GameProfiles.resx | 77 |
+| UI.Settings.resx | 66 |
+| UI.Updates.resx | 28 |
+| UI.Tools.resx | 32 |
+| UI.Downloads.resx | 18 |
+| Errors.Validation.resx | 22 |
+| Errors.Operations.resx | 43 |
+| Messages.Success.resx | 29 |
+| Messages.Confirmations.resx | 28 |
+| Tooltips.resx | 38 |
+
+By category:
+
+- UI resources: ~277 strings
+- Error messages: ~65 strings
+- Success messages: ~29 strings
+- Confirmations: ~28 strings
+- Tooltips: ~38 strings
+
+## Additional Resources
+
+- [.NET Globalization and Localization](https://docs.microsoft.com/en-us/dotnet/core/extensions/globalization-and-localization)
+- [Resource Files (.resx)](https://docs.microsoft.com/en-us/dotnet/framework/resources/creating-resource-files-for-desktop-apps)
+- [Satellite Assemblies](https://docs.microsoft.com/en-us/dotnet/framework/resources/creating-satellite-assemblies-for-desktop-apps)