Translating product fields using .resx-files

Trying to use .resx-files to manage field translations but cannot get it to work with updates. I have tried to follow https://docs.litium.com/platform/guides/back-office-ui-extensions/translations:

  • Created a new file Litium.Accelerator/Resources/MyTest.sv-se.resx
  • Adjusted .csproj to <LitiumLocalization Include="Resources\*.resx" /> and added <AssemblyAttribute Include="Litium.Localization.UseAdministrationResource" />

If I create a new field definition with a resx-translation it gets created and the translation works, but the only way I have managed to get the resx-file to apply changes is to delete the field to get it re-created?

  1. Is there a way to update field translations using .resx-files out of the box that I am missing?
  2. Should it possible to manage translations with resx for a field that is created manually in Litium admin UI, or is it only possible for fields managed in code?
  3. According to an older answer (How to access ui translations? - #11 by patric.forsgard) strings are not stored in DB, but in Litium 8 I find them in DB table FieldFramework.FieldDefinitionFieldData so this seem to have changed, when exactly is the database getting updated from the .resx-value and which source (db vs. resx) is overriding the other? In my tests I am also getting duplicate rows in FieldFramework.FieldDefinitionFieldData, shouldn’t the combination of Ownersystemid+FieldDefinitionId+Culture be unique here?
  4. I thought that method DefinitionSetup.InitFields() would be the source for updating fields, but I do not understand the logic in that method (looking at version 8.19.1), validations are being done comparing existing fields with the definitions in code, but then the code does not actually seem to update the existing fields?

Litium version: 8.19.1

I have only looked a 8.18.1 and before and unfortunately the accelerator is coded to only set translations when a field or template is created and doesn’t update them.

Code below is for accelerator Definitionsetup .

Since IsAlreadyExecuted is set first time definition setup is run for a field you need to remove the field and also remove IsAlreadyExecuted for the field in the database from the common settings table to update the translation.

I would guess the reasoning is that you after that handle translations i litium backoffice. The problem is that there is no import or export of translation so if you need to add a language you need to do it from litium backoffice manually field by field instead of sending resx to translator and then just adding the translated file to the solution.

We usually rewrite the definitionsetup to also update translations and properties if fields and templates already exist.

private void InitFields(IEnumerable<FieldDefinition> fields)
{
    foreach (var item in fields)
    {
        if (IsAlreadyExecuted<FieldDefinition>(item.Id, item.AreaType.Name))
        {
            continue;
        }

        var currentField = _fieldDefinitionService.Get(item.AreaType, item.Id);
        if (currentField == null)
        {
            _fieldFrameworkSetupLocalizationService.Localize(item);
            _fieldDefinitionService.Create(item);
            SetAlreadyExecuted<FieldDefinition>(item.Id, item.AreaType.Name);
            continue;
        }

        if (item.FieldType != currentField.FieldType)
        {
            _logger.LogError("Accelerator \"{Id}\" field with \"{FieldType}\" type can't be created. The system already has the \"{CurrentId}\" field with \"{CurrentFieldType}\" type. Accelerator deployment would fail.", item.Id, item.FieldType, currentField.Id, currentField.FieldType);
            _settingService.Set<bool?>("Accelerator.DefinitionsError", true);
            continue;
        }
        if (item.MultiCulture != currentField.MultiCulture)
        {
            _logger.LogError("Accelerator \"{Id}\" field with \"MultiCulture\" setting and \"{MultiCulture}\" value can't be created. The system already has the \"{CurrentId}\" field with \"MultiCulture\" setting and \"{CurrentMultiCulture}\" value. Accelerator deployment would fail.", item.Id, item.MultiCulture, currentField.Id, currentField.MultiCulture);
            _settingService.Set<bool?>("Accelerator.DefinitionsError", true);
            continue;
        }

        SetAlreadyExecuted<FieldDefinition>(item.Id, item.AreaType.Name);
    }
}

Thank you Jonas, I guessed that was the case, will adjust the setup to always update fields then.

And I guess that InitFields in the Accelerator could possibly be shortened to:

private void InitFields(IEnumerable<FieldDefinition> fields)
{
    foreach (var item in fields)
    {
        var currentField = _fieldDefinitionService.Get(item.AreaType, item.Id);
        if (currentField == null)
        {
            _fieldFrameworkSetupLocalizationService.Localize(item);
            _fieldDefinitionService.Create(item);
        }
    }
}

Adjusted version working with updates:

private void InitFields(IEnumerable<FieldDefinition> fields)
{
    foreach (var item in fields)
    {
        if (IsAlreadyExecuted<FieldDefinition>(item.Id, item.AreaType.Name))
        {
            // continue;
        }

        var currentField = _fieldDefinitionService.Get(item.AreaType, item.Id);
        if (currentField == null)
        {
            _fieldFrameworkSetupLocalizationService.Localize(item);
            _fieldDefinitionService.Create(item);
            SetAlreadyExecuted<FieldDefinition>(item.Id, item.AreaType.Name);
            continue;
        }

        if (item.FieldType != currentField.FieldType)
        {
            _logger.LogError("Accelerator \"{Id}\" field with \"{FieldType}\" type can't be created. The system already has the \"{CurrentId}\" field with \"{CurrentFieldType}\" type. Accelerator deployment would fail.", item.Id, item.FieldType, currentField.Id, currentField.FieldType);
            _settingService.Set<bool?>("Accelerator.DefinitionsError", true);
            continue;
        }
        if (item.MultiCulture != currentField.MultiCulture)
        {
            _logger.LogError("Accelerator \"{Id}\" field with \"MultiCulture\" setting and \"{MultiCulture}\" value can't be created. The system already has the \"{CurrentId}\" field with \"MultiCulture\" setting and \"{CurrentMultiCulture}\" value. Accelerator deployment would fail.", item.Id, item.MultiCulture, currentField.Id, currentField.MultiCulture);
            _settingService.Set<bool?>("Accelerator.DefinitionsError", true);
            continue;
        }

        _fieldFrameworkSetupLocalizationService.Localize(item);
        item.SystemId = currentField.SystemId;
        _fieldDefinitionService.Update(item);

        SetAlreadyExecuted<FieldDefinition>(item.Id, item.AreaType.Name);
    }
}

Note that then you also have to reset IsAlreadyExecuted before application start. If we do it like this we usually create service to reset it via the swagger ui since you before could not access the database in a simple way in LCC.

We also fetch the field and update it to not lose field options since we allow options to be added in litium backoffice. your code will remove existing options not in the definition.

// NOTE: This results in the localization is run every application start up
currentField = currentField.MakeWritableClone();
_fieldFrameworkSetupLocalizationService.Localize(currentField);
currentField.CanBeGridColumn = item.CanBeGridColumn;
currentField.CanBeGridFilter = item.CanBeGridFilter;
currentField.UseInSearchEngine = item.UseInSearchEngine;
currentField.UseInStorefront = item.UseInStorefront;
currentField.Editable = item.Editable;
currentField.Hidden = item.Hidden;
// NOTE: options are not updated.
_fieldDefinitionService.Update(currentField);

In this specific case I want to reset everything to the definition but good point.

Why is it required to to reset IsAlreadyExecuted?

Backside of doing this type of changes every time is that it will delay the startup of the application and changes to templates may trigger other operations in the platform. Delaying the startup is not optimal when running in Litium Serverless Cloud because the instances may dynamic be changed depend on load to scale up or down.

In one support case that I can remember the project was doing just this, and the change of the field template was triggering a full rebuild of the search index and this was having the affect that the site went down every time a new instance was started because the search queries for site structure (menu etc) didn’t return any data.

We are usually doing this during development where the template and translation are a living organism. The biggest winning with templates in code is removed if you can’t update them or add translations after they are created.

After golive we often use IsAlreadyExecuted and set it to false when template updates are needed. This makes the updates run only when we need them to. But i have been thinking of a solution where one can set a property in code that make the templates updated only first deployment to a environment if you have template changes. IsAlreadyExecuted is a bit of a annoyance since you ned to remove or reset it for each field and template you want to update.

@marten sorry i missed that you commented the continue.

It would be nice to find a way to always keep templates and translations updated by code over time, not only during the initial development phase. Manually working with IsAlreadyExecuted seems like it could be missed in deployments.

Did you find the source of startup delay Patric? Perhaps just comparing the definition to database and only triggering the update if the definition has actually been modified would be enough?