Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to do extreme branding/internationalization in .NET

We are planning a pretty big application.

-We want to internationalize our application for 30 countries.

-In most countries 1 to 6 different brands are available.

-Each combination of a certain locale like 'de' and brand like 'XXX' might occur multiple times therefore we need another identifier to get something unique:

"locale_brand_siteorigin"

Therefore we have .resx file like:

Configurations.de.burgerking.px10.resx

The bold printed is the unique identifier.

During runtime we create a:

var rm = new ResourceManager("MyNamespace.Configurations.UniqueIdentifier",Assembly.GetExecuting());

Depending on our business logic we can create the above resourceManager.

Finally we will end up having 180+ resx files with all combinations of the unique identifier.

Do you know of a better way to do this kind of branding?

4 years ago someone asked this question, but none answered:

Industry standard for implementing application branding?

UPDATE

I also want to extend my question asking for a solution showing the benefits of using the cultureandregioninfobuilder class to create those many custom cultures.

https://msdn.microsoft.com/en-us/library/system.globalization.cultureandregioninfobuilder(v=vs.110).aspx

like image 657
HelloWorld Avatar asked Apr 12 '16 19:04

HelloWorld


2 Answers

I wouldn't recommend using .resx files for such a huge project. When a website is translated in many different languages, usually a lot of people are involved in the copy management, translation etc. These people will not be able to edit the .resx files since they are technically embedded in the application code. This means that your developers will have to constantly update the resources every time there are changes... a real nightmare for everybody.

I recently build a database-driven system for the SumoSoft.Cms. All the strings can be managed through the Admin panel, while in the code you just use:

@CmsMethods.StringContent("ContentSection_Name", "Fallback_Value")

This Helper queries the Database looking for an entity of Type "ContentSection" which is structured more or less like this:

public class ContentSection
{
    public string Name { get; set; }

    public ICollection<ContentSectionLocalizedString> LocalizedStrings { get; set; }
}

Each LocalizedString contains a reference to a specific Country and a property "Content", so all the Helper does is to choose the one that matches the Current Culture of the Thread.

like image 155
tocqueville Avatar answered Oct 03 '22 22:10

tocqueville


Expanding on @FrancescoLorenzetti84 answer, one way I've done it in the past to make it easier to maintain is to wrap the database retrieval in a ResourceString class so that you can do something like:

private static readonly ResourceString res = "The value";

and then refer to that in the code. Behind the scene, the ResourceString class does the work. Here is an example of that:

namespace ResString
{

    public interface IResourceResolver
    {
        string Resolve(string key, string defaultValue);
    }

    public class ResourceString
    {
        public ResourceString(string value)
        {
            this.defaultValue = value;
            GetOwner();
        }

        public string Value
        {
            get
            {
                if (!resolved)
                    Resolve();
                return value;
            }
        }

        public override string ToString()
        {
            return Value;
        }

        public static implicit operator string(ResourceString rhs)
        {
            return rhs.Value;
        }

        public static implicit operator ResourceString(string rhs)
        {
            return new ResourceString(rhs);
        }

        protected virtual void Resolve()
        {
            if (Resolver != null)
            {
                if (key == null)
                    key = GetKey();
                value = Resolver.Resolve(key, defaultValue);
            }
            else
            {
                value = defaultValue;
            }
            resolved = true;
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        protected virtual void GetOwner()
        {
            StackTrace trace = new StackTrace();
            StackFrame frame = null;
            int i = 1;
            while (i < trace.FrameCount && (owner == null || typeof(ResourceString).IsAssignableFrom(owner)))
            {
                frame = trace.GetFrame(i);
                MethodBase meth = frame.GetMethod();
                owner = meth.DeclaringType;
                i++;
            }
        }

        protected virtual string GetKey()
        {
            string result = owner.FullName;
            FieldInfo field = owner.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static).Where(f =>
                typeof(ResourceString).IsAssignableFrom(f.FieldType) && f.GetValue(null) == this
            ).FirstOrDefault();
            if (field != null)
                result += "." + field.Name;
            return result;
        }


        public static IResourceResolver Resolver { get; set; }

        private string defaultValue;
        private string value;

        private bool resolved;
        private string key;
        private Type owner;


    }
}

And an example program:

namespace ResString
{
    class Program
    {
        /// <summary>
        /// Description for the first resource.
        /// </summary>
        private static readonly ResourceString firstRes = "First";
        /// <summary>
        /// Description for the second resource.
        /// </summary>
        private static readonly ResourceString secondRes = "Second";
        /// <summary>
        /// Description for the format string.
        /// </summary>
        private static readonly ResourceString format = "{0} {1}";

        static void Main(string[] args)
        {
            ResourceString.Resolver = new French();
            Console.WriteLine(String.Format(format, firstRes, secondRes));
        }

        private class French : IResourceResolver
        {
            public string Resolve(string key, string defaultValue)
            {
                switch (key)
                {
                    case "ResString.Program.firstRes":
                        return "Premier";
                    case "ResString.Program.secondRes":
                        return "Deuxième";
                    case "ResString.Program.format":
                        return "{1} {0}";
                }

                return defaultValue;
            }
        }

    }
}

If you run that, it will output: Deuxième Premier

Comment out the Resolver assignment and you will get: First Second

Any where you would use a string in the UI, use a declared ResourceString instead.

Changing the resolver after the string values have been resolved will not alter their value as the values are retrieved only once. You will of course need to write a real resolver that pulls from a database.

What you will then need is a utility program to run through the compiled classes and pull out the ResourceString declarations and put the key and default values into a database or text file so they can be translated. This should also go through the generated help XML files for each assembly and pull the comment for the ResourceString declarations so the translator has some context to work with. The key declarations will also provide context as you can easily group resources by UI class.

Add this to a build script the make sure it is updated regularly.

You can use the same approach with images and the like.

like image 45
Frank Hagenson Avatar answered Oct 03 '22 22:10

Frank Hagenson