Polyconseil/vue-gettext

xgettext fails with some .vue files [was: makefile ignore strings]

thujone25 opened this issue ยท 19 comments

Hi! I have a Vue component file which are ignored by Makefile. What do I make wrong?
The text appears in the view but doesn't appear in .po files after make makemessages

screenshot from 2017-06-22 17-01-53

I've found out that everything works good if I remove line โ„–7 or โ„–8
But I can't understand why.

kemar commented

Which one is line 7? (it's difficult to help you when the only thing I have is a screenshot)

<template>
  <form class="auth-form"
        :class="{'auth-form--password-pages': (forgotPass || newPass)}">
    <h1 class="auth-form__title">
      <span>{{ headerText }}</span>
    </h1>
    <special-error class="auth-form__errors-block"></special-error>
    <p class="auth-form__forgot-pass-text" v-if="forgotPass">{{ text3 }}</p>
    <slot name="emailField"></slot>
    <slot name="passwordField"></slot>
    <slot name="submitBtn"></slot>
    <em class="auth-form__password-text"
        v-if="newPass"></em>
    <router-link :to="{name: 'SignIn'}" 
                 v-if="forgotPass"
                 class="tt-link tt-link--destroy-action auth-form__forgot-pass-link">{{ text4 }}</router-link>
    <div class="auth-form__remember-forgot-cont" v-if="rememberSection">
      <div class="auth-form__remember-forgot-section">
        <slot name="rememberCheckbox"></slot>
      </div>
      <div class="auth-form__remember-forgot-section auth-form__remember-forgot-section--link">
        <router-link class="tt-link"
                     :to="{name: 'RestorePassword'}">{{ text1 }}</router-link>
      </div>
    </div>
    <social-links :hr-text="text2"
                  v-if="socialLinks"></social-links>
  </form>
</template>

<script>
  export default {
    props: [
      'headerText',
      'paddingRight',
      'rememberSection',
      'socialLinks',
      'forgotPass',
      'newPass'
    ],
    computed: {
      text1() {return this.$gettext('Forgot your password?');},
      text2() {return this.$gettext('or join in with');},
      text3() {return this.$gettext('Enter the e-mail address associated with your account, and we will send you a link to reset your password.');},
      text4() {return this.$gettext('Cancel');},
      text5() {return this.$gettext('8 characters minimum');},
    }
  }
</script>

Lines:

<special-error class="auth-form__errors-block"></special-error>
    <p class="auth-form__forgot-pass-text" v-if="forgotPass">{{ text3 }}</p>
kemar commented

Ok, now I can reproduce and confirm your issue.

I think that xgettext fails during extraction (here). I don't know exactly why but I think this is related to the way xgettext is parsing your .vue file.

By default xgettext cannot extracts strings directly from .vue files. I used a trick in order to make it think it's parsing a JavaScript file.

However, I can see that it's not working for you :'(

Unfortunately I have no easy solution for this problem yet. The ideal solution could be to code a custom extractor for .vue files (see also #9) but I don't have much time yet to implement it.

Hi. I've spotted that wrapping template markup with parentheses solves the problem.
Also it allows us to use Inline expression. from #9
Unfortunately I've not found a solution to use Inline expression for props (<component :prop-name="$gettext('some text')"></component>) yet.
I hope that this small trick with parentheses can help.
So instead of

<template>
  <translate>text 1</translate>
  <p v-translate>text 2</p>
  <p>{{ text3 }}</p>
</template>

<script>
  export default {
    computed: {
      text3() {return this.$gettext('Text 3');}
    }
  }
</script>

we should wrap <template>

(<template>
  <translate>text 1</translate>
  <p v-translate>text 2</p>
  <!-- Inline expression. -->
  <p>{{ $gettext('Text 3') }}</p>
</template>)

It's hack so some testing is needed.

GUI commented

As one other potential option for xgettext parsing of Vue files, I've created gettext-vue which integrates with xgettext-template to do some pretty basic parsing of Vue files.

The parsing is just done with some basic regexes (based on gettext-ejs) that might not handle more complicated parsing situations, but it seems to work well for our use-case. The parsing doesn't take any context into account, so it's just looking for any function calls named $t, $gettext, or $ngettext by default (although those keywords can be configured with xgettext-template). But this simplistic approach does mean it picks up references in both the <template> and <script> sections of Vue files, and it should also pick up the various syntaxes in the templates (eg, {{ $t('Hello') }} or <div v-bind:placeholder="$t('World')">).

So while this might not cover all parsing situations, hopefully it might be useful to others. And if we can improve the parsing for other situations, I'm open to pull requests or feedback.

kemar commented

@GUI thanx for letting us know. This is really interesting.

I did find one bug in the xgettext parsing of .vue files. It has something to do with the amount of '/' the file contains.

Suppose you have the following test.vue file:

<template>
	<div>
		<div></div>
	</div>
</template>

<script>
	export default {
		computed: {
			zoomLabel() {
				return this.$gettext('Zoom');
			}
		}
</script>

Running the command
xgettext --language=JavaScript --keyword=npgettext:1c,2,3 --from-code=utf-8 --join-existing --no-wrap --output translations.pot test.vue will not extract the Zoom string in the translations.pot file.

However when you change the file to one of the following 2 templates, it will work!

<template>
	<div></div>
</template>

<script>
	export default {
		computed: {
			zoomLabel() {
				return this.$gettext('Zoom');
			}
		}
</script>

--> 2 / now because of the removal of one <div>

<template>
	<div>
		<div></div>
	</div>
</template>

<script>
	import foo from './foo';

	export default {
		computed: {
			zoomLabel() {
				return this.$gettext('Zoom');
			}
		}
</script>

--> 4 / now because of the import statement

Do mind I'm not counting the / in </script>.

@thujone25 If I counted well, your template also contains an odd number of /, so I think adding an element would probably resolve the issue, not only removing line 7 or 8. Of course it's not an elegant solution to add extra elements but I don't know anything about xgettext parsers.

@bart-1990 actually I wrap every template with () and I've not faced with problems yet. So I think that () better than counting /

It's still a bug that needs solving. :) Wrapping all my templates in () is not something I'm particularly fond of but indeed it helps.

For webpack users that want to use "$gettext" etc in slots: I've created a webpack loader that plugs behind vue-loader, gets the compiled vue source code, runs GNU xgettext on that source code and emits a pot file for each vue component file. You can also use this for other files that are transpiled to javascript.

https://gist.github.com/narrowtux/6b745e9346db97cec7f122f5bdf9bba4

Putting the <script> tag at the top of vue files solved the missing msgid problem for me.

Is anyone still looking at this? The repo doesn't seem to be maintained anymore? I tried all solutions in this thread (wrapping in (), adding a /, moving the script tag, gettext-vue), but none of them seem to work. Is there a better solution than using vue-gettext right now?

kemar commented

Sorry @pieterjandesmedt I don't have time anymore to contribute to vue-gettext. In fact I haven't worked with Vue.js in over three years. I'm barely merging PR and keeping deps up to date.

mitar commented

@kemar Still thank you for doing that!

@kemar thank you for your great work! I used (and still use) vue-gettext a lot. If you want, I can help you maintain it.

I made a workaround with two oneliners in the makefile:

perl -ne 'print if(/<script>/../<\/script>/)' $(GETTEXT_HTML_SOURCES) | sed 's|<script>||' | sed 's|</script>||' > vue-scripts.tmp.js

This concatenates all javascript of the single file components into one file (named vue-scripts.tmp.js)

sed -n '/\<template\>/,/\<\/template\>/p' $(GETTEXT_HTML_SOURCES) |  perl -ne 'print if (/gettext\(/../\)/)' | sed -E 's|.*([n$$]+)(gettext\(.*\)).*|\1\2|g' > vue-scripts2.tmp.js

This extracts all gettext(-lookalikes from the <template>, so also those in the html attributes into vue-scripts2.tmp.js

Then I add these to files into the xgettext extraction like so:

xgettext --language=JavaScript --keyword=npgettext:1c,2,3 \
		--from-code=utf-8 --join-existing --no-wrap \
		--package-name=$(shell node -e "console.log(require('./package.json').name);") \
		--package-version=$(shell node -e "console.log(require('./package.json').version);") \
		--output $@ $(GETTEXT_JS_SOURCES) vue-scripts.tmp.js vue-scripts2.tmp.js

It's not perfect (e.g. it doesn't detect npgettext in attributes) but it does the trick for me now.

kemar commented

@pieterjandesmedt thx :)

@vperron I'm not able to maintain vue-gettext anymore, what do you think about adding maintainers or transferring ownership?

I'm 100% open to adding external maintainers, as many as possible actually. As long as you've successfully added a PR you should be able to get the maintaining rights. I'll look into it.

@pieterjandesmedt @mitar @GUI @bart-1990 could you please summarize very quickly what's teh topic at hand, I'm not sure anymore what's the issue here :)
I'd recommend to use https://github.com/Polyconseil/easygettext which has been really well maintained and supports everything from Vue 3 templates to Typescript and so on, bu maybe I'm missing the point.

Also, if anyone is up for getting upgraded and get write rights to the repo and help maintain it, feel free !

@kemar thanks a lot for all the good stuff on this for so long !