Monday, October 14, 2024

Localization in Blazor

Here are the lessons that I learned while performing the translation to Spanish for The Bed Brigade National website.  I created documentation specific for our project.

  • If you have a brownfield project, you have to find all of the strings that need to be localized in your Razor files.  I ended up creating logic to do this.  
  • You have to localize any validation messages.  I still wanted to use DataAttributes for validation so I ended up writing some custom code to pull out the validation messages and then more custom logic to actually do the validation and get the translation.
  • The need to do partial translations.  Sometimes dropdown lists, enums, or other dynamic content can be partially translated.  I created code to do this.
  • Content Management System translations.  We are seeding our whole site so that when you run it locally or you deploy it to a new environment you have data to work with.  So the content can be pre-translated.  However once the user changes words on the page, we need to check if we can translated it using what we have already translated, if not we need to queue it for translation.  
  • Translators cannot handle weird line feeds in the middle of your content.  I like to format my HTML before I save it and this does not affect the browser rendering at all.  However the language translators have no idea what you are doing and will return back strange translations.  So you have to replace all tabs, and line feeds with spaces.
  • There is one free translator available.  It is slow but it works.
Before embarking on localization in Blazor I watched most of the videos on YouTube relating to localization.  Basically there are two options, the traditional resource .resx files that have been around since WinForm days and resource files using YAML.  I would encourage you to watch both of these videos:  


I chose the second option; the multilanguages open source project to perform localization.  I contributed a bunch of new features to this project which I originally created for the Bed Brigade National website.  The main reason why I chose it was for its ability to translate the YAML files into 69 different languages.  Although my wife speaks Spanish, she did not have to translate a single word for our website.  

Bed Brigade is a charity and I did not want to break the bank on translations.  We ended up going with the free API Layer translator service.  Since everything on the entire site is pre-translated with resource files, it is only when the user adds or changes content that we might need to translate something.

Here are the other options for translating text dynamically if you have a content management system on your site.  

    Wednesday, August 14, 2024

    Resources for Learning Blazor 8

    Official Microsoft Resources

    Microsoft Official Blazor Page

    https://learn.microsoft.com/en-us/aspnet/core/blazor/?view=aspnetcore-8.0


    Build your first web app with ASP.NET Core using Blazor

    https://dotnet.microsoft.com/en-us/learn/aspnet/blazor-tutorial/intro


    Blazor 8 Paid Courses

    Blazor Deep Dive - From Beginner to Advanced in .NET 8

    https://www.udemy.com/course/blazor-deep-dive-from-beginner-to-advanced


    Master Blazor: Build Inventory Management System in .NET 8

    https://www.udemy.com/course/learn-blazor-while-creating-an-inventory-management-system/


    Free Courses on YouTube Ordered by Likes


    Full stack web UI with Blazor in .NET 8 | .NET Conf 2023 (3.1K Likes)

    https://www.youtube.com/watch?v=YwZdtLEtROA


    Blazor Full Course For Beginners (2.1K Likes)

    https://www.youtube.com/watch?v=RBVIclt4sOo


    CRUD w/ Blazor in .NET 8 All Render Modes (SSR, Server, Wasm, Auto), Entity Framework & SQL Server (1.9K Likes)

    https://www.youtube.com/watch?v=w8imy7LT9zY


    Intro to Blazor in .NET 8 - SSR, Stream Rendering, Auto, and more... (1.9K Likes)

    https://www.youtube.com/watch?v=walv3nLTJ5g 


    Modern Full-Stack Web Development with ASP.NET Core & Blazor (821 Likes)

    https://www.youtube.com/watch?v=NbfhbDKiFpM


    Blazor in .NET 8 in 3 Hours | Blazor Server | WebAssembly | Entity Framework Core | ASP.NET Identity (605 Likes)

    https://www.youtube.com/watch?v=LMDo38DPxZc


    Learn Blazor: Build an Inventory Management System in .NET 8 (532 Likes)

    https://www.youtube.com/watch?v=JgEjgobtDg0


    Full Complete Blazor Students Management System with Admin LTE in .NET8.0,EF Core,SQL Server (234 Likes)

    https://www.youtube.com/watch?v=2t1ZYxOMycw


    .NET 8 Blazor The Ultimate Beginners Guide (116 Likes)

    https://www.youtube.com/watch?v=a_HUyGhUI9M&list=PL285LgYq_FoI2LFCSUTpc8PSkFkLxN7cB


    People to Subscribe to on YouTube

    https://www.youtube.com/@PatrickGod

    https://www.youtube.com/@IAmTimCorey

    Friday, July 19, 2024

    Running .NET Core Tests and collecting coverage with Azure Pipelines

    Installing Coverlet

    Coverlet is an open source project that produces code coverage reports.

    In Visual Studio install the coverlet.collector and coverlet.msbuild NuGet package.

    nuget install coverlet.collector

    nuget install coverlet.msbuild

    Running Coverlet Locally

    dotnet test /p:CollectCoverage=true

    Viewing Coverage in Visual Studio

    This Visual Studio 2022 extension allows you to view coverage line by line and as a summary

    https://marketplace.visualstudio.com/items?itemName=FortuneNgwenya.FineCodeCoverage

    Defining the Azure Pipeline

    Using the wizard follow the steps below.

    Use the .NET Core SDK

    Select the .NET Core SDK appropriate for your project.



    Perform a .NET Restore

    A restore will download the Nuget packages for your project.



    Perform a .NET Build



    Run the Tests

    Ensure to define the arguments to collect coverage



    Publish the Code Coverage

    Task version 1 will be deprecated. See this post from Microsoft:

    https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/publish-code-coverage-results-v2?view=azure-pipelines




    Tuesday, July 16, 2024

    Running Jasmine Tests Using Karma and Azure Pipelines

    In order to run Jasmine tests, the runner Karma must be used.  Karma requires Chrome.  So in Azure we will be installing all of that in a container in order to run the tests.  We will also be collecting coverage.  It is important that a full Linux distribution be used like Ubuntu.  We will be using the Azure pipeline wizard.

    Install Jasmine

    This will add Jasmine to your package.json

    npm install --save-dev jasmine-core @types/jasmine @types/jasminewd2


    Install Karma

    This will add Karma to your package.json

    npm install --save-dev karma karma-chrome-launcher karma-coverage karma-jasmine
    karma-jasmine-html-reporter

    Add a karma.conf.js to your project

    Generating detailed code coverage reports can help you identify untested parts of your codebase. Add the coverageReporter to your karma.config.js

    // Karma configuration file, see link for more information
    // https://karma-runner.github.io/1.0/config/configuration-file.html

    module.exports = function (config) {
      config.set({
        basePath: "",
        frameworks: ["jasmine", "@angular-devkit/build-angular"],
        plugins: [
          require("karma-jasmine"),
          require("karma-chrome-launcher"),
          require("karma-jasmine-html-reporter"),
          require("karma-coverage"),
          require("@angular-devkit/build-angular/plugins/karma"),
        ],
        client: {
          jasmine: {
            // you can add configuration options for Jasmine here
            // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
            // for example, you can disable the random execution with `random: false`
            // or set a specific seed with `seed: 4321`
          },
          clearContext: false, // leave Jasmine Spec Runner output visible in browser
        },
        jasmineHtmlReporter: {
          suppressAll: true, // removes the duplicated traces
        },
        coverageReporter: {
          dir: require("path").join(__dirname, "./coverage/angularapp"),
          subdir: ".",
          reporters: [{ type: "html" }, { type: "text-summary" }],
        },
        reporters: ["progress", "kjhtml", "coverage"],
        port: 9876,
        colors: true,
        logLevel: config.LOG_INFO,
        autoWatch: true,
        browsers: ["Chrome", "ChromeHeadless"],
        singleRun: false,
        restartOnFileChange: true,
      });
    };


    Add test and test-headless to your package.json

      "scripts": {
        "test": "ng test",
        "test-headless": "ng test --code-coverage --watch=false --browsers=ChromeHeadless",
      },

    In Azure Define the Pipeline



    Specify the Source Repository



    Using Bash to Install Chrome




    Here are the commands
    sudo apt-get update
    sudo apt-get install -y wget
    wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
    sudo apt-get install -y ./google-chrome-stable_current_amd64.deb
    google-chrome --version

    Use Node

    Use the appropriate version of node for your project.





    Use NPM to install the Angular CLI




    Use NPM to install the node packages





    Use NPM to run the build




    Use NPM to run the tests








    Friday, February 9, 2024

    Cleanup Local Git Branches

     Below is a Powershell script that will remove local Git branches except for a list of excluded branches.


    # Usage:  .\RemoveGitBranches.ps1 -directoryPath "C:\path\to\your\repository"
    param (
        [string]$directoryPath
    )

    # Define the branches to keep
    $branchesToKeep = @("dev", "develop", "development", "main", "master", "qa", "test", "stage")

    # Change to the specified directory
    Set-Location -Path $directoryPath

    # Get a list of all local branches
    $branches = git branch --format "%(refname:short)"

    foreach ($branch in $branches) {
        $branch = $branch.Trim()

        # Check if the branch is not in the list of branches to keep
        if ($branch -notin $branchesToKeep) {
            # Delete the branch
            git branch -D $branch
        }
    }

    Friday, December 29, 2023

    Using MOQ

    MOQ is a popular mocking framework for .NET used to create mock objects for testing. MOQ allows you to isolate the class under test by replacing its dependencies with controlled mock objects, making it easier to test behavior in isolation.  Here are the steps to use it.  

    1.  Install MOQ.  Add MOQ using the NuGet package manager.  https://www.nuget.org/packages/Moq

    2.  Create a Mock Object. Use Mock<T> where T is the type you want to mock. For example:

    _webAPIClientMock = new Mock<IWebAPIClient>();

    3,  Setup Method Behavior. Define how the mock object should behave. For example:

    _webAPIClientMock.Setup(client => client.GetApplicationSetting(It.IsAny<GetAppSettingRequest>()).Result).Returns(appSettingResponse);

    4.  Inject the mock. Inject the mock object into the class you are testing.

    _apiSettingsProvider = new ApiSettingsProvider(Mock.Of<ILogger<ApiSettingsProvider>>(), _webAPIClientMock.Object);

    5.  Verify Interactions. Optionally, verify that certain interactions with the mock object occurred, like _webAPIClientMock.Verify(x => x.GetApplicationSetting(), Times.Once);

    6.  Run Your Test.  Execute your test method as usual.

    Full Example

    [TestClass]
    public class ApiSettingsProviderTests
    {
    private  Mock<IWebAPIClient> _webAPIClientMock;
    private  ApiSettingsProvider _apiSettingsProvider;

    [TestInitialize]
    public void Initialize()
    {
    _webAPIClientMock = new Mock<IWebAPIClient>();
    _apiSettingsProvider = new ApiSettingsProvider(Mock.Of<ILogger<ApiSettingsProvider>>(), _webAPIClientMock.Object);
    }

    [TestMethod]
    public void GetAppSettingValue_ShouldReturnFirstValueFromAppSettingsResponse()
    {
       // Arrange
    var appSettingName = "TestSetting";
    var settingValues = new string[] { "Value1", "Value2" };
    var appSettingResponse = new GetAppSettingResponse
    {
    SettingValues = new List<string>(settingValues)
    };
    _webAPIClientMock.Setup(client => client.GetApplicationSetting(It.IsAny<GetAppSettingRequest>()).Result).Returns(appSettingResponse);
    // Act
    var value = _apiSettingsProvider.GetAppSettingValue(appSettingName);
    // Assert
    Assert.AreEqual("Value1", value);
    }
    }

    Resources

    Creating Angular Tests and Mocking with Jasmine

    When creating Angular Components, by default a spec.ts file is created.  By definition, a unit test should not call any files, databases, or services.  We use a mock, or a stand in for services.  

    Mocking Services

    To mock services, the jasmine.createSpyObj is used. Example:

    loggingService = jasmine.createSpyObj('LoggingService', ['Info','Error']); 

    Providing Mocked Services

    To use the mocked service, it is provied.  Example:

        TestBed.configureTestingModule({
          providers: [
            ErrorHandlerService, 
            { provide: LoggingService, useValue: loggingService },
            { provide: HttpTestingController, useValue: httpTestingController }
          ],
        });


    Full Example

    We have an error handler service and we want to ensure that the message is logged by using the Error method.

    import { TestBed } from '@angular/core/testing';
    import { HttpErrorResponse } from '@angular/common/http';
    import { ErrorHandlerService } from './error-handler.service';
    import { LoggingService } from './logging.service';
    import { HttpTestingController } from '@angular/common/http/testing';

    describe('ErrorHandlerService', () => {
      let errorHandlerService: ErrorHandlerService;
      let loggingService: LoggingService;
      let httpTestingController: HttpTestingController;
      
      beforeEach(() => {
        loggingService = jasmine.createSpyObj('LoggingService', ['Info','Error']);
        TestBed.configureTestingModule({
          providers: [
            ErrorHandlerService, 
            { provide: LoggingService, useValue: loggingService },
            { provide: HttpTestingController, useValue: httpTestingController }
          ],
        });
        errorHandlerService = TestBed.inject(ErrorHandlerService);
        httpTestingController = TestBed.inject(HttpTestingController);
      });
      it('should be created', () => {
        expect(errorHandlerService).toBeTruthy();
      });
      it('should handle an error and log it', () => {
        spyOn(console, 'error');
        const error = new Error('Test error message');
        errorHandlerService.handleError(error);
        expect(console.error).toHaveBeenCalledWith(error);
        expect(loggingService.Error).toHaveBeenCalledWith(error.message, [error]);
      });
      
    });