vsch / laravel-translation-manager

Enhanced Management of Laravel 4 & 5 Translations, with Yandex Translation API assisted translations.
MIT License
181 stars 84 forks source link

how to access translations from javascript #112

Closed vesper8 closed 6 years ago

vesper8 commented 6 years ago

Hey, sorry if this is already documented but I wasn't able to locate the answer

I love your package and have been using it in my apps that use Laravel Blade

But now I am transitioning to using more and more Vue and I am finding myself needing to access these translated strings from inside the javascript

And I am wondering what is the optimal way of accessing what I need from a JS content. I am looking for a way that is not too hard on performance.. any advice would be much appreciated!

Thanks!

vesper8 commented 6 years ago

wheee! Finally!! it all works good now!! hooray

now if you could get around to merging my PR so I don't have to modify the vendor code every time you make an update :)

Otherwise I think you can finally close this issue!

vesper8 commented 6 years ago

ooh no.. boo.. I spoke too fast

almost perfect! But the same problem with the missing JSON keys is still happening

I ran translations:find just now and it found 1 new key

I entered the value for English, and then French.. and then I clicked publish

And it did the same thing as before, where it published it with an empty key in the en.json and fr.json

And inside the json.json file there's also no key, just like before

vesper8 commented 6 years ago

ahh.. but if I publish again.. then it properly filled it

so no big deal.. just gotta publish 2x in a row

vsch commented 6 years ago

I'll try it now. Forgot about that issue. Should not have to publish twice. I have the keys filled in on publish. I merged your PR and will test this to make sure it works.

vesper8 commented 6 years ago

you know there's just one more annoyance that i'm dealing with

in order for translations:find to work.. I've had to name all my keys as JSON.something

but then when I publish this, it creates a file that looks like this:

{
    "about": "About"
}

But what I actually need is for the file to look like

{
    "JSON": {
        "about": "About"
    }
}

Right? Otherwise I have to hack it.. I've been manually editing the files but it gets tedious

Honestly what I would really like is to have group support like LTM normally has.. for php translations.. and not be forced to use a group called "JSON"

Instead I could have a group called "nav" and "ads" and "whatever", just like normally

And then there'd be a button to "export to json"

And what this would do is it would concatenate all my groups into a single en.json that looks like this:

en.json

{
    "nav": {
        "about": "About",
    },
    "ads": {
        "buy": "Buy",
    },
    "whatever": {
        "whee": "Hooray"
    }
}

And then I'd really be in heaven

vsch commented 6 years ago

@vesper8, the issue is that JSON support is made for Laravel JSON files which dictates how the files are named and laid out. Your use case is for vue to use laravel files.

I still don't understand why do you need to specify JSON if vue cannot use Laravel translation files and therefore the group names make no sense. It always grabs the default JSON translations. You should be able to specify just the key.

What needs to be done in your case is a service to take the translations and massage them to your format on publishing. Including translations in the PHP files. Then out of the box you can use all the php translation groups and not have to worry about JSON group at all because it would be generated from the php groups.

The WebUI generates a TranslationsPublished($group, $errors) event after publishing a group or all groups (group is '*' for all groups).

On getting that event you should generate your translation files. They are easy to manipulate since they are nothing but associative arrays. Then you json_encode() your assoc array and write it out to the file for the locale.

With that implementation you can plugin vue javascript into a laravel project and have all the existing translations available for it. It would probably be easier to write this code than to keep manually modifying the file.

I cannot make this conversion standard part of LTM because then it would generate JSON files that Laravel cannot use.

vesper8 commented 6 years ago

Ok.. I might do that

It's not vue that requires this format.. rather.. it's your own translations:find that requires that keys follow the pattern of group.keyname

I tried modifying the default regexp so that it didn't require a group, but then it failed at some later point which I didn't investigate

I'd be happier not having to name all my keys JSON.keyname

vsch commented 6 years ago

@vesper8, if you explained yourself earlier we might have found a better solution.

BTW, Its not my find since I only cleaned it up originally to have less false positives and gave up when I realized that it is not reliable. Now that i merged your PR its more like your find. ;)

The issue with find is that it needs to be constrained otherwise it will trigger on so many false positives you will get tired of deleting them from the database. I got tired of even the improved version and haven't looked at that code it in years. Until it was brought up by you, I forgot it existed.

I don't use it because it misses dynamic key use, which I do a lot. I tend to define things like dialog or message box type translations with a unique key for id, then add -title, -footer, -message, etc. for the different parts of the dialog/message/email. That way to display the information I only need the id and can get the other parts by adding standard suffixes. Hence, the add suffixes button in suffixed keys. It allowed me to quickly create the relevant new keys.

Then I added Missing Key tracking to eliminate key creation all together. My workflow is to create and use translation keys in the code. Display the view, e-mail, etc, once to trigger use of new keys. Then go into LTM and add translations to all the missing keys. The overview shows all groups with missing translations. All these features were added to avoid having to use find.

BTW, the primary locale I fill in with auto-fill, it helps if the keys are relevantly named. Then auto-fill will be close to the desired content 80% of the time. The rest I auto-translate and massage.

In my JavaScript code I intercept missing key event from i18next and accumulate missing translations. Then pump them after a short delay to the server. I also trigger a refresh of the translation table if the group being displayed is the one triggering the missing key event. So I don't even need to refresh the table. Just trigger the GUI with new translations and they appear in my table. In your case you would need to refresh the translation table since your app is running separately but accumulating missing keys and sending them to the server would probably be better and easier than trying to perfect find. It also means that any translations that were not caught by find will at least be caught when you run the app.

It is a 100% fool proof method of getting keys you create during development. It will not add keys that you did not translate but since you are developing you need to try the code at least once so it takes care of the new keys.

It is something to consider implementing then you won't need find and can use whatever keys as Laravel JSON files allow. I am going to fix the empty keys in the code. So that created missing keys will not be an issue.

vesper8 commented 6 years ago

hrmm.. really interesting how you intercept missing keys.. although I'm not sure how you do that exactly.. an example would really help.. how does triggering a view or an email once trigger the interception of untranslated keys exactly?

in my case the find command will always work perfectly since all my translation keys are wrapped in either one of these patterns:

$t('JSON.somekey')
i18n.t('JSON.somekey')
this.$i18n.t('JSON.somekey')

depending on scope

And my find settings are:

    'find' => [
        'functions' => ($functions = [
            '\$t',
            'this.\$i18n.t',
            'i18n.t',
        ]),
        'pattern' => [                              // See http://regexr.com/392hu
            '(' . implode('|', $functions) . ')',   // Must start with one of the functions
            '\\(',                                  // Match opening parentheses
            "(['\"])",                              // Match " or '
            '(',                                    // Start a new group to match:
            '[a-zA-Z0-9_-]+',                       // Must start with group
            "([.][^\1)]+)+",                        // Be followed by one or more items/keys
            ')',                                    // Close group
            "['\"]",                                // Closing quote
            '[\\),]',                               // Close parentheses or new parameter
        ],
        'files' => [
            'vue',
            'js',
        ],
    ],

So it's pretty much impossible for false-positives to be returned. I'm confused how it is that translations:find was getting false-positives in your case since I assume all of your translations are also always wrapped in translation methods that are easily picked up by the regex in the find manager

Anyway in my case it works well enough that I don't have a need for the keys to be preceded by a GROUP.

I tried to comment out the part of the regex that requires the key to be preceded by a group but I think I just didn't do it properly.. I'll try again. However I wonder what will happen if the find command finds keys that have no group.. what group will they be added to in that case? I'd still need for that group tbe "JSON" if I want these to be exportable to json

I'm still confused about why this is a requirement and why you can't just "export any group to JSON" even if the group is called something else

Anyway in the meantime using the TranslationsPublished event sounds like a good enough solution!

BTW thanks for accepting my PR! But I see you haven't tagged a release yet for it.. maybe tomorrow :)

All I did was move the existing logic into the configuration file.. so it's still "yours" ;-) but I'm happy to have contributed to making it a little more flexible

Cheers!

vesper8 commented 6 years ago

I'm trying to use the TranslationsPublished event now. I've created a listener and it's working, but the $event only contains the group, in this case "JSON", and doesn't contain the actual array.

I tried looking into your export function to see if I could reuse it in a way that would let me manipulate the structure.. but I decided that was not very easy.. instead I quickly put this together which is a bit hacky and very specific to my use-case but it works

<?php

namespace App\Listeners;

use Vsch\TranslationManager\Models\Translation;
use Vsch\TranslationManager\Events\TranslationsPublished;

class TranslationsPublishedListener
{
    /**
     * @param Vsch\TranslationManager\Events\TranslationsPublished $event
     */
    public function handle(TranslationsPublished $event)
    {
        if ($event->groups === 'JSON') {
            $locales = [
                'en',
                'fr',
            ];
            foreach ($locales as $locale) {
                $translations = Translation::where('group', $event->groups)
                    ->where('locale', $locale)
                    ->get()
                    ->pluck('value', 'key')
                    ->toArray();

                $translations = ['JSON' => $translations];

                \File::put(base_path("resources/lang/{$locale}.json"), json_encode($translations, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
            }
        }
    }
}
vsch commented 6 years ago

@vesper8, in your case you don't dynamically generate the translation key. I do that a lot in my code. In your case you don't need the group just have to double check the find command to make sure it does not filter out any references that do not have a group.

As for missing keys, it is any translation that equals its key:

Translator.php

This is the reason I love having code I can modify.

I'm still confused about why this is a requirement and why you can't just "export any group to JSON" even if the group is called something else

JSON group in Laravel has special handling and so in LTM. The location of translation files are in locale subdirectories, for JSON in files named with the locale. So for Laravel there is only a single group with JSON translations. No group resolution if using _() translations for JSON. Only keys are specified. At least according to the docs. I don't use this function.

Now it is possible to export any group but then it would still be just one group so there is no reason to call it or not to call it JSON. In your case the only reason you call it JSON is because this is how you extract the keys from source and how you tweak the output files for i18n. It is not an LTM requirement.

I am doing major clean up of Manager code. I have been hacking at it with little tweaks trying to disturb as little as possible. Plus some original recursion in publish code that I have hated since the beginning and finally today I had enough and decided to clean up.

Too many condition plus recursion made it impossible to understand. Now it is more straight forward. Also cleaning up handling of JSON key generation on export. Already no blanks and there is a setting to turn off any garbage as json key so for new keys it will export LTM keys when this mode is set.

Also, now any missing keys get added with whatever key is being used. So export will use the correct key for JSON.

I'll probably release it some time today/early tomorrow. Want to make sure nothing in the import/export is broken first. I could really use having unit tests but I don't have to maintain the code much less write tests for it.

vsch commented 6 years ago

@vesper8, in Translator::getTranslations Translator.php

Takes namespace, which for Laravel means vendor package, group and locale and returns an assoc array of key to translation.

You can get any translations from there. I added this function, which is protected in Laravel translator, so I can get LTM translations for React. You can use that too. Then the translations you are looking for don't need to be in the table.

Nothing wrong with a hack that solves a problem. All of LTM started out mostly as hacked features I needed to address immediate needs then evolved into something that I find hard to believe I spent time creating. ;)

vsch commented 6 years ago

@vesper8, new release is out. Stick a fork in me. I'm done. I think it is all fixed. Just realized that I forgot to add your PR to the version notes. Sorry. It'll be there next git push.

If you have no objections, I will keep your translations in my database so I have some real JSON to test against. Let me know if it presents a problem and I'll ditch them.

vesper8 commented 6 years ago

I have no issue with you keeping my translations :)

Now it is possible to export any group but then it would still be just one group so there is no reason to call it or not to call it JSON. In your case the only reason you call it JSON is because this is how you extract the keys from source and how you tweak the output files for i18n. It is not an LTM requirement.

Is this something new? Last week when I started using JSON as my group, it seemed like the only way to get json files exported was if the group was called JSON.. this totally seemed like a requirement to me.. has this changed? Is it now possible to export any other group to JSON?

vsch commented 6 years ago

@vesper8, what I was referring to is the name used in LTM for Laravel JSON translations. Outside of LTM the group name JSON has no significance and does not appear anywhere in the use of JSON translations.

Laravel JSON translations have no group name and are stored in lng.json files where lng is the locale of the translations.

LTM gives a fixed name to these translations so that they fall in line with LTM expectation of all translations being associated with a group. The choice of this group has no effect on any code using Laravel translations. Only code that deals with translations through LTM internal API.

Your use of JSON in your code is independent of the JSON group name in LTM. This is what I was referring to when I said it could be anything. You can make that anything you want. Access to other Laravel groups is done through Laravel API and whether they can be accessed through JavaScript or not is dictated by Laravel. Including making these groups available in JSON format.

However, conversion from PHP to JSON is a simple matter of loading the translation file and json_encode() and save it in a new or the same location.

If you would like to see standard Laravel translation files also saved in a JSON format then this is something that can be easily added via a configuration item. Something like publish_include_json with an array of group names.

However, the export of these JSON files would be one way. To have JSON files used in import is also doable but more complex and would require code changes. It would also make import use only the JSON file and ignore PHP on import. Export would still export both formats.