core icon indicating copy to clipboard operation
core copied to clipboard

In a JSON file, allow references to other keys

Open kanidjar opened this issue 7 years ago • 10 comments

I'm submitting a ... (check one with "x")

[ ] bug report => check the FAQ and search github for a similar issue or PR before submitting
[ ] support request => check the FAQ and search github for a similar issue before submitting
[ x ] feature request

Current behavior

Today, we can only set strings and variables like {{myVar}}. It would be great to be able to include other existing keys (like a partial translation).

Expected/desired behavior

{
   "greetings":"Hi",
   "text":"[greetings] from Github!",
   "text2":"[greetings] there!"
}
myService.get('text').subscribe(
    (value) => {
         console.log(value);
         // Hi, from Github!
    }
);

What is the motivation / use case for changing the behavior?

Today, we have to concatenate the translations on our side:

myService.get(['greetings', 'text']).subscribe(
    (translations) => {
         console.log(translations.greetings + ' ' +  translations.text);
    }
);

kanidjar avatar Mar 13 '17 18:03 kanidjar

Don't see any valid reason to do that. Just repeat the Hi and make it inline. This would just add complexity to the library without a real benefit imo.

SamVerschueren avatar Mar 13 '17 20:03 SamVerschueren

Hi, what about some basic names? Ex: { "general:{ "CANCEL":"Cancel" ..... } } and for a view "myView":{ "TITLE": "My view title", "CANCEL":"@general.CANCEL" } or is it better to just refer to them separately?

Jensiator avatar Mar 23 '17 16:03 Jensiator

angular-translate, the popular library for translation in AngularJS, has this feature and calls it "links".

Here is their explanation and justification for it taken from their Guide page:

Another cool feature that angular-translate provides is the ability to link within your translation table from one translation id to another. Let's say we have the following translation table:

{
  "SOME_NAMESPACE": {
    "OK_TEXT": "OK"
  },
  "ANOTHER_NAMESPACE": {
    "OK_TEXT": "OK"
  }
}

So, as you can see, we have introduced two namespaces here, but both of them kind of need a text that probably just says "OK". This case isn't unusual if you just think about a confirmation button or similar in your app. However, it isn't hard to recognize that we have a redundancy here and we as developers don't like redundancy, right?

If there's a translation id that will always have the same concrete text as another one you can just link to it. To link to another translation id, all you have to do is to prefix its contents with an @: sign followed by the full name of the translation id including the namespace you want to link to. So the example above could look like this:

{
  "SOME_NAMESPACE": {
    "OK_TEXT": "OK"
  },
  "ANOTHER_NAMESPACE": {
    "OK_TEXT": "@:SOME_NAMESPACE.OK_TEXT"
  }
}

This was a feature that we used in angular-translate, and would be in favour of adding to this library as well.

greglockwood avatar Jul 12 '17 00:07 greglockwood

@SamVerschueren I'll give you a real-life example:

{
    "hello": "olá",
    "hi:" "olá",
}

In my language, both "hi" and "hello" mean the same. now imagine that there's a whole bunch of other words that mean "olá", I could just reference them with @:hello and switch it just in one place

You can give the counter that 'then, why not just use "hello" as the translation key instead where you need "olá"' and I'll answer to you with another question "what if 'hello' and 'hi' have different meanings in other languages?" - using "hello" would break the translation.

Now,

This would just add complexity to the library without a real benefit imo.

That's just plain not true. There are benefits to have from a linkable translation, I just presented you with one - and the Chinese would like to speak to you about their intricate word-meaning ;) Sure, it all boils down to a "nuance" problem, but it's a problem nonetheless.

.. Can I survive without this? Sure. Would it be a cool feature? F* yeah.


a "real world" example found in the wild:

    "modal": {
      "option-Orden": "Order",
      "option-Venta": "Sale",
      "option-PosVenda": "After Sale",
      "option-Movil - Prepago": "Pre-paid mobile"
    },
    "selected": {
      "Type-null": "Select a Type",
      "SubArea-null": "Select a SubType",
      "Area-null": "Select a Area",
      "Type-Orden": "@:modal.option-Orden"
      "Type-Venta": "@:modal.option-Venta"
    }

moshmage avatar Aug 02 '17 15:08 moshmage

This can be easily implemented via a TranslateCompiler (see ngx-translate documentation on how to register it). We are using this simple implementation:

export class TranslationCompiler extends TranslateCompiler {
    public compile(value: string, lang: string): string {
        return value;
    }

    public compileTranslations(translations: any, lang: string) {
        for (const key in translations) {
            if (translations.hasOwnProperty(key)) {
                translations[key] = this.resolveReferences(translations[key], translations);
            }
        }
        return translations;
    }

    private resolveReferences(value: string, translations: any) {
        return value.replace(/@:(\S+)/, (matches, key) => this.resolveReferences(translations[key], translations));
    }
}

Please be aware of the following problems you might face with this very simplistic implementation:

  • The compiler does support recursion, but does not check for infinite loops (aka back-and-forth references)
  • The compiler does not support nested translations
  • The compiler does not work with the compile method, as at this point the translations are no longer known (would require to keep track of all translations based on their language and merge them if multiple setTranslation calls for the same translation would be made)
  • Because of obvious syntax restrictions, the compiler can only reference keys which do not contain whitespace-characters

pfeigl avatar Jan 24 '18 15:01 pfeigl

Thanks for the pointers @pfeigl. Your Compiler helped point me in the right direction. However, it wasn't able to parse my complex en.json file. Based off of your direction, I was able to create a recursive filter that works for our needs.

Here's what it looks like:

import { TranslateCompiler } from '@ngx-translate/core';

export class JSONPointerCompiler extends TranslateCompiler {

    /*
    * Needed by ngx-translate
    */
    public compile(value: string, lang: string): string {
        return value;
    }

    /*
    * Triggered once from TranslateCompiler
    * Initiates recurive this.parseReferencePointers()
    * Returns modified translations object for ngx-translate to process
    */
    public compileTranslations(translations: any, lang: string) {
        this.parseReferencePointers(translations, translations);
        return translations;
    }

    /*
     * Triggered once from this.compileTranslations()
     * Recursively loops through an object,
     * replacing any property value that has a string starting with "@APP_CORE." with the APP_CORE global string definition.
     * i.e. @APP_CORE.LOCATION.OVERVIEW becomes Location Overview
     */
    private parseReferencePointers(currentTranslations, masterLanguageFile) {
        Object.keys(currentTranslations).forEach((key) => {
            if (currentTranslations[key] !== null && typeof currentTranslations[key] === 'object') {
                this.parseReferencePointers(currentTranslations[key], masterLanguageFile);
                return;
            }
            if (typeof currentTranslations[key] === 'string') {
                if (currentTranslations[key].includes("@APP_CORE.")) {
                    let replacementProperty = this.getDescendantPropertyValue(masterLanguageFile, currentTranslations[key].substring(1));
                    currentTranslations[key] = replacementProperty;
                }
            }
        });
    }

    /*
     * Takes a string representation of an objects dot notation
     * i.e. "APP_CORE.LABEL.LOCATION"
     * and returns the property value of the input objects property
     */
    private getDescendantPropertyValue(obj, desc) {
        var arr = desc.split(".");
        while(arr.length && (obj = obj[arr.shift()]));
        return obj;
    }

}

AndrewCer avatar Jan 29 '18 23:01 AndrewCer

any implementation planned?

wissog avatar Jul 19 '22 08:07 wissog

any implementation planned?

????

edraft avatar Feb 23 '23 09:02 edraft

Would be really useful.

sovushka-utrom avatar Jun 15 '23 13:06 sovushka-utrom

Would indeed be helpful. I find it easier to organize my translations key close to the way they appear in the components and templates hierarchy. This means i tend to repeat the same translations several times. I would like it to have a shared translations json file and point to it the other files.

Yolo-plop avatar Apr 04 '24 10:04 Yolo-plop