├── samples ├── output.xml ├── Mocale.Samples │ ├── ViewModels │ │ ├── BindingViewModel.cs │ │ ├── BaseViewModel.cs │ │ ├── ConverterViewModel.cs │ │ ├── ParameterViewModel.cs │ │ └── IntroductionPageViewModel.cs │ ├── Resources │ │ ├── Fonts │ │ │ ├── OpenSans-Regular.ttf │ │ │ └── OpenSans-Semibold.ttf │ │ ├── AppIcon │ │ │ ├── appicon.svg │ │ │ └── appiconfg.svg │ │ ├── Raw │ │ │ └── AboutAssets.txt │ │ ├── Splash │ │ │ └── splash.svg │ │ └── Styles │ │ │ └── Colors.xaml │ ├── Properties │ │ ├── GlobalUsings.cs │ │ ├── launchSettings.json │ │ └── GlobalSuppression.cs │ ├── AppShell.xaml.cs │ ├── Platforms │ │ ├── Android │ │ │ ├── Resources │ │ │ │ └── values │ │ │ │ │ └── colors.xml │ │ │ ├── MainApplication.cs │ │ │ ├── MainActivity.cs │ │ │ └── AndroidManifest.xml │ │ ├── iOS │ │ │ ├── AppDelegate.cs │ │ │ ├── Program.cs │ │ │ └── Info.plist │ │ ├── MacCatalyst │ │ │ ├── AppDelegate.cs │ │ │ ├── Entitlements.plist │ │ │ ├── Program.cs │ │ │ └── Info.plist │ │ ├── Windows │ │ │ ├── App.xaml │ │ │ ├── App.xaml.cs │ │ │ ├── app.manifest │ │ │ └── Package.appxmanifest │ │ └── Tizen │ │ │ ├── Main.cs │ │ │ └── tizen-manifest.xml │ ├── Pages │ │ ├── ParameterPage.xaml.cs │ │ ├── IntroductionPage.xaml.cs │ │ ├── BasePage.cs │ │ ├── BindingPage.xaml.cs │ │ ├── ConverterPage.xaml.cs │ │ ├── IntroductionPage.xaml │ │ ├── CodePage.cs │ │ └── ParameterPage.xaml │ ├── App.xaml.cs │ ├── App.xaml │ ├── Enums │ │ └── Cities.cs │ ├── Converters │ │ ├── CityDescriptionConverter.cs │ │ └── LanguageEmojiConverter.cs │ └── AppShell.xaml ├── Mocale.GeneratorSample │ ├── Program.cs │ ├── Locales │ │ ├── fr-FR.json │ │ └── en-GB.json │ └── Mocale.GeneratorSample.csproj └── Locales │ ├── en-GB.json │ └── fr-FR.json ├── .github └── funding.yml ├── assets ├── mocale_icon_dark.png ├── mocale_icon_light.png ├── mocale_logo_color.png ├── mocale_icon_dark_small.png ├── Mocale_Main_Logo_800x600.jpg ├── mocale_icon_dark_rounded.png ├── mocale_icon_light_rounded.png ├── mocale_icon_light_small.png └── Mocale_Inverted_Color_Crop.png ├── src ├── Mocale │ ├── Properties │ │ ├── AssemblyInfo.cs │ │ ├── GlobalUsings.cs │ │ └── GlobalSuppression.cs │ ├── build │ │ ├── Package.targets │ │ └── Package.props │ ├── Exceptions │ │ ├── InitializationException.cs │ │ └── MocaleException.cs │ ├── Abstractions │ │ ├── ILocalizationParser.cs │ │ ├── IExternalFileNameHelper.cs │ │ ├── IInternalTranslatorManager.cs │ │ ├── IAppResourcesConfig.cs │ │ ├── IExternalProviderConfiguration.cs │ │ ├── IExternalLocalizationResult.cs │ │ ├── IResourceFileDetails.cs │ │ ├── ITranslationUpdater.cs │ │ ├── ICurrentCultureManager.cs │ │ ├── IEmbeddedResourcesConfig.cs │ │ ├── IExternalLocalizationProvider.cs │ │ ├── IInternalLocalizationProvider.cs │ │ ├── IKeyConverter.cs │ │ ├── IConfigurationManager.cs │ │ ├── ILocalizationManager.cs │ │ ├── ITranslationResolver.cs │ │ ├── ILocalisationCacheManager.cs │ │ ├── ITranslatorManager.cs │ │ ├── IMocaleConfiguration.cs │ │ └── ICacheUpdateManager.cs │ ├── Models │ │ ├── AppResourcesConfig.cs │ │ ├── ExternalLocalizationResult.cs │ │ ├── JsonResourceFileDetails.cs │ │ ├── EmbeddedResourcesConfig.cs │ │ ├── TranslationLoadResult.cs │ │ ├── Localization.cs │ │ ├── ResxResourceFileDetails.cs │ │ ├── MocaleConfiguration.cs │ │ └── LocalizeEnumBehavior.cs │ ├── Constants.cs │ ├── Enums │ │ ├── LocaleResourceType.cs │ │ └── TranslationSource.cs │ ├── Extensions │ │ ├── LocalizeEnumBehaviorExtension.cs │ │ ├── UriExtension.cs │ │ ├── TaskExtensions.cs │ │ ├── DictionaryExtension.cs │ │ ├── StringExtension.cs │ │ ├── EnumExtensions.cs │ │ ├── LocalizeExtension.cs │ │ ├── TranslatorManagerExtensions.cs │ │ └── LocalizeExtensionBase.cs │ ├── Providers │ │ ├── InactiveExternalLocalizationProvider.cs │ │ └── AppResourceProvider.cs │ ├── Managers │ │ ├── ConfigurationManager.cs │ │ └── CurrentCultureManager.cs │ ├── MocaleInitializeService.cs │ ├── Parsers │ │ ├── JsonLocalizationParser.cs │ │ └── ResxLocalizationParser.cs │ ├── Helper │ │ ├── EnumTranslationKeyHelper.cs │ │ ├── ExternalJsonFileNameHelper.cs │ │ └── ExternalResxFileNameHelper.cs │ ├── Cache │ │ └── InMemoryCacheManager.cs │ ├── Mocale.csproj │ ├── MocaleLocator.cs │ └── MocaleBuilder.cs ├── Mocale.SourceGenerators │ ├── build │ │ ├── Package.targets │ │ └── Package.props │ ├── AnalyzerReleases.Unshipped.md │ ├── Properties │ │ └── launchSettings.json │ ├── SourceProductionContextExtensions.cs │ ├── AnalyzerReleases.Shipped.md │ ├── Diagnostics.cs │ └── Mocale.SourceGenerators.csproj ├── Mocale.Cache.SQLite │ ├── Constants.cs │ ├── Properties │ │ ├── GlobalUsings.cs │ │ └── GlobalSuppression.cs │ ├── Abstractions │ │ ├── IDatabasePathProvider.cs │ │ ├── IDatabaseConnectionProvider.cs │ │ ├── ISqliteConfig.cs │ │ ├── ITranslationsRepository.cs │ │ └── ICacheRepository.cs │ ├── Entities │ │ ├── UpdateHistoryItem.cs │ │ └── TranslationItem.cs │ ├── Models │ │ └── SqliteConfig.cs │ ├── Providers │ │ ├── DatabasePathProvider.cs │ │ └── DatabaseConnectionProvider.cs │ ├── Repositories │ │ ├── RepositoryBase.cs │ │ └── CacheRepository.cs │ ├── Mocale.Cache.SQLite.csproj │ ├── Managers │ │ ├── LocalisationCacheManager.cs │ │ └── SqlCacheUpdateManager.cs │ └── MocaleBuilderExtension.cs ├── Mocale.Providers.AWS.S3 │ ├── Properties │ │ └── GlobalUsings.cs │ ├── Abstractions │ │ └── IBucketConfig.cs │ ├── Models │ │ └── BucketConfig.cs │ ├── Mocale.Providers.AWS.S3.csproj │ ├── MocaleBuilderExtension.cs │ └── S3BucketProvider.cs ├── Mocale.Providers.GitHub.Raw │ ├── Properties │ │ ├── GlobalUsings.cs │ │ └── GlobalSuppression.cs │ ├── Helpers │ │ └── RawUrlBuilder.cs │ ├── Models │ │ └── GithubRawConfig.cs │ ├── Abstractions │ │ └── IGithubRawConfig.cs │ ├── Mocale.Providers.GitHub.Raw.csproj │ └── MocaleBuilderExtension.cs ├── Mocale.Providers.Azure.Blob │ ├── Properties │ │ ├── GlobalUsings.cs │ │ └── GlobalSuppression.cs │ ├── Models │ │ ├── BlobResourceInfo.cs │ │ └── BlobStorageConfig.cs │ ├── Abstractions │ │ ├── IBlobResourceLocator.cs │ │ └── IBlobStorageConfig.cs │ ├── Mocale.Providers.Azure.Blob.csproj │ └── MocaleBuilderExtension.cs ├── Mocale.Testing │ ├── Mocale.Testing.csproj │ ├── MocaleLocatorHelper.cs │ └── Extensions │ │ └── DictionaryExtension.cs └── directory.build.props ├── global.json ├── tests ├── Mocale.UnitTests │ ├── Snapshots │ │ ├── Locales │ │ │ └── en-GB.json │ │ ├── TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenNoAdditionalFiles_ShouldGenerateNoKeys#MocaleTranslationKeys.g.verified.cs │ │ ├── TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenNoAdditionalFiles_ShouldGenerateNoKeys.verified.txt │ │ ├── TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenSingleFile_ShouldGenerateCorrectly#MocaleTranslationKeys.g.verified.cs │ │ ├── TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenMultipleFilesButAllTheSameKeys_ShouldContainOnlyUniqueKeys#MocaleTranslationKeys.g.verified.cs │ │ └── TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenMultipleFilesButDifferentKeys_ShouldContainOnlyUniqueKeys#MocaleTranslationKeys.g.verified.cs │ ├── TestUtils │ │ ├── TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenKeysContainIllegalCharacters_ShouldGenerateSanitizedFields#MocaleTranslationKeys.g.verified.cs │ │ ├── TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenNoAdditionalFiles_ShouldGenerateNoKeys#MocaleTranslationKeys.g.verified.cs │ │ ├── TestAdditionalText.cs │ │ ├── TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenNoAdditionalFiles_ShouldGenerateNoKeys.verified.txt │ │ ├── TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenSingleFile_ShouldGenerateCorrectly#MocaleTranslationKeys.g.verified.cs │ │ ├── TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenKeysContainIllegalCharacters_ShouldGenerateSanitizedFields#MocaleTranslationKeys.g.received.cs │ │ ├── TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenMultipleFilesButAllTheSameKeys_ShouldContainOnlyUniqueKeys#MocaleTranslationKeys.g.verified.cs │ │ ├── TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenMultipleFilesButDifferentKeys_ShouldContainOnlyUniqueKeys#MocaleTranslationKeys.g.verified.cs │ │ └── ServiceCollectionAssertions.cs │ ├── Properties │ │ ├── GlobalUsings.cs │ │ └── GlobalSuppressions.cs │ ├── MocaleTestInitializer.cs │ ├── .editorconfig │ ├── Fixtures │ │ ├── MocaleLocatorFixture.cs │ │ ├── FixtureBase.cs │ │ └── ControlsFixtureBase.cs │ ├── Collections │ │ ├── CollectionNames.cs │ │ └── CollectionDefinitions.cs │ ├── Mocale.UnitTests.csproj.DotSettings │ ├── Resources │ │ ├── en-GB.json │ │ ├── Misc │ │ │ ├── TestFile.json │ │ │ └── RandomFile.txt │ │ ├── Invalid │ │ │ └── en-GB.json │ │ └── Resx │ │ │ ├── TestResources.fr-fr.resx │ │ │ ├── TestResources.resx │ │ │ └── TestResources.Designer.cs │ ├── MocaleLocatorTests.cs │ ├── Providers │ │ └── InactiveExternalLocalizationProviderTests.cs │ ├── Extensions │ │ ├── StringExtensionTests.cs │ │ ├── UriExtensionTests.cs │ │ ├── LocalizeEnumBehaviorExtensionTests.cs │ │ └── EnumExtensionsTests.cs │ ├── Testing │ │ └── MocaleLocatorHelperTests.cs │ ├── Helpers │ │ ├── RawUrlBuilderTests.cs │ │ └── ExternalJsonFileNameHelperTests.cs │ └── MocaleInitializeServiceTests.cs └── directory.packages.props ├── nuget.config ├── version.json ├── LICENSE ├── directory.build.props └── directory.packages.props /samples/output.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: axemasta 2 | buy_me_a_coffee: axemasta 3 | -------------------------------------------------------------------------------- /assets/mocale_icon_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axemasta/Mocale/HEAD/assets/mocale_icon_dark.png -------------------------------------------------------------------------------- /assets/mocale_icon_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axemasta/Mocale/HEAD/assets/mocale_icon_light.png -------------------------------------------------------------------------------- /assets/mocale_logo_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axemasta/Mocale/HEAD/assets/mocale_logo_color.png -------------------------------------------------------------------------------- /assets/mocale_icon_dark_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axemasta/Mocale/HEAD/assets/mocale_icon_dark_small.png -------------------------------------------------------------------------------- /assets/Mocale_Main_Logo_800x600.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axemasta/Mocale/HEAD/assets/Mocale_Main_Logo_800x600.jpg -------------------------------------------------------------------------------- /assets/mocale_icon_dark_rounded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axemasta/Mocale/HEAD/assets/mocale_icon_dark_rounded.png -------------------------------------------------------------------------------- /assets/mocale_icon_light_rounded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axemasta/Mocale/HEAD/assets/mocale_icon_light_rounded.png -------------------------------------------------------------------------------- /assets/mocale_icon_light_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axemasta/Mocale/HEAD/assets/mocale_icon_light_small.png -------------------------------------------------------------------------------- /assets/Mocale_Inverted_Color_Crop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axemasta/Mocale/HEAD/assets/Mocale_Inverted_Color_Crop.png -------------------------------------------------------------------------------- /src/Mocale/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | [assembly: XmlnsDefinition("http://axemasta.com/schemas/2022/mocale", "Mocale.Extensions")] 2 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.200", 4 | "rollForward": "latestFeature", 5 | "allowPrerelease": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/ViewModels/BindingViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Samples.ViewModels; 2 | 3 | public sealed partial class BindingViewModel : BaseViewModel; 4 | -------------------------------------------------------------------------------- /src/Mocale/build/Package.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Resources/Fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axemasta/Mocale/HEAD/samples/Mocale.Samples/Resources/Fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /samples/Mocale.Samples/Resources/Fonts/OpenSans-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Axemasta/Mocale/HEAD/samples/Mocale.Samples/Resources/Fonts/OpenSans-Semibold.ttf -------------------------------------------------------------------------------- /src/Mocale/Exceptions/InitializationException.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Exceptions; 2 | 3 | internal class InitializationException(string message) : MocaleException(message); 4 | -------------------------------------------------------------------------------- /src/Mocale.SourceGenerators/build/Package.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /samples/Mocale.GeneratorSample/Program.cs: -------------------------------------------------------------------------------- 1 | using Mocale.Translations; 2 | 3 | Console.WriteLine("Debug Source Generators Project"); 4 | Console.WriteLine(TranslationKeys.MocaleTitle); 5 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Properties/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Mocale.Abstractions; 2 | global using CommunityToolkit.Mvvm.ComponentModel; 3 | global using Mocale.Samples.ObjectModel; 4 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Snapshots/Locales/en-GB.json: -------------------------------------------------------------------------------- 1 | { 2 | "KeyOne": "Value One", 3 | "Key_Two": "Value Two", 4 | "Key@!_/Three": "Value Three", 5 | "Key.Four": "Value Four" 6 | } 7 | -------------------------------------------------------------------------------- /src/Mocale.Cache.SQLite/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Cache.SQLite; 2 | 3 | internal static class Constants 4 | { 5 | public const string DatabaseFileName = "Mocale.db"; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/TestUtils/TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenKeysContainIllegalCharacters_ShouldGenerateSanitizedFields#MocaleTranslationKeys.g.verified.cs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Windows Machine": { 4 | "commandName": "MsixPackage", 5 | "nativeDebugging": false 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/ViewModels/BaseViewModel.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | 3 | namespace Mocale.Samples.ViewModels; 4 | public partial class BaseViewModel : ObservableObject 5 | { 6 | } 7 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/AppShell.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Samples; 2 | 3 | public partial class AppShell : Shell 4 | { 5 | public AppShell() 6 | { 7 | InitializeComponent(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Mocale.SourceGenerators/AnalyzerReleases.Unshipped.md: -------------------------------------------------------------------------------- 1 | ; Unshipped analyzer release 2 | ; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md 3 | -------------------------------------------------------------------------------- /src/Mocale/Abstractions/ILocalizationParser.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Abstractions; 2 | 3 | internal interface ILocalizationParser 4 | { 5 | Dictionary? ParseLocalizationStream(Stream resourceStream); 6 | } 7 | -------------------------------------------------------------------------------- /src/Mocale/Properties/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Mocale.Abstractions; 2 | global using Mocale.Enums; 3 | global using Mocale.Extensions; 4 | global using Mocale.Models; 5 | global using Microsoft.Extensions.Logging; 6 | -------------------------------------------------------------------------------- /src/Mocale/build/Package.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | Resources\Locales\*.json 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Properties/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; 2 | global using FluentAssertions; 3 | global using Moq; 4 | global using Mocale.UnitTests.TestUtils; 5 | global using Mocale.UnitTests.Fixtures; 6 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Mocale.SourceGenerators/build/Package.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | Resources\Locales\*.json 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/Mocale/Abstractions/IExternalFileNameHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace Mocale.Abstractions; 4 | 5 | internal interface IExternalFileNameHelper 6 | { 7 | string GetExpectedFileName(CultureInfo culture); 8 | } 9 | -------------------------------------------------------------------------------- /src/Mocale/Models/AppResourcesConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Models; 2 | 3 | /// 4 | public class AppResourcesConfig : IAppResourcesConfig 5 | { 6 | /// 7 | public Type? AppResourcesType { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/Mocale.Cache.SQLite/Properties/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Mocale.Cache.SQLite.Abstractions; 2 | global using Mocale.Cache.SQLite.Models; 3 | global using Ardalis.GuardClauses; 4 | global using SQLite; 5 | global using Mocale.Cache.SQLite.Entities; 6 | -------------------------------------------------------------------------------- /src/Mocale/Abstractions/IInternalTranslatorManager.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Abstractions; 2 | 3 | internal interface IInternalTranslatorManager : ITranslatorManager, ITranslationUpdater 4 | { 5 | void RaisePropertyChanged(string? propertyName = null); 6 | } 7 | -------------------------------------------------------------------------------- /src/Mocale.Providers.AWS.S3/Properties/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Mocale.Providers.AWS.S3.Abstractions; 2 | global using Mocale.Providers.AWS.S3.Models; 3 | global using Mocale.Abstractions; 4 | global using Mocale.Models; 5 | global using System.Globalization; 6 | -------------------------------------------------------------------------------- /src/Mocale.SourceGenerators/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Debug": { 4 | "commandName": "DebugRoslynComponent", 5 | "targetProject": "..\\..\\samples\\Mocale.GeneratorSample\\Mocale.GeneratorSample.csproj" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Resources/AppIcon/appicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Mocale.Providers.GitHub.Raw/Properties/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Mocale.Abstractions; 2 | global using Mocale.Models; 3 | global using Mocale.Providers.GitHub.Raw.Abstractions; 4 | global using Mocale.Providers.GitHub.Raw.Models; 5 | global using Microsoft.Extensions.Logging; 6 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Platforms/Android/Resources/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #512BD4 4 | #2B0B98 5 | #2B0B98 6 | 7 | -------------------------------------------------------------------------------- /samples/Locales/en-GB.json: -------------------------------------------------------------------------------- 1 | { 2 | "CurrentLocaleName": "English", 3 | "LocalizationCurrentProviderIs": "GR_The current localization provider is:", 4 | "LocalizationProviderName": "GR_Json", 5 | "MocaleDescription": "GR_Localization framework for .NET Maui", 6 | "MocaleTitle": "GR_Mocale" 7 | } 8 | -------------------------------------------------------------------------------- /src/Mocale/Models/ExternalLocalizationResult.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Models; 2 | 3 | internal class ExternalLocalizationResult : IExternalLocalizationResult 4 | { 5 | public required bool Success { get; set; } 6 | 7 | public Dictionary? Localizations { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /samples/Locales/fr-FR.json: -------------------------------------------------------------------------------- 1 | { 2 | "CurrentLocaleName": "French", 3 | "LocalizationCurrentProviderIs": "GR_Le fournisseur de localisation actuel est:", 4 | "LocalizationProviderName": "GR_Json", 5 | "MocaleDescription": "GR_Framework de localisation pour .NET Maui", 6 | "MocaleTitle": "GR_Mocale" 7 | } 8 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Snapshots/TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenNoAdditionalFiles_ShouldGenerateNoKeys#MocaleTranslationKeys.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: MocaleTranslationKeys.g.cs 2 | namespace Mocale.Translations 3 | { 4 | public static class TranslationKeys 5 | { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/TestUtils/TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenNoAdditionalFiles_ShouldGenerateNoKeys#MocaleTranslationKeys.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: MocaleTranslationKeys.g.cs 2 | namespace Mocale.Translations 3 | { 4 | public static class TranslationKeys 5 | { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /samples/Mocale.GeneratorSample/Locales/fr-FR.json: -------------------------------------------------------------------------------- 1 | { 2 | "CurrentLocaleName": "French", 3 | "LocalizationCurrentProviderIs": "Le fournisseur de localisation actuel est:", 4 | "LocalizationProviderName": "Json", 5 | "MocaleDescription": "Framework de localisation pour .NET Maui", 6 | "MocaleTitle": "Mocale" 7 | } 8 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/MocaleTestInitializer.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Mocale.UnitTests; 4 | 5 | public static class MocaleTestInitializer 6 | { 7 | [ModuleInitializer] 8 | public static void Init() 9 | { 10 | VerifySourceGenerators.Initialize(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", 3 | "version": "1.0-pre", 4 | "assemblyVersion": { 5 | "precision": "revision" 6 | }, 7 | "publicReleaseRefSpec": [ 8 | "^refs/heads/main$" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Platforms/iOS/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using Foundation; 2 | namespace Mocale.Samples; 3 | 4 | [Register("AppDelegate")] 5 | public class AppDelegate : MauiUIApplicationDelegate 6 | { 7 | protected override MauiApp CreateMauiApp() 8 | { 9 | return MauiProgram.CreateMauiApp(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Platforms/MacCatalyst/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using Foundation; 2 | namespace Mocale.Samples; 3 | 4 | [Register("AppDelegate")] 5 | public class AppDelegate : MauiUIApplicationDelegate 6 | { 7 | protected override MauiApp CreateMauiApp() 8 | { 9 | return MauiProgram.CreateMauiApp(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Platforms/MacCatalyst/Entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Platforms/Windows/App.xaml: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /src/Mocale/Abstractions/IAppResourcesConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Abstractions; 2 | 3 | /// 4 | /// Configuration For App Resources 5 | /// 6 | public interface IAppResourcesConfig 7 | { 8 | /// 9 | /// The type for the app resources 10 | /// 11 | Type? AppResourcesType { get; } 12 | } 13 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Pages/ParameterPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using Mocale.Samples.ViewModels; 2 | 3 | namespace Mocale.Samples.Pages; 4 | 5 | public partial class ParameterPage : BasePage 6 | { 7 | public ParameterPage(ParameterViewModel viewModel) 8 | : base(viewModel) 9 | { 10 | InitializeComponent(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Mocale/Exceptions/MocaleException.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Exceptions; 2 | 3 | internal class MocaleException : Exception 4 | { 5 | public MocaleException(string message) 6 | : base(message) 7 | { 8 | } 9 | 10 | public MocaleException(string message, Exception inner) 11 | : base(message, inner) 12 | { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Mocale/Models/JsonResourceFileDetails.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Models; 2 | 3 | /// 4 | public class JsonResourceFileDetails : IResourceFileDetails 5 | { 6 | /// 7 | public LocaleResourceType ResourceType { get; } = LocaleResourceType.Json; 8 | /// 9 | public string? VersionPrefix { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/Mocale/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale; 2 | 3 | internal class Constants 4 | { 5 | public const string LastUsedCultureKey = "Mocale_LastUsedCulture"; 6 | 7 | public const string LastUpdatedExternalResourceKey = "Mocale_LastUpdatedExternalResources"; 8 | 9 | public const string UpdateCacheStorageKey = "Mocale_UpdateCacheStorage"; 10 | } 11 | -------------------------------------------------------------------------------- /src/Mocale/Enums/LocaleResourceType.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Enums; 2 | 3 | /// 4 | /// Locale Resource Type 5 | /// 6 | public enum LocaleResourceType 7 | { 8 | /// 9 | /// Resx File 10 | /// 11 | Resx = 0, 12 | 13 | /// 14 | /// Json File 15 | /// 16 | Json = 1, 17 | } 18 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/App.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Samples; 2 | 3 | public partial class App : Application 4 | { 5 | public App() 6 | { 7 | InitializeComponent(); 8 | } 9 | 10 | protected override Window CreateWindow(IActivationState activationState) 11 | { 12 | return new Window(new AppShell()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Mocale.Providers.AWS.S3/Abstractions/IBucketConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Providers.AWS.S3.Abstractions; 2 | 3 | /// 4 | /// S3 Bucket Config 5 | /// 6 | public interface IBucketConfig : IExternalProviderConfiguration 7 | { 8 | /// 9 | /// Uri for the blob container 10 | /// 11 | Uri? BucketUri { get; } 12 | } 13 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/.editorconfig: -------------------------------------------------------------------------------- 1 | ############################### 2 | # Core EditorConfig Options # 3 | ############################### 4 | 5 | # Csharp files 6 | [*.{cs,vb}] 7 | dotnet_diagnostic.CA1309.severity = none 8 | dotnet_diagnostic.CA1861.severity = none 9 | 10 | # Resharper 11 | [resharper_] 12 | resharper_structured_message_template_problem_highlighting = none -------------------------------------------------------------------------------- /samples/Mocale.Samples/Pages/IntroductionPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using Mocale.Samples.ViewModels; 2 | namespace Mocale.Samples.Pages; 3 | 4 | public partial class IntroductionPage : BasePage 5 | { 6 | public IntroductionPage(IntroductionPageViewModel viewModel) 7 | : base(viewModel) 8 | { 9 | 10 | InitializeComponent(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Fixtures/MocaleLocatorFixture.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.UnitTests.Fixtures; 2 | 3 | public class MocaleLocatorFixture : IDisposable 4 | { 5 | public virtual void Dispose() 6 | { 7 | MocaleLocator.MocaleConfiguration = null; 8 | MocaleLocator.TranslatorManager = null; 9 | GC.SuppressFinalize(this); 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Collections/CollectionNames.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.UnitTests.Collections; 2 | 3 | public static class CollectionNames 4 | { 5 | public const string MocaleLocatorTests = "MocaleLocatorCollection"; 6 | 7 | public const string ThreadCultureTests = "ThreadCultureCollection"; 8 | 9 | public const string TestingTests = "TestingCollection"; 10 | } 11 | -------------------------------------------------------------------------------- /src/Mocale.Providers.AWS.S3/Models/BucketConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Providers.AWS.S3.Models; 2 | 3 | /// 4 | public class BucketConfig : IBucketConfig 5 | { 6 | /// 7 | public Uri? BucketUri { get; set; } 8 | 9 | /// 10 | public IResourceFileDetails ResourceFileDetails { get; set; } = new JsonResourceFileDetails(); 11 | } 12 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Pages/BasePage.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Samples.Pages; 2 | 3 | public abstract class BasePage : ContentPage 4 | where TViewModel : ObservableObject 5 | { 6 | protected BasePage(TViewModel viewModel) 7 | { 8 | base.BindingContext = viewModel; 9 | } 10 | 11 | public new TViewModel BindingContext => (TViewModel)base.BindingContext; 12 | } 13 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Pages/BindingPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using Mocale.Extensions; 2 | using Mocale.Translations; 3 | 4 | namespace Mocale.Samples.Pages; 5 | 6 | public partial class BindingPage : ContentPage 7 | { 8 | public BindingPage() 9 | { 10 | InitializeComponent(); 11 | 12 | Label.SetTranslation(Label.TextProperty, TranslationKeys.BindingPageCodeBehindLabelKey); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Mocale.Cache.SQLite/Abstractions/IDatabasePathProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Cache.SQLite.Abstractions; 2 | 3 | /// 4 | /// Database Path Provider 5 | /// 6 | public interface IDatabasePathProvider 7 | { 8 | /// 9 | /// Get SQLite database path on the device 10 | /// 11 | /// 12 | string GetDatabasePath(); 13 | } 14 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Platforms/Android/MainApplication.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Runtime; 3 | namespace Mocale.Samples; 4 | 5 | [Application] 6 | public class MainApplication(IntPtr handle, JniHandleOwnership ownership) : MauiApplication(handle, ownership) 7 | { 8 | protected override MauiApp CreateMauiApp() 9 | { 10 | return MauiProgram.CreateMauiApp(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /samples/Mocale.GeneratorSample/Locales/en-GB.json: -------------------------------------------------------------------------------- 1 | { 2 | "CurrentLocaleName": "English", 3 | "LocalizationCurrentProviderIs": "The current localization provider is:", 4 | "LocalizationProviderName": "Json", 5 | "MocaleDescription": "Localization framework for .NET Maui", 6 | "MocaleTitle": "Mocale", 7 | "nonCamelTitle": "ThisIsCamel", 8 | "Hello World": "Hello World", 9 | "Acme.Com": "www.acme.com" 10 | } 11 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Platforms/Tizen/Main.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Maui; 3 | using Microsoft.Maui.Hosting; 4 | 5 | namespace Mocale.Samples; 6 | 7 | class Program : MauiApplication 8 | { 9 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 10 | 11 | static void Main(string[] args) 12 | { 13 | var app = new Program(); 14 | app.Run(args); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Mocale/Abstractions/IExternalProviderConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Abstractions; 2 | 3 | /// 4 | /// External Provider Configuration 5 | /// 6 | public interface IExternalProviderConfiguration 7 | { 8 | /// 9 | /// Details about the resource files returned by the external provider 10 | /// 11 | IResourceFileDetails ResourceFileDetails { get; } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/Mocale.Cache.SQLite/Entities/UpdateHistoryItem.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Cache.SQLite.Entities; 2 | 3 | #nullable disable 4 | 5 | [Table("UpdateHistory")] 6 | internal class UpdateHistoryItem 7 | { 8 | [PrimaryKey] 9 | [AutoIncrement] 10 | public int Id { get; set; } 11 | 12 | public string CultureName { get; set; } 13 | 14 | public DateTime LastUpdated { get; set; } 15 | } 16 | 17 | #nullable enable 18 | -------------------------------------------------------------------------------- /src/Mocale.Cache.SQLite/Abstractions/IDatabaseConnectionProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Cache.SQLite.Abstractions; 2 | 3 | /// 4 | /// SQLite Database Connection Provider 5 | /// 6 | public interface IDatabaseConnectionProvider 7 | { 8 | /// 9 | /// Get connection to the sqlite database 10 | /// 11 | /// 12 | SQLiteConnection GetDatabaseConnection(); 13 | } 14 | -------------------------------------------------------------------------------- /src/Mocale.Providers.Azure.Blob/Properties/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Mocale.Abstractions; 2 | global using Mocale.Models; 3 | global using Mocale.Providers.Azure.Blob.Abstractions; 4 | global using Mocale.Providers.Azure.Blob.Models; 5 | global using Microsoft.Extensions.Logging; 6 | global using System.Globalization; 7 | global using Ardalis.GuardClauses; 8 | global using Azure.Storage.Blobs; 9 | global using Azure.Storage.Blobs.Models; 10 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Platforms/Android/MainActivity.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Content.PM; 3 | namespace Mocale.Samples; 4 | 5 | [Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] 6 | public class MainActivity : MauiAppCompatActivity 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Platforms/iOS/Program.cs: -------------------------------------------------------------------------------- 1 | using UIKit; 2 | namespace Mocale.Samples; 3 | 4 | public class Program 5 | { 6 | // This is the main entry point of the application. 7 | private static void Main(string[] args) 8 | { 9 | // if you want to use a different Application Delegate class from "AppDelegate" 10 | // you can specify it here. 11 | UIApplication.Main(args, null, typeof(AppDelegate)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Mocale.SourceGenerators/SourceProductionContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | namespace Mocale.SourceGenerators; 3 | 4 | internal static class SourceProductionContextExtensions 5 | { 6 | public static void Report(this SourceProductionContext context, DiagnosticDescriptor diagnosticDescriptor, params object?[]? args) 7 | { 8 | context.ReportDiagnostic(Diagnostic.Create(diagnosticDescriptor, null, args)); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Mocale.Cache.SQLite/Entities/TranslationItem.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Cache.SQLite.Entities; 2 | 3 | #nullable disable 4 | 5 | [Table("Translations")] 6 | internal class TranslationItem 7 | { 8 | [PrimaryKey] 9 | [AutoIncrement] 10 | public int Id { get; set; } 11 | 12 | public string CultureName { get; set; } 13 | 14 | public string Key { get; set; } 15 | 16 | public string Value { get; set; } 17 | } 18 | 19 | #nullable enable 20 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/TestUtils/TestAdditionalText.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.Text; 3 | 4 | namespace Mocale.UnitTests.TestUtils; 5 | 6 | public class TestAdditionalText(string path, string content) : AdditionalText 7 | { 8 | public override string Path => path; 9 | 10 | public override SourceText GetText(CancellationToken cancellationToken = default) 11 | { 12 | return SourceText.From(content); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Platforms/MacCatalyst/Program.cs: -------------------------------------------------------------------------------- 1 | using UIKit; 2 | namespace Mocale.Samples; 3 | 4 | public class Program 5 | { 6 | // This is the main entry point of the application. 7 | private static void Main(string[] args) 8 | { 9 | // if you want to use a different Application Delegate class from "AppDelegate" 10 | // you can specify it here. 11 | UIApplication.Main(args, null, typeof(AppDelegate)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Platforms/Android/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/Mocale/Extensions/LocalizeEnumBehaviorExtension.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Extensions; 2 | 3 | internal static class LocalizeEnumBehaviorExtension 4 | { 5 | public static bool ShouldLocalizeEnum(this LocalizeEnumBehavior localizeEnumBehavior, Enum enumValue) 6 | { 7 | return localizeEnumBehavior.OverrideRules.TryGetValue(enumValue.GetType(), out var rule) 8 | ? rule.UseAttribute 9 | : localizeEnumBehavior.UseAttribute; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Mocale/Models/EmbeddedResourcesConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | namespace Mocale.Models; 3 | 4 | /// 5 | public class EmbeddedResourcesConfig : IEmbeddedResourcesConfig 6 | { 7 | /// 8 | public string ResourcesPath { get; set; } = "Locales"; 9 | 10 | /// 11 | public Assembly? ResourcesAssembly { get; set; } 12 | 13 | /// 14 | public bool UseResourceFolder { get; set; } = true; 15 | } 16 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Mocale.UnitTests.csproj.DotSettings: -------------------------------------------------------------------------------- 1 | 5 | DO_NOT_SHOW 7 | 8 | -------------------------------------------------------------------------------- /src/Mocale/Properties/GlobalSuppression.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage attributes that are applied to 2 | // this project. Project-level suppressions either have no target or are given a specific target and 3 | // scoped to a namespace, type, member, etc. 4 | 5 | using System.Diagnostics.CodeAnalysis; 6 | [assembly: SuppressMessage("Performance", "CA1848:Use the LoggerMessage delegates", Justification = "This message is annoying, why provide methods we can't use?")] 7 | -------------------------------------------------------------------------------- /src/Mocale.Cache.SQLite/Models/SqliteConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Cache.SQLite.Models; 2 | 3 | /// 4 | public class SqliteConfig : ISqliteConfig 5 | { 6 | /// 7 | public string DatabaseName { get; private set; } = Constants.DatabaseFileName; 8 | 9 | /// 10 | public string DatabaseDirectory { get; internal set; } = string.Empty; 11 | 12 | /// 13 | public TimeSpan UpdateInterval { get; set; } = TimeSpan.FromDays(1); 14 | } 15 | -------------------------------------------------------------------------------- /src/Mocale.Cache.SQLite/Properties/GlobalSuppression.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage attributes that are applied to 2 | // this project. Project-level suppressions either have no target or are given a specific target and 3 | // scoped to a namespace, type, member, etc. 4 | 5 | using System.Diagnostics.CodeAnalysis; 6 | [assembly: SuppressMessage("Performance", "CA1848:Use the LoggerMessage delegates", Justification = "This message is annoying, why provide methods we can't use?")] 7 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Resources/en-GB.json: -------------------------------------------------------------------------------- 1 | { 2 | "CurrentLocaleName": "English", 3 | "LocalizationCurrentProviderIs": "The current localization provider is:", 4 | "LocalizationProviderName": "Json", 5 | "MocaleDescription": "Localization framework for .NET Maui", 6 | "MocaleTitle": "Mocale", 7 | "ExternalPrefixExplanation": "Strings prefixed with GR_ indicate they have been pulled from the external provider (GitHub.Raw), when the local cache expires if these values change, so will the text displayed!" 8 | } 9 | -------------------------------------------------------------------------------- /src/Mocale.Providers.Azure.Blob/Properties/GlobalSuppression.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage attributes that are applied to 2 | // this project. Project-level suppressions either have no target or are given a specific target and 3 | // scoped to a namespace, type, member, etc. 4 | 5 | using System.Diagnostics.CodeAnalysis; 6 | [assembly: SuppressMessage("Performance", "CA1848:Use the LoggerMessage delegates", Justification = "This message is annoying, why provide methods we can't use?")] 7 | -------------------------------------------------------------------------------- /src/Mocale.Providers.GitHub.Raw/Properties/GlobalSuppression.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage attributes that are applied to 2 | // this project. Project-level suppressions either have no target or are given a specific target and 3 | // scoped to a namespace, type, member, etc. 4 | 5 | using System.Diagnostics.CodeAnalysis; 6 | [assembly: SuppressMessage("Performance", "CA1848:Use the LoggerMessage delegates", Justification = "This message is annoying, why provide methods we can't use?")] 7 | -------------------------------------------------------------------------------- /src/Mocale/Abstractions/IExternalLocalizationResult.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Abstractions; 2 | 3 | /// 4 | /// External Localization Result 5 | /// 6 | public interface IExternalLocalizationResult 7 | { 8 | /// 9 | /// Whether the attempt succeeded 10 | /// 11 | bool Success { get; } 12 | 13 | /// 14 | /// Localizations from the external provider 15 | /// 16 | Dictionary? Localizations { get; } 17 | } 18 | -------------------------------------------------------------------------------- /src/Mocale.Providers.Azure.Blob/Models/BlobResourceInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Providers.Azure.Blob.Models; 2 | 3 | /// 4 | /// Blob Resource Information 5 | /// 6 | public class BlobResourceInfo 7 | { 8 | /// 9 | /// Whether the theorized blob resource actually exists 10 | /// 11 | public bool Exists { get; init; } 12 | 13 | /// 14 | /// The name of the theorized blob resource 15 | /// 16 | public string? ResourceName { get; init; } 17 | } 18 | -------------------------------------------------------------------------------- /src/Mocale/Abstractions/IResourceFileDetails.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Abstractions; 2 | 3 | /// 4 | /// Resource File Details 5 | /// 6 | public interface IResourceFileDetails 7 | { 8 | /// 9 | /// The type of file being stored externally 10 | /// 11 | LocaleResourceType ResourceType { get; } 12 | 13 | /// 14 | /// The version prefix of the files, this will be prepended after the url & before the file name 15 | /// 16 | string? VersionPrefix { get; } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Collections/CollectionDefinitions.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.UnitTests.Collections; 2 | 3 | [CollectionDefinition(CollectionNames.MocaleLocatorTests, DisableParallelization = true)] 4 | public class MocaleLocatorTestsCollectionDefinition; 5 | 6 | [CollectionDefinition(CollectionNames.ThreadCultureTests, DisableParallelization = true)] 7 | public class ThreadCultureTestsCollectionDefinition; 8 | 9 | [CollectionDefinition(CollectionNames.TestingTests, DisableParallelization = true)] 10 | public class TestingTestsCollectionDefinition; 11 | -------------------------------------------------------------------------------- /src/Mocale.Providers.Azure.Blob/Abstractions/IBlobResourceLocator.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Providers.Azure.Blob.Abstractions; 2 | 3 | /// 4 | /// Blob Resource Locator 5 | /// 6 | public interface IBlobResourceLocator 7 | { 8 | /// 9 | /// Try to locate blob resource for the given culture 10 | /// 11 | /// The culture to try locate blob resource for 12 | /// Result of location 13 | Task TryLocateResource(CultureInfo cultureInfo); 14 | } 15 | -------------------------------------------------------------------------------- /src/Mocale/Abstractions/ITranslationUpdater.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Abstractions; 2 | 3 | /// 4 | /// Translation Updater 5 | /// 6 | internal interface ITranslationUpdater 7 | { 8 | /// 9 | /// Update Translations From Source 10 | /// 11 | /// 12 | /// 13 | /// Whether this should notify observers 14 | void UpdateTranslations(Localization localization, TranslationSource source, bool notify = true); 15 | } 16 | -------------------------------------------------------------------------------- /src/Mocale/Providers/InactiveExternalLocalizationProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | namespace Mocale.Providers; 3 | 4 | internal sealed class InactiveExternalLocalizationProvider : IExternalLocalizationProvider 5 | { 6 | public Task GetValuesForCultureAsync(CultureInfo cultureInfo) 7 | { 8 | IExternalLocalizationResult blankResult = new ExternalLocalizationResult() 9 | { 10 | Success = false, 11 | Localizations = null, 12 | }; 13 | 14 | return Task.FromResult(blankResult); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Mocale.Providers.GitHub.Raw/Helpers/RawUrlBuilder.cs: -------------------------------------------------------------------------------- 1 | using Mocale.Extensions; 2 | namespace Mocale.Providers.GitHub.Raw.Helpers; 3 | 4 | internal static class RawUrlBuilder 5 | { 6 | public static Uri BuildResourceUrl(string username, string repository, string branch, string filePath, string fileName) 7 | { 8 | // https://raw.githubusercontent.com/Axemasta/Mocale/github-provider/samples/Locales/fr-FR.json 9 | return new Uri("https://raw.githubusercontent.com/") 10 | .Append(username, repository, branch, filePath, fileName); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/MocaleLocatorTests.cs: -------------------------------------------------------------------------------- 1 | using Mocale.Abstractions; 2 | using Mocale.Testing; 3 | 4 | namespace Mocale.UnitTests; 5 | 6 | public class MocaleLocatorTests 7 | { 8 | [Fact] 9 | public void SetInstance_Should_SetInstance() 10 | { 11 | // Arrange 12 | var translatorManager = new Mock(); 13 | 14 | // Act 15 | MocaleLocatorHelper.SetTranslatorManager(translatorManager.Object); 16 | 17 | // Assert 18 | Assert.Equal(translatorManager.Object, MocaleLocator.TranslatorManager); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Mocale/Abstractions/ICurrentCultureManager.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace Mocale.Abstractions; 4 | 5 | /// 6 | /// Current Culture Manager 7 | /// 8 | public interface ICurrentCultureManager 9 | { 10 | /// 11 | /// Get Active Culture 12 | /// 13 | /// 14 | CultureInfo GetActiveCulture(); 15 | 16 | /// 17 | /// Set Active Culture 18 | /// 19 | /// 20 | void SetActiveCulture(CultureInfo cultureInfo); 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/Mocale/Models/TranslationLoadResult.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Models; 2 | 3 | /// 4 | /// Translation Load Result 5 | /// 6 | public class TranslationLoadResult 7 | { 8 | /// 9 | /// Whether translations were loaded 10 | /// 11 | public bool Loaded { get; set; } 12 | 13 | /// 14 | /// Source of the translations 15 | /// 16 | public TranslationSource Source { get; set; } 17 | 18 | /// 19 | /// 20 | /// 21 | public required Localization Localization { get; set; } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Snapshots/TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenNoAdditionalFiles_ShouldGenerateNoKeys.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | Diagnostics: [ 3 | { 4 | Message: No localizations files were found to process, 5 | Severity: Warning, 6 | WarningLevel: 1, 7 | Descriptor: { 8 | Id: MOCW001, 9 | Title: No localizations files were found to process, 10 | MessageFormat: No localizations files were found to process, 11 | Category: Mocale.SourceGenerators, 12 | DefaultSeverity: Warning, 13 | IsEnabledByDefault: true 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/TestUtils/TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenNoAdditionalFiles_ShouldGenerateNoKeys.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | Diagnostics: [ 3 | { 4 | Message: No localizations files were found to process, 5 | Severity: Warning, 6 | WarningLevel: 1, 7 | Descriptor: { 8 | Id: MOCW001, 9 | Title: No localizations files were found to process, 10 | MessageFormat: No localizations files were found to process, 11 | Category: Mocale.SourceGenerators, 12 | DefaultSeverity: Warning, 13 | IsEnabledByDefault: true 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/Mocale.Cache.SQLite/Providers/DatabasePathProvider.cs: -------------------------------------------------------------------------------- 1 | using Mocale.Abstractions; 2 | namespace Mocale.Cache.SQLite.Providers; 3 | 4 | internal class DatabasePathProvider(IConfigurationManager configurationManager) 5 | : IDatabasePathProvider 6 | { 7 | private readonly IConfigurationManager configurationManager = Guard.Against.Null(configurationManager, nameof(configurationManager)); 8 | 9 | public string GetDatabasePath() 10 | { 11 | var config = configurationManager.Configuration; 12 | 13 | return Path.Combine(config.DatabaseDirectory, config.DatabaseName); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Mocale/Abstractions/IEmbeddedResourcesConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | namespace Mocale.Abstractions; 3 | 4 | internal interface IEmbeddedResourcesConfig 5 | { 6 | /// 7 | /// Path to the resources directory 8 | /// 9 | string ResourcesPath { get; } 10 | 11 | /// 12 | /// Assembly that contains the resources 13 | /// 14 | Assembly? ResourcesAssembly { get; } 15 | 16 | /// 17 | /// Whether the resources live inside the Maui Resources folder or relative to the assembly 18 | /// 19 | bool UseResourceFolder { get; } 20 | } 21 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/App.xaml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Mocale.Providers.Azure.Blob/Models/BlobStorageConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Providers.Azure.Blob.Models; 2 | 3 | /// 4 | public class BlobStorageConfig : IBlobStorageConfig 5 | { 6 | /// 7 | public Uri BlobContainerUri { get; set; } = new Uri("app://mocale"); // Default value so we don't have to mark as nullable 8 | 9 | /// 10 | public bool RequiresAuthentication { get; set; } 11 | 12 | /// 13 | public bool CheckForFile { get; set; } = true; 14 | 15 | /// 16 | public IResourceFileDetails ResourceFileDetails { get; set; } = new JsonResourceFileDetails(); 17 | } 18 | -------------------------------------------------------------------------------- /src/Mocale/Abstractions/IExternalLocalizationProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | namespace Mocale.Abstractions; 3 | 4 | /// 5 | /// External Localization Provider - Provides localizations from an external source 6 | /// 7 | public interface IExternalLocalizationProvider 8 | { 9 | /// 10 | /// Gets Values For Culture using the external source 11 | /// 12 | /// The culture to attempt to load 13 | /// External localization result for the given culture 14 | Task GetValuesForCultureAsync(CultureInfo cultureInfo); 15 | } 16 | -------------------------------------------------------------------------------- /src/Mocale.Cache.SQLite/Abstractions/ISqliteConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Cache.SQLite.Abstractions; 2 | 3 | /// 4 | /// Configuration For SQLite Cache Library 5 | /// 6 | public interface ISqliteConfig 7 | { 8 | /// 9 | /// The name of the cache database 10 | /// 11 | string DatabaseName { get; } 12 | 13 | /// 14 | /// The database directory: FileSystem.AppDataDirectory 15 | /// 16 | string DatabaseDirectory { get; } 17 | 18 | /// 19 | /// The interval in which the cache should be updated 20 | /// 21 | TimeSpan UpdateInterval { get; } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Resources/Misc/TestFile.json: -------------------------------------------------------------------------------- 1 | { 2 | "glossary": { 3 | "title": "example glossary", 4 | "GlossDiv": { 5 | "title": "S", 6 | "GlossList": { 7 | "GlossEntry": { 8 | "ID": "SGML", 9 | "SortAs": "SGML", 10 | "GlossTerm": "Standard Generalized Markup Language", 11 | "Acronym": "SGML", 12 | "Abbrev": "ISO 8879:1986", 13 | "GlossDef": { 14 | "para": "A meta-markup language, used to create markup languages such as DocBook.", 15 | "GlossSeeAlso": ["GML", "XML"] 16 | }, 17 | "GlossSee": "markup" 18 | } 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Mocale/Models/Localization.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | namespace Mocale.Models; 3 | 4 | /// 5 | /// Localization 6 | /// 7 | public class Localization(CultureInfo cultureInfo) 8 | { 9 | /// 10 | /// Corresponding culture 11 | /// 12 | public CultureInfo CultureInfo { get; } = cultureInfo; 13 | 14 | /// 15 | /// Translations 16 | /// 17 | public Dictionary Translations { get; set; } = []; 18 | 19 | /// 20 | /// Blank localization 21 | /// 22 | public static Localization Invariant => new(CultureInfo.InvariantCulture); 23 | } 24 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Resources/Invalid/en-GB.json: -------------------------------------------------------------------------------- 1 | 2 | 3 | English 4 | The current localization provider is: 5 | Json 6 | Localization framework for .NET Maui 7 | Mocale 8 | Strings prefixed with GR_ indicate they have been pulled from the external provider (GitHub.Raw), when the local cache expires if these values change, so will the text displayed! 9 | 10 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Properties/GlobalSuppression.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable IDE0076 2 | /* 3 | * Platform Suppressions 4 | * 5 | * The dotnet analyzers don't work properly when suppressions apply only to files not currently beign targetted. 6 | * ie a suppression rule for iOS / macOS won't apply when targetting windows / android and the compiler with 7 | * erroniously assume the suppression is invalid. 8 | */ 9 | using System.Diagnostics.CodeAnalysis; 10 | 11 | [assembly: SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "iOS / macOS Platform Name", Scope = "type", Target = "~T:Mocale.Samples.AppDelegate")] 12 | #pragma warning restore IDE0076 13 | -------------------------------------------------------------------------------- /src/Mocale.Providers.GitHub.Raw/Models/GithubRawConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Providers.GitHub.Raw.Models; 2 | 3 | /// 4 | public class GithubRawConfig : IGithubRawConfig 5 | { 6 | /// 7 | public string Username { get; set; } = string.Empty; 8 | 9 | /// 10 | public string Repository { get; set; } = string.Empty; 11 | 12 | /// 13 | public string Branch { get; set; } = "main"; 14 | 15 | /// 16 | public string LocaleDirectory { get; set; } = string.Empty; 17 | 18 | /// 19 | public IResourceFileDetails ResourceFileDetails { get; set; } = new JsonResourceFileDetails(); 20 | } 21 | -------------------------------------------------------------------------------- /src/Mocale/Abstractions/IInternalLocalizationProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | namespace Mocale.Abstractions; 3 | 4 | /// 5 | /// IInternalLocalizationProvider - Responsible for providing localizations from internally within 6 | /// the application. 7 | /// 8 | public interface IInternalLocalizationProvider 9 | 10 | { 11 | /// 12 | /// Gets the localization values for the given culture 13 | /// 14 | /// The culture to get localizations for 15 | /// The localizations as a key value pair 16 | Dictionary? GetValuesForCulture(CultureInfo cultureInfo); 17 | } 18 | -------------------------------------------------------------------------------- /src/Mocale/Managers/ConfigurationManager.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.GuardClauses; 2 | 3 | namespace Mocale.Managers; 4 | 5 | /// 6 | /// Simple Singleton Style Class For Storing Configuration Objects 7 | /// 8 | /// 9 | public class ConfigurationManager(TConfig config) 10 | : IConfigurationManager, IConfigurationUpdateManager 11 | { 12 | /// 13 | public TConfig Configuration { get; } = Guard.Against.Null(config, nameof(config)); 14 | 15 | /// 16 | public void UpdateConfiguration(Action configuration) 17 | { 18 | configuration(Configuration); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Mocale/MocaleInitializeService.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale; 2 | 3 | internal class MocaleInitializeService : IMauiInitializeService 4 | { 5 | public void Initialize(IServiceProvider services) 6 | { 7 | var localizationManager = services.GetRequiredService(); 8 | var translatorManager = services.GetRequiredService(); 9 | var configManager = services.GetRequiredService>(); 10 | 11 | MocaleLocator.MocaleConfiguration = configManager.Configuration; 12 | MocaleLocator.TranslatorManager = translatorManager; 13 | 14 | Task.Run(localizationManager.Initialize); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Resources/Raw/AboutAssets.txt: -------------------------------------------------------------------------------- 1 | Any raw assets you want to be deployed with your application can be placed in 2 | this directory (and child directories). Deployment of the asset to your application 3 | is automatically handled by the following `MauiAsset` Build Action within your `.csproj`. 4 | 5 | 6 | 7 | These files will be deployed with you package and will be accessible using Essentials: 8 | 9 | async Task LoadMauiAsset() 10 | { 11 | using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt"); 12 | using var reader = new StreamReader(stream); 13 | 14 | var contents = reader.ReadToEnd(); 15 | } 16 | -------------------------------------------------------------------------------- /src/Mocale/Enums/TranslationSource.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Enums; 2 | 3 | /// 4 | /// Translation Source 5 | /// 6 | public enum TranslationSource 7 | { 8 | /// 9 | /// Translations came from internal provider (locally). 10 | /// 11 | Internal = 0, 12 | 13 | /// 14 | /// Translations came from external provider. 15 | /// 16 | External = 1, 17 | 18 | /// 19 | /// Translations came from cache, and the cache is up to date. 20 | /// 21 | WarmCache = 2, 22 | 23 | /// 24 | /// Translations came from cache, and the cache is expired (can be updated). 25 | /// 26 | ColdCache = 4, 27 | } 28 | -------------------------------------------------------------------------------- /src/Mocale/Models/ResxResourceFileDetails.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace Mocale.Models; 4 | 5 | /// 6 | public class ResxResourceFileDetails : IResourceFileDetails 7 | { 8 | /// 9 | public LocaleResourceType ResourceType { get; } = LocaleResourceType.Resx; 10 | 11 | /// 12 | public string? VersionPrefix { get; set; } 13 | 14 | /// 15 | /// The name of the resource file 16 | /// 17 | public required string ResourcePrefix { get; set; } 18 | 19 | /// 20 | /// The primary resources culture (if this exists). This will not have the culture suffix in its file path. 21 | /// 22 | public CultureInfo? PrimaryCulture { get; set; } 23 | } 24 | -------------------------------------------------------------------------------- /src/Mocale/Parsers/JsonLocalizationParser.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using Ardalis.GuardClauses; 3 | 4 | namespace Mocale.Parsers; 5 | 6 | internal class JsonLocalizationParser(ILogger logger) : ILocalizationParser 7 | { 8 | private readonly ILogger logger = Guard.Against.Null(logger, nameof(logger)); 9 | 10 | public Dictionary? ParseLocalizationStream(Stream resourceStream) 11 | { 12 | try 13 | { 14 | return JsonSerializer.Deserialize>(resourceStream); 15 | } 16 | catch (Exception ex) 17 | { 18 | logger.LogError(ex, "An exception occurred parsing localization stream"); 19 | return null; 20 | } 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/Mocale/Abstractions/IKeyConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace Mocale.Abstractions; 4 | 5 | /// 6 | /// Translation Key Converter 7 | /// 8 | public interface IKeyConverter 9 | { 10 | /// 11 | /// Convert the current object into a translation key 12 | /// 13 | /// The value to convert into a translation key 14 | /// The target type for the control the localization is being applied 15 | /// The parameter to use for the conversion 16 | /// The culture for the conversion 17 | /// 18 | string? Convert(object? value, Type targetType, object? parameter, CultureInfo culture); 19 | } 20 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Platforms/Windows/App.xaml.cs: -------------------------------------------------------------------------------- 1 | // To learn more about WinUI, the WinUI project structure, 2 | // and more about our project templates, see: http://aka.ms/winui-project-info. 3 | 4 | namespace Mocale.Samples.WinUI; 5 | 6 | /// 7 | /// Provides application-specific behavior to supplement the default Application class. 8 | /// 9 | public partial class App : MauiWinUIApplication 10 | { 11 | /// 12 | /// Initializes the singleton application object. This is the first line of authored code 13 | /// executed, and as such is the logical equivalent of main() or WinMain(). 14 | /// 15 | public App() 16 | { 17 | this.InitializeComponent(); 18 | } 19 | 20 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 21 | } 22 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Platforms/Windows/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | true/PM 12 | PerMonitorV2, PerMonitor 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Resources/Resx/TestResources.fr-fr.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | text/microsoft-resx 4 | 5 | 6 | 1.3 7 | 8 | 9 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 10 | 11 | 12 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 13 | 14 | 15 | Clé deux 16 | 17 | 18 | Clé un 19 | 20 | -------------------------------------------------------------------------------- /src/Mocale.Testing/Mocale.Testing.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(StandardTargetFramework) 5 | 6 | Mocale.Testing 7 | Mocale.Testing 8 | Mocale.Testing 9 | Testing compatibility package for mocale 10 | maui,mocale,localization,localisation,translation,testing,mocale.testing 11 | Mocale.Testing 12 | Testing compatibility package for Mocale to enable seemless unit testing of Maui components that use mocale. 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Mocale/Abstractions/IConfigurationManager.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Abstractions; 2 | 3 | /// 4 | /// Configuration Manager 5 | /// 6 | /// The config for this manager 7 | public interface IConfigurationManager 8 | { 9 | /// 10 | /// The configuration for the given area 11 | /// 12 | TConfig Configuration { get; } 13 | } 14 | 15 | /// 16 | /// Configuration Update Manager 17 | /// 18 | /// 19 | public interface IConfigurationUpdateManager 20 | { 21 | /// 22 | /// Update Configuration 23 | /// 24 | /// 25 | void UpdateConfiguration(Action configuration); 26 | } 27 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Platforms/Tizen/tizen-manifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | maui-appicon-placeholder 7 | 8 | 9 | 10 | 11 | http://tizen.org/privilege/internet 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Mocale/Abstractions/ILocalizationManager.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | namespace Mocale.Abstractions; 3 | 4 | /// 5 | /// Localization Manager 6 | /// 7 | public interface ILocalizationManager 8 | { 9 | /// 10 | /// The current culture 11 | /// 12 | CultureInfo CurrentCulture { get; } 13 | 14 | /// 15 | /// Mocale Bootstrapper, this is called internall by mocale during maui app startup 16 | /// 17 | /// Success 18 | Task Initialize(); 19 | 20 | /// 21 | /// Set culture 22 | /// 23 | /// The culture to attempt to display 24 | /// Success 25 | Task SetCultureAsync(CultureInfo culture); 26 | } 27 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Enums/Cities.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace Mocale.Samples.Enums; 4 | 5 | public enum Cities 6 | { 7 | [Description("CityDescription_London")] 8 | [MocaleTranslationKey("CityDescription_London")] 9 | London, 10 | 11 | [Description("CityDescription_Nottingham")] 12 | [MocaleTranslationKey("CityDescription_Nottingham")] 13 | Nottingham, 14 | 15 | [Description("CityDescription_Newcastle")] 16 | [MocaleTranslationKey("CityDescription_Newcastle")] 17 | Newcastle, 18 | 19 | [Description("CityDescription_Manchester")] 20 | [MocaleTranslationKey("CityDescription_Manchester")] 21 | Manchester, 22 | } 23 | 24 | [AttributeUsage(AttributeTargets.Field)] 25 | public class MocaleTranslationKeyAttribute(string key) : Attribute 26 | { 27 | public string Key { get; set; } = key; 28 | } 29 | -------------------------------------------------------------------------------- /src/Mocale.Providers.GitHub.Raw/Abstractions/IGithubRawConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Providers.GitHub.Raw.Abstractions; 2 | 3 | /// 4 | /// External provider configuration for github raw source 5 | /// 6 | public interface IGithubRawConfig : IExternalProviderConfiguration 7 | { 8 | /// 9 | /// The github username 10 | /// 11 | string Username { get; } 12 | 13 | /// 14 | /// The repository to target 15 | /// 16 | string Repository { get; } 17 | 18 | /// 19 | /// The branch the localizations exist on 20 | /// 21 | string Branch { get; } 22 | 23 | /// 24 | /// The directory in the repository that contains the locale files 25 | /// 26 | string LocaleDirectory { get; } 27 | } 28 | -------------------------------------------------------------------------------- /samples/Mocale.GeneratorSample/Mocale.GeneratorSample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | $(StandardTargetFramework) 6 | enable 7 | enable 8 | true 9 | $(NoWarn);IDE0005 10 | Locales\*.json 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Mocale/Extensions/UriExtension.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Extensions; 2 | 3 | internal static class UriExtension 4 | { 5 | public static Uri Append(this Uri uri, params string[] paths) 6 | { 7 | // https://stackoverflow.com/a/7993235/8828057 8 | return new Uri(paths.Aggregate(uri.AbsoluteUri, (current, path) => $"{current.TrimEnd('/')}/{path.TrimStart('/')}")); 9 | } 10 | 11 | public static bool TryAppend(this Uri uri, out Uri result, params string[] paths) 12 | { 13 | result = default!; 14 | 15 | var appendedUri = uri.Append(paths); 16 | 17 | var wellFormed = Uri.IsWellFormedUriString(appendedUri.ToString(), UriKind.Absolute); 18 | 19 | if (wellFormed) 20 | { 21 | result = appendedUri; 22 | } 23 | 24 | return wellFormed; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Mocale.Cache.SQLite/Repositories/RepositoryBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | namespace Mocale.Cache.SQLite.Repositories; 3 | 4 | internal abstract class RepositoryBase 5 | { 6 | #pragma warning disable IDE1006 7 | // ReSharper disable once InconsistentNaming 8 | protected ILogger logger { get; } 9 | #pragma warning restore IDE1006 10 | 11 | protected SQLiteConnection Connection { get; } 12 | 13 | protected RepositoryBase( 14 | IDatabaseConnectionProvider databaseConnectionProvider, 15 | ILogger logger) 16 | { 17 | databaseConnectionProvider = Guard.Against.Null(databaseConnectionProvider, nameof(databaseConnectionProvider)); 18 | this.logger = Guard.Against.Null(logger, nameof(logger)); 19 | 20 | Connection = databaseConnectionProvider.GetDatabaseConnection(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Mocale/Abstractions/ITranslationResolver.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | namespace Mocale.Abstractions; 3 | 4 | /// 5 | /// Translation Resolver 6 | /// 7 | public interface ITranslationResolver 8 | { 9 | /// 10 | /// Load Translations For Given Culture 11 | /// 12 | /// The culture to load 13 | /// The load result 14 | Task LoadTranslations(CultureInfo cultureInfo); 15 | 16 | /// 17 | /// Load Local Translations For Given Culture 18 | /// Local is defined as either from cache of on the device 19 | /// 20 | /// The culture to load 21 | /// The load result 22 | TranslationLoadResult LoadLocalTranslations(CultureInfo cultureInfo); 23 | } 24 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Snapshots/TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenSingleFile_ShouldGenerateCorrectly#MocaleTranslationKeys.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: MocaleTranslationKeys.g.cs 2 | namespace Mocale.Translations 3 | { 4 | public static class TranslationKeys 5 | { 6 | /// 7 | /// Looks up a localized string using key KeyOne. 8 | /// 9 | public const string KeyOne = "KeyOne"; 10 | 11 | /// 12 | /// Looks up a localized string using key Key_Two. 13 | /// 14 | public const string KeyTwo = "Key_Two"; 15 | 16 | /// 17 | /// Looks up a localized string using key Key@!_/Three. 18 | /// 19 | public const string KeyThree = "Key@!_/Three"; 20 | 21 | /// 22 | /// Looks up a localized string using key Key.Four. 23 | /// 24 | public const string KeyFour = "Key.Four"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/TestUtils/TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenSingleFile_ShouldGenerateCorrectly#MocaleTranslationKeys.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: MocaleTranslationKeys.g.cs 2 | namespace Mocale.Translations 3 | { 4 | public static class TranslationKeys 5 | { 6 | /// 7 | /// Looks up a localized string using key KeyOne. 8 | /// 9 | public const string KeyOne = "KeyOne"; 10 | 11 | /// 12 | /// Looks up a localized string using key Key_Two. 13 | /// 14 | public const string KeyTwo = "Key_Two"; 15 | 16 | /// 17 | /// Looks up a localized string using key Key@!_/Three. 18 | /// 19 | public const string KeyThree = "Key@!_/Three"; 20 | 21 | /// 22 | /// Looks up a localized string using key Key.Four. 23 | /// 24 | public const string KeyFour = "Key.Four"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/ViewModels/ConverterViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Samples.ViewModels; 2 | 3 | public partial class ConverterViewModel : BaseViewModel 4 | { 5 | [ObservableProperty] 6 | public partial OrderStatus CurrentStatus { get; set; } 7 | 8 | public ObservableRangeCollection OrderStatuses { get; } = 9 | [ 10 | new OrderStatus("Pending", 0), 11 | new OrderStatus("Shipped", 1), 12 | new OrderStatus("Delivered", 2), 13 | new OrderStatus("Cancelled", 3), 14 | ]; 15 | 16 | public ConverterViewModel() 17 | { 18 | CurrentStatus = OrderStatuses[0]; 19 | } 20 | } 21 | 22 | public partial class OrderStatus(string name, int stage) : ObservableObject 23 | { 24 | [ObservableProperty] 25 | public partial string Name { get; set; } = name; 26 | 27 | [ObservableProperty] 28 | public partial int Stage { get; set; } = stage; 29 | } 30 | -------------------------------------------------------------------------------- /src/Mocale/Abstractions/ILocalisationCacheManager.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | namespace Mocale.Abstractions; 3 | 4 | /// 5 | /// Localisation Cache Manager 6 | /// 7 | public interface ILocalisationCacheManager 8 | { 9 | /// 10 | /// Get Cached Localizations For Culture 11 | /// 12 | /// Culture to get cached values for 13 | /// Cached localizations if they exist 14 | Dictionary? GetCachedLocalizations(CultureInfo cultureInfo); 15 | 16 | /// 17 | /// Set Cached Localizations For Culture 18 | /// 19 | /// 20 | /// 21 | /// 22 | bool SaveCachedLocalizations(CultureInfo cultureInfo, Dictionary localizations); 23 | } 24 | -------------------------------------------------------------------------------- /src/Mocale/Extensions/TaskExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Extensions; 2 | 3 | internal static class TaskExtensions 4 | { 5 | // https://www.meziantou.net/fire-and-forget-a-task-in-dotnet.htm 6 | public static void Forget(this Task task) 7 | { 8 | // Only care about tasks that may fault or are faulted, 9 | // so fast-path for SuccessfullyCompleted and Canceled tasks 10 | if (!task.IsCompleted || task.IsFaulted) 11 | { 12 | _ = ForgetAwaited(task); 13 | } 14 | 15 | static async Task ForgetAwaited(Task task) 16 | { 17 | try 18 | { 19 | // No need to resume on the original SynchronizationContext 20 | await task.ConfigureAwait(false); 21 | } 22 | catch 23 | { 24 | // Nothing to do here 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Fixtures/FixtureBase.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.UnitTests.Fixtures; 2 | 3 | public abstract class FixtureBase : MocaleLocatorFixture 4 | { 5 | private Lazy SutLazy { get; init; } 6 | 7 | protected TSut Sut => SutLazy.Value; 8 | 9 | public FixtureBase() 10 | { 11 | SutLazy = new Lazy(CreateSystemUnderTest, LazyThreadSafetyMode.ExecutionAndPublication); 12 | } 13 | 14 | public abstract TSut CreateSystemUnderTest(); 15 | } 16 | 17 | public abstract class FixtureBase : MocaleLocatorFixture 18 | { 19 | private Lazy SutLazy { get; init; } 20 | 21 | protected TSut GetSut() => (TSut)SutLazy.Value; 22 | 23 | public FixtureBase() 24 | { 25 | SutLazy = new Lazy(CreateSystemUnderTest, LazyThreadSafetyMode.ExecutionAndPublication); 26 | } 27 | 28 | public abstract object CreateSystemUnderTest(); 29 | } 30 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Snapshots/TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenMultipleFilesButAllTheSameKeys_ShouldContainOnlyUniqueKeys#MocaleTranslationKeys.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: MocaleTranslationKeys.g.cs 2 | namespace Mocale.Translations 3 | { 4 | public static class TranslationKeys 5 | { 6 | /// 7 | /// Looks up a localized string using key KeyOne. 8 | /// 9 | public const string KeyOne = "KeyOne"; 10 | 11 | /// 12 | /// Looks up a localized string using key Key_Two. 13 | /// 14 | public const string KeyTwo = "Key_Two"; 15 | 16 | /// 17 | /// Looks up a localized string using key Key@!_/Three. 18 | /// 19 | public const string KeyThree = "Key@!_/Three"; 20 | 21 | /// 22 | /// Looks up a localized string using key Key.Four. 23 | /// 24 | public const string KeyFour = "Key.Four"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/TestUtils/TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenKeysContainIllegalCharacters_ShouldGenerateSanitizedFields#MocaleTranslationKeys.g.received.cs: -------------------------------------------------------------------------------- 1 | //HintName: MocaleTranslationKeys.g.cs 2 | namespace Mocale.Translations 3 | { 4 | public static class TranslationKeys 5 | { 6 | /// 7 | /// Looks up a localized string using key KeyOne. 8 | /// 9 | public const string KeyOne = "KeyOne"; 10 | 11 | /// 12 | /// Looks up a localized string using key Key_Two. 13 | /// 14 | public const string KeyTwo = "Key_Two"; 15 | 16 | /// 17 | /// Looks up a localized string using key Key@!_/Three. 18 | /// 19 | public const string KeyThree = "Key@!_/Three"; 20 | 21 | /// 22 | /// Looks up a localized string using key Key.Four. 23 | /// 24 | public const string KeyFour = "Key.Four"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/TestUtils/TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenMultipleFilesButAllTheSameKeys_ShouldContainOnlyUniqueKeys#MocaleTranslationKeys.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: MocaleTranslationKeys.g.cs 2 | namespace Mocale.Translations 3 | { 4 | public static class TranslationKeys 5 | { 6 | /// 7 | /// Looks up a localized string using key KeyOne. 8 | /// 9 | public const string KeyOne = "KeyOne"; 10 | 11 | /// 12 | /// Looks up a localized string using key Key_Two. 13 | /// 14 | public const string KeyTwo = "Key_Two"; 15 | 16 | /// 17 | /// Looks up a localized string using key Key@!_/Three. 18 | /// 19 | public const string KeyThree = "Key@!_/Three"; 20 | 21 | /// 22 | /// Looks up a localized string using key Key.Four. 23 | /// 24 | public const string KeyFour = "Key.Four"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Mocale/Helper/EnumTranslationKeyHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Helper; 2 | 3 | internal static class EnumTranslationKeyHelper 4 | { 5 | public static string GetTranslationKey(Enum @enum, LocalizeEnumBehavior behavior) 6 | { 7 | return behavior.OverrideRules.TryGetValue(@enum.GetType(), out var rule) 8 | ? GetKey(@enum, rule.UseAttribute, rule.LocalizeAttribute, rule.AttributePropertyName) 9 | : GetKey(@enum, behavior.UseAttribute, behavior.LocalizeAttribute, behavior.AttributePropertyName); 10 | } 11 | 12 | private static string GetKey(Enum enumValue, bool useAttribute, Type localizeAttribute, string propertyName) 13 | { 14 | if (useAttribute) 15 | { 16 | return enumValue.GetAttributeValue(localizeAttribute, propertyName) ?? enumValue.ToString(); 17 | } 18 | else 19 | { 20 | return enumValue.ToString(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Mocale/Abstractions/ITranslatorManager.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Globalization; 3 | namespace Mocale.Abstractions; 4 | 5 | /// 6 | /// Translator Manager 7 | /// 8 | public interface ITranslatorManager : INotifyPropertyChanged 9 | { 10 | /// 11 | /// Current Culture 12 | /// 13 | CultureInfo? CurrentCulture { get; } 14 | 15 | /// 16 | /// Translate 17 | /// 18 | /// Key to translate 19 | /// Translation result 20 | string Translate(string key); 21 | 22 | /// 23 | /// Translate 24 | /// 25 | /// Key to translate 26 | /// Parameters to translate 27 | /// Translation result 28 | string Translate(string key, object[] parameters); 29 | } 30 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Providers/InactiveExternalLocalizationProviderTests.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using Mocale.Abstractions; 3 | using Mocale.Providers; 4 | 5 | namespace Mocale.UnitTests.Providers; 6 | 7 | public class InactiveExternalLocalizationProviderTests : FixtureBase 8 | { 9 | #region Setup 10 | 11 | public override IExternalLocalizationProvider CreateSystemUnderTest() 12 | { 13 | return new InactiveExternalLocalizationProvider(); 14 | } 15 | 16 | #endregion Setup 17 | 18 | #region Tests 19 | 20 | [Fact] 21 | public async Task GetValuesForCultureAsync_WhenCalled_ShouldReturnBlankResult() 22 | { 23 | var loadResult = await Sut.GetValuesForCultureAsync(new CultureInfo("en-GB")); 24 | 25 | Assert.False(loadResult.Success); 26 | Assert.Null(loadResult.Localizations); 27 | } 28 | 29 | #endregion Tests 30 | } 31 | -------------------------------------------------------------------------------- /src/Mocale/Models/MocaleConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | namespace Mocale.Models; 3 | 4 | /// 5 | public class MocaleConfiguration : IMocaleConfiguration 6 | { 7 | /// 8 | public LocaleResourceType ResourceType { get; internal set; } 9 | 10 | /// 11 | public CultureInfo DefaultCulture { get; set; } = Thread.CurrentThread.CurrentCulture; // Should this check the device for its language setting? 12 | 13 | /// 14 | public bool ShowMissingKeys { get; set; } = true; 15 | 16 | /// 17 | public string NotFoundSymbol { get; set; } = "$"; 18 | 19 | /// 20 | public bool UseExternalProvider { get; set; } = true; 21 | 22 | /// 23 | public bool SaveCultureChanged { get; set; } = true; 24 | 25 | /// 26 | public LocalizeEnumBehavior EnumBehavior { get; set; } = new(); 27 | } 28 | -------------------------------------------------------------------------------- /src/Mocale.Testing/MocaleLocatorHelper.cs: -------------------------------------------------------------------------------- 1 | using Mocale.Abstractions; 2 | 3 | namespace Mocale.Testing; 4 | 5 | /// 6 | /// Mocale Locator Test Helper For Setting Internal Classes For Testing Purposes 7 | /// 8 | public static class MocaleLocatorHelper 9 | { 10 | /// 11 | /// Set Translator Manager instance during testing 12 | /// 13 | /// 14 | public static void SetTranslatorManager(ITranslatorManager manager) 15 | { 16 | MocaleLocator.TranslatorManager = manager; 17 | } 18 | 19 | /// 20 | /// Set Mocale Configuration instance during testing, this is required when localizing enums 21 | /// 22 | /// 23 | public static void SetMocaleConfiguration(IMocaleConfiguration configuration) 24 | { 25 | MocaleLocator.MocaleConfiguration = configuration; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Mocale.Providers.AWS.S3/Mocale.Providers.AWS.S3.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MauiPlatformTargetFrameworks); 5 | true 6 | 7 | Mocale.Providers.AWS.S3 8 | Mocale.Providers.AWS.S3 9 | Mocale.Providers.AWS.S3 10 | AWS S3 Storage Provider For Mocale 11 | maui,mocale,localization,localisation,translation,aws,s3,awss3,s3bucket,bucket,object,storage,provider,mocaleprovider 12 | Mocale.Providers.Azure.Blob 13 | AWS S3 object storage external provider for Mocale. This packages provides the ability to query S3 object storage for localization files to be used in your maui application. 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Mocale.Providers.Azure.Blob/Abstractions/IBlobStorageConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Providers.Azure.Blob.Abstractions; 2 | 3 | /// 4 | /// Configuration for aure blob storage external provider 5 | /// 6 | public interface IBlobStorageConfig : IExternalProviderConfiguration 7 | { 8 | /// 9 | /// Uri for the blob container 10 | /// 11 | Uri BlobContainerUri { get; } 12 | 13 | /// 14 | /// Whether the requests require authentication. 15 | /// 16 | /// If true then the authentication host builder extension will need to be provided in order for 17 | /// the configured authenticaton to kick in 18 | /// 19 | bool RequiresAuthentication { get; } 20 | 21 | /// 22 | /// Whether the blob provider should check for a file before attempting to download it. This 23 | /// will require an extra api call but reduce exception noise. 24 | /// 25 | bool CheckForFile { get; } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Properties/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Test naming convention uses underscores")] 9 | [assembly: SuppressMessage("Performance", "CA1848:Use the LoggerMessage delegates", Justification = "This is a unit test project")] 10 | [assembly: SuppressMessage("Usage", "CA2201:Do not raise reserved exception types", Justification = "Unit tests will raise bogus exceptions for test cases")] 11 | [assembly: SuppressMessage("Globalization", "CA1304:Specify CultureInfo", Justification = "I'm explicitly testing this method...", Scope = "member", Target = "~M:Mocale.UnitTests.Cache.InMemoryCacheManagerTests.ClearCache_CallsClearMethod")] 12 | -------------------------------------------------------------------------------- /src/Mocale.Cache.SQLite/Abstractions/ITranslationsRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | namespace Mocale.Cache.SQLite.Abstractions; 3 | 4 | /// 5 | /// Translation Repository 6 | /// 7 | public interface ITranslationsRepository 8 | { 9 | /// 10 | /// Get Translations For Culture 11 | /// 12 | /// 13 | /// 14 | Dictionary? GetTranslations(CultureInfo cultureInfo); 15 | 16 | /// 17 | /// Add Translations For Culture 18 | /// 19 | /// 20 | /// 21 | /// 22 | bool AddTranslations(CultureInfo cultureInfo, Dictionary translations); 23 | 24 | /// 25 | /// Delete Translations For Culture 26 | /// 27 | /// 28 | /// 29 | bool DeleteTranslations(CultureInfo cultureInfo); 30 | } 31 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Snapshots/TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenMultipleFilesButDifferentKeys_ShouldContainOnlyUniqueKeys#MocaleTranslationKeys.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: MocaleTranslationKeys.g.cs 2 | namespace Mocale.Translations 3 | { 4 | public static class TranslationKeys 5 | { 6 | /// 7 | /// Looks up a localized string using key KeyOne. 8 | /// 9 | public const string KeyOne = "KeyOne"; 10 | 11 | /// 12 | /// Looks up a localized string using key Key_Two. 13 | /// 14 | public const string KeyTwo = "Key_Two"; 15 | 16 | /// 17 | /// Looks up a localized string using key Key@!_/Three. 18 | /// 19 | public const string KeyThree = "Key@!_/Three"; 20 | 21 | /// 22 | /// Looks up a localized string using key Key.Four. 23 | /// 24 | public const string KeyFour = "Key.Four"; 25 | 26 | /// 27 | /// Looks up a localized string using key KeyFive. 28 | /// 29 | public const string KeyFive = "KeyFive"; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/TestUtils/TranslationKeySourceGeneratorSnapshotTests.GeneratesTranslationKeys_WhenMultipleFilesButDifferentKeys_ShouldContainOnlyUniqueKeys#MocaleTranslationKeys.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: MocaleTranslationKeys.g.cs 2 | namespace Mocale.Translations 3 | { 4 | public static class TranslationKeys 5 | { 6 | /// 7 | /// Looks up a localized string using key KeyOne. 8 | /// 9 | public const string KeyOne = "KeyOne"; 10 | 11 | /// 12 | /// Looks up a localized string using key Key_Two. 13 | /// 14 | public const string KeyTwo = "Key_Two"; 15 | 16 | /// 17 | /// Looks up a localized string using key Key@!_/Three. 18 | /// 19 | public const string KeyThree = "Key@!_/Three"; 20 | 21 | /// 22 | /// Looks up a localized string using key Key.Four. 23 | /// 24 | public const string KeyFour = "Key.Four"; 25 | 26 | /// 27 | /// Looks up a localized string using key KeyFive. 28 | /// 29 | public const string KeyFive = "KeyFive"; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Mocale.SourceGenerators/AnalyzerReleases.Shipped.md: -------------------------------------------------------------------------------- 1 | ; Shipped analyzer releases 2 | ; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md 3 | 4 | ## Release 1.0 5 | 6 | ### New Rules 7 | 8 | | Rule ID | Category | Severity | Notes | 9 | |---------|----------|-------------|---------------------------------------------------------------------------------------------------| 10 | | MOCE001 | Usage | Error | MOCE001_ParsingException, Raised if an exception is thrown during translation file parsing | 11 | | MOCW001 | Usage | Warning | MOCW001_NoLocalizationFilesDetected, Raised if no translation files are discovered for parsing | 12 | | MOCW002 | Usage | Warning | MOCW002_FileNotJsonLocalization, Raised when a file processed by the generator is not appropriate | 13 | | MOCI001 | Usage | Information | MOCI001_ProcessingFiles, Raised when the generator starts processing files | 14 | -------------------------------------------------------------------------------- /src/Mocale.Cache.SQLite/Mocale.Cache.SQLite.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MauiPlatformTargetFrameworks); 5 | true 6 | 7 | Mocale.Cache.SQLite 8 | Mocale.Cache.SQLite 9 | Mocale.Cache.SQLite 10 | Caching implementation for Mocale. 11 | maui,mocale,localization,localisation,translation,cache,sqlite 12 | Mocale.Cache.SQLite 13 | Caching implementation for Mocale. This cache provider utilizes a SQLite database to store translations and other details important for Mocales library functions. 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Resources/Resx/TestResources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | text/microsoft-resx 11 | 12 | 13 | 1.3 14 | 15 | 16 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 17 | 18 | 19 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 20 | 21 | 22 | Value One 23 | 24 | 25 | Value Two 26 | 27 | 28 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Platforms/MacCatalyst/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIDeviceFamily 6 | 7 | 1 8 | 2 9 | 10 | UIRequiredDeviceCapabilities 11 | 12 | arm64 13 | 14 | UISupportedInterfaceOrientations 15 | 16 | UIInterfaceOrientationPortrait 17 | UIInterfaceOrientationLandscapeLeft 18 | UIInterfaceOrientationLandscapeRight 19 | 20 | UISupportedInterfaceOrientations~ipad 21 | 22 | UIInterfaceOrientationPortrait 23 | UIInterfaceOrientationPortraitUpsideDown 24 | UIInterfaceOrientationLandscapeLeft 25 | UIInterfaceOrientationLandscapeRight 26 | 27 | XSAppIconAssets 28 | Assets.xcassets/appicon.appiconset 29 | 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Axemasta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Mocale.Testing/Extensions/DictionaryExtension.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Testing.Extensions; 2 | 3 | internal static class DictionaryExtension 4 | { 5 | public static void AddOrUpdateValues(this Dictionary currentValues, Dictionary updatedValues) 6 | { 7 | var newValues = updatedValues.Where(uv => !currentValues.ContainsKey(uv.Key)) 8 | .ToList(); 9 | 10 | if (newValues.Count > 0) 11 | { 12 | foreach (var newValue in newValues) 13 | { 14 | currentValues.Add(newValue.Key, newValue.Value); 15 | } 16 | } 17 | 18 | var modifiedValues = updatedValues.Where( 19 | uv => currentValues.ContainsKey(uv.Key) && 20 | !currentValues[uv.Key].Equals(uv.Value, StringComparison.Ordinal)) 21 | .ToList(); 22 | 23 | if (modifiedValues.Count > 0) 24 | { 25 | foreach (var modifiedValue in modifiedValues) 26 | { 27 | currentValues[modifiedValue.Key] = modifiedValue.Value; 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Mocale/Extensions/DictionaryExtension.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Extensions; 2 | 3 | internal static class DictionaryExtension 4 | { 5 | public static void AddOrUpdateValues(this Dictionary currentValues, Dictionary updatedValues) 6 | { 7 | var newValues = updatedValues.Where(uv => !currentValues.ContainsKey(uv.Key)) 8 | .ToList(); 9 | 10 | if (newValues.Count > 0) 11 | { 12 | foreach (var newValue in newValues) 13 | { 14 | currentValues.Add(newValue.Key, newValue.Value); 15 | } 16 | } 17 | 18 | var modifiedValues = updatedValues.Where( 19 | uv => currentValues.ContainsKey(uv.Key) && 20 | !currentValues[uv.Key].Equals(uv.Value, StringComparison.Ordinal)) 21 | .ToList(); 22 | 23 | if (modifiedValues.Count > 0) 24 | { 25 | foreach (var modifiedValue in modifiedValues) 26 | { 27 | currentValues[modifiedValue.Key] = modifiedValue.Value; 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Mocale/Extensions/StringExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | namespace Mocale.Extensions; 3 | 4 | internal static class StringExtension 5 | { 6 | public static bool TryParseCultureInfo(this string cultureString, out CultureInfo cultureInfo) 7 | { 8 | cultureInfo = null!; 9 | 10 | if (string.IsNullOrEmpty(cultureString) || !DoesCultureExist(cultureString)) 11 | { 12 | return false; 13 | } 14 | 15 | cultureInfo = new CultureInfo(cultureString); 16 | return true; 17 | } 18 | 19 | // https://stackoverflow.com/a/16476935/8828057 20 | private static bool DoesCultureExist(string cultureName) 21 | { 22 | return CultureInfo.GetCultures(CultureTypes.AllCultures).Any(culture => string.Equals(culture.Name, cultureName, StringComparison.OrdinalIgnoreCase)); 23 | } 24 | 25 | internal static string Reverse(string s) 26 | { 27 | // https://stackoverflow.com/a/228060/8828057 28 | var charArray = s.ToCharArray(); 29 | Array.Reverse(charArray); 30 | return new string(charArray); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Platforms/iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LSRequiresIPhoneOS 6 | 7 | UIDeviceFamily 8 | 9 | 1 10 | 2 11 | 12 | UIRequiredDeviceCapabilities 13 | 14 | arm64 15 | 16 | UISupportedInterfaceOrientations 17 | 18 | UIInterfaceOrientationPortrait 19 | UIInterfaceOrientationLandscapeLeft 20 | UIInterfaceOrientationLandscapeRight 21 | 22 | UISupportedInterfaceOrientations~ipad 23 | 24 | UIInterfaceOrientationPortrait 25 | UIInterfaceOrientationPortraitUpsideDown 26 | UIInterfaceOrientationLandscapeLeft 27 | UIInterfaceOrientationLandscapeRight 28 | 29 | XSAppIconAssets 30 | Assets.xcassets/appicon.appiconset 31 | 32 | 33 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Converters/CityDescriptionConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace Mocale.Samples.Converters; 4 | 5 | internal sealed class CityDescriptionConverter : IValueConverter 6 | { 7 | private readonly ITranslatorManager translatorManager = MocaleLocator.TranslatorManager; 8 | 9 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 10 | { 11 | if (value == null) 12 | { 13 | return null; 14 | } 15 | 16 | if (value is not string) 17 | { 18 | throw new InvalidOperationException($"Value must be of type {nameof(String)}"); 19 | } 20 | 21 | var key = $"CityDescription_{value}"; 22 | 23 | //return new Binding 24 | //{ 25 | // Mode = BindingMode.OneWay, 26 | // Path = $"[{key}]", 27 | // Source = translatorManager, 28 | //}; 29 | 30 | return translatorManager.Translate(key); 31 | } 32 | 33 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 34 | { 35 | throw new NotImplementedException(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Mocale.Providers.AWS.S3/MocaleBuilderExtension.cs: -------------------------------------------------------------------------------- 1 | using Mocale.Managers; 2 | namespace Mocale.Providers.AWS.S3; 3 | 4 | /// 5 | /// Mocale Builder Extension 6 | /// 7 | public static class MocaleBuilderExtension 8 | { 9 | /// 10 | /// [External Provider] 11 | /// Use AWS S3 Bucket to retrieve localizations 12 | /// 13 | /// 14 | /// 15 | /// 16 | public static MocaleBuilder UseS3Bucket(this MocaleBuilder builder, Action configureBucket) 17 | { 18 | var config = new BucketConfig(); 19 | configureBucket(config); 20 | 21 | builder.RegisterExternalProvider(typeof(S3BucketProvider), config); 22 | 23 | var configurationManager = new ConfigurationManager(config); 24 | 25 | builder.AppBuilder.Services.AddSingleton>(configurationManager); 26 | builder.AppBuilder.Services.AddSingleton(); 27 | 28 | return builder; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Mocale/Helper/ExternalJsonFileNameHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using Ardalis.GuardClauses; 3 | 4 | namespace Mocale.Helper; 5 | 6 | internal class ExternalJsonFileNameHelper : IExternalFileNameHelper 7 | { 8 | private readonly JsonResourceFileDetails resourceFileDetails; 9 | 10 | public ExternalJsonFileNameHelper(IConfigurationManager configurationManager) 11 | { 12 | configurationManager = Guard.Against.Null(configurationManager, nameof(configurationManager)); 13 | 14 | if (configurationManager.Configuration.ResourceFileDetails is not JsonResourceFileDetails fileDetails) 15 | { 16 | throw new NotSupportedException("Resource file details were not for json files"); 17 | } 18 | 19 | this.resourceFileDetails = fileDetails; 20 | } 21 | 22 | public string GetExpectedFileName(CultureInfo culture) 23 | { 24 | var fileName = $"{culture.Name}.json"; 25 | 26 | if (!string.IsNullOrEmpty(resourceFileDetails.VersionPrefix)) 27 | { 28 | fileName = string.Join("/", resourceFileDetails.VersionPrefix, fileName); 29 | } 30 | 31 | return fileName; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Mocale/Parsers/ResxLocalizationParser.cs: -------------------------------------------------------------------------------- 1 | using System.Resources.NetStandard; 2 | using Ardalis.GuardClauses; 3 | 4 | namespace Mocale.Parsers; 5 | 6 | internal class ResxLocalizationParser(ILogger logger) : ILocalizationParser 7 | { 8 | private readonly ILogger logger = Guard.Against.Null(logger, nameof(logger)); 9 | 10 | public Dictionary? ParseLocalizationStream(Stream resourceStream) 11 | { 12 | try 13 | { 14 | var reader = new ResXResourceReader(resourceStream); 15 | 16 | var localizations = new Dictionary(); 17 | 18 | var enumerator = reader.GetEnumerator(); 19 | 20 | while (enumerator.MoveNext()) 21 | { 22 | var key = (string)enumerator.Key; 23 | var value = enumerator.Value as string ?? string.Empty; 24 | 25 | localizations.Add(key, value); 26 | } 27 | 28 | return localizations; 29 | } 30 | catch (Exception ex) 31 | { 32 | logger.LogError(ex, "An exception occurred parsing localization stream"); 33 | return null; 34 | } 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/Mocale/Extensions/EnumExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Mocale.Extensions; 4 | 5 | internal static class EnumExtensions 6 | { 7 | public static string? GetAttributeValue(this Enum enumValue, Type attributeType, string propertyName) 8 | { 9 | if (!typeof(Attribute).IsAssignableFrom(attributeType)) 10 | { 11 | throw new ArgumentException($"Provided type {attributeType.Name} is not an attribute.", nameof(attributeType)); 12 | } 13 | 14 | var attribute = enumValue.GetType() 15 | .GetField(enumValue.ToString()) 16 | ?.GetCustomAttributes(attributeType, false) 17 | .FirstOrDefault(); 18 | 19 | if (attribute is null) 20 | { 21 | Trace.WriteLine($"Attribute {attributeType.Name} not found on {enumValue}."); 22 | return null; 23 | } 24 | 25 | var property = attributeType.GetProperty(propertyName); 26 | 27 | if (property is null) 28 | { 29 | Trace.WriteLine($"Property '{propertyName}' not found on attribute {attributeType.Name}."); 30 | return null; 31 | } 32 | 33 | return property.GetValue(attribute) as string; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Mocale.Providers.Azure.Blob/Mocale.Providers.Azure.Blob.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MauiPlatformTargetFrameworks); 5 | true 6 | true 7 | 8 | Mocale.Providers.Azure.Blob 9 | Mocale.Providers.Azure.Blob 10 | Mocale.Providers.Azure.Blob 11 | Azure Blob Storage Provider For Mocale 12 | maui,mocale,localization,localisation,translation,azure,blob,storage,provider,mocaleprovider 13 | Mocale.Providers.Azure.Blob 14 | Azure blob storage external provider for Mocale. This package provides the ability to query azure blob storage for localization files to be used in your maui application. 15 | true 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Mocale.Providers.GitHub.Raw/Mocale.Providers.GitHub.Raw.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MauiPlatformTargetFrameworks); 5 | true 6 | true 7 | 8 | Mocale.Providers.Github.Raw 9 | Mocale.Providers.Github.Raw 10 | Mocale.Providers.Github.Raw 11 | GitHub Raw Provider For Mocale 12 | maui,mocale,localization,localisation,translation,github,raw,githubraw,provider,mocaleprovider 13 | Mocale.Providers.GitHub.Raw 14 | GitHub raw external provider for Mocale. This package will attempt to use the configured GitHub repository to download localizations to be used in your maui application. 15 | true 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Mocale.Cache.SQLite/Abstractions/ICacheRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace Mocale.Cache.SQLite.Abstractions; 4 | 5 | /// 6 | /// Cache Repository 7 | /// 8 | internal interface ICacheRepository 9 | { 10 | /// 11 | /// Add Or Update Item 12 | /// 13 | /// The culture to add / update 14 | /// The last time this culture was updated 15 | /// Success 16 | bool AddOrUpdateItem(CultureInfo cultureInfo, DateTime lastUpdated); 17 | 18 | /// 19 | /// Get update item 20 | /// 21 | /// The culture to check for 22 | /// Update record if it exists 23 | UpdateHistoryItem? GetItem(CultureInfo cultureInfo); 24 | 25 | /// 26 | /// Delete all records 27 | /// 28 | /// Success 29 | bool DeleteAll(); 30 | 31 | /// 32 | /// Delete the given culture 33 | /// 34 | /// The culture to delete 35 | /// Success 36 | bool DeleteItem(CultureInfo cultureInfo); 37 | } 38 | -------------------------------------------------------------------------------- /src/Mocale/Cache/InMemoryCacheManager.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using Microsoft.Extensions.Caching.Memory; 3 | 4 | namespace Mocale.Cache; 5 | 6 | internal class InMemoryCacheManager(MemoryCache memoryCache) : ILocalisationCacheManager, ICacheUpdateManager 7 | { 8 | public bool CanUpdateCache(CultureInfo cultureInfo) 9 | { 10 | return !memoryCache.TryGetValue(cultureInfo, out _); 11 | } 12 | 13 | public void ClearCache(CultureInfo cultureInfo) 14 | { 15 | memoryCache.Remove(cultureInfo); 16 | } 17 | 18 | public void ClearCache() 19 | { 20 | memoryCache.Clear(); 21 | } 22 | 23 | public Dictionary? GetCachedLocalizations(CultureInfo cultureInfo) 24 | { 25 | if (memoryCache.TryGetValue>(cultureInfo, out var cachedLocalizations)) 26 | { 27 | return cachedLocalizations; 28 | } 29 | 30 | return null; 31 | } 32 | 33 | public bool SaveCachedLocalizations(CultureInfo cultureInfo, Dictionary localizations) 34 | { 35 | memoryCache.Set(cultureInfo, localizations); 36 | 37 | return true; 38 | } 39 | 40 | public bool SetCacheUpdated(CultureInfo cultureInfo) 41 | { 42 | return true; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/directory.packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Mocale/Mocale.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MauiPlatformTargetFrameworks); 5 | true 6 | Mocale 7 | Mocale 8 | Mocale 9 | Localization framework for .NET Maui 10 | maui,mocale,localization,localisation,translation 11 | Mocale 12 | Localization framework for .NET Maui with support for over the air localization updates from external data sources. 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Mocale/Abstractions/IMocaleConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | namespace Mocale.Abstractions; 3 | 4 | /// 5 | /// Mocale Library Configuration 6 | /// 7 | public interface IMocaleConfiguration 8 | { 9 | /// 10 | /// The type of localisation resource being used 11 | /// 12 | LocaleResourceType ResourceType { get; } 13 | 14 | /// 15 | /// The default culture to load 16 | /// 17 | CultureInfo DefaultCulture { get; } 18 | 19 | /// 20 | /// Whether missing keys should be shown on the UI 21 | /// 22 | bool ShowMissingKeys { get; } 23 | 24 | /// 25 | /// The wrapping symbol to use for missing keys 26 | /// 27 | string NotFoundSymbol { get; } 28 | 29 | /// 30 | /// Whether an external resource provider is in use 31 | /// 32 | bool UseExternalProvider { get; } 33 | 34 | /// 35 | /// Indicated whether culture changes are saved for use in future app sessions. 36 | /// 37 | bool SaveCultureChanged { get; } 38 | 39 | /// 40 | /// Configuration for the behavior of enum localization 41 | /// 42 | LocalizeEnumBehavior EnumBehavior { get; } 43 | } 44 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/AppShell.xaml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/ViewModels/ParameterViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Samples.ViewModels; 2 | 3 | public sealed partial class ParameterViewModel : BaseViewModel 4 | { 5 | [ObservableProperty] 6 | public partial string Name { get; set; } 7 | 8 | [ObservableProperty] 9 | public partial DateTime Date { get; set; } 10 | 11 | [ObservableProperty] 12 | public partial int Number { get; set; } 13 | 14 | private static readonly Random Random = new(); 15 | 16 | private readonly string[] names = 17 | [ 18 | "Ivory", 19 | "Abdul", 20 | "Maurise", 21 | "Jared", 22 | "Gilles", 23 | "Andrew", 24 | "Bernadina", 25 | "Ron", 26 | "Joshua", 27 | "Silas", 28 | "Marc" 29 | ]; 30 | 31 | 32 | public ParameterViewModel() 33 | { 34 | Name = ChooseRandomName(); 35 | Date = new DateTime(2015, 11, 14); 36 | Number = Random.Next(1, 1000); 37 | } 38 | 39 | private string ChooseRandomName() 40 | { 41 | var index = Random.Next(0, names.Length); 42 | 43 | var name = names[index]; 44 | 45 | if (!name.Equals(Name, StringComparison.Ordinal)) 46 | { 47 | return name; 48 | } 49 | 50 | return ChooseRandomName(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Mocale/Extensions/LocalizeExtension.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.GuardClauses; 2 | 3 | namespace Mocale.Extensions; 4 | 5 | /// 6 | /// Localize Markup Extension. 7 | /// Used to localize a given translation key. 8 | /// 9 | /// Translator Manager 10 | [AcceptEmptyServiceProvider] 11 | [ContentProperty(nameof(Key))] 12 | public class LocalizeExtension(ITranslatorManager translatorManager) : LocalizeBindingExtensionBase(translatorManager) 13 | { 14 | /// 15 | /// Localize Extension 16 | /// 17 | public LocalizeExtension() 18 | : this(MocaleLocator.TranslatorManager) 19 | { 20 | } 21 | 22 | /// 23 | /// The translation key 24 | /// 25 | public string? Key { get; set; } 26 | 27 | /// 28 | /// Converter 29 | /// 30 | public IValueConverter? Converter { get; set; } 31 | 32 | /// 33 | public override Binding ProvideValue(IServiceProvider serviceProvider) 34 | { 35 | Guard.Against.NullOrEmpty(Key, nameof(Key)); 36 | 37 | return new Binding 38 | { 39 | Mode = BindingMode.OneWay, 40 | Path = $"[{Key}]", 41 | Source = translatorManager, 42 | Converter = Converter 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Fixtures/ControlsFixtureBase.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace Mocale.UnitTests.Fixtures; 4 | 5 | public class ControlsFixtureBase 6 | { 7 | public Application TestApplication { get; } 8 | 9 | public ControlsFixtureBase() 10 | { 11 | var app = new Application(); 12 | SetDispatcher(app); 13 | TestApplication = app; 14 | } 15 | 16 | private static void SetDispatcher(BindableObject target) 17 | { 18 | var bindableObjectType = typeof(BindableObject); 19 | 20 | var dispatcherField = 21 | bindableObjectType.GetField("_dispatcher", BindingFlags.Instance | BindingFlags.NonPublic) 22 | ?? throw new InvalidOperationException("Could not find _dispatcher field on BindableObject"); 23 | 24 | dispatcherField.SetValue(target, new DispatcherStub()); 25 | } 26 | } 27 | 28 | public class DispatcherStub : IDispatcher 29 | { 30 | public bool Dispatch(Action action) 31 | { 32 | action.Invoke(); 33 | return true; 34 | } 35 | 36 | public bool DispatchDelayed(TimeSpan delay, Action action) 37 | { 38 | action.Invoke(); 39 | return true; 40 | } 41 | 42 | public IDispatcherTimer CreateTimer() 43 | { 44 | return Mock.Of(); 45 | } 46 | 47 | public bool IsDispatchRequired { get; } 48 | } 49 | -------------------------------------------------------------------------------- /src/Mocale/Abstractions/ICacheUpdateManager.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | namespace Mocale.Abstractions; 3 | 4 | /// 5 | /// Cache Update Manager 6 | /// Responsible for tracking when the localization cache has been updated 7 | /// 8 | public interface ICacheUpdateManager 9 | { 10 | /// 11 | /// Whether the cache for the current culture can be updated. 12 | /// This is called before checking external providers 13 | /// 14 | /// The culture to check whether updates can occur 15 | /// True if external provider should be used (cache expired / no localizations for culture) 16 | bool CanUpdateCache(CultureInfo cultureInfo); 17 | 18 | /// 19 | /// Sets when the cache has been updated for the given culture 20 | /// 21 | /// The culture that has just been updated 22 | /// Success 23 | bool SetCacheUpdated(CultureInfo cultureInfo); 24 | 25 | /// 26 | /// Clear the cache for a specific culture 27 | /// 28 | /// Culture to clear cache for 29 | void ClearCache(CultureInfo cultureInfo); 30 | 31 | /// 32 | /// Clear the cache for all cultures 33 | /// 34 | void ClearCache(); 35 | } 36 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Extensions/StringExtensionTests.cs: -------------------------------------------------------------------------------- 1 | using Mocale.Extensions; 2 | 3 | namespace Mocale.UnitTests.Extensions; 4 | 5 | public class StringExtensionTests 6 | { 7 | [Theory] 8 | [InlineData("")] 9 | [InlineData(" ")] 10 | [InlineData(" ")] 11 | public void TryParseCultureInfo_WhenInputIsNullOrEmpty_ShouldReturnFalse(string invalidInput) 12 | { 13 | // Arrange 14 | 15 | // Act 16 | var result = invalidInput.TryParseCultureInfo(out var cultureInfo); 17 | 18 | // Assert 19 | Assert.False(result); 20 | Assert.Null(cultureInfo); 21 | } 22 | 23 | [Theory] 24 | [InlineData("en-GB", true)] 25 | [InlineData("en-US", true)] 26 | [InlineData("fr-FR", true)] 27 | [InlineData("HELLO WORLD", false)] 28 | [InlineData("ThIs-IsNot_A-CulTuR3", false)] 29 | public void TryParseCultureInfo_WhenProvidedInput_ShouldParseCorrectly(string cultureString, bool expectedValid) 30 | { 31 | // Arrange 32 | 33 | // Act 34 | var result = cultureString.TryParseCultureInfo(out var cultureInfo); 35 | 36 | // Assert 37 | Assert.Equal(expectedValid, result); 38 | 39 | if (expectedValid) 40 | { 41 | Assert.NotNull(cultureInfo); 42 | } 43 | else 44 | { 45 | Assert.Null(cultureInfo); 46 | } 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Testing/MocaleLocatorHelperTests.cs: -------------------------------------------------------------------------------- 1 | using Mocale.Abstractions; 2 | using Mocale.Testing; 3 | using Mocale.UnitTests.Collections; 4 | using Mocale.UnitTests.Fixtures; 5 | 6 | namespace Mocale.UnitTests.Testing; 7 | 8 | [Collection(CollectionNames.TestingTests)] 9 | public class MocaleLocatorHelperTests : MocaleLocatorFixture 10 | { 11 | public MocaleLocatorHelperTests() 12 | { 13 | MocaleLocator.MocaleConfiguration = null; 14 | MocaleLocator.TranslatorManager = null; 15 | } 16 | 17 | [Fact] 18 | public void SetTranslatorManager() 19 | { 20 | // Arrange 21 | Assert.Null(MocaleLocator.TranslatorManager); 22 | 23 | var mockTranslatorManager = Mock.Of(); 24 | 25 | // Act 26 | MocaleLocatorHelper.SetTranslatorManager(mockTranslatorManager); 27 | 28 | // Assert 29 | Assert.Equal(mockTranslatorManager, MocaleLocator.TranslatorManager); 30 | } 31 | 32 | [Fact] 33 | public void SetMocaleConfiguration() 34 | { 35 | // Arrange 36 | Assert.Null(MocaleLocator.MocaleConfiguration); 37 | 38 | var mockTranslatorManager = Mock.Of(); 39 | 40 | // Act 41 | MocaleLocatorHelper.SetMocaleConfiguration(mockTranslatorManager); 42 | 43 | // Assert 44 | Assert.Equal(mockTranslatorManager, MocaleLocator.MocaleConfiguration); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Helpers/RawUrlBuilderTests.cs: -------------------------------------------------------------------------------- 1 | 2 | using Mocale.Providers.GitHub.Raw.Helpers; 3 | namespace Mocale.UnitTests.Helpers; 4 | 5 | public class RawUrlBuilderTests 6 | { 7 | [Theory] 8 | [InlineData( 9 | "Axemasta", "Mocale", 10 | "main", "samples/Locales", 11 | "en-GB.json", "https://raw.githubusercontent.com/Axemasta/Mocale/main/samples/Locales/en-GB.json")] 12 | [InlineData( 13 | "Axemasta", "Mocale", 14 | "main", "samples/Locales", 15 | "fr-FR.json", "https://raw.githubusercontent.com/Axemasta/Mocale/main/samples/Locales/fr-FR.json")] 16 | [InlineData( 17 | "Axemasta", "Mocale", 18 | "develop", "samples/Locales", 19 | "en-GB.json", "https://raw.githubusercontent.com/Axemasta/Mocale/develop/samples/Locales/en-GB.json")] 20 | [InlineData( 21 | "Axemasta", "Mocale", 22 | "develop", "samples/Locales", 23 | "fr-FR.json", "https://raw.githubusercontent.com/Axemasta/Mocale/develop/samples/Locales/fr-FR.json")] 24 | public void TryBuildResourceUrl_WhenProvidedValues_ShouldReturnSuccess(string username, string repository, string branch, string filePath, string fileName, string expectedUrl) 25 | { 26 | // Arrange 27 | 28 | // Act 29 | var result = RawUrlBuilder.BuildResourceUrl(username, repository, branch, filePath, fileName); 30 | 31 | // Assert 32 | Assert.Equal(expectedUrl, result.ToString()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Mocale.Providers.Azure.Blob/MocaleBuilderExtension.cs: -------------------------------------------------------------------------------- 1 | using Mocale.Exceptions; 2 | using Mocale.Providers.Azure.Blob.Managers; 3 | namespace Mocale.Providers.Azure.Blob; 4 | 5 | /// 6 | /// Mocale Builder Extension 7 | /// 8 | public static class MocaleBuilderExtension 9 | { 10 | /// 11 | /// [External Provider] 12 | /// Use Azure Blob Storage to retrieve localizations 13 | /// 14 | /// 15 | /// 16 | /// 17 | /// 18 | public static MocaleBuilder UseBlobStorage(this MocaleBuilder builder, Action configureBlobStorage) 19 | { 20 | var config = new BlobStorageConfig(); 21 | configureBlobStorage(config); 22 | 23 | builder.RegisterExternalProvider(typeof(BlobLocalizationProvider), config); 24 | 25 | if (config.BlobContainerUri.OriginalString.Equals("app://mocale", StringComparison.Ordinal)) 26 | { 27 | throw new InitializationException("You must set a blob container uri to use this provider"); 28 | } 29 | 30 | builder.AppBuilder.Services.AddSingleton(); 31 | builder.AppBuilder.Services.AddSingleton(); 32 | 33 | return builder; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Mocale/MocaleLocator.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale; 2 | 3 | /* 4 | * I'm not sure how to nullable refactor this service locator since its set in initialization so 5 | * should be marked nullable however I dislike the side effect on the rest of the code using this that i now need to check 6 | * the value before using it. This value will be set by the lib and always here, end of sentence :) 7 | */ 8 | #nullable disable 9 | 10 | /// 11 | /// Mocale Service Locator for services that need to fallback to a service locator pattern. 12 | /// The primary reason for this class is for XamlExtensions lacking access to the app's main service 13 | /// provider. 14 | /// See issue: 15 | /// https://github.com/dotnet/maui/issues/8824 16 | /// When the ability to peek the apps main service provider from markup extensions becomes available, 17 | /// this service locator will be obsolete. 18 | /// I've tried to make this not hang off statically stored instances as much as possible, at runtime 19 | /// the app 20 | /// will use the service provider and not store any state. During a test you can set a custom instance 21 | /// to 22 | /// mock any functionality required. 23 | /// 24 | public static class MocaleLocator 25 | { 26 | /// 27 | /// Localization Manager 28 | /// 29 | public static ITranslatorManager TranslatorManager { get; internal set; } 30 | 31 | internal static IMocaleConfiguration MocaleConfiguration { get; set; } 32 | } 33 | 34 | #nullable enable 35 | -------------------------------------------------------------------------------- /src/Mocale.Providers.GitHub.Raw/MocaleBuilderExtension.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Providers.GitHub.Raw; 2 | 3 | /// 4 | /// Mocale Builder Extension 5 | /// 6 | public static class MocaleBuilderExtension 7 | { 8 | /// 9 | /// [External Provider] 10 | /// Use GitHub Raw API to retrieve localizations. 11 | /// 12 | /// Mocale Builder 13 | /// Configuration Action 14 | /// HTTP Client Configuration Action 15 | /// Mocale Builder 16 | public static MocaleBuilder UseGitHubRaw(this MocaleBuilder builder, Action configureGithub, Action? configureHttpClient = null) 17 | { 18 | var config = new GithubRawConfig(); 19 | configureGithub(config); 20 | 21 | builder.RegisterExternalProvider(typeof(GitHubRawProvider), config); 22 | 23 | builder.AppBuilder.Services.AddSingleton(); 24 | 25 | if (configureHttpClient is not null) 26 | { 27 | builder.AppBuilder.Services.AddHttpClient(configureHttpClient); 28 | } 29 | else 30 | { 31 | builder.AppBuilder.Services.AddHttpClient(); 32 | } 33 | 34 | return builder; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Mocale/Helper/ExternalResxFileNameHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using Ardalis.GuardClauses; 3 | 4 | namespace Mocale.Helper; 5 | 6 | internal class ExternalResxFileNameHelper : IExternalFileNameHelper 7 | { 8 | private readonly ResxResourceFileDetails resourceFileDetails; 9 | 10 | public ExternalResxFileNameHelper(IConfigurationManager configurationManager) 11 | { 12 | configurationManager = Guard.Against.Null(configurationManager, nameof(configurationManager)); 13 | 14 | if (configurationManager.Configuration.ResourceFileDetails is not ResxResourceFileDetails fileDetails) 15 | { 16 | throw new NotSupportedException("Resource file details were not for resx files"); 17 | } 18 | 19 | this.resourceFileDetails = fileDetails; 20 | } 21 | 22 | public string GetExpectedFileName(CultureInfo culture) 23 | { 24 | string fileName; 25 | 26 | if (resourceFileDetails.PrimaryCulture != null && resourceFileDetails.PrimaryCulture.Equals(culture)) 27 | { 28 | fileName = $"{resourceFileDetails.ResourcePrefix}.resx"; 29 | } 30 | else 31 | { 32 | fileName = $"{resourceFileDetails.ResourcePrefix}.{culture.Name}.resx"; 33 | } 34 | 35 | if (!string.IsNullOrEmpty(resourceFileDetails.VersionPrefix)) 36 | { 37 | fileName = string.Join("/", resourceFileDetails.VersionPrefix, fileName); 38 | } 39 | 40 | return fileName; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /directory.build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | true 5 | 6 | 7 | 8 | 9.0.40 9 | 10 | net9.0 11 | net9.0-ios 12 | net9.0-android 13 | net9.0-maccatalyst 14 | net9.0-windows10.0.19041.0 15 | 16 | $(StandardTargetFramework);$(AndroidTargetFramework);$(IosTargetFramework);$(MacTargetFramework); 17 | $(StandardTargetFramework);$(AndroidTargetFramework);$(IosTargetFramework);$(MacTargetFramework);$(WindowsTargetFramework); 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /directory.packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Mocale/MocaleBuilder.cs: -------------------------------------------------------------------------------- 1 | using Mocale.Managers; 2 | namespace Mocale; 3 | 4 | /// 5 | /// Mocale Builder 6 | /// 7 | public class MocaleBuilder 8 | { 9 | /// 10 | /// Maui App Builder 11 | /// 12 | public required MauiAppBuilder AppBuilder { get; init; } 13 | 14 | /// 15 | /// Configuration Manager 16 | /// 17 | public required ConfigurationManager ConfigurationManager { get; init; } 18 | 19 | internal string? LocalProviderName { get; set; } 20 | 21 | internal bool LocalProviderRegistered { get; set; } 22 | 23 | internal string? ExternalProviderName { get; set; } 24 | 25 | internal bool ExternalProviderRegistered { get; set; } 26 | 27 | internal bool CacheProviderRegistered { get; set; } 28 | 29 | /// 30 | /// Use Mocale With Given Configuration 31 | /// 32 | /// Action to configure mocale 33 | /// Mocale builder 34 | /// 35 | public MocaleBuilder WithConfiguration(Action configureMocale) 36 | { 37 | // I do this so that IMocaleConfiguration preserves its immutability with only getters, little jank... 38 | var config = ConfigurationManager.Configuration as MocaleConfiguration 39 | ?? throw new InvalidCastException($"Unable to cast {nameof(IMocaleConfiguration)} as {nameof(MocaleConfiguration)}"); 40 | 41 | configureMocale.Invoke(config); 42 | 43 | return this; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Mocale/Extensions/TranslatorManagerExtensions.cs: -------------------------------------------------------------------------------- 1 | using Mocale.Helper; 2 | 3 | namespace Mocale.Extensions; 4 | 5 | /// 6 | /// Translator Manager Extensions 7 | /// 8 | public static class TranslatorManagerExtensions 9 | { 10 | /// 11 | /// Translate Enum 12 | /// 13 | /// The translator manager instance 14 | /// The enum value to translate 15 | /// Translation result 16 | public static string TranslateEnum(this ITranslatorManager translatorManager, Enum enumValue) 17 | { 18 | var mocaleConfiguration = MocaleLocator.MocaleConfiguration; 19 | 20 | return TranslateEnum(translatorManager, enumValue, mocaleConfiguration.EnumBehavior); 21 | } 22 | 23 | /// 24 | /// Translate Enum 25 | /// 26 | /// The translator manager instance 27 | /// The enum value to translate 28 | /// The localization behavior to use for the enum 29 | /// 30 | internal static string TranslateEnum(this ITranslatorManager translatorManager, Enum enumValue, LocalizeEnumBehavior localizeEnumBehavior) 31 | { 32 | if (!localizeEnumBehavior.ShouldLocalizeEnum(enumValue)) 33 | { 34 | return enumValue.ToString(); 35 | } 36 | 37 | var translationKey = EnumTranslationKeyHelper.GetTranslationKey(enumValue, localizeEnumBehavior); 38 | 39 | return translatorManager.Translate(translationKey); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Resources/Misc/RandomFile.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed facilisis tempus nisl, et faucibus elit gravida eleifend. Aliquam consectetur nibh nec nisl malesuada, nec rhoncus nulla aliquet. Sed dictum tortor id auctor consectetur. Quisque ante nisi, iaculis non euismod et, sodales et nisl. Quisque laoreet eu mauris eget aliquam. Suspendisse nec sem non quam tristique fermentum eu at odio. Nullam pharetra non augue quis egestas. Proin pulvinar et neque id congue. Integer congue, nunc nec elementum vulputate, tortor arcu congue dui, a pulvinar odio ante at orci. 2 | 3 | Pellentesque tellus felis, congue ac mollis sed, venenatis vel erat. Cras blandit lectus sit amet magna euismod tempus. Nullam porttitor sollicitudin eleifend. Nullam scelerisque finibus mollis. Maecenas feugiat, urna condimentum iaculis laoreet, massa ligula venenatis libero, ut sodales nunc turpis vel tortor. Nullam vitae justo porta, congue ipsum vitae, ornare lacus. Sed condimentum interdum nisl, eget commodo arcu pellentesque quis. 4 | 5 | Integer interdum rutrum risus, iaculis condimentum tellus auctor eget. Quisque ut dignissim orci. In sit amet augue magna. Etiam neque orci, lobortis rhoncus sagittis non, aliquam ut nulla. Proin quis ex id lorem cursus rutrum. Ut iaculis lorem dolor, vehicula dictum nunc commodo eget. Maecenas fringilla mauris sit amet molestie viverra. Curabitur pretium lacus leo, at auctor tellus blandit quis. Nam vitae nisi gravida, bibendum nunc vel, venenatis tortor. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec vel arcu posuere, congue erat a, finibus tortor. Fusce pellentesque lorem vitae arcu pulvinar, vitae semper est commodo. Duis rhoncus leo ultricies ex gravida, nec elementum odio sollicitudin. Proin sollicitudin diam dui, id gravida sapien imperdiet scelerisque. 6 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Resources/Splash/splash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Resources/AppIcon/appiconfg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Mocale.Cache.SQLite/Providers/DatabaseConnectionProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | namespace Mocale.Cache.SQLite.Providers; 3 | 4 | internal class DatabaseConnectionProvider : IDatabaseConnectionProvider 5 | { 6 | #region Fields 7 | 8 | private readonly Lazy connectionLazy; 9 | private readonly IDatabasePathProvider databasePathProvider; 10 | private readonly ILogger logger; 11 | 12 | #endregion 13 | 14 | #region Constructors 15 | 16 | public DatabaseConnectionProvider( 17 | IDatabasePathProvider databasePathProvider, 18 | ILogger logger) 19 | { 20 | this.databasePathProvider = Guard.Against.Null(databasePathProvider, nameof(databasePathProvider)); 21 | this.logger = Guard.Against.Null(logger, nameof(logger)); 22 | 23 | // This could cause issues if we need to rebuild the connection... 24 | this.connectionLazy = new Lazy(BuildConnection, LazyThreadSafetyMode.ExecutionAndPublication); 25 | } 26 | 27 | #endregion Constructors 28 | 29 | #region Methods 30 | 31 | private SQLiteConnection BuildConnection() 32 | { 33 | var databasePath = databasePathProvider.GetDatabasePath(); 34 | 35 | logger.LogTrace("Opening connecting to database: {DatabasePath}", databasePath); 36 | 37 | return new SQLiteConnection( 38 | databasePath, 39 | SQLiteOpenFlags.Create | SQLiteOpenFlags.FullMutex | SQLiteOpenFlags.ReadWrite, 40 | true); 41 | } 42 | 43 | #endregion Methods 44 | 45 | #region Interface Implementations 46 | 47 | public SQLiteConnection GetDatabaseConnection() 48 | { 49 | return connectionLazy.Value; 50 | } 51 | 52 | #endregion Interface Implementations 53 | } 54 | -------------------------------------------------------------------------------- /src/Mocale/Models/LocalizeEnumBehavior.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace Mocale.Models; 4 | 5 | /// 6 | /// Localize Enum Behavior 7 | /// 8 | public record LocalizeEnumBehavior 9 | { 10 | /// 11 | /// Whether to get translation key from the enum's attribute. If false it will use ToString(). 12 | /// 13 | public bool UseAttribute { get; init; } = true; 14 | 15 | /// 16 | /// The attribute to get the translation key from the enum. 17 | /// 18 | public Type LocalizeAttribute { get; init; } = typeof(DescriptionAttribute); 19 | 20 | /// 21 | /// The attribute property from which to retrieve the translation key for the enum. 22 | /// 23 | public string AttributePropertyName { get; init; } = nameof(DescriptionAttribute.Description); 24 | 25 | /// 26 | /// Enum specific rules, you can override the behavior for specific enum types here if you wish. 27 | /// 28 | public Dictionary OverrideRules { get; } = []; 29 | } 30 | 31 | /// 32 | /// Localize Enum Rule 33 | /// 34 | public record LocalizeEnumRule 35 | { 36 | /// 37 | /// Whether to get translation key from the enum's attribute. If false it will use ToString(). 38 | /// 39 | public bool UseAttribute { get; init; } = true; 40 | 41 | /// 42 | /// The attribute to get the translation key from the enum. 43 | /// 44 | public Type LocalizeAttribute { get; init; } = typeof(DescriptionAttribute); 45 | 46 | /// 47 | /// The attribute property from which to retrieve the translation key for the enum. 48 | /// 49 | public string AttributePropertyName { get; init; } = nameof(DescriptionAttribute.Description); 50 | } 51 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Converters/LanguageEmojiConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace Mocale.Samples.Converters; 4 | 5 | internal sealed class LanguageEmojiConverter : IValueConverter 6 | { 7 | private readonly ITranslatorManager translatorManager; 8 | 9 | public LanguageEmojiConverter() 10 | { 11 | translatorManager = MocaleLocator.TranslatorManager; 12 | } 13 | 14 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 15 | { 16 | var currentCulture = translatorManager.CurrentCulture; 17 | 18 | if (currentCulture is null) 19 | { 20 | return null; 21 | } 22 | 23 | return GetFlag(currentCulture.EnglishName); 24 | } 25 | 26 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 27 | { 28 | throw new NotImplementedException(); 29 | } 30 | 31 | private static string GetFlag(string country) 32 | { 33 | // Adapted from 34 | // https://itnext.io/convert-country-name-to-flag-emoji-in-c-the-net-ecosystem-115f714d3ef9 35 | var regions = CultureInfo.GetCultures(CultureTypes.SpecificCultures).ToList(); 36 | var englishRegion = regions.FirstOrDefault(region => region.EnglishName.Contains(country)); 37 | 38 | if (englishRegion == null) 39 | { 40 | return "🏳"; 41 | } 42 | 43 | var region = new RegionInfo(englishRegion.LCID); 44 | 45 | var countryAbbrev = region.TwoLetterISORegionName; 46 | var flag = IsoCountryCodeToFlagEmoji(countryAbbrev); 47 | return flag; 48 | } 49 | 50 | private static string IsoCountryCodeToFlagEmoji(string countryCode) 51 | { 52 | return string.Concat(countryCode.ToUpperInvariant().Select(x => char.ConvertFromUtf32(x + 0x1F1A5))); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/ViewModels/IntroductionPageViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace Mocale.Samples.ViewModels; 4 | 5 | public partial class IntroductionPageViewModel : BaseViewModel 6 | { 7 | private readonly ILocalizationManager localizationManager; 8 | 9 | public ObservableRangeCollection Locales { get; } 10 | 11 | [ObservableProperty] 12 | public partial string SelectedLocale { get; set; } 13 | 14 | public IntroductionPageViewModel(ILocalizationManager localizationManager) 15 | { 16 | this.localizationManager = localizationManager; 17 | 18 | Locales = new ObservableRangeCollection( 19 | [ 20 | "en-GB", 21 | "fr-FR", 22 | "it-IT", 23 | ]); 24 | 25 | var selectedLocale = Locales.FirstOrDefault(localizationManager.CurrentCulture.Name.Equals); 26 | 27 | SelectedLocale = selectedLocale ?? Locales[0]; 28 | } 29 | 30 | partial void OnSelectedLocaleChanged(string oldValue, string newValue) 31 | { 32 | RaiseLocaleSelected(oldValue, newValue); 33 | } 34 | 35 | private async void RaiseLocaleSelected(string oldValue, string newValue) 36 | { 37 | if (string.IsNullOrEmpty(oldValue)) 38 | { 39 | return; 40 | } 41 | 42 | if (oldValue.Equals(newValue, StringComparison.Ordinal)) 43 | { 44 | return; 45 | } 46 | 47 | var culture = new CultureInfo(newValue); 48 | 49 | if (culture.Equals(localizationManager.CurrentCulture)) 50 | { 51 | return; 52 | } 53 | 54 | var loaded = await localizationManager.SetCultureAsync(culture); 55 | 56 | if (!loaded) 57 | { 58 | SelectedLocale = oldValue; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/directory.build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | true 7 | enable 8 | true 9 | 10 | 11 | 12 | Axemasta 13 | Axemasta 14 | en 15 | Copyright © 2025 Axemasta 16 | https://github.com/Axemasta/Mocale 17 | git 18 | MIT 19 | false 20 | https://github.com/Axemasta/Mocale 21 | mocale_icon_light_small.png 22 | README.md 23 | true 24 | True 25 | snupkg 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | all 42 | runtime; build; native; contentfiles; analyzers 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/Mocale.Providers.AWS.S3/S3BucketProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.Providers.AWS.S3; 2 | 3 | internal sealed class S3BucketProvider : IExternalLocalizationProvider 4 | { 5 | #pragma warning disable CS1998 6 | public async Task GetValuesForCultureAsync(CultureInfo cultureInfo) 7 | #pragma warning restore CS1998 8 | { 9 | if (cultureInfo.ToString() == "en-GB") 10 | { 11 | return new ExternalLocalizationResult() 12 | { 13 | Success = true, 14 | Localizations = new Dictionary() 15 | { 16 | { "CurrentLocaleName", "S3English" }, 17 | { "LocalizationCurrentProviderIs", "S3The current localization provider is:" }, 18 | { "LocalizationProviderName", "S3Json" }, 19 | { "MocaleDescription", "S3Localization framework for .NET Maui" }, 20 | { "MocaleTitle", "S3Mocale" }, 21 | } 22 | }; 23 | } 24 | 25 | if (cultureInfo.ToString() == "fr-FR") 26 | { 27 | return new ExternalLocalizationResult() 28 | { 29 | Success = true, 30 | Localizations = new Dictionary() 31 | { 32 | { "CurrentLocaleName", "S3French" }, 33 | { "LocalizationCurrentProviderIs", "S3Le fournisseur de localisation actuel est:" }, 34 | { "LocalizationProviderName", "S3Json" }, 35 | { "MocaleDescription", "S3Framework de localisation pour .NET Maui" }, 36 | { "MocaleTitle", "S3Mocale" }, 37 | } 38 | }; 39 | } 40 | 41 | return new ExternalLocalizationResult() 42 | { 43 | Success = false, 44 | Localizations = [], 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Mocale.Cache.SQLite/Managers/LocalisationCacheManager.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using Microsoft.Extensions.Logging; 3 | using Mocale.Abstractions; 4 | namespace Mocale.Cache.SQLite.Managers; 5 | 6 | internal class LocalisationCacheManager( 7 | ICacheUpdateManager cacheUpdateManager, 8 | ILogger logger, 9 | ITranslationsRepository translationsRepository) 10 | : ILocalisationCacheManager 11 | { 12 | #region Fields 13 | 14 | private readonly ICacheUpdateManager cacheUpdateManager = Guard.Against.Null(cacheUpdateManager, nameof(cacheUpdateManager)); 15 | private readonly ILogger logger = Guard.Against.Null(logger, nameof(logger)); 16 | private readonly ITranslationsRepository translationsRepository = Guard.Against.Null(translationsRepository, nameof(translationsRepository)); 17 | 18 | #endregion Fields 19 | 20 | #region Interface Implementations 21 | 22 | public Dictionary? GetCachedLocalizations(CultureInfo cultureInfo) 23 | { 24 | return translationsRepository.GetTranslations(cultureInfo); 25 | } 26 | 27 | public bool SaveCachedLocalizations(CultureInfo cultureInfo, Dictionary localizations) 28 | { 29 | var saved = translationsRepository.AddTranslations(cultureInfo, localizations); 30 | 31 | if (!saved) 32 | { 33 | logger.LogWarning("Failed to add translations for culture: {CultureName}", cultureInfo.Name); 34 | return false; 35 | } 36 | 37 | var cacheUpdated = cacheUpdateManager.SetCacheUpdated(cultureInfo); 38 | 39 | if (!cacheUpdated) 40 | { 41 | logger.LogWarning("Translations for culture: {CultureName} were saved to the cache database but the cache history was not updated", cultureInfo.Name); 42 | } 43 | 44 | return cacheUpdated; 45 | } 46 | 47 | #endregion Interface Implementations 48 | } 49 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Extensions/UriExtensionTests.cs: -------------------------------------------------------------------------------- 1 | using Mocale.Extensions; 2 | 3 | namespace Mocale.UnitTests.Extensions; 4 | 5 | public class UriExtensionTests 6 | { 7 | [Fact] 8 | public void Append_ShouldAppendPathsCorrectly() 9 | { 10 | // Arrange 11 | var baseUri = new Uri("https://example.com/api"); 12 | var paths = new[] { "v1", "users" }; 13 | 14 | // Act 15 | var result = baseUri.Append(paths); 16 | 17 | // Assert 18 | Assert.Equal("https://example.com/api/v1/users", result.ToString()); 19 | } 20 | 21 | [Fact] 22 | public void Append_ShouldHandleTrailingAndLeadingSlashes() 23 | { 24 | // Arrange 25 | var baseUri = new Uri("https://example.com/api/"); 26 | var paths = new[] { "/v1/", "/users/" }; 27 | 28 | // Act 29 | var result = baseUri.Append(paths); 30 | 31 | // Assert 32 | Assert.Equal("https://example.com/api/v1/users/", result.ToString()); 33 | } 34 | 35 | [Fact] 36 | public void TryAppend_ShouldReturnTrueAndAppendPaths() 37 | { 38 | // Arrange 39 | var baseUri = new Uri("https://example.com/api"); 40 | var paths = new[] { "v1", "users" }; 41 | 42 | // Act 43 | var success = baseUri.TryAppend(out var result, paths); 44 | 45 | // Assert 46 | Assert.True(success); 47 | Assert.Equal("https://example.com/api/v1/users", result.ToString()); 48 | } 49 | 50 | [Fact] 51 | public void TryAppend_ShouldReturnFalseOnInvalidUri() 52 | { 53 | // Arrange 54 | var baseUri = new Uri("https://example.com/api"); 55 | var paths = new[] { "\0invalid\turi<<{{>}_!!##" }; 56 | 57 | // Act 58 | var success = baseUri.TryAppend(out var result, paths); 59 | 60 | // Assert 61 | Assert.False(success); 62 | Assert.Null(result); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Mocale.SourceGenerators/Diagnostics.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | namespace Mocale.SourceGenerators; 3 | 4 | internal static class Diagnostics 5 | { 6 | /* 7 | * I am using diagnostics to perform logging, after some research it seemed like this was one of the easier approaches... 8 | * 9 | * If what i am doing here is sacrilege, please raise an issue and let me know of a better way to add diagnostics to the generators 😇 10 | */ 11 | 12 | public static class Errors 13 | { 14 | private static DiagnosticDescriptor Create(string id, string text) 15 | { 16 | return new DiagnosticDescriptor(id, text, text, "Mocale.SourceGenerators", DiagnosticSeverity.Error, true); 17 | } 18 | 19 | public static readonly DiagnosticDescriptor ParsingException = Create("MOCE001", "An exception occurred processing translations, Exception: {0}, Json {1}"); 20 | } 21 | 22 | public static class Warnings 23 | { 24 | private static DiagnosticDescriptor Create(string id, string text) 25 | { 26 | return new DiagnosticDescriptor(id, text, text, "Mocale.SourceGenerators", DiagnosticSeverity.Warning, true); 27 | } 28 | 29 | public static readonly DiagnosticDescriptor NoLocalizationFilesDetected = Create("MOCW001", "No localizations files were found to process"); 30 | 31 | public static readonly DiagnosticDescriptor FileNotJsonLocalization = Create("MOCW002", "The following file was not recognized as a valid localization json file: {0}"); 32 | } 33 | 34 | public static class Information 35 | { 36 | private static DiagnosticDescriptor Create(string id, string text) 37 | { 38 | return new DiagnosticDescriptor(id, text, text, "Mocale.SourceGenerators", DiagnosticSeverity.Info, false); 39 | } 40 | 41 | public static readonly DiagnosticDescriptor ProcessingFiles = Create("MOCI001", "Processing translation files"); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Platforms/Windows/Package.appxmanifest: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | $placeholder$ 15 | User Name 16 | $placeholder$.png 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Extensions/LocalizeEnumBehaviorExtensionTests.cs: -------------------------------------------------------------------------------- 1 | using Mocale.Extensions; 2 | using Mocale.Models; 3 | 4 | namespace Mocale.UnitTests.Extensions; 5 | 6 | public class LocalizeEnumBehaviorExtensionTests 7 | { 8 | [Theory] 9 | [InlineData(true)] 10 | [InlineData(false)] 11 | public void ShouldLocalizeEnum_WhenEnumValueIsNotInOverrideRules_ShouldReturnGlobalUseAttribute(bool useAttribute) 12 | { 13 | // Arrange 14 | var localizeEnumBehavior = new LocalizeEnumBehavior() 15 | { 16 | UseAttribute = useAttribute 17 | }; 18 | 19 | var enumValue = EnumOne.A; 20 | 21 | // Act 22 | var result = localizeEnumBehavior.ShouldLocalizeEnum(enumValue); 23 | 24 | // Assert 25 | Assert.Equal(useAttribute, result); 26 | } 27 | 28 | [Theory] 29 | [InlineData(true, true)] 30 | [InlineData(false, true)] 31 | [InlineData(true, false)] 32 | [InlineData(false, false)] 33 | public void ShouldLocalizeEnum_WhenEnumValueIsInOverrideRules_ShouldReturnRuleUseAttribute(bool globalUseAttribute, bool ruleUseAttribute) 34 | { 35 | // Arrange 36 | var localizeEnumBehavior = new LocalizeEnumBehavior() 37 | { 38 | UseAttribute = globalUseAttribute, 39 | OverrideRules = 40 | { 41 | { 42 | typeof(EnumTwo), new LocalizeEnumRule() 43 | { 44 | UseAttribute = ruleUseAttribute, 45 | } 46 | } 47 | } 48 | }; 49 | 50 | var enumValue = EnumTwo.Y; 51 | 52 | // Act 53 | var result = localizeEnumBehavior.ShouldLocalizeEnum(enumValue); 54 | 55 | // Assert 56 | Assert.Equal(ruleUseAttribute, result); 57 | } 58 | 59 | private enum EnumOne 60 | { 61 | A, 62 | B, 63 | C 64 | } 65 | 66 | private enum EnumTwo 67 | { 68 | X, 69 | Y, 70 | Z 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Mocale/Extensions/LocalizeExtensionBase.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using Ardalis.GuardClauses; 3 | 4 | namespace Mocale.Extensions; 5 | 6 | /// 7 | /// Localize Extension Base 8 | /// 9 | /// The translator manager instance to bind to 10 | public abstract class LocalizeExtensionBase(ITranslatorManager translatorManager) 11 | { 12 | // ReSharper disable once InconsistentNaming 13 | #pragma warning disable IDE1006 // Naming Styles 14 | internal readonly ITranslatorManager translatorManager = Guard.Against.Null(translatorManager, nameof(translatorManager)); 15 | #pragma warning restore IDE1006 // Naming Styles 16 | 17 | [EditorBrowsable(EditorBrowsableState.Never)] 18 | internal ITranslatorManager GetTranslatorManager() 19 | { 20 | return translatorManager; 21 | } 22 | } 23 | 24 | /// 25 | /// Base class for localize extensions 26 | /// 27 | /// 28 | public abstract class LocalizeBindingExtensionBase(ITranslatorManager translatorManager) 29 | : LocalizeExtensionBase(translatorManager), IMarkupExtension 30 | { 31 | /// 32 | public abstract Binding ProvideValue(IServiceProvider serviceProvider); 33 | 34 | /// 35 | object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider) 36 | { 37 | return ProvideValue(serviceProvider); 38 | } 39 | } 40 | 41 | /// 42 | /// Base class for localize extensions that use MultiBindings 43 | /// 44 | /// 45 | public abstract class LocalizeMultiBindingExtensionBase(ITranslatorManager translatorManager) 46 | : LocalizeExtensionBase(translatorManager), IMarkupExtension 47 | { 48 | /// 49 | public abstract MultiBinding ProvideValue(IServiceProvider serviceProvider); 50 | 51 | object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider) 52 | { 53 | return ProvideValue(serviceProvider); 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /src/Mocale.Cache.SQLite/MocaleBuilderExtension.cs: -------------------------------------------------------------------------------- 1 | using Mocale.Abstractions; 2 | using Mocale.Cache.SQLite.Managers; 3 | using Mocale.Cache.SQLite.Providers; 4 | using Mocale.Cache.SQLite.Repositories; 5 | using Mocale.Managers; 6 | namespace Mocale.Cache.SQLite; 7 | 8 | /// 9 | /// Mocale Builder Extension 10 | /// 11 | public static class MocaleBuilderExtension 12 | { 13 | /// 14 | /// Use SQLite to cache localizations acquired by external providers 15 | /// 16 | /// 17 | /// 18 | /// 19 | public static MocaleBuilder UseSqliteCache(this MocaleBuilder builder, Action configureSql) 20 | { 21 | return UseSqliteCache(builder, FileSystem.Current, configureSql); 22 | } 23 | 24 | internal static MocaleBuilder UseSqliteCache(this MocaleBuilder builder, IFileSystem fileSystem, Action configureSql) 25 | { 26 | var config = new SqliteConfig 27 | { 28 | DatabaseDirectory = fileSystem.AppDataDirectory, 29 | }; 30 | 31 | configureSql(config); 32 | 33 | var configurationManager = new ConfigurationManager(config); 34 | 35 | builder.AppBuilder.Services.AddSingleton>(configurationManager); 36 | 37 | builder.AppBuilder.Services.AddTransient(); 38 | builder.AppBuilder.Services.AddSingleton(); 39 | builder.AppBuilder.Services.AddTransient(); 40 | builder.AppBuilder.Services.AddSingleton(); 41 | builder.AppBuilder.Services.AddSingleton(); 42 | builder.AppBuilder.Services.AddSingleton(); 43 | 44 | builder.CacheProviderRegistered = true; 45 | 46 | return builder; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Pages/ConverterPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Text; 3 | using Mocale.Samples.ViewModels; 4 | using Mocale.Translations; 5 | 6 | namespace Mocale.Samples.Pages; 7 | 8 | public partial class ConverterPage : BasePage 9 | { 10 | public ConverterPage(ConverterViewModel viewModel) 11 | : base(viewModel) 12 | { 13 | InitializeComponent(); 14 | } 15 | } 16 | 17 | public class SpongebobCaseConverter : IValueConverter 18 | { 19 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 20 | { 21 | if (value is null || value is not string strValue) 22 | { 23 | return value; 24 | } 25 | 26 | var result = new StringBuilder(); 27 | var toUpper = true; 28 | 29 | foreach (var c in strValue) 30 | { 31 | if (char.IsLetter(c)) 32 | { 33 | result.Append(toUpper ? char.ToUpper(c, culture) : char.ToLower(c, culture)); 34 | toUpper = !toUpper; 35 | } 36 | else 37 | { 38 | result.Append(c); 39 | } 40 | } 41 | 42 | return result.ToString(); 43 | } 44 | 45 | public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 46 | { 47 | throw new NotImplementedException(); 48 | } 49 | } 50 | 51 | public class OrderStatusKeySelector : IKeyConverter 52 | { 53 | public string? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 54 | { 55 | if (value is null || value is not OrderStatus orderStatus) 56 | { 57 | return null; 58 | } 59 | 60 | return orderStatus.Name switch 61 | { 62 | "Pending" => TranslationKeys.OrderStatusPending, 63 | "Shipped" => TranslationKeys.OrderStatusShipped, 64 | "Delivered" => TranslationKeys.OrderStatusDelivered, 65 | "Cancelled" => TranslationKeys.OrderStatusCancelled, 66 | _ => "Unknown order status." 67 | }; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Extensions/EnumExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using Mocale.Extensions; 3 | 4 | namespace Mocale.UnitTests.Extensions; 5 | 6 | public class EnumExtensionsTests 7 | { 8 | private enum TestEnum 9 | { 10 | [Description("Test Description")] ValueWithAttribute, 11 | ValueWithoutAttribute 12 | } 13 | 14 | [Fact] 15 | public void GetAttributeValue_WhenTypeIsNotAttribute_ShouldThrow() 16 | { 17 | // Arrange 18 | var enumValue = TestEnum.ValueWithAttribute; 19 | var invalidType = typeof(string); 20 | 21 | // Act & Assert 22 | var exception = Assert.Throws(() => 23 | enumValue.GetAttributeValue(invalidType, "SomeProperty")); 24 | 25 | Assert.Contains("is not an attribute", exception.Message); 26 | } 27 | 28 | [Fact] 29 | public void GetAttributeValue_WhenTargetDoesNotHaveAttribute_ShouldReturnNull() 30 | { 31 | // Arrange 32 | var enumValue = TestEnum.ValueWithoutAttribute; 33 | var attributeType = typeof(DescriptionAttribute); 34 | 35 | // Act 36 | var result = enumValue.GetAttributeValue(attributeType, "Description"); 37 | 38 | // Assert 39 | Assert.Null(result); 40 | } 41 | 42 | [Fact] 43 | public void GetAttributeValue_WhenTargetPropertyDoesNotExist_ShouldReturnNull() 44 | { 45 | // Arrange 46 | var enumValue = TestEnum.ValueWithAttribute; 47 | var attributeType = typeof(DescriptionAttribute); 48 | var nonExistentProperty = "NonExistentProperty"; 49 | 50 | // Act 51 | var result = enumValue.GetAttributeValue(attributeType, nonExistentProperty); 52 | 53 | // Assert 54 | Assert.Null(result); 55 | } 56 | 57 | [Fact] 58 | public void GetAttributeValue_WhenAttributeAndTargetPropertyExist_ShouldGetValue() 59 | { 60 | // Arrange 61 | var enumValue = TestEnum.ValueWithAttribute; 62 | var attributeType = typeof(DescriptionAttribute); 63 | var propertyName = "Description"; 64 | 65 | // Act 66 | var result = enumValue.GetAttributeValue(attributeType, propertyName); 67 | 68 | // Assert 69 | Assert.Equal("Test Description", result); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Resources/Styles/Colors.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | #FECA51 6 | #DFD8F7 7 | #FB828B 8 | White 9 | Black 10 | #E1E1E1 11 | #C8C8C8 12 | #ACACAC 13 | #919191 14 | #6E6E6E 15 | #404040 16 | #212121 17 | #141414 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | #F7B548 33 | #FFD590 34 | #FFE5B9 35 | #28C2D1 36 | #7BDDEF 37 | #C3F2F4 38 | #3E8EED 39 | #72ACF1 40 | #A7CBF6 41 | 42 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/TestUtils/ServiceCollectionAssertions.cs: -------------------------------------------------------------------------------- 1 | namespace Mocale.UnitTests.TestUtils; 2 | 3 | public static class ServiceCollectionAssertions 4 | { 5 | /// 6 | /// Asserts that a given service type is registered in the service collection. 7 | /// 8 | public static void ShouldContainService(this IServiceCollection services) 9 | { 10 | var service = services.FirstOrDefault(s => s.ServiceType == typeof(TService)); 11 | Assert.NotNull(service); 12 | } 13 | 14 | /// 15 | /// Asserts that a given service type is NOT registered in the service collection. 16 | /// 17 | public static void ShouldNotContainService(this IServiceCollection services) 18 | { 19 | var service = services.FirstOrDefault(s => s.ServiceType == typeof(TService)); 20 | Assert.Null(service); 21 | } 22 | 23 | /// 24 | /// Asserts that a specific implementation type is registered in the service collection. 25 | /// 26 | public static void ShouldContainImplementation(this IServiceCollection services) 27 | { 28 | var service = services.FirstOrDefault(s => s.ImplementationType == typeof(TImplementation)); 29 | Assert.NotNull(service); 30 | } 31 | 32 | /// 33 | /// Asserts that a specific implementation type is NOT registered in the service collection. 34 | /// 35 | public static void ShouldNotContainImplementation(this IServiceCollection services) 36 | { 37 | var service = services.FirstOrDefault(s => s.ImplementationType == typeof(TImplementation)); 38 | Assert.Null(service); 39 | } 40 | 41 | public static void ShouldHaveRegisteredService(this IServiceCollection services, ServiceLifetime lifetime) 42 | { 43 | var service = services.FirstOrDefault(s => s.ServiceType == typeof(TService) && s.Lifetime == lifetime); 44 | Assert.NotNull(service); 45 | } 46 | 47 | public static void ShouldHaveRegisteredService(this IServiceCollection services, ServiceLifetime lifetime) 48 | { 49 | var service = services.FirstOrDefault(s => s.ServiceType == typeof(TService) && s.ImplementationType == typeof(TImplementation) && s.Lifetime == lifetime); 50 | Assert.NotNull(service); 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Resources/Resx/TestResources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // 5 | // Changes to this file may cause incorrect behavior and will be lost if 6 | // the code is regenerated. 7 | // 8 | //------------------------------------------------------------------------------ 9 | 10 | namespace Mocale.UnitTests.Resources.Resx { 11 | using System; 12 | 13 | 14 | [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 15 | [System.Diagnostics.DebuggerNonUserCodeAttribute()] 16 | [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 17 | internal class TestResources { 18 | 19 | private static System.Resources.ResourceManager resourceMan; 20 | 21 | private static System.Globalization.CultureInfo resourceCulture; 22 | 23 | [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 24 | internal TestResources() { 25 | } 26 | 27 | [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] 28 | internal static System.Resources.ResourceManager ResourceManager { 29 | get { 30 | if (object.Equals(null, resourceMan)) { 31 | System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Mocale.UnitTests.Resources.Resx.TestResources", typeof(TestResources).Assembly); 32 | resourceMan = temp; 33 | } 34 | return resourceMan; 35 | } 36 | } 37 | 38 | [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static System.Globalization.CultureInfo Culture { 40 | get { 41 | return resourceCulture; 42 | } 43 | set { 44 | resourceCulture = value; 45 | } 46 | } 47 | 48 | internal static string KeyOne { 49 | get { 50 | return ResourceManager.GetString("KeyOne", resourceCulture); 51 | } 52 | } 53 | 54 | internal static string KeyTwo { 55 | get { 56 | return ResourceManager.GetString("KeyTwo", resourceCulture); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Mocale/Managers/CurrentCultureManager.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using Ardalis.GuardClauses; 3 | 4 | namespace Mocale.Managers; 5 | 6 | internal class CurrentCultureManager : ICurrentCultureManager 7 | { 8 | #region Fields 9 | 10 | private readonly IMocaleConfiguration mocaleConfiguration; 11 | private readonly ILogger logger; 12 | private readonly IPreferences preferences; 13 | 14 | #endregion Fields 15 | 16 | #region Constructors 17 | 18 | public CurrentCultureManager( 19 | IConfigurationManager mocaleConfigurationManager, 20 | ILogger logger, 21 | IPreferences preferences) 22 | { 23 | mocaleConfigurationManager = Guard.Against.Null(mocaleConfigurationManager, nameof(mocaleConfigurationManager)); 24 | 25 | mocaleConfiguration = mocaleConfigurationManager.Configuration; 26 | this.logger = Guard.Against.Null(logger, nameof(logger)); 27 | this.preferences = Guard.Against.Null(preferences, nameof(preferences)); 28 | } 29 | 30 | #endregion Constructors 31 | 32 | #region Interface Implementations 33 | 34 | public CultureInfo GetActiveCulture() 35 | { 36 | var defaultCulture = mocaleConfiguration.DefaultCulture; 37 | 38 | if (!mocaleConfiguration.SaveCultureChanged) 39 | { 40 | return defaultCulture; 41 | } 42 | 43 | // Load Here 44 | var lastUsedCulture = preferences.Get(Constants.LastUsedCultureKey, string.Empty); 45 | 46 | if (string.IsNullOrEmpty(lastUsedCulture)) 47 | { 48 | logger.LogTrace("Setting Last Used Culture as: {DefaultCulture}", defaultCulture); 49 | SetActiveCulture(defaultCulture); 50 | return defaultCulture; 51 | } 52 | 53 | if (lastUsedCulture.TryParseCultureInfo(out var cultureInfo)) 54 | { 55 | return cultureInfo; 56 | } 57 | 58 | // TODO: Wipe preferences if this happens? 59 | 60 | logger.LogWarning("Unable to parse culture from preferences: {LastUsedCulture}", lastUsedCulture); 61 | return defaultCulture; 62 | } 63 | 64 | public void SetActiveCulture(CultureInfo cultureInfo) 65 | { 66 | if (!mocaleConfiguration.SaveCultureChanged) 67 | { 68 | return; 69 | } 70 | 71 | var cultureString = cultureInfo.ToString(); 72 | 73 | preferences.Set(Constants.LastUsedCultureKey, cultureString); 74 | } 75 | 76 | #endregion Interface Implementations 77 | } 78 | 79 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Pages/IntroductionPage.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 28 | 32 | 33 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Pages/CodePage.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Maui.Markup; 2 | using Mocale.Extensions; 3 | using Mocale.Samples.Enums; 4 | using Mocale.Translations; 5 | 6 | namespace Mocale.Samples.Pages; 7 | 8 | internal sealed partial class CodePage : ContentPage 9 | { 10 | public CodePage() 11 | { 12 | this.SetTranslation(TitleProperty, TranslationKeys.CodePageTitle); 13 | 14 | 15 | var cityPicker = new Picker 16 | { 17 | ItemsSource = new List 18 | { 19 | Cities.London, 20 | Cities.Manchester, 21 | Cities.Nottingham, 22 | Cities.Newcastle 23 | } 24 | }.SetTranslation(Picker.TitleProperty, TranslationKeys.BindingPageCityDemoPickerPlaceholder); 25 | 26 | Content = new ScrollView 27 | { 28 | Content = new VerticalStackLayout 29 | { 30 | Spacing = 20, 31 | HorizontalOptions = LayoutOptions.Fill, 32 | VerticalOptions = LayoutOptions.Start, 33 | Children = 34 | { 35 | new Label().SetTranslation(Label.TextProperty, TranslationKeys.BindingPageHeading), 36 | 37 | new Border 38 | { 39 | Content = new VerticalStackLayout 40 | { 41 | Spacing = 10, 42 | Children = 43 | { 44 | new Label().SetTranslation(Label.TextProperty, TranslationKeys.BindingPageCityDemoTitle), 45 | new Label().SetTranslation(Label.TextProperty, TranslationKeys.BindingPageCityDemoHeading), 46 | cityPicker, 47 | new Label().SetEnumTranslation(Label.TextProperty, new Binding(nameof(Picker.SelectedItem), source: cityPicker)), 48 | } 49 | } 50 | }, 51 | 52 | new Border 53 | { 54 | Content = new VerticalStackLayout 55 | { 56 | Spacing = 10, 57 | Children = 58 | { 59 | new Label().SetTranslation(Label.TextProperty, TranslationKeys.BindingPageCodeBehindTitle), 60 | new Label().SetTranslation(Label.TextProperty, TranslationKeys.BindingPageCodeBehindHeading), 61 | } 62 | } 63 | } 64 | } 65 | } 66 | }; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Mocale.SourceGenerators/Mocale.SourceGenerators.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | false 6 | 13.0 7 | enable 8 | enable 9 | true 10 | true 11 | true 12 | true 13 | false 14 | 15 | Mocale.SourceGenerators 16 | Mocale.SourceGenerators 17 | Mocale.SourceGenerators 18 | Source Generators to assist with Mocale library functions 19 | maui,mocale,localization,localisation,translation,source,generator,generators,sourcegenerator,sourcegenerators 20 | Mocale.SourceGenerators 21 | Source generators to assist and enhance Mocale library functions. 22 | 23 | RS2007 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | $(GetTargetPathDependsOn);GetDependencyTargetPaths 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /samples/Mocale.Samples/Pages/ParameterPage.xaml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 18 | 19 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/MocaleInitializeServiceTests.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using Mocale.Abstractions; 3 | using Mocale.Enums; 4 | using Mocale.Managers; 5 | using Mocale.Models; 6 | using Mocale.UnitTests.Collections; 7 | 8 | namespace Mocale.UnitTests; 9 | 10 | [Collection(CollectionNames.MocaleLocatorTests)] 11 | public class MocaleInitializeServiceTests : FixtureBase 12 | { 13 | #region Setup 14 | 15 | public override object CreateSystemUnderTest() 16 | { 17 | return new MocaleInitializeService(); 18 | } 19 | 20 | #endregion Setup 21 | 22 | #region Tests 23 | 24 | [Fact] 25 | public void Initialize_ShouldSetupLocator_AndInitializeServices() 26 | { 27 | // Arrange 28 | var appBuilder = MauiApp.CreateBuilder(); 29 | 30 | var localizationManager = new Mock(); 31 | var translatorManager = new Mock(); 32 | var configManager = new Mock>(); 33 | 34 | configManager.Setup(m => m.Configuration) 35 | .Returns(new MocaleConfiguration() 36 | { 37 | ResourceType = LocaleResourceType.Json, 38 | DefaultCulture = new CultureInfo("fr-FR"), 39 | ShowMissingKeys = false, 40 | NotFoundSymbol = "__", 41 | UseExternalProvider = false, 42 | SaveCultureChanged = false, 43 | }); 44 | 45 | appBuilder.Services.AddSingleton(localizationManager.Object); 46 | appBuilder.Services.AddSingleton(translatorManager.Object); 47 | appBuilder.Services.AddSingleton(configManager.Object); 48 | 49 | var serviceProvider = appBuilder.Services.BuildServiceProvider(); 50 | 51 | var sut = GetSut(); 52 | 53 | // Act 54 | sut.Initialize(serviceProvider); 55 | 56 | // Assert 57 | Assert.NotNull(MocaleLocator.MocaleConfiguration); 58 | Assert.Equal(LocaleResourceType.Json, MocaleLocator.MocaleConfiguration.ResourceType); 59 | Assert.Equal(new CultureInfo("fr-FR"), MocaleLocator.MocaleConfiguration.DefaultCulture); 60 | Assert.False(MocaleLocator.MocaleConfiguration.ShowMissingKeys); 61 | Assert.Equal("__", MocaleLocator.MocaleConfiguration.NotFoundSymbol); 62 | Assert.False(MocaleLocator.MocaleConfiguration.UseExternalProvider); 63 | Assert.False(MocaleLocator.MocaleConfiguration.SaveCultureChanged); 64 | 65 | Assert.NotNull(MocaleLocator.TranslatorManager); 66 | Assert.Equal(translatorManager.Object, MocaleLocator.TranslatorManager); 67 | 68 | localizationManager.Verify(m => m.Initialize(), Times.Once); 69 | } 70 | 71 | #endregion Tests 72 | } 73 | -------------------------------------------------------------------------------- /src/Mocale.Cache.SQLite/Managers/SqlCacheUpdateManager.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using Microsoft.Extensions.Logging; 3 | using Mocale.Abstractions; 4 | 5 | namespace Mocale.Cache.SQLite.Managers; 6 | 7 | internal class SqlCacheUpdateManager : ICacheUpdateManager 8 | { 9 | private readonly ICacheRepository cacheRepository; 10 | private readonly ILogger logger; 11 | 12 | private readonly ISqliteConfig sqliteConfig; 13 | private readonly TimeProvider timeProvider; 14 | 15 | public SqlCacheUpdateManager( 16 | ICacheRepository cacheRepository, 17 | ILogger logger, 18 | IConfigurationManager sqliteConfigurationManager, 19 | TimeProvider timeProvider) 20 | { 21 | this.cacheRepository = Guard.Against.Null(cacheRepository, nameof(cacheRepository)); 22 | this.logger = Guard.Against.Null(logger, nameof(logger)); 23 | this.timeProvider = Guard.Against.Null(timeProvider, nameof(timeProvider)); 24 | 25 | sqliteConfigurationManager = Guard.Against.Null(sqliteConfigurationManager, nameof(sqliteConfigurationManager)); 26 | this.sqliteConfig = sqliteConfigurationManager.Configuration; 27 | } 28 | 29 | #region Interface Implementations 30 | 31 | /// 32 | public bool CanUpdateCache(CultureInfo cultureInfo) 33 | { 34 | var updateItem = cacheRepository.GetItem(cultureInfo); 35 | 36 | if (updateItem is null) 37 | { 38 | return true; 39 | } 40 | 41 | var nextUpdateWindow = updateItem.LastUpdated.Add(sqliteConfig.UpdateInterval); 42 | 43 | return nextUpdateWindow < timeProvider.GetUtcNow(); 44 | } 45 | 46 | /// 47 | public bool SetCacheUpdated(CultureInfo cultureInfo) 48 | { 49 | return cacheRepository.AddOrUpdateItem(cultureInfo, timeProvider.GetUtcNow().DateTime); 50 | } 51 | 52 | /// 53 | public void ClearCache(CultureInfo cultureInfo) 54 | { 55 | var deleted = cacheRepository.DeleteItem(cultureInfo); 56 | 57 | if (!deleted) 58 | { 59 | logger.LogWarning("Unable to delete cache for culture: {CultureName}", cultureInfo.Name); 60 | return; 61 | } 62 | 63 | logger.LogTrace("Deleted update cache for culture: {CultureName}", cultureInfo.Name); 64 | } 65 | 66 | /// 67 | public void ClearCache() 68 | { 69 | var deleted = cacheRepository.DeleteAll(); 70 | 71 | if (!deleted) 72 | { 73 | logger.LogWarning("Unable to delete cache for all cultures"); 74 | return; 75 | } 76 | 77 | logger.LogTrace("Deleted update cache for all cultures"); 78 | } 79 | 80 | #endregion Interface Implementations 81 | } 82 | -------------------------------------------------------------------------------- /src/Mocale/Providers/AppResourceProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Globalization; 3 | using System.Resources; 4 | using Ardalis.GuardClauses; 5 | using Mocale.Exceptions; 6 | 7 | namespace Mocale.Providers; 8 | 9 | internal class AppResourceProvider : IInternalLocalizationProvider 10 | { 11 | private readonly ILogger logger; 12 | private readonly IMocaleConfiguration mocaleConfiguration; 13 | private readonly ResourceManager resourceManager; 14 | 15 | public AppResourceProvider( 16 | IConfigurationManager appResourcesConfigurationManager, 17 | IConfigurationManager mocaleConfigurationManager, 18 | ILogger logger) 19 | { 20 | this.logger = Guard.Against.Null(logger, nameof(logger)); 21 | 22 | var appResourcesConfig = appResourcesConfigurationManager.Configuration; 23 | mocaleConfiguration = mocaleConfigurationManager.Configuration; 24 | 25 | if (appResourcesConfig.AppResourcesType is null) 26 | { 27 | throw new InitializationException("App Resource Type has not been set, this should be configured during startup"); 28 | } 29 | 30 | resourceManager = new ResourceManager(appResourcesConfig.AppResourcesType); 31 | } 32 | 33 | public Dictionary? GetValuesForCulture(CultureInfo cultureInfo) 34 | { 35 | // https://stackoverflow.com/a/1970941/8828057 36 | var resourceSet = resourceManager.GetResourceSet(cultureInfo, true, false); 37 | 38 | if (resourceSet is null) 39 | { 40 | // The default culture will not have a name (ie en-GB.resx) so we will have to trust the 41 | // user configured this correctly?! 42 | if (cultureInfo.Equals(mocaleConfiguration.DefaultCulture)) 43 | { 44 | var defaultSet = resourceManager.GetResourceSet(CultureInfo.InvariantCulture, true, false); 45 | 46 | if (defaultSet is null) 47 | { 48 | logger.LogWarning("Unable to load default resource set"); 49 | return null; 50 | } 51 | 52 | return ConvertResourceSet(defaultSet); 53 | } 54 | 55 | logger.LogWarning("No resources found for culture {CultureName}", cultureInfo.Name); 56 | return null; 57 | } 58 | 59 | return ConvertResourceSet(resourceSet); 60 | } 61 | 62 | private static Dictionary ConvertResourceSet(ResourceSet resourceSet) 63 | { 64 | return resourceSet 65 | .OfType() 66 | .ToDictionary(r => 67 | r.Key.ToString() ?? string.Empty, 68 | r => r.Value?.ToString() ?? string.Empty); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Mocale.Cache.SQLite/Repositories/CacheRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace Mocale.Cache.SQLite.Repositories; 5 | 6 | internal class CacheRepository : RepositoryBase, ICacheRepository 7 | { 8 | #region Constructors 9 | 10 | public CacheRepository( 11 | IDatabaseConnectionProvider databaseConnectionProvider, 12 | ILogger logger) 13 | : base( 14 | databaseConnectionProvider, 15 | logger) 16 | { 17 | Connection.CreateTable(); 18 | } 19 | 20 | #endregion Constructors 21 | 22 | #region Interface Implementations 23 | 24 | public bool AddItem(UpdateHistoryItem updateItem) 25 | { 26 | var rows = Connection.Insert(updateItem); 27 | 28 | return rows == 1; 29 | } 30 | 31 | public bool AddOrUpdateItem(CultureInfo cultureInfo, DateTime lastUpdated) 32 | { 33 | var existingItem = Connection.Table() 34 | .FirstOrDefault(e => e.CultureName == cultureInfo.Name); 35 | 36 | if (existingItem is not null) 37 | { 38 | existingItem.LastUpdated = lastUpdated; 39 | 40 | var rows = Connection.Update(existingItem); 41 | 42 | return rows == 1; 43 | } 44 | else 45 | { 46 | var entity = new UpdateHistoryItem() 47 | { 48 | CultureName = cultureInfo.Name, 49 | LastUpdated = lastUpdated, 50 | }; 51 | 52 | var rows = Connection.Insert(entity); 53 | 54 | return rows == 1; 55 | } 56 | } 57 | 58 | public bool DeleteAll() 59 | { 60 | var count = Connection.Table() 61 | .Count(); 62 | 63 | var rows = Connection.DeleteAll(); 64 | 65 | return rows == count; 66 | } 67 | 68 | public bool DeleteItem(CultureInfo cultureInfo) 69 | { 70 | var itemToDelete = Connection.Table() 71 | .FirstOrDefault(e => e.CultureName == cultureInfo.Name); 72 | 73 | if (itemToDelete is null) 74 | { 75 | return false; 76 | } 77 | 78 | var rows = Connection.Delete(itemToDelete); 79 | 80 | return rows == 1; 81 | } 82 | 83 | public UpdateHistoryItem? GetItem(CultureInfo cultureInfo) 84 | { 85 | return Connection.Table() 86 | .FirstOrDefault(e => e.CultureName == cultureInfo.Name); 87 | } 88 | 89 | public bool UpdateItem(UpdateHistoryItem updateItem) 90 | { 91 | var rows = Connection.Update(updateItem); 92 | 93 | return rows == 1; 94 | } 95 | 96 | #endregion Interface Implementations 97 | } 98 | 99 | -------------------------------------------------------------------------------- /tests/Mocale.UnitTests/Helpers/ExternalJsonFileNameHelperTests.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using Mocale.Abstractions; 3 | using Mocale.Helper; 4 | using Mocale.Models; 5 | 6 | namespace Mocale.UnitTests.Helpers; 7 | 8 | public class ExternalJsonFileNameHelperTests : FixtureBase 9 | { 10 | #region Setup 11 | 12 | private readonly Mock> configurationManager; 13 | 14 | public ExternalJsonFileNameHelperTests() 15 | { 16 | configurationManager = new Mock>(); 17 | } 18 | 19 | public override object CreateSystemUnderTest() 20 | { 21 | return new ExternalJsonFileNameHelper(configurationManager.Object); 22 | } 23 | 24 | #endregion Setup 25 | 26 | #region Tests 27 | 28 | [Fact] 29 | public void Constructor_WhenConfigIsWrongType_ShouldThrow() 30 | { 31 | // Arrange 32 | var resxFileConfig = new ResxResourceFileDetails() 33 | { 34 | ResourcePrefix = "test", 35 | }; 36 | 37 | var config = new Mock(); 38 | 39 | config.SetupGet(m => m.ResourceFileDetails) 40 | .Returns(resxFileConfig); 41 | 42 | configurationManager.SetupGet(m => m.Configuration) 43 | .Returns(config.Object); 44 | 45 | // Act 46 | var ex = Record.Exception(GetSut); 47 | 48 | // Assert 49 | Assert.IsType(ex); 50 | Assert.Equal("Resource file details were not for json files", ex.Message); 51 | } 52 | 53 | [Theory] 54 | [InlineData("en-GB", null, "en-GB.json")] 55 | [InlineData("fr-FR", null, "fr-FR.json")] 56 | [InlineData("en-GB", "", "en-GB.json")] 57 | [InlineData("fr-FR", "", "fr-FR.json")] 58 | [InlineData("en-GB", "v1", "v1/en-GB.json")] 59 | [InlineData("fr-FR", "v1", "v1/fr-FR.json")] 60 | [InlineData("en-GB", "v2", "v2/en-GB.json")] 61 | [InlineData("fr-FR", "v2", "v2/fr-FR.json")] 62 | public void GetExpectedFileName_WhenCalled_ShouldCreateCorrectName(string cultureString, string? versionPrefix, string expectedFileName) 63 | { 64 | // Arrange 65 | var resxFileConfig = new JsonResourceFileDetails() 66 | { 67 | VersionPrefix = versionPrefix, 68 | }; 69 | 70 | var config = new Mock(); 71 | 72 | config.SetupGet(m => m.ResourceFileDetails) 73 | .Returns(resxFileConfig); 74 | 75 | configurationManager.SetupGet(m => m.Configuration) 76 | .Returns(config.Object); 77 | 78 | var culture = new CultureInfo(cultureString); 79 | 80 | var sut = GetSut(); 81 | 82 | // Act 83 | var fileName = sut.GetExpectedFileName(culture); 84 | 85 | // Arrange 86 | Assert.Equal(expectedFileName, fileName); 87 | } 88 | 89 | #endregion Tests 90 | } 91 | --------------------------------------------------------------------------------