Search code examples
javascripttypescripttypesinterfacemultilingual

Typescript Multilingual Interfaces Implementation


I am using MongoDB, NodeJS & React-Native to build an app. Since I wanted everything to be available in 2 languages (English & Hebrew in my case), I used i18n-next in my RN app to have all texts in my app in both languages.

My database is where it gets tricky, there are string fields which I need to have in both languages (like name for an item), so I made a Typescript type called MultilingualString, which just has a language code field for each of my supported languages, and used it as these field's type.

type MultilingualString = {
    en: string,
    he: string
};

The issue is that once I get the data into my RN app, I want to determine the wanted language and transform all those fields into a simple string field in the wanted language, so that it will be simple to access and use it through out the app's components.

To tackle that, I made a generic recursive function that takes an Object and checks its keys one by one, for every key that is a MultilingualString, it takes the value of the wanted language key code and sets it as the original key's value.

async function determineObjectLanguage<T extends Object>(object: T): Promise<T> {
    currentLanguageCode = "en"; // Might be "he" or "en", simplified for the example

    for (let key of Object.keys(object)) {
        const tKey = key as keyof T; 

        if (isMultilingualString(object[tKey])) {
            object[tKey] = object[tKey][currentLanguageCode as keyof MultilingualString];
        } 
        else if (isObject(object[tKey])) {
            object[tKey] = await determineObjectLanguage(object[tKey] as Object, currentLanguageCode) as T[keyof T];
        }
    }

    return object;
}

export const isMultilingualString = (value: any): value is MultilingualString => {
    return (value instanceof Object) && ("en" in value) && ("he" in value);
}

The problem is that to be able to get along with Typescript through that I had to set all of those fields to be of type 'MultilingualString | string', which basically means big trouble later on with the type safety on these.

To clarify, I want to store objects in my database like this:


export interface Item {
    name: MultilingualString
    description: MultilingualString,
    ...
}

Then parse them in my app in to this:


export interface Item {
    name: string
    description: string,
    ...
}

That way I pass it on to my components as if it was always locale, allowing me to ignore the multilingual implementation everywhere else besides when loading and parsing it from the server for the first time.

The question is how can I achieve the described functionality in a both elegant and type-safe way? Am I going about this all wrong?

Would appreciate any tips and recommendations guys, thanks!


Solution

  • From what I know the best approaches for multilanguage are:

    Have default language, to which any untranslated text will be forwarded to. For example, you won't translate the 'MB' for Mega-bytes in most languages.

    Access the object by the locale. it's best to let the translated text be returned dynamically.

    So in your case, I will do something like this.

    I'm providing a short version, of course, you will have to import the JSON files or objects for the locale

    export default class I18Translation {
        private static defaultLocale = 'en';
    
        private static locales = {
            en: {
                "hello": "Hello",
                "mb"   : "MB"
            },
            he: {
                "hello": "שלום",
            }
        }
    
        public static locale(text: string, locale: string): string {
            // First try to get to required
            if (this.locales[locale][text] !== undefined) {
                return this.locales[locale][text];
            }
            // If not we would go for the default
            if (this.locales[this.defaultLocale][text] !== undefined) {
                return this.locales[this.defaultLocale][text];
            }
    
            // Eventuality if we don't have any translation will just return 
            // the text as is
            return text;
        }
    
        // This is a short version of the same function
          public static localeShort(text: string, locale: string): string {
            return this.locales[locale][text] !== undefined ?
                this.locales[locale][text]
                : this.locales[defaultLocale][text] !== undefined ?
                    this.locales[defaultLocale][text]
                    : text;
    
        }
    }