├── .babelrc
├── .eslintrc.js
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── README_DEV.md
├── build
├── config.js
├── karma.conf.js
├── release.sh
├── rollup.config.js
├── server-dev.js
├── webpack-dev.config.js
└── webpack-test.config.js
├── dev
├── components
│ ├── alert.vue
│ ├── customTags.vue
│ ├── directive.vue
│ ├── if
│ │ ├── index.js
│ │ └── template.html
│ ├── languageSelect.vue
│ ├── momentFilter.vue
│ ├── multilines.vue
│ └── plural
│ │ ├── index.js
│ │ ├── styles.css
│ │ └── template.html
├── index.js
├── index.tpl.html
├── locale
│ ├── en_GB
│ │ └── LC_MESSAGES
│ │ │ └── app.po
│ ├── fr_FR
│ │ └── LC_MESSAGES
│ │ │ └── app.po
│ └── it_IT
│ │ └── LC_MESSAGES
│ │ └── app.po
├── styles
│ ├── global.css
│ └── variables.css
└── translations.json
├── dist
├── vue-gettext.js
└── vue-gettext.min.js
├── package-lock.json
├── package.json
├── src
├── component.js
├── config.js
├── directive.js
├── index.js
├── interpolate.js
├── localVue.js
├── looseEqual.js
├── object-assign-polyfill.js
├── override.js
├── plurals.js
├── translate.js
└── uuid.js
├── test
├── index.js
├── specs
│ ├── component.spec.js
│ ├── directive.arabic.spec.js
│ ├── directive.spec.js
│ ├── interpolate.spec.js
│ ├── json
│ │ ├── component.json
│ │ ├── directive.arabic.json
│ │ ├── directive.json
│ │ ├── plugin.config.json
│ │ └── translate.json
│ ├── plugin.config.spec.js
│ ├── plurals.spec.js
│ └── translate.spec.js
└── testUtils.js
└── types
├── index.d.ts
├── vue-gettext.d.ts
└── vue.d.ts
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015"],
3 | "plugins": ["transform-runtime"],
4 | "comments": false
5 | }
6 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // http://eslint.org/docs/user-guide/configuring
2 |
3 | module.exports = {
4 |
5 | root: true,
6 |
7 | 'env': {
8 | 'mocha': true, // Used for unit tests.
9 | },
10 |
11 | 'globals': {
12 | 'expect': true, // Used for unit tests.
13 | 'sinon': true, // Used for unit tests.
14 | },
15 |
16 | parserOptions: {
17 | sourceType: 'module'
18 | },
19 |
20 | extends: 'standard',
21 |
22 | plugins: [
23 |
24 | // Required to lint *.vue files.
25 | // https://github.com/BenoitZugmeyer/eslint-plugin-html/tree/40c728#usage
26 | 'html',
27 |
28 | ],
29 |
30 | rules: {
31 |
32 | // https://eslint.org/docs/user-guide/migrating-to-4.0.0#-the-no-multi-spaces-rule-is-more-strict-by-default
33 | 'no-multi-spaces': ['error', {'ignoreEOLComments': true}],
34 |
35 | // Require or disallow trailing commas http://eslint.org/docs/rules/comma-dangle
36 | 'comma-dangle': ['error', 'always-multiline'],
37 |
38 | // Limit multiple empty lines http://eslint.org/docs/rules/no-multiple-empty-lines
39 | 'no-multiple-empty-lines': ['error', { 'max': 2 }],
40 |
41 | // Disable padding within blocks http://eslint.org/docs/rules/padded-blocks.html
42 | 'padded-blocks': 'off',
43 |
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.tmp/
2 | /node_modules/
3 |
4 | npm-debug.log
5 | manifest.json
6 | *.DS_Store
7 | *.pot
8 | *~
9 | .*.swp
10 | .*.pid
11 | *.mo
12 |
13 | .tm_properties
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Polyconseil
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # On OSX the PATH variable isn't exported unless "SHELL" is also set, see: http://stackoverflow.com/a/25506676
2 | SHELL = /bin/bash
3 | NODE_BINDIR = ./node_modules/.bin
4 | export PATH := $(NODE_BINDIR):$(PATH)
5 | LOGNAME ?= $(shell logname)
6 |
7 | # adding the name of the user's login name to the template file, so that
8 | # on a multi-user system several users can run this without interference
9 | TEMPLATE_POT ?= /tmp/template-$(LOGNAME).pot
10 |
11 | # Where to find input files (it can be multiple paths).
12 | INPUT_FILES = ./dev
13 |
14 | # Where to write the files generated by this makefile.
15 | OUTPUT_DIR = ./dev
16 |
17 | # Available locales for the app.
18 | LOCALES = en_GB fr_FR it_IT
19 |
20 | # Name of the generated .po files for each available locale.
21 | LOCALE_FILES ?= $(patsubst %,$(OUTPUT_DIR)/locale/%/LC_MESSAGES/app.po,$(LOCALES))
22 |
23 | GETTEXT_SOURCES ?= $(shell find $(INPUT_FILES) -name '*.jade' -o -name '*.html' -o -name '*.js' -o -name '*.vue' 2> /dev/null)
24 |
25 | # Makefile Targets
26 | .PHONY: clean makemessages translations all
27 |
28 | all:
29 | @echo choose a target from: clean makemessages translations
30 |
31 | clean:
32 | rm -f $(TEMPLATE_POT) $(OUTPUT_DIR)/translations.json
33 |
34 | makemessages: $(TEMPLATE_POT)
35 |
36 | translations: ./$(OUTPUT_DIR)/translations.json
37 |
38 | # Create a main .pot template, then generate .po files for each available language.
39 | # Thanx to Systematic: https://github.com/Polyconseil/systematic/blob/866d5a/mk/main.mk#L167-L183
40 | $(TEMPLATE_POT): $(GETTEXT_SOURCES)
41 | # `dir` is a Makefile built-in expansion function which extracts the directory-part of `$@`.
42 | # `$@` is a Makefile automatic variable: the file name of the target of the rule.
43 | # => `mkdir -p /tmp/`
44 | mkdir -p $(dir $@)
45 | # Extract gettext strings from templates files and create a POT dictionary template.
46 | gettext-extract --quiet --attribute v-translate --output $@ $(GETTEXT_SOURCES)
47 | # Generate .po files for each available language.
48 | @for lang in $(LOCALES); do \
49 | export PO_FILE=$(OUTPUT_DIR)/locale/$$lang/LC_MESSAGES/app.po; \
50 | mkdir -p $$(dirname $$PO_FILE); \
51 | if [ -f $$PO_FILE ]; then \
52 | echo "msgmerge --update $$PO_FILE $@"; \
53 | msgmerge --lang=$$lang --update $$PO_FILE $@ || break ;\
54 | else \
55 | msginit --no-translator --locale=$$lang --input=$@ --output-file=$$PO_FILE || break ; \
56 | msgattrib --no-wrap --no-obsolete -o $$PO_FILE $$PO_FILE || break; \
57 | fi; \
58 | done;
59 |
60 | $(OUTPUT_DIR)/translations.json: $(LOCALE_FILES)
61 | mkdir -p $(OUTPUT_DIR)
62 | gettext-compile --output $@ $(LOCALE_FILES)
63 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vue-gettext
2 |
3 | Translate [Vue.js](http://vuejs.org) applications with [gettext](https://en.wikipedia.org/wiki/Gettext).
4 |
5 | [Live demo](https://polyconseil.github.io/vue-gettext/).
6 |
7 | ## No Vue 3 support
8 |
9 | This project **does not** support Vue 3. Please, have a look at [vue3-gettext](https://github.com/jshmrtn/vue3-gettext) for Vue 3 support.
10 |
11 | ## Contribution
12 |
13 | Please make sure to read the [Pull request guidelines](https://github.com/Polyconseil/vue-gettext/blob/master/README_DEV.md#pull-request-guidelines) before making a pull request.
14 |
15 | ## Known issues
16 |
17 | Any help is greatly appreciated:
18 |
19 | - It could be tricky to parse some `.vue` files, see [#28](https://github.com/Polyconseil/vue-gettext/issues/28)
20 | - Translations in attributes is not supported yet, see [#9](https://github.com/Polyconseil/vue-gettext/issues/9)
21 | - `vue-gettext` is not SSR compliant, see [#51](https://github.com/Polyconseil/vue-gettext/issues/51)
22 |
23 | # Introduction
24 |
25 | `vue-gettext` is a plugin to translate Vue.js applications with [`gettext`](http://www.labri.fr/perso/fleury/posts/programming/a-quick-gettext-tutorial.html). It relies on the [GNU gettext toolset](https://www.gnu.org/software/gettext/manual/index.html) and [`easygettext`](https://github.com/Polyconseil/easygettext).
26 |
27 | ## How does `vue-gettext` work at a high level?
28 |
29 | 1. **Annotating strings**: to make a Vue.js app translatable, you have to annotate the strings you want to translate in your JavaScript code and/or templates.
30 |
31 | 2. **Extracting strings**: once strings are annotated, you have to run extraction tools ([`gettext-extract`](https://github.com/Polyconseil/easygettext#gettext-extract) and some GNU gettext utilities) to run over a Vue.js app source tree and pulls out all strings marked for translation to create a message file. A message file is just a plain-text file with a `.po` file extension, representing a single language, that contains all available translation strings as keys and how they should be represented in the given language.
32 |
33 | 3. **Translating message files**: a translator needs to fill out the translations of each generated `.po` files.
34 |
35 | 4. **Compiling translations**: once all message files have been translated, use [`gettext-compile`](https://github.com/Polyconseil/easygettext#gettext-compile) to make the translated `.po` files usable in a Vue app. This will basically merge all translated `.po` files into a unique `.json` translation file.
36 |
37 | 5. **Dynamically render translated strings to the DOM**: `vue-gettext` currently uses a custom component for this.
38 |
39 | ## What does `vue-gettext` provide?
40 |
41 | - a custom `component` and a custom `directive` to annotate strings in templates and dynamically render translated strings to the DOM
42 |
43 | - a set of methods to annotate strings in JavaScript code and translate them
44 |
45 | - a `language` ViewModel exposed to every Vue instances that you can use to:
46 |
47 | - get all available languages (defined at configuration time)
48 |
49 | - get or set the current language (*initially* defined at configuration time)
50 |
51 | - access whatever you passed to the plugin mixin (defined at configuration time)
52 |
53 | - a global and reactive `language` property added to `Vue.config` you can use to get or set the current language *outside* of Vue instances
54 |
55 | ## What does `vue-gettext` depend on?
56 |
57 | - [`easygettext`](https://github.com/Polyconseil/easygettext)
58 |
59 | - [`gettext-extract`](https://github.com/Polyconseil/easygettext#gettext-extract) to extract annotated strings from template files and produce a `.pot` (Portable Object Template) file.
60 |
61 | - [`gettext-compile`](https://github.com/Polyconseil/easygettext#gettext-compile) to produce the sanitized JSON version of a `.po` file.
62 |
63 | - Some GNU gettext utilities to extract annotated strings from JavaScript files and generate `.po` files
64 |
65 | - [`msgmerge`](https://www.gnu.org/software/gettext/manual/html_node/msgmerge-Invocation.html#msgmerge-Invocation)
66 |
67 | - [`msginit`](https://www.gnu.org/software/gettext/manual/html_node/msginit-Invocation.html#msginit-Invocation)
68 |
69 | - [`msgattrib`](https://www.gnu.org/software/gettext/manual/html_node/msgattrib-Invocation.html#msgattrib-Invocation)
70 |
71 | Those tools should be integrated in your build process. We'll show you an example later.
72 |
73 | # Installation
74 |
75 | ## NPM
76 |
77 | ```javascript
78 | npm install vue-gettext
79 | ```
80 |
81 | ## Basic installation
82 |
83 | Basic installation with ES6 modules:
84 |
85 | ```javascript
86 | // ES6
87 | import Vue from 'vue'
88 | import GetTextPlugin from 'vue-gettext'
89 | import translations from './path/to/translations.json'
90 |
91 | Vue.use(GetTextPlugin, {translations: translations})
92 | ```
93 |
94 | ## Configuration
95 |
96 | There are a number of options you can use to configure the `vue-gettext` plugin:
97 |
98 | | Option | Type | Requirement | Description |
99 | | ------------- | ------------- | ------------- | ------------- |
100 | | `autoAddKeyAttributes` | `{Boolean}` | optional | If `true`, key attributes are auto-generated if not present in your code. See the [`key` documentation](https://vuejs.org/v2/api/#key) and issues [#29](https://github.com/Polyconseil/vue-gettext/issues/29) and [#66](https://github.com/Polyconseil/vue-gettext/issues/66). Default value is `false`. Enable this option only if you know what you're doing. |
101 | | `availableLanguages` | `{Object}` | optional | An object that represents the list of the available languages for the app whose keys are [**local names**](http://www.localeplanet.com/icu/) (e.g. [`en`](https://www.gnu.org/software/gettext/manual/html_node/Language-Codes.html#Language-Codes) or [`en_US`](https://www.gnu.org/software/gettext/manual/html_node/Country-Codes.html#Country-Codes)) and whose values are [**language names**](http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/languagenames.html) used for the display in UI, e.g. `English (United States)`. It's exposed in all Vue instances via `vm.$language.available` |
102 | | `defaultLanguage` | `{String}` | optional | The [**local name**](http://www.localeplanet.com/icu/) of the default language, e.g. `en_US`. This will be the current active language. It's exposed in all Vue instances via `vm.$language.current` |
103 | | `muteLanguages` | `{Array}` | optional | Discard warnings for missing translations for all languages of the list. This is useful to avoid messages from the language used in source code. |
104 | | `languageVmMixin` | `{Object}` | optional | A [**mixin**](https://vuejs.org/v2/guide/mixins.html#Option-Merging) that will be passed to the main `languageVm` instance (exposed via `$language`) that can be used, for example, to add custom computed properties |
105 | | `silent` | `{Boolean}` | optional | Enable or disable logs/warnings for missing translations and untranslated keys. Default value is [`Vue.config.silent`](https://vuejs.org/v2/api/#silent). |
106 | | `translations` | `{Object}` | required | The JSON file of the application's translations (produced by `gettext-compile`). It's exposed as a Vue global property as `Vue.$translations` |
107 |
108 | The key special attribute is primarily used as a hint for Vue's virtual DOM algorithm to identify VNodes when diffing ... Vue uses an algorithm that minimizes element movement and tries to patch/reuse elements of the same type in-place as much as possible.
109 |
110 |
111 |
112 | Example:
113 |
114 | ```javascript
115 | // ES6
116 | import Vue from 'vue'
117 | import GetTextPlugin from 'vue-gettext'
118 | import translations from './path/to/translations.json'
119 |
120 | Vue.use(GetTextPlugin, {
121 | availableLanguages: {
122 | en_GB: 'British English',
123 | en_US: 'American English',
124 | es_US: 'Español',
125 | fr_FR: 'Français',
126 | it_IT: 'Italiano',
127 | },
128 | defaultLanguage: 'fr_FR',
129 | languageVmMixin: {
130 | computed: {
131 | currentKebabCase: function () {
132 | return this.current.toLowerCase().replace('_', '-')
133 | },
134 | },
135 | },
136 | translations: translations,
137 | silent: true,
138 | })
139 | ```
140 |
141 | ## `vm.$language`
142 |
143 | After the plugin initialization, a `languageVm` Vue instance is injected
144 | into every component as `vm.$language`.
145 |
146 | It exposes the following properties:
147 |
148 | - `vm.$language.available`: an object that represents the list of the available languages (defined at configuration time)
149 |
150 | - `vm.$language.current`: the current language (defined at configuration time)
151 |
152 | - whatever you passed to the plugin mixin
153 |
154 | You can use `vm.$language.current` and `vm.$language.available` to e.g. easily build a language switch component with a single template:
155 |
156 | ```html
157 |
158 |
159 |
162 |
163 |
164 | ```
165 |
166 | ## `Vue.config.language`
167 |
168 | After the plugin initialization, a global and reactive `language` property is added to `Vue.config` that you can use to get or set the current language outside of Vue instances.
169 |
170 | ```javascript
171 | > Vue.config.language
172 | 'en_GB'
173 | > Vue.config.language = 'fr_FR'
174 | ```
175 |
176 | You can use `Vue.config.language` to e.g. configure a third party plugin in a filter:
177 |
178 | ```javascript
179 | import moment from 'moment'
180 | import Vue from 'vue'
181 |
182 | const dateFormat = function (value, formatString) {
183 | moment.locale(Vue.config.language)
184 | return moment(value).format(arguments.length > 1 ? formatString : 'dddd D MMMM HH:mm:ss')
185 | }
186 | ```
187 |
188 | # Workflow
189 |
190 | 1. Annotate your strings
191 |
192 | 2. Extract translations (`make makemessages`)
193 |
194 | 3. Translate message files
195 |
196 | 4. Compile translations (`make translations`)
197 |
198 | ```
199 | Annotate | Extract | Translate | Compile
200 | --------------------------------------------------------------------------------------------------------
201 | component.js
202 | component.vue ---> /tmp/template.pot ---> app/locale/fr_FR/LC_MESSAGES/app.po ---> app/translations.json
203 | template.html
204 | ```
205 |
206 | ## 1a) Annotating strings in templates (`.html` or `.vue` files)
207 |
208 | ### Use the component or the directive
209 |
210 | Strings are marked as translatable in your templates using either the `translate` component or the `v-translate` directive:
211 |
212 | ```html
213 | Hello!
214 | Hello!
215 | ```
216 |
217 | This will automatically be translated. For instance, in French, it might read *Bonjour !*.
218 |
219 | #### Singular
220 |
221 | ```html
222 | Hello!
223 | ```
224 |
225 | #### Plural
226 |
227 | ```html
228 | %{ count } car
229 | ```
230 |
231 | #### Context
232 |
233 | ```html
234 | Foo
235 | ```
236 |
237 | #### Comment
238 |
239 | ```html
240 | Foo
241 | ```
242 |
243 | #### Custom parameters
244 |
245 | You can set up translation strings that are agnostic to how your app state is structured. This way you can change variable names within your app, it won't break your translation strings.
246 |
247 | ```html
248 | Foo %{name}
249 | ```
250 |
251 | ### HTML support: difference between the component and the directive
252 |
253 | It proves to be tricky to support interpolation with HTML content in Vue.js **components** because it's hard to access the raw content of the component itself.
254 |
255 | So if you need to include HTML content in your translations you may use the **directive**.
256 |
257 | The directive has the same set of capabilities as the component, **except for translate-params** which should be passed in as an expression.
258 |
259 | ```html
260 |
266 | %{ count } car
267 |
268 | ```
269 |
270 | ### Custom HTML tag for the `translate` component
271 |
272 | When rendered, the content of the `translate` component will be wrapped in a `span` element by default. You can also use another tag:
273 |
274 | ```html
275 | Hello!
276 | ```
277 |
278 | ### Interpolation support
279 |
280 | Since [interpolation inside attributes are deprecated](https://vuejs.org/v2/guide/syntax.html#Attributes) in Vue 2, we have to use another set of delimiters. Instead of the "Mustache" syntax (double curly braces), we use `%{` and `}`:
281 |
282 | ```html
283 | Hello %{ name }
284 | ```
285 |
286 | ### Directive, interpolation and raw HTML in data
287 |
288 | Raw HTML in data is interpreted as plain text, not HTML. In order to output real HTML, you will need to use the `render-html` attribute and set it to `true`.
289 |
290 | ```html
291 |
297 | ```
298 |
299 | Dynamically rendering arbitrary HTML on your website can be very dangerous because it can easily lead to XSS vulnerabilities. Only use HTML `render-html="true"` on trusted content and never on user-provided content.
300 |
301 | ### Caveats
302 |
303 | #### Caveat when using `v-translate` with interpolation
304 |
305 | It's not possible (yet) to detect changes on the parent component's data, so you have to add an expression to the directive to provide a changing binding value. This is so that it can do a comparison on old and current value before running the translation in its `update` hook.
306 |
307 | It is described in the [official guide](https://vuejs.org/v2/guide/custom-directive.html#Hook-Functions):
308 |
309 | > update: called after the containing component has updated, but possibly before its children have updated. The directive's value may or may not have changed, but you can skip unnecessary updates by comparing the binding's current and old values...
310 |
311 | ```html
312 |
318 | %{ count } %{brand} car
319 |
320 | ```
321 |
322 | #### Caveat when using either the component `` or directive `v-translate` with interpolation inside `v-for`
323 |
324 | It's not possible (yet) to access the scope within `v-for`, example:
325 |
326 | ```html
327 |
328 | Hello %{name}
329 | Hello %{name}
330 |
331 | ```
332 |
333 | Will result in all `Hello %{name}` being rendered as `Hello name`.
334 |
335 | You need to pass in custom parameters for it to work:
336 |
337 | ```html
338 |
339 | Hello %{name}
340 | Hello %{name}
341 |
342 | ```
343 |
344 | #### Caveat when using `v-translate` with Vue components or Vue specific attributes
345 |
346 | It's not possible (yet) to support components or attributes like `v-bind` and `v-on`. So make sure that your HTML translations stay basic for now.
347 |
348 | For example, this is *not supported*:
349 |
350 | ```html
351 |
352 | Please here to view
353 |
354 | ```
355 |
356 | ## 1b) Annotating strings in JavaScript code (`.js` or `.vue` files)
357 |
358 | Strings are marked as translatable in your Vue instances JavaScript code using methods attached to `Vue.prototype`.
359 |
360 | ### Singular
361 |
362 | ```javascript
363 | vm.$gettext(msgid)
364 | ```
365 |
366 | ### Plural
367 |
368 | ```javascript
369 | vm.$ngettext(msgid, plural, n)
370 | ```
371 |
372 | ### Context
373 |
374 | ```javascript
375 | vm.$pgettext(context, msgid)
376 | ```
377 |
378 | ### Context + Plural
379 |
380 | ```javascript
381 | vm.$npgettext(context, msgid, plural, n)
382 | ```
383 |
384 | ### Interpolation support
385 |
386 | You can use interpolation in your JavaScript using another method attached to `Vue.prototype`: `vm.$gettextInterpolate`.
387 |
388 | ```javascript
389 | ...
390 | methods: {
391 | alertPlural (n) {
392 | let translated = this.$ngettext('%{ n } foo', '%{ n } foos', n)
393 | let interpolated = this.$gettextInterpolate(translated, {n: n})
394 | return window.alert(interpolated)
395 | },
396 | },
397 | ...
398 | ```
399 |
400 | `vm.$gettextInterpolate` dynamically populates a translation string with a given context object.
401 |
402 | ## 2) Extracting strings
403 |
404 | This should be a step in your build process and this can be done in several ways.
405 |
406 | Here are the things we must do:
407 |
408 | 1. extracting annotated strings from templates (`.html` and/or `.vue` files),
409 |
410 | 2. extracting annotated strings from JavaScript code (`.js` and/or `.vue` files),
411 |
412 | 3. creating a main `.pot` template based on the extracted strings,
413 |
414 | 4. creating editable `.po` files for each available language.
415 |
416 | You'll need to install [`easygettext`](https://github.com/Polyconseil/easygettext) and use `gettext-extract` to extract annotated strings from template files and produce a `.pot` file.
417 |
418 | You'll also need some GNU gettext utilities, namely `msgmerge`, `msginit` and `msgattrib` to generate `.po` files from the `.pot` dictionary file.
419 |
420 | We use a `Makefile` with a `makemessages` target to automate this step. To give you an example, I included a `Makefile` with a `makemessages` target in this project that you can include in your build process.
421 |
422 | Extracting strings and generating `.po` files becomes as easy as running:
423 |
424 | ```shell
425 | make makemessages
426 | ```
427 |
428 | ## 3) Translating message files
429 |
430 | The translator needs to fill out the translations of each generated `.po` files.
431 |
432 | This can be done by you or outsourced to other firms or individuals since `.po` files are the industry standard for multilingual websites.
433 |
434 | There is also a wide range of translation tools available in the gettext ecosystem. Some of them are listed on [Wikipedia](https://en.wikipedia.org/wiki/Gettext#See_also).
435 |
436 | ## 4) Compiling translations
437 |
438 | This step focuses on making the translated `.po` files usable in your Vue.js app.
439 |
440 | Once translated, install `easygettext` and use [`gettext-compile`](https://github.com/Polyconseil/easygettext#gettext-compile) to merge all translated `.po` files into a unique `.json` translation file.
441 |
442 | Embed the `.json` translation file back into your application. This is done only one time at `vue-gettext` configuration time.
443 |
444 | We use a `Makefile` with a `translations` target to automate this step.
445 |
446 | Compiling translations becomes as easy as running:
447 |
448 | ```shell
449 | make translations
450 | ```
451 |
452 | Look at the included `Makefile` for an example.
453 |
454 | # Usage of `translate` without Vue
455 |
456 | For convenience `translate` can be imported directly in JavaScript files for cases where you need the translations from `translations.json` outside of `vue-gettext` (see [#113](https://github.com/Polyconseil/vue-gettext/pull/113)):
457 |
458 | ```js
459 | import {translate} from 'vue-gettext';
460 |
461 | const {gettext: $gettext, gettextInterpolate} = translate;
462 |
463 | const str = $gettext('Hello, %{name}');
464 | const strFR = $gettext('Hello, %{name}', 'fr');
465 | const interpolated = gettextInterpolate(str, { name: 'Jerom' })
466 | ```
467 |
468 | # Elsewhere
469 |
470 | ## Support for Pug templates
471 |
472 | If you are using a template language, i.e. [Pug.js](https://pugjs.org/api/getting-started.html) in [Single File Component](https://vuejs.org/v2/guide/single-file-components.html) within a webpack setup (using vue-loader), have a look at [vue-webpack-gettext](https://github.com/kennyki/vue-webpack-gettext).
473 |
474 | # Credits
475 |
476 | This plugin was inspired by:
477 |
478 | - [`systematic`](https://github.com/Polyconseil/systematic) for Makefile and
479 | extraction of translatable strings.
480 | - [`angular-gettext`](https://angular-gettext.rocketeer.be)
481 | - [`vue-i18n`](https://github.com/kazupon/vue-i18n)
482 |
483 | # License
484 |
485 | [MIT](http://opensource.org/licenses/MIT)
486 |
--------------------------------------------------------------------------------
/README_DEV.md:
--------------------------------------------------------------------------------
1 | # vue-gettext
2 |
3 | ## Table of contents
4 |
5 | - [Project Structure](#project-structure)
6 | - [Dev setup](#dev-setup)
7 | - [Pull request guidelines](#pull-request-guidelines)
8 | - [Implementation notes](#implementation-notes)
9 | - [Dev setup notes](#dev-setup-notes)
10 |
11 | ## Project structure
12 |
13 | ```
14 | .
15 | ├── build/ # Build and environment config files.
16 | ├── dev/ # Files used for the development of the plugin.
17 | ├── dist/ # The production version of the plugin.
18 | ├── src/ # Source code of the plugin.
19 | ├── test/ # Unit tests.
20 | ├── .babelrc # Babel config
21 | ├── .eslintrc.js # Eslint config
22 | ├── Makefile # A Makefile to extract translations and generate .po files
23 | └── package.json # Build scripts and dependencies
24 | ```
25 |
26 | ## Dev setup
27 |
28 | Node v10+ is required for development.
29 |
30 | ```shell
31 | # install deps
32 | npm install
33 |
34 | # serve examples at localhost:8080
35 | npm run dev
36 |
37 | # lint & run all tests
38 | npm run test
39 | ```
40 |
41 | ## Pull request guidelines
42 |
43 | [Inspired by Vue](https://github.com/vuejs/vue/blob/299ecfc19fa0f59effef71d24686bd7eb70ecbab/.github/CONTRIBUTING.md#pull-request-guidelines).
44 |
45 | - explain why/what you are doing in the PR description, so that anybody can quickly understand what you want
46 | - all development should be done in dedicated branches
47 | - do not touch files in `dist` because they are automatically generated at release time
48 | - add accompanying test case(s)
49 | - make sure `npm test` passes
50 |
51 | ## Implementation notes
52 |
53 | ### Version number
54 |
55 | I changed the plugin version number to 2.x to match Vue.js 2.0 version.
56 |
57 | ### New component tag name
58 |
59 | By popular demand, `` has been renamed ``.
60 |
61 | ### Interpolation in the `` component
62 |
63 | Interpolation inside attributes are deprecated in Vue 2. See my question on
64 | the Vue.js forum:
65 |
66 | > [What is the Vue 2 vm.$interpolate alternative?](https://forum.vuejs.org/t/what-is-the-vue-2-vm-interpolate-alternative/2866)
67 |
68 | This breaks the old `vue-gettext 1` component.
69 |
70 | The solution I have reached is to use a set of custom delimiters for
71 | placeholders in component templates together with a custom interpolation
72 | function, e.g.:
73 |
74 | ```
75 | Hello %{name}, I am the translation key!
76 | ↓
77 | Hello %{name}, I am the translation key!
78 | ↓
79 | Hello John, I am the translation key!
80 | ```
81 |
82 | Drawbacks:
83 |
84 | - `vue-gettext 2` works only with Vue 2.0
85 | - it add a minimal hook to your templates code
86 |
87 | But it works very well while waiting for something better. Practicality beats
88 | purity I guess.
89 |
90 | ## Dev setup notes
91 |
92 | My notes about the plugin setup.
93 |
94 | I wanted to explore the Webpack ecosystem and some choices made in
95 | [the Webpack template](https://github.com/vuejs-templates/webpack/)
96 | for [vue-cli](https://github.com/vuejs/vue-cli).
97 |
98 | `npm`'s `package.json`:
99 |
100 | - [package.json](https://docs.npmjs.com/files/package.json)
101 | - [devDependencies](https://docs.npmjs.com/files/package.json#devdependencies)
102 |
103 | ### 1) Webpack and HMR (Hot Module Replacement)
104 |
105 | ```
106 | express
107 | html-webpack-plugin
108 | webpack
109 | webpack-dev-middleware
110 | webpack-hot-middleware
111 | ```
112 |
113 | ```
114 | npm install --save-dev express html-webpack-plugin webpack webpack-dev-middleware webpack-hot-middleware
115 | ```
116 |
117 | Webpack [is a module bundler](https://webpack.github.io/docs/what-is-webpack.html).
118 | It takes a bunch of files (JS, CSS, Images, HTML etc.), treating each as a
119 | module, figuring out the dependencies between them, and bundle them into
120 | static assets that are ready for deployment.
121 |
122 | ["Hot Module Replacement" (HMR)](https://webpack.github.io/docs/hot-module-replacement.html)
123 | is a Webpack **development** feature to inject updated modules into the active
124 | runtime.
125 |
126 | There are [3 ways to set up HMR](http://andrewhfarmer.com/3-ways-webpack-hmr/).
127 | We use [`webpack-hot-middleware`](https://github.com/glenjamin/webpack-hot-middleware/)
128 | to run a Webpack dev server with HMR inside an [`express`](https://expressjs.com)
129 | server. Compilation should be faster because the packaged files are written
130 | to memory rather than to disk.
131 |
132 | [In Express, a middleware](http://expressjs.com/en/guide/writing-middleware.html)
133 | is a function that receives the request and response objects of an HTTP
134 | request/response cycle. It may modify (transform) these objects before passing
135 | them to the next middleware function in the chain. It may decide to write to
136 | the response; it may also end the response without continuing the chain.
137 |
138 | [The Webpack Hot Middleware](https://github.com/glenjamin/webpack-hot-middleware/blob/04f953/README.md#how-it-works)
139 | installs itself as a Webpack plugin, and listens for compiler events.
140 | Each connected client gets a
141 | [Server Sent Events](https://www.html5rocks.com/en/tutorials/eventsource/basics/)
142 | connection, the server will publish notifications to connected clients on
143 | compiler events. When the client receives a message, it will check to see
144 | if the local code is up to date. If it isn't up to date, it will trigger
145 | Webpack Hot Module Replacement.
146 |
147 | HMR is opt-in, so we also need to put some code at chosen points of our
148 | application. This had not yet been done since we have to dive into the
149 | HMR JavaScript API (but state preserving hot-reload is implemented in
150 | [`vue-webpack-boilerplate`](https://github.com/vuejs-templates/webpack/)).
151 |
152 | The [HTML Webpack Plugin](https://github.com/ampedandwired/html-webpack-plugin)
153 | will generate an `index.html` entry point to our application, and auto inject
154 | our Webpack bundles. This is especially useful for multiple environment builds,
155 | to stop the HTML getting out of sync between environments, avoiding
156 | hard-written paths and simplifying the cache busting process. Here's how the
157 | entry point is automatically added: in Webpack there is a `make` plugin hook
158 | on the compilation in which entry points can be added ; see
159 | [this](https://github.com/webpack/webpack/issues/536#issuecomment-121316002),
160 | [this](https://github.com/webpack/webpack/blob/fb7958/lib/SingleEntryPlugin.js#L19-L21)
161 | and [this](https://github.com/ampedandwired/html-webpack-plugin/blob/62c9e7/lib/compiler.js#L52).
162 |
163 | Note: the Webpack template
164 | [serves static files with Express](http://expressjs.com/en/starter/static-files.html).
165 |
166 | - [Webpack — Concepts](https://webpack.js.org/concepts/)
167 | - [Webpack — Configuration](https://webpack.js.org/configuration/)
168 | - [Webpack — The Missing Tutorial](https://github.com/shekhargulati/52-technologies-in-2016/blob/master/36-webpack/README.md)
169 | - [Webpack — The Confusing Parts](https://medium.com/@rajaraodv/webpack-the-confusing-parts-58712f8fcad9)
170 | - [Webpack + Express — The simplest Webpack and Express setup](https://alejandronapoles.com/2016/03/12/the-simplest-webpack-and-express-setup/)
171 | - [HMR — Hot Module Replacement with Webpack](http://webpack.github.io/docs/hot-module-replacement-with-webpack.html)
172 | - [HMR — Understanding Webpack HMR](http://andrewhfarmer.com/understanding-hmr/)
173 | - [HMR — Webpack HMR Tutorial](http://andrewhfarmer.com/webpack-hmr-tutorial/)
174 | - [HMR — Webpack Middleware and Hot Module Replacement](https://alejandronapoles.com/2016/03/12/webpack-middleware-and-hot-module-replacement/)
175 |
176 | ### 2) Splitting the Webpack configuration for multiple environments
177 |
178 | There are several ways of splitting the Webpack configuration for multiple
179 | environments.
180 |
181 | Some people prefer maintaining configuration
182 | [within a single file and branch there](http://survivejs.com/webpack/developing-with-webpack/splitting-configuration/),
183 | other prefer [partial configurations files](https://github.com/vuejs-templates/webpack/tree/94e921/template/build)
184 | that then can be merged together using specialized tools like
185 | [`webpack-merge`](https://github.com/survivejs/webpack-merge).
186 |
187 | We took a different approach here: one file per environment, because we don't
188 | provide production environment so we just have one file for the development
189 | environment. The distribution build is made with Rollup (cf section 11 of this
190 | file).
191 |
192 | The current environment is set in an environment variable. Node.js provides the
193 | [`process.env` property](https://nodejs.org/api/process.html#process_process_env)
194 | containing the user environment and [`NODE_ENV`](https://stackoverflow.com/a/16979503)
195 | is an environment variable made popular by the Express webserver framework.
196 |
197 | If `NODE_ENV` is not set explicitly, it will be undefined. So we explicitly set
198 | it in JavaScript for the Express application, e.g.:
199 | `process.env.NODE_ENV = 'development'`.
200 |
201 | Sometimes we also need to use environment variables (or other constants) in the
202 | client code. They can be exposed as global constants via
203 | [`webpack.DefinePlugin`](https://webpack.github.io/docs/list-of-plugins.html#defineplugin).
204 |
205 | - [NodeJs Best Practices: Environment-Specific Configuration](http://eng.datafox.co/nodejs/2014/09/28/nodejs-config-best-practices/)
206 | - [Simple production environment with Webpack and Express](https://alejandronapoles.com/2016/09/29/simple-production-environment-with-webpack-and-express/)
207 | - [How to load different .env.json files into the app depending on environment](https://forum-archive.vuejs.org/topic/3838/how-to-load-different-env-json-files-into-the-app-depending-on-environment)
208 | - [Why does Webpack's DefinePlugin require us to wrap everything in JSON.stringify?](https://stackoverflow.com/q/39564802)
209 |
210 | ### 3) Linting with ESLint
211 |
212 | ```
213 | eslint
214 | eslint-config-standard
215 | eslint-plugin-promise // Required by `eslint-config-standard`.
216 | eslint-plugin-standard // Required by `eslint-config-standard`.
217 | ```
218 |
219 | ```
220 | npm install --save-dev eslint eslint-config-standard eslint-plugin-promise eslint-plugin-standard
221 | ```
222 |
223 | We use the Standard preset with some small customizations, see rules
224 | in `.eslintrc.js`.
225 |
226 | Note: I'm using the
227 | [JavaScript ESLint TextMate Bundle](https://github.com/natesilva/javascript-eslint.tmbundle),
228 | in a personal capacity.
229 |
230 | - [ESLint](http://eslint.org)
231 | - [Configuring ESLint](http://eslint.org/docs/user-guide/configuring)
232 | - [JavaScript Standard Style](http://standardjs.com)
233 | - [`eslint-config-standard` - ESLint Shareable Config](https://github.com/feross/eslint-config-standard/blob/c879df/README.md)
234 |
235 | ### 4) Linting with Webpack
236 |
237 | ```
238 | eslint-loader
239 | eslint-friendly-formatter
240 | ```
241 |
242 | ```
243 | npm install --save-dev eslint-loader eslint-friendly-formatter
244 | ```
245 |
246 | - [Linting in Webpack](http://survivejs.com/webpack/advanced-techniques/linting/)
247 | - [`eslint-loader`](https://github.com/MoOx/eslint-loader/blob/81d743/README.md)
248 | - [`eslint-friendly-formatter`](https://github.com/royriojas/eslint-friendly-formatter/blob/f83e20/README.md)
249 |
250 | ### 5) Babel
251 |
252 | ```
253 | babel-core // Babel compiler core.
254 | babel-loader // Allows transpiling JavaScript files using Babel and Webpack.
255 | babel-preset-es2015 // ES6/ES2015 support.
256 | babel-plugin-transform-runtime // Avoid repeated inclusion of Babel's helper functions.
257 | ```
258 |
259 | ```
260 | npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-plugin-transform-runtime
261 | ```
262 |
263 | See the Babel configuration in
264 | [`.babelrc`](https://babeljs.io/docs/usage/babelrc/).
265 |
266 | - [Setting up ES6](https://leanpub.com/setting-up-es6/read)
267 | - [Babel](https://babeljs.io)
268 | - [Clearing up the Babel 6 Ecosystem](https://medium.com/@jcse/clearing-up-the-babel-6-ecosystem-c7678a314bf3#.4d6dujqy6)
269 | - [Using ES6 and ES7 in the Browser, with Babel 6 and Webpack](http://jamesknelson.com/using-es6-in-the-browser-with-babel-6-and-webpack/)
270 | - [The Six Things You Need To Know About Babel 6](http://jamesknelson.com/the-six-things-you-need-to-know-about-babel-6/)
271 |
272 | ### 6) Vue.js
273 |
274 | ```
275 | vue
276 | ```
277 |
278 | ```
279 | npm install --save-dev vue
280 | ```
281 |
282 | We use [the standalone build](https://vuejs.org/guide/installation.html#Standalone-vs-Runtime-only-Build)
283 | which includes the compiler and supports the template option.
284 |
285 | ### 7) Vue loader
286 |
287 | ```
288 | vue-loader
289 | vue-template-compiler
290 | eslint-plugin-html
291 | ```
292 |
293 | ```
294 | npm install --save-dev vue-loader vue-template-compiler eslint-plugin-html
295 | ```
296 |
297 | `vue-loader` is a loader for Webpack that can transform Vue components into a
298 | plain JavaScript module.
299 |
300 | Since [Vue 2.1.0](https://github.com/vuejs/vue/releases/tag/v2.1.0), `vue-template-compiler` is a peer dependency of `vue-loader` instead of a direct dependency.
301 |
302 | We also need the [`eslint-html-plugin`](https://github.com/BenoitZugmeyer/eslint-plugin-html)
303 | with supports extracting and linting the JavaScript inside `*.vue` files and
304 | [enable it](https://github.com/BenoitZugmeyer/eslint-plugin-html/tree/40c728#usage)
305 | in the `.eslintrc.js` config file.
306 |
307 | - [`vue-loader`](https://github.com/vuejs/vue-loader/)
308 | - [Vue Loader doc](https://vue-loader.vuejs.org/en/index.html)
309 |
310 | ### 8) Common loaders
311 |
312 | ```
313 | html-loader
314 | json-loader
315 | ```
316 |
317 | ```
318 | npm install --save-dev html-loader json-loader
319 | ```
320 |
321 | ### 9) CSS
322 |
323 | ```
324 | style-loader // Adds CSS to the DOM by injecting a style tag.
325 | css-loader
326 | postcss-loader
327 | postcss-cssnext
328 | postcss-import
329 | ```
330 |
331 | ```
332 | npm install --save-dev css-loader style-loader postcss-loader postcss-cssnext postcss-import
333 | ```
334 |
335 | This is how I use scoped CSS in components for the development of the plugin:
336 |
337 | Component's styles are locally scoped in each of them to avoid class name
338 | conflicts. This is done via
339 | [`css-loader`'s *Local scope*](https://github.com/webpack/css-loader/tree/22f662#local-scope).
340 |
341 | The [`PostCSS-cssnext`](http://cssnext.io) syntax is used across components:
342 | it's a [PostCSS](https://github.com/postcss/postcss#readme) plugin that let us
343 | use the latest CSS syntax today.
344 |
345 | [`postcss-import`](https://github.com/postcss/postcss-import) lets us import
346 | CSS variables like this: `@import './styles/variables.css';`.
347 |
348 | There are other way to scope CSS:
349 |
350 | - [The End of Global CSS](https://medium.com/seek-developers/the-end-of-global-css-90d2a4a06284#.p75rvxr1x)
351 | - [Local Scope in CSS](http://mattfairbrass.com/2015/08/17/css-achieving-encapsulation-scope/)
352 | - [CSS Modules in vue-loader](https://vue-loader.vuejs.org/en/features/css-modules.html)
353 |
354 | Note: CSS are buried inside our Javascript bundles by default. We can use the
355 | `ExtractTextPlugin` to extracts them into external `.css` files in a
356 | production environment.
357 |
358 | ### 10) Unit tests
359 |
360 | ```
361 | karma
362 | mocha
363 | karma-mocha
364 | puppeteer
365 | karma-chrome-launcher
366 | chai // Required by `karma-sinon-chai`.
367 | sinon // Required by `karma-sinon-chai`.
368 | sinon-chai // Required by `karma-sinon-chai`.
369 | karma-sinon-chai
370 | karma-webpack
371 | ```
372 |
373 | ```
374 | npm install --save-dev karma mocha karma-mocha karma-chrome-launcher chai sinon sinon-chai karma-sinon-chai karma-webpack
375 | ```
376 |
377 | [Karma](https://karma-runner.github.io/1.0/intro/installation.html) is a
378 | JavaScript command line tool that can be used to spawn a web server which
379 | loads application's source code, executes tests and reports the results.
380 | It runs on Node.js and is available as an NPM package.
381 |
382 | [Mocha](https://mochajs.org) is the test framework that we write test specs
383 | with. `karma-mocha` lets Karma use Mocha as the test framework.
384 |
385 | [`karma-chrome-launcher`](https://github.com/karma-runner/karma-chrome-launcher) lets Karma run tests with [Headless Chrome](https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md). [Puppeteer](https://github.com/GoogleChrome/puppeteer) is a Node library which provides a high-level API to control headless Chrome or Chromium over the DevTools Protocol.
386 |
387 | [Chai](http://chaijs.com) and [Sinon](http://sinonjs.org)
388 | ([Sinon-Chai](https://github.com/domenic/sinon-chai)) are integrated using
389 | [`karma-sinon-chai`](https://github.com/kmees/karma-sinon-chai), so all Chai
390 | interfaces (`should`, `expect`, `assert`) and `sinon` are globally available
391 | in test files. Just let `.eslintrc` know about them.
392 |
393 | [`karma-webpack`](https://github.com/webpack/karma-webpack) uses Webpack to
394 | preprocess files in Karma.
395 |
396 | - Cheatsheets: [mocha](http://ricostacruz.com/cheatsheets/mocha.html) [chai](http://ricostacruz.com/cheatsheets/chai.html) [sinon](http://ricostacruz.com/cheatsheets/sinon.html) [sinon-chai](http://ricostacruz.com/cheatsheets/sinon-chai.html)
397 | - [The Ultimate Unit Testing Cheat-sheet](https://gist.github.com/yoavniran/1e3b0162e1545055429e)
398 |
399 | TODO: reporters and coverage.
400 |
401 | ### 11) Using `rollup` for packaging
402 |
403 | ```
404 | rollup
405 | buble
406 | rollup-plugin-buble
407 | rollup-plugin-commonjs
408 | uglify-js
409 | ```
410 |
411 | ```
412 | npm install --save-dev rollup buble rollup-plugin-buble rollup-plugin-commonjs
413 | ```
414 |
415 | Using [Rollup](https://github.com/rollup/rollup) for packaging
416 | [seems fast](https://twitter.com/vuejs/status/666316850863714304).
417 |
418 | [Rollup plugins](https://github.com/rollup/rollup/wiki/Plugins) change the
419 | behaviour of Rollup at key points in the bundling process.
420 |
421 | - [rollup-plugin-buble](https://gitlab.com/Rich-Harris/rollup-plugin-buble) for [buble](https://gitlab.com/Rich-Harris/buble)
422 | - [rollup-plugin-commonjs](https://github.com/rollup/rollup-plugin-commonjs)
423 |
--------------------------------------------------------------------------------
/build/config.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 |
3 | module.exports = {
4 | build: {
5 | bundleRoot: path.resolve(__dirname, '../dist'), // Where to emit the Webpack bundle.
6 | },
7 | dev: {
8 | bundlePublicPath: '/',
9 | port: 8080,
10 | // The env's keys will be passed to `webpack.DefinePlugin`.
11 | // https://webpack.github.io/docs/list-of-plugins.html#defineplugin
12 | // https://stackoverflow.com/q/39564802
13 | env: {
14 | NODE_ENV: JSON.stringify('development'),
15 | },
16 | },
17 | test: {
18 | // The env's keys will be passed to `webpack.DefinePlugin`.
19 | // https://webpack.github.io/docs/list-of-plugins.html#defineplugin
20 | // https://stackoverflow.com/q/39564802
21 | env: {
22 | NODE_ENV: JSON.stringify('testing'),
23 | },
24 | },
25 | }
26 |
--------------------------------------------------------------------------------
/build/karma.conf.js:
--------------------------------------------------------------------------------
1 | var webpackTestConfig = require('./webpack-test.config')
2 |
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | browsers: ['ChromeHeadless'],
7 | frameworks: ['mocha', 'sinon-chai'],
8 | files: ['../test/index.js'],
9 | preprocessors: {
10 | '../test/index.js': ['webpack'],
11 | },
12 | webpack: webpackTestConfig,
13 | webpackMiddleware: {
14 | noInfo: true,
15 | },
16 | })
17 | }
18 |
--------------------------------------------------------------------------------
/build/release.sh:
--------------------------------------------------------------------------------
1 | # set -e tells bash that it should exit the script if any statement returns a non-true return value.
2 | set -e
3 | echo "Enter release version: "
4 | read VERSION
5 |
6 | read -p "Releasing $VERSION - are you sure? (y/n)" -n 1 -r
7 | echo # (optional) move to a new line
8 |
9 | if [[ $REPLY =~ ^[Yy]$ ]]
10 | then
11 | echo "Releasing $VERSION ..."
12 | npm test
13 | VERSION=$VERSION npm run build
14 |
15 | git add -A
16 | git commit -m "[build] $VERSION"
17 | npm login
18 | npm version $VERSION --message "[release] $VERSION"
19 |
20 | # publish
21 | git push origin refs/tags/v$VERSION
22 | git push
23 | npm publish
24 | npm logout
25 | fi
26 |
--------------------------------------------------------------------------------
/build/rollup.config.js:
--------------------------------------------------------------------------------
1 | const buble = require('rollup-plugin-buble')
2 | const commonjs = require('rollup-plugin-commonjs')
3 | const version = process.env.VERSION || require('../package.json').version
4 | const globals = {
5 | vue: 'Vue',
6 | }
7 |
8 | module.exports = {
9 | input: 'src/index.js',
10 | output: {
11 | banner:
12 | `/**
13 | * vue-gettext v${version}
14 | * (c) ${new Date().getFullYear()} Polyconseil
15 | * @license MIT
16 | */`,
17 | file: 'dist/vue-gettext.js',
18 | format: 'umd',
19 | globals,
20 | name: 'VueGettext',
21 | exports: 'named',
22 | },
23 | external: Object.keys(globals),
24 | plugins: [
25 | commonjs(),
26 | buble(),
27 | ],
28 | }
29 |
--------------------------------------------------------------------------------
/build/server-dev.js:
--------------------------------------------------------------------------------
1 | var express = require('express')
2 | var webpack = require('webpack')
3 | var webpackDevMiddleware = require('webpack-dev-middleware')
4 | var webpackHotMiddleware = require('webpack-hot-middleware')
5 |
6 | var config = require('./config')
7 | var webpackConfig = require('./webpack-dev.config.js')
8 |
9 |
10 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
11 |
12 | // Create an Express application.
13 | var app = express()
14 |
15 | // Start Webpack.
16 | var compiler = webpack(webpackConfig)
17 |
18 | // webpack-dev-middleware uses Webpack to compile assets in-memory and serve them.
19 | // https://github.com/webpack/webpack-dev-middleware/blob/47fd5b/README.md
20 | app.use(webpackDevMiddleware(compiler, {
21 | publicPath: webpackConfig.output.publicPath,
22 | stats: {
23 | colors: true,
24 | },
25 | }))
26 |
27 | // Webpack hot reloading using only webpack-dev-middleware.
28 | // https://github.com/glenjamin/webpack-hot-middleware/blob/04f953/README.md
29 | app.use(webpackHotMiddleware(compiler))
30 |
31 | var port = config.dev.port
32 |
33 | module.exports = app.listen(port, function (err) {
34 | if (err) {
35 | console.log(err)
36 | return
37 | }
38 | var uri = 'http://localhost:' + port
39 | console.log('Listening at ' + uri + '\n')
40 | })
41 |
--------------------------------------------------------------------------------
/build/webpack-dev.config.js:
--------------------------------------------------------------------------------
1 | var HtmlWebpackPlugin = require('html-webpack-plugin')
2 | var path = require('path')
3 | var webpack = require('webpack')
4 |
5 | var config = require('./config')
6 |
7 |
8 | var root = path.resolve(__dirname, '..')
9 |
10 | var webpackConfig = {
11 |
12 | // Entry points.
13 | // https://webpack.js.org/concepts/entry-points/#object-syntax
14 | entry: {
15 | app: [
16 | // https://github.com/glenjamin/webpack-hot-middleware/blob/04f953/README.md#config
17 | 'webpack-hot-middleware/client?reload=true',
18 | './dev/index.js',
19 | ],
20 | },
21 |
22 | output: {
23 | // Use the [name] placeholder.
24 | // https://webpack.js.org/configuration/output/#output-filename
25 | filename: '[name].js',
26 | // Since webpack-dev-middleware handles files in memory, there's no need to configure a specific path,
27 | // we can use any path.
28 | path: config.build.bundleRoot,
29 | // `publicPath` is required by webpack-dev-middleware, see `server-dev.js`.
30 | publicPath: config.dev.bundlePublicPath,
31 | },
32 |
33 | resolve: {
34 | extensions: ['', '.js', '.vue'],
35 | alias: {
36 | 'vue$': 'vue/dist/vue.common.js', // Use the Vue.js standalone build.
37 | },
38 | },
39 |
40 | plugins: [
41 |
42 | // Enable hot reloading.
43 | // https://github.com/glenjamin/webpack-hot-middleware/blob/04f953/README.md#installation--usage
44 | new webpack.optimize.OccurenceOrderPlugin(),
45 | new webpack.HotModuleReplacementPlugin(),
46 | new webpack.NoErrorsPlugin(),
47 |
48 | // Generate an `index.html` entry point, and auto inject our Webpack bundles.
49 | // https://github.com/ampedandwired/html-webpack-plugin/blob/033207/README.md
50 | new HtmlWebpackPlugin({
51 | filename: 'index.html', // Use the same value as webpack-dev-middleware `index` default value (`index.html`).
52 | template: 'dev/index.tpl.html',
53 | inject: 'body',
54 | }),
55 |
56 | // Expose global constants in the bundle (i.e. in client code).
57 | new webpack.DefinePlugin({
58 | 'process.env': config.dev.env,
59 | }),
60 |
61 | ],
62 |
63 | // Pass eslint global options.
64 | // https://github.com/MoOx/eslint-loader/tree/81d743/README.md#options
65 | eslint: {
66 | formatter: require('eslint-friendly-formatter'),
67 | },
68 |
69 | module: {
70 | loaders: [
71 | {
72 | test: /\.js$/,
73 | loader: 'babel',
74 | include: root,
75 | exclude: /node_modules/,
76 | },
77 | {
78 | test: /\.css$/,
79 | loader: 'style!css?localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader',
80 | },
81 | {
82 | test: /\.html$/,
83 | loader: 'html',
84 | },
85 | {
86 | test: /\.json$/,
87 | loader: 'json',
88 | },
89 | {
90 | test: /\.vue$/,
91 | loader: 'vue',
92 | },
93 | ],
94 | },
95 |
96 | postcss: function (webpack) {
97 | return [
98 | // https://github.com/postcss/postcss-import/tree/95aa38#adddependencyto
99 | require('postcss-import')({ addDependencyTo: webpack }),
100 | require('postcss-cssnext')(),
101 | ]
102 | },
103 |
104 | }
105 |
106 | module.exports = webpackConfig
107 |
--------------------------------------------------------------------------------
/build/webpack-test.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var webpack = require('webpack')
3 |
4 | var config = require('./config')
5 |
6 |
7 | var root = path.resolve(__dirname, '..')
8 |
9 | var webpackConfig = {
10 |
11 | output: {
12 | filename: 'bundle.js',
13 | path: config.build.bundleRoot,
14 | },
15 |
16 | resolve: {
17 | extensions: ['', '.js', '.vue'],
18 | alias: {
19 | 'vue$': 'vue/dist/vue.common.js',
20 | },
21 | },
22 |
23 | plugins: [
24 | new webpack.DefinePlugin({
25 | 'process.env': config.test.env,
26 | }),
27 | ],
28 |
29 | eslint: {
30 | formatter: require('eslint-friendly-formatter'),
31 | },
32 |
33 | module: {
34 | preLoaders: [
35 | {
36 | test: /\.vue$/,
37 | loader: 'eslint',
38 | include: root,
39 | exclude: /node_modules/,
40 | },
41 | {
42 | test: /\.js$/,
43 | loader: 'eslint',
44 | include: root,
45 | exclude: /node_modules/,
46 | },
47 | ],
48 | loaders: [
49 | {
50 | test: /\.js$/,
51 | loader: 'babel',
52 | include: root,
53 | exclude: /node_modules/,
54 | },
55 | {
56 | test: /\.css$/,
57 | loader: 'style!css?localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader',
58 | },
59 | {
60 | test: /\.html$/,
61 | loader: 'html',
62 | },
63 | {
64 | test: /\.json$/,
65 | loader: 'json',
66 | },
67 | {
68 | test: /\.vue$/,
69 | loader: 'vue',
70 | },
71 | ],
72 | },
73 |
74 | postcss: function (webpack) {
75 | return [
76 | require('postcss-import')({ addDependencyTo: webpack }),
77 | require('postcss-cssnext')(),
78 | ]
79 | },
80 |
81 | }
82 |
83 | module.exports = webpackConfig
84 |
--------------------------------------------------------------------------------
/dev/components/alert.vue:
--------------------------------------------------------------------------------
1 |
2 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/dev/locale/en_GB/LC_MESSAGES/app.po:
--------------------------------------------------------------------------------
1 | # English translations for vue-gettext package.
2 | # Copyright (C) 2016 THE vue-gettext'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the vue-gettext package.
4 | # Automatically generated, 2016.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: vue-gettext 2.0.0\n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2017-06-13 15:49+0200\n"
11 | "PO-Revision-Date: 2016-11-22 16:24+0100\n"
12 | "Last-Translator: Automatically generated\n"
13 | "Language-Team: none\n"
14 | "Language: en_GB\n"
15 | "MIME-Version: 1.0\n"
16 | "Content-Type: text/plain; charset=UTF-8\n"
17 | "Content-Transfer-Encoding: 8bit\n"
18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n"
19 |
20 | #: dev/components/plural/template.html:28
21 | msgid "%{ countForUntranslated } item. This is left untranslated on purpose."
22 | msgid_plural "%{ countForUntranslated } items. This is left untranslated on purpose."
23 | msgstr[0] ""
24 | msgstr[1] ""
25 |
26 | #: dev/components/plural/template.html:13
27 | msgid "%{ n } book"
28 | msgid_plural "%{ n } books"
29 | msgstr[0] "%{ n } book"
30 | msgstr[1] "%{ n } books"
31 |
32 | #: dev/components/plural/template.html:17
33 | msgid "%{ nComputed } book"
34 | msgid_plural "%{ nComputed } books"
35 | msgstr[0] "%{ nComputed } book"
36 | msgstr[1] "%{ nComputed } books"
37 |
38 | #: dev/components/directive.vue:16
39 | msgid "%{ count } apple"
40 | msgid_plural "%{ count } apples"
41 | msgstr[0] "%{ count } apple"
42 | msgstr[1] "%{ count } apples"
43 |
44 | #: dev/components/directive.vue:5
45 | msgid "A random number: %{ random }"
46 | msgstr "A random number: %{ random }"
47 |
48 | #: dev/components/multilines.vue:3
49 | msgid ""
50 | "Forgotten your password? Enter your \"email address\" below,\n"
51 | " and we'll email instructions for setting a new one."
52 | msgstr ""
53 | "Forgotten your password? Enter your \"email address\" below,\n"
54 | " and we'll email instructions for setting a new one."
55 |
56 | #: dev/components/customTags.vue:3
57 | msgid "Headline 1"
58 | msgstr "Headline 1"
59 |
60 | #: dev/components/customTags.vue:4
61 | msgid "Headline 2"
62 | msgstr "Headline 2"
63 |
64 | #: dev/components/customTags.vue:5
65 | msgid "Headline 3"
66 | msgstr "Headline 3"
67 |
68 | #: dev/components/customTags.vue:6
69 | msgid "Headline 4"
70 | msgstr "Headline 4"
71 |
72 | #: dev/components/directive.vue:10
73 | msgid "Hello %{ name }"
74 | msgstr "Hello %{ name }"
75 |
76 | #: dev/components/plural/template.html:4
77 | msgid "In English, '0' (zero) is always plural."
78 | msgstr "In English, '0' (zero) is always plural."
79 |
80 | #: dev/components/customTags.vue:7
81 | msgid "Paragraph"
82 | msgstr "Paragraph"
83 |
84 | #: dev/components/languageSelect.vue:4
85 | msgid "Select your language:"
86 | msgstr "Select your language:"
87 |
88 | #: dev/components/if/template.html:15 dev/components/if/template.html:16
89 | #: dev/components/if/template.html:17
90 | msgid "This is %{ obj.name }"
91 | msgstr "This is %{ obj.name }"
92 |
93 | #: dev/components/plural/template.html:23
94 | msgid "Use default singular or plural form when there is no translation. This is left untranslated on purpose."
95 | msgstr ""
96 |
97 | #: dev/components/if/template.html:5
98 | msgid "Welcome %{ firstname }"
99 | msgstr "Welcome %{ firstname }"
100 |
101 | #: dev/components/alert.vue:24
102 | msgid "Good bye!"
103 | msgstr "Good bye!"
104 |
105 | #: dev/components/alert.vue:35
106 | msgid "%{ n } car"
107 | msgid_plural "%{ n } cars"
108 | msgstr[0] "%{ n } car"
109 | msgstr[1] "%{ n } cars"
110 |
--------------------------------------------------------------------------------
/dev/locale/fr_FR/LC_MESSAGES/app.po:
--------------------------------------------------------------------------------
1 | # French translations for vue-gettext package
2 | # Traductions françaises du paquet vue-gettext.
3 | # Copyright (C) 2016 THE vue-gettext'S COPYRIGHT HOLDER
4 | # This file is distributed under the same license as the vue-gettext package.
5 | # Automatically generated, 2016.
6 | #
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: vue-gettext 2.0.0\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2017-06-13 15:49+0200\n"
12 | "PO-Revision-Date: 2016-11-22 16:24+0100\n"
13 | "Last-Translator: Automatically generated\n"
14 | "Language-Team: none\n"
15 | "Language: fr_FR\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=UTF-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Plural-Forms: nplurals=2; plural=(n > 1);\n"
20 |
21 | #: dev/components/plural/template.html:28
22 | msgid "%{ countForUntranslated } item. This is left untranslated on purpose."
23 | msgid_plural "%{ countForUntranslated } items. This is left untranslated on purpose."
24 | msgstr[0] ""
25 | msgstr[1] ""
26 |
27 | #: dev/components/plural/template.html:13
28 | msgid "%{ n } book"
29 | msgid_plural "%{ n } books"
30 | msgstr[0] "%{ n } livre"
31 | msgstr[1] "%{ n } livres"
32 |
33 | #: dev/components/plural/template.html:17
34 | msgid "%{ nComputed } book"
35 | msgid_plural "%{ nComputed } books"
36 | msgstr[0] "%{ nComputed } livre"
37 | msgstr[1] "%{ nComputed } livres"
38 |
39 | #: dev/components/directive.vue:16
40 | msgid "%{ count } apple"
41 | msgid_plural "%{ count } apples"
42 | msgstr[0] "%{ count } pomme"
43 | msgstr[1] "%{ count } pommes"
44 |
45 | #: dev/components/directive.vue:5
46 | msgid "A random number: %{ random }"
47 | msgstr "Un nombre aléatoire : %{ random }"
48 |
49 | #: dev/components/multilines.vue:3
50 | msgid ""
51 | "Forgotten your password? Enter your \"email address\" below,\n"
52 | " and we'll email instructions for setting a new one."
53 | msgstr ""
54 | "Mot de passe perdu ? Saisissez votre \"adresse électronique\" ci-dessous\n"
55 | " et nous vous enverrons les instructions pour en créer un nouveau."
56 |
57 | #: dev/components/customTags.vue:3
58 | msgid "Headline 1"
59 | msgstr "Titre 1"
60 |
61 | #: dev/components/customTags.vue:4
62 | msgid "Headline 2"
63 | msgstr "Titre 2"
64 |
65 | #: dev/components/customTags.vue:5
66 | msgid "Headline 3"
67 | msgstr "Titre 3"
68 |
69 | #: dev/components/customTags.vue:6
70 | msgid "Headline 4"
71 | msgstr "Titre 4"
72 |
73 | #: dev/components/directive.vue:10
74 | msgid "Hello %{ name }"
75 | msgstr "Bonjour %{ name }"
76 |
77 | #: dev/components/plural/template.html:4
78 | msgid "In English, '0' (zero) is always plural."
79 | msgstr "En anglais, '0' (zero) prend toujours le pluriel."
80 |
81 | #: dev/components/customTags.vue:7
82 | msgid "Paragraph"
83 | msgstr "Paragraphe"
84 |
85 | #: dev/components/languageSelect.vue:4
86 | msgid "Select your language:"
87 | msgstr "Sélectionner votre langage"
88 |
89 | #: dev/components/if/template.html:15 dev/components/if/template.html:16
90 | #: dev/components/if/template.html:17
91 | msgid "This is %{ obj.name }"
92 | msgstr "C'est %{ obj.name }"
93 |
94 | #: dev/components/plural/template.html:23
95 | msgid "Use default singular or plural form when there is no translation. This is left untranslated on purpose."
96 | msgstr ""
97 |
98 | #: dev/components/if/template.html:5
99 | msgid "Welcome %{ firstname }"
100 | msgstr "Bienvenue %{ firstname }"
101 |
102 | #: dev/components/alert.vue:24
103 | msgid "Good bye!"
104 | msgstr "Au revoir !"
105 |
106 | #: dev/components/alert.vue:35
107 | msgid "%{ n } car"
108 | msgid_plural "%{ n } cars"
109 | msgstr[0] "%{ n } voiture"
110 | msgstr[1] "%{ n } voitures"
111 |
--------------------------------------------------------------------------------
/dev/locale/it_IT/LC_MESSAGES/app.po:
--------------------------------------------------------------------------------
1 | # Italian translations for vue-gettext package
2 | # Traduzioni italiane per il pacchetto vue-gettext..
3 | # Copyright (C) 2016 THE vue-gettext'S COPYRIGHT HOLDER
4 | # This file is distributed under the same license as the vue-gettext package.
5 | # Automatically generated, 2016.
6 | #
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: vue-gettext 2.0.0\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2017-06-13 15:49+0200\n"
12 | "PO-Revision-Date: 2016-11-22 16:26+0100\n"
13 | "Last-Translator: Automatically generated\n"
14 | "Language-Team: none\n"
15 | "Language: it_IT\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=UTF-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n"
20 |
21 | #: dev/components/plural/template.html:28
22 | msgid "%{ countForUntranslated } item. This is left untranslated on purpose."
23 | msgid_plural "%{ countForUntranslated } items. This is left untranslated on purpose."
24 | msgstr[0] ""
25 | msgstr[1] ""
26 |
27 | #: dev/components/plural/template.html:13
28 | msgid "%{ n } book"
29 | msgid_plural "%{ n } books"
30 | msgstr[0] "%{ n } libbra"
31 | msgstr[1] "%{ n } libri"
32 |
33 | #: dev/components/plural/template.html:17
34 | msgid "%{ nComputed } book"
35 | msgid_plural "%{ nComputed } books"
36 | msgstr[0] "%{ nComputed } libbra"
37 | msgstr[1] "%{ nComputed } libri"
38 |
39 | #: dev/components/directive.vue:16
40 | msgid "%{ count } apple"
41 | msgid_plural "%{ count } apples"
42 | msgstr[0] "%{ count } mela"
43 | msgstr[1] "%{ count } mele"
44 |
45 | #: dev/components/directive.vue:5
46 | msgid "A random number: %{ random }"
47 | msgstr "Un numero casuale: %{ random }"
48 |
49 | #: dev/components/multilines.vue:3
50 | msgid ""
51 | "Forgotten your password? Enter your \"email address\" below,\n"
52 | " and we'll email instructions for setting a new one."
53 | msgstr ""
54 | "Password dimenticata? Inserisci il tuo \"indirizzo email\" qui\n"
55 | " sotto, e ti invieremo istruzioni per impostarne una nuova."
56 |
57 | #: dev/components/customTags.vue:3
58 | msgid "Headline 1"
59 | msgstr "Titolo 1"
60 |
61 | #: dev/components/customTags.vue:4
62 | msgid "Headline 2"
63 | msgstr "Titolo 2"
64 |
65 | #: dev/components/customTags.vue:5
66 | msgid "Headline 3"
67 | msgstr "Titolo 3"
68 |
69 | #: dev/components/customTags.vue:6
70 | msgid "Headline 4"
71 | msgstr "Titolo 4"
72 |
73 | #: dev/components/directive.vue:10
74 | msgid "Hello %{ name }"
75 | msgstr "Buongiorno %{ name }"
76 |
77 | #: dev/components/plural/template.html:4
78 | msgid "In English, '0' (zero) is always plural."
79 | msgstr "In inglese, '0' (zero) è sempre al plurale."
80 |
81 | #: dev/components/customTags.vue:7
82 | msgid "Paragraph"
83 | msgstr "Paragrafo"
84 |
85 | #: dev/components/languageSelect.vue:4
86 | msgid "Select your language:"
87 | msgstr "Seleziona la tua lingua:"
88 |
89 | #: dev/components/if/template.html:15 dev/components/if/template.html:16
90 | #: dev/components/if/template.html:17
91 | msgid "This is %{ obj.name }"
92 | msgstr "Questo è %{ obj.name }"
93 |
94 | #: dev/components/plural/template.html:23
95 | msgid "Use default singular or plural form when there is no translation. This is left untranslated on purpose."
96 | msgstr ""
97 |
98 | #: dev/components/if/template.html:5
99 | msgid "Welcome %{ firstname }"
100 | msgstr "Benvenuto %{ firstname }"
101 |
102 | #: dev/components/alert.vue:24
103 | msgid "Good bye!"
104 | msgstr "Arriverdeci!"
105 |
106 | #: dev/components/alert.vue:35
107 | msgid "%{ n } car"
108 | msgid_plural "%{ n } cars"
109 | msgstr[0] "%{ n } auto"
110 | msgstr[1] "%{ n } auto"
111 |
--------------------------------------------------------------------------------
/dev/styles/global.css:
--------------------------------------------------------------------------------
1 | @import './variables.css';
2 |
3 |
4 | body {
5 | background: var(--color-grey);
6 | }
7 |
--------------------------------------------------------------------------------
/dev/styles/variables.css:
--------------------------------------------------------------------------------
1 | :root {
2 |
3 | --color-grey: #e5e5e5;
4 | --color-blue: #00aff2;
5 |
6 | }
7 |
--------------------------------------------------------------------------------
/dev/translations.json:
--------------------------------------------------------------------------------
1 | {"en_GB":{"%{ n } book":["%{ n } book","%{ n } books"],"%{ nComputed } book":["%{ nComputed } book","%{ nComputed } books"],"%{ count } apple":["%{ count } apple","%{ count } apples"],"A random number: %{ random }":"A random number: %{ random }","Forgotten your password? Enter your \"email address\" below,\n and we'll email instructions for setting a new one.":"Forgotten your password? Enter your \"email address\" below,\n and we'll email instructions for setting a new one.","Headline 1":"Headline 1","Headline 2":"Headline 2","Headline 3":"Headline 3","Headline 4":"Headline 4","Hello %{ name }":"Hello %{ name }","In English, '0' (zero) is always plural.":"In English, '0' (zero) is always plural.","Paragraph":"Paragraph","Select your language:":"Select your language:","This is %{ obj.name }":"This is %{ obj.name }","Welcome %{ firstname }":"Welcome %{ firstname }","Good bye!":"Good bye!","%{ n } car":["%{ n } car","%{ n } cars"]},"fr_FR":{"%{ n } book":["%{ n } livre","%{ n } livres"],"%{ nComputed } book":["%{ nComputed } livre","%{ nComputed } livres"],"%{ count } apple":["%{ count } pomme","%{ count } pommes"],"A random number: %{ random }":"Un nombre aléatoire : %{ random }","Forgotten your password? Enter your \"email address\" below,\n and we'll email instructions for setting a new one.":"Mot de passe perdu ? Saisissez votre \"adresse électronique\" ci-dessous\n et nous vous enverrons les instructions pour en créer un nouveau.","Headline 1":"Titre 1","Headline 2":"Titre 2","Headline 3":"Titre 3","Headline 4":"Titre 4","Hello %{ name }":"Bonjour %{ name }","In English, '0' (zero) is always plural.":"En anglais, '0' (zero) prend toujours le pluriel.","Paragraph":"Paragraphe","Select your language:":"Sélectionner votre langage","This is %{ obj.name }":"C'est %{ obj.name }","Welcome %{ firstname }":"Bienvenue %{ firstname }","Good bye!":"Au revoir !","%{ n } car":["%{ n } voiture","%{ n } voitures"]},"it_IT":{"%{ n } book":["%{ n } libbra","%{ n } libri"],"%{ nComputed } book":["%{ nComputed } libbra","%{ nComputed } libri"],"%{ count } apple":["%{ count } mela","%{ count } mele"],"A random number: %{ random }":"Un numero casuale: %{ random }","Forgotten your password? Enter your \"email address\" below,\n and we'll email instructions for setting a new one.":"Password dimenticata? Inserisci il tuo \"indirizzo email\" qui\n sotto, e ti invieremo istruzioni per impostarne una nuova.","Headline 1":"Titolo 1","Headline 2":"Titolo 2","Headline 3":"Titolo 3","Headline 4":"Titolo 4","Hello %{ name }":"Buongiorno %{ name }","In English, '0' (zero) is always plural.":"In inglese, '0' (zero) è sempre al plurale.","Paragraph":"Paragrafo","Select your language:":"Seleziona la tua lingua:","This is %{ obj.name }":"Questo è %{ obj.name }","Welcome %{ firstname }":"Benvenuto %{ firstname }","Good bye!":"Arriverdeci!","%{ n } car":["%{ n } auto","%{ n } auto"]}}
--------------------------------------------------------------------------------
/dist/vue-gettext.js:
--------------------------------------------------------------------------------
1 | /**
2 | * vue-gettext v2.1.12
3 | * (c) 2020 Polyconseil
4 | * @license MIT
5 | */
6 | (function (global, factory) {
7 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
8 | typeof define === 'function' && define.amd ? define(['exports'], factory) :
9 | (global = global || self, factory(global.VueGettext = {}));
10 | }(this, function (exports) { 'use strict';
11 |
12 | // Polyfill Object.assign for legacy browsers.
13 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
14 |
15 | if (typeof Object.assign !== 'function') {
16 | (function () {
17 | Object.assign = function (target) {
18 | var arguments$1 = arguments;
19 |
20 | var output;
21 | var index;
22 | var source;
23 | var nextKey;
24 | if (target === undefined || target === null) {
25 | throw new TypeError('Cannot convert undefined or null to object')
26 | }
27 | output = Object(target);
28 | for (index = 1; index < arguments.length; index++) {
29 | source = arguments$1[index];
30 | if (source !== undefined && source !== null) {
31 | for (nextKey in source) {
32 | if (source.hasOwnProperty(nextKey)) {
33 | output[nextKey] = source[nextKey];
34 | }
35 | }
36 | }
37 | }
38 | return output
39 | };
40 | }());
41 | }
42 |
43 | /**
44 | * Plural Forms
45 | *
46 | * This is a list of the plural forms, as used by Gettext PO, that are appropriate to each language.
47 | * http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html
48 | *
49 | * This is a replica of angular-gettext's plural.js
50 | * https://github.com/rubenv/angular-gettext/blob/master/src/plural.js
51 | */
52 | var plurals = {
53 |
54 | getTranslationIndex: function (languageCode, n) {
55 |
56 | n = Number(n);
57 | n = typeof n === 'number' && isNaN(n) ? 1 : n; // Fallback to singular.
58 |
59 | // Extract the ISO 639 language code. The ISO 639 standard defines
60 | // two-letter codes for many languages, and three-letter codes for
61 | // more rarely used languages.
62 | // https://www.gnu.org/software/gettext/manual/html_node/Language-Codes.html#Language-Codes
63 | if (languageCode.length > 2 && languageCode !== 'pt_BR') {
64 | languageCode = languageCode.split('_')[0];
65 | }
66 |
67 | switch (languageCode) {
68 | case 'ay': // Aymará
69 | case 'bo': // Tibetan
70 | case 'cgg': // Chiga
71 | case 'dz': // Dzongkha
72 | case 'fa': // Persian
73 | case 'id': // Indonesian
74 | case 'ja': // Japanese
75 | case 'jbo': // Lojban
76 | case 'ka': // Georgian
77 | case 'kk': // Kazakh
78 | case 'km': // Khmer
79 | case 'ko': // Korean
80 | case 'ky': // Kyrgyz
81 | case 'lo': // Lao
82 | case 'ms': // Malay
83 | case 'my': // Burmese
84 | case 'sah': // Yakut
85 | case 'su': // Sundanese
86 | case 'th': // Thai
87 | case 'tt': // Tatar
88 | case 'ug': // Uyghur
89 | case 'vi': // Vietnamese
90 | case 'wo': // Wolof
91 | case 'zh': // Chinese
92 | // 1 form
93 | return 0
94 | case 'is': // Icelandic
95 | // 2 forms
96 | return (n % 10 !== 1 || n % 100 === 11) ? 1 : 0
97 | case 'jv': // Javanese
98 | // 2 forms
99 | return n !== 0 ? 1 : 0
100 | case 'mk': // Macedonian
101 | // 2 forms
102 | return n === 1 || n % 10 === 1 ? 0 : 1
103 | case 'ach': // Acholi
104 | case 'ak': // Akan
105 | case 'am': // Amharic
106 | case 'arn': // Mapudungun
107 | case 'br': // Breton
108 | case 'fil': // Filipino
109 | case 'fr': // French
110 | case 'gun': // Gun
111 | case 'ln': // Lingala
112 | case 'mfe': // Mauritian Creole
113 | case 'mg': // Malagasy
114 | case 'mi': // Maori
115 | case 'oc': // Occitan
116 | case 'pt_BR': // Brazilian Portuguese
117 | case 'tg': // Tajik
118 | case 'ti': // Tigrinya
119 | case 'tr': // Turkish
120 | case 'uz': // Uzbek
121 | case 'wa': // Walloon
122 | /* eslint-disable */
123 | /* Disable "Duplicate case label" because there are 2 forms of Chinese plurals */
124 | case 'zh': // Chinese
125 | /* eslint-enable */
126 | // 2 forms
127 | return n > 1 ? 1 : 0
128 | case 'lv': // Latvian
129 | // 3 forms
130 | return (n % 10 === 1 && n % 100 !== 11 ? 0 : n !== 0 ? 1 : 2)
131 | case 'lt': // Lithuanian
132 | // 3 forms
133 | return (n % 10 === 1 && n % 100 !== 11 ? 0 : n % 10 >= 2 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2)
134 | case 'be': // Belarusian
135 | case 'bs': // Bosnian
136 | case 'hr': // Croatian
137 | case 'ru': // Russian
138 | case 'sr': // Serbian
139 | case 'uk': // Ukrainian
140 | // 3 forms
141 | return (
142 | n % 10 === 1 && n % 100 !== 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2)
143 | case 'mnk': // Mandinka
144 | // 3 forms
145 | return (n === 0 ? 0 : n === 1 ? 1 : 2)
146 | case 'ro': // Romanian
147 | // 3 forms
148 | return (n === 1 ? 0 : (n === 0 || (n % 100 > 0 && n % 100 < 20)) ? 1 : 2)
149 | case 'pl': // Polish
150 | // 3 forms
151 | return (n === 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2)
152 | case 'cs': // Czech
153 | case 'sk': // Slovak
154 | // 3 forms
155 | return (n === 1) ? 0 : (n >= 2 && n <= 4) ? 1 : 2
156 | case 'csb': // Kashubian
157 | // 3 forms
158 | return (n === 1) ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2
159 | case 'sl': // Slovenian
160 | // 4 forms
161 | return (n % 100 === 1 ? 0 : n % 100 === 2 ? 1 : n % 100 === 3 || n % 100 === 4 ? 2 : 3)
162 | case 'mt': // Maltese
163 | // 4 forms
164 | return (n === 1 ? 0 : n === 0 || (n % 100 > 1 && n % 100 < 11) ? 1 : (n % 100 > 10 && n % 100 < 20) ? 2 : 3)
165 | case 'gd': // Scottish Gaelic
166 | // 4 forms
167 | return (n === 1 || n === 11) ? 0 : (n === 2 || n === 12) ? 1 : (n > 2 && n < 20) ? 2 : 3
168 | case 'cy': // Welsh
169 | // 4 forms
170 | return (n === 1) ? 0 : (n === 2) ? 1 : (n !== 8 && n !== 11) ? 2 : 3
171 | case 'kw': // Cornish
172 | // 4 forms
173 | return (n === 1) ? 0 : (n === 2) ? 1 : (n === 3) ? 2 : 3
174 | case 'ga': // Irish
175 | // 5 forms
176 | return n === 1 ? 0 : n === 2 ? 1 : (n > 2 && n < 7) ? 2 : (n > 6 && n < 11) ? 3 : 4
177 | case 'ar': // Arabic
178 | // 6 forms
179 | return (n === 0 ? 0 : n === 1 ? 1 : n === 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5)
180 | default: // Everything else
181 | return n !== 1 ? 1 : 0
182 | }
183 | },
184 |
185 | };
186 |
187 | // Ensure to always use the same Vue instance throughout the plugin.
188 | //
189 | // This was previously done in `index.js` using both named and default exports.
190 | // However, this currently must be kept in a separate file because we are using
191 | // Rollup to build the dist files and it has a drawback when using named and
192 | // default exports together, see:
193 | // https://github.com/rollup/rollup/blob/fca14d/src/utils/getExportMode.js#L27
194 | // https://github.com/rollup/rollup/wiki/JavaScript-API#exports
195 | //
196 | // If we had kept named and default exports in `index.js`, a user would have to
197 | // do something like this to access the default export: GetTextPlugin['default']
198 |
199 | var _Vue;
200 |
201 | function shareVueInstance (Vue) {
202 | _Vue = Vue;
203 | }
204 |
205 | var EVALUATION_RE = /[[\].]{1,2}/g;
206 |
207 | /* Interpolation RegExp.
208 | *
209 | * Because interpolation inside attributes are deprecated in Vue 2 we have to
210 | * use another set of delimiters to be able to use `translate-plural` etc.
211 | * We use %{ } delimiters.
212 | *
213 | * /
214 | * %\{ => Starting delimiter: `%{`
215 | * ( => Start capture
216 | * (?:.|\n) => Non-capturing group: any character or newline
217 | * +? => One or more times (ungreedy)
218 | * ) => End capture
219 | * \} => Ending delimiter: `}`
220 | * /g => Global: don't return after first match
221 | */
222 | var INTERPOLATION_RE = /%\{((?:.|\n)+?)\}/g;
223 |
224 | var MUSTACHE_SYNTAX_RE = /\{\{((?:.|\n)+?)\}\}/g;
225 |
226 | /**
227 | * Evaluate a piece of template string containing %{ } placeholders.
228 | * E.g.: 'Hi %{ user.name }' => 'Hi Bob'
229 | *
230 | * This is a vm.$interpolate alternative for Vue 2.
231 | * https://vuejs.org/v2/guide/migration.html#vm-interpolate-removed
232 | *
233 | * @param {String} msgid - The translation key containing %{ } placeholders
234 | * @param {Object} context - An object whose elements are put in their corresponding placeholders
235 | *
236 | * @return {String} The interpolated string
237 | */
238 | var interpolate = function (msgid, context, disableHtmlEscaping) {
239 | if ( context === void 0 ) context = {};
240 | if ( disableHtmlEscaping === void 0 ) disableHtmlEscaping = false;
241 |
242 |
243 | if (_Vue && !_Vue.config.getTextPluginSilent && MUSTACHE_SYNTAX_RE.test(msgid)) {
244 | console.warn(("Mustache syntax cannot be used with vue-gettext. Please use \"%{}\" instead of \"{{}}\" in: " + msgid));
245 | }
246 |
247 | var result = msgid.replace(INTERPOLATION_RE, function (match, token) {
248 |
249 | var expression = token.trim();
250 | var evaluated;
251 |
252 | var escapeHtmlMap = {
253 | '&': '&',
254 | '<': '<',
255 | '>': '>',
256 | '"': '"',
257 | '\'': ''',
258 | };
259 |
260 | // Avoid eval() by splitting `expression` and looping through its different properties if any, see #55.
261 | function getProps (obj, expression) {
262 | var arr = expression.split(EVALUATION_RE).filter(function (x) { return x; });
263 | while (arr.length) {
264 | obj = obj[arr.shift()];
265 | }
266 | return obj
267 | }
268 |
269 | function evalInContext (expression) {
270 | try {
271 | evaluated = getProps(this, expression);
272 | } catch (e) {
273 | // Ignore errors, because this function may be called recursively later.
274 | }
275 | if (evaluated === undefined) {
276 | if (this.$parent) {
277 | // Recursively climb the $parent chain to allow evaluation inside nested components, see #23 and #24.
278 | return evalInContext.call(this.$parent, expression)
279 | } else {
280 | console.warn(("Cannot evaluate expression: " + expression));
281 | evaluated = expression;
282 | }
283 | }
284 | var result = evaluated.toString();
285 | if (disableHtmlEscaping) {
286 | // Do not escape HTML, see #78.
287 | return result
288 | }
289 | // Escape HTML, see #78.
290 | return result.replace(/[&<>"']/g, function (m) { return escapeHtmlMap[m] })
291 | }
292 |
293 | return evalInContext.call(context, expression)
294 |
295 | });
296 |
297 | return result
298 |
299 | };
300 |
301 | // Store this values as function attributes for easy access elsewhere to bypass a Rollup
302 | // weak point with `export`:
303 | // https://github.com/rollup/rollup/blob/fca14d/src/utils/getExportMode.js#L27
304 | interpolate.INTERPOLATION_RE = INTERPOLATION_RE;
305 | interpolate.INTERPOLATION_PREFIX = '%{';
306 |
307 | var SPACING_RE = /\s{2,}/g;
308 |
309 | // Default configuration if only the translation is passed.
310 | var _config = {
311 | language: '',
312 | getTextPluginSilent: false,
313 | getTextPluginMuteLanguages: [],
314 | silent: false,
315 | };
316 | var _translations = {};
317 |
318 | var translate = {
319 |
320 | /*
321 | * Get the translated string from the translation.json file generated by easygettext.
322 | *
323 | * @param {String} msgid - The translation key
324 | * @param {Number} n - The number to switch between singular and plural
325 | * @param {String} context - The translation key context
326 | * @param {String} defaultPlural - The default plural value (optional)
327 | * @param {String} language - The language ID (e.g. 'fr_FR' or 'en_US')
328 | *
329 | * @return {String} The translated string
330 | */
331 | getTranslation: function (msgid, n, context, defaultPlural, language) {
332 | if ( n === void 0 ) n = 1;
333 | if ( context === void 0 ) context = null;
334 | if ( defaultPlural === void 0 ) defaultPlural = null;
335 | if ( language === void 0 ) language = _config.language;
336 |
337 |
338 | if (!msgid) {
339 | return '' // Allow empty strings.
340 | }
341 |
342 | var silent = _config.getTextPluginSilent || (_config.getTextPluginMuteLanguages.indexOf(language) !== -1);
343 |
344 | // Default untranslated string, singular or plural.
345 | var untranslated = defaultPlural && plurals.getTranslationIndex(language, n) > 0 ? defaultPlural : msgid;
346 |
347 | // `easygettext`'s `gettext-compile` generates a JSON version of a .po file based on its `Language` field.
348 | // But in this field, `ll_CC` combinations denoting a language’s main dialect are abbreviated as `ll`,
349 | // for example `de` is equivalent to `de_DE` (German as spoken in Germany).
350 | // See the `Language` section in https://www.gnu.org/software/gettext/manual/html_node/Header-Entry.html
351 | // So try `ll_CC` first, or the `ll` abbreviation which can be three-letter sometimes:
352 | // https://www.gnu.org/software/gettext/manual/html_node/Language-Codes.html#Language-Codes
353 | var translations = _translations[language] || _translations[language.split('_')[0]];
354 |
355 | if (!translations) {
356 | if (!silent) {
357 | console.warn(("No translations found for " + language));
358 | }
359 | return untranslated
360 | }
361 |
362 | // Currently easygettext trims entries since it needs to output consistent PO translation content
363 | // even if a web template designer added spaces between lines (which are ignored in HTML or jade,
364 | // but are significant in text). See #65.
365 | // Replicate the same behaviour here.
366 | msgid = msgid.trim();
367 |
368 | var translated = translations[msgid];
369 |
370 | // Sometimes `msgid` may not have the same number of spaces than its translation key.
371 | // This could happen because we use the private attribute `_renderChildren` to access the raw uninterpolated
372 | // string to translate in the `created` hook of `component.js`: spaces are not exactly the same between the
373 | // HTML and the content of `_renderChildren`, e.g. 6 spaces becomes 4 etc. See #15, #38.
374 | // In such cases, we need to compare the translation keys and `msgid` with the same number of spaces.
375 | if (!translated && SPACING_RE.test(msgid)) {
376 | Object.keys(translations).some(function (key) {
377 | if (key.replace(SPACING_RE, ' ') === msgid.replace(SPACING_RE, ' ')) {
378 | translated = translations[key];
379 | return translated
380 | }
381 | });
382 | }
383 |
384 | if (translated && context) {
385 | translated = translated[context];
386 | }
387 |
388 | if (!translated) {
389 | if (!silent) {
390 | var msg = "Untranslated " + language + " key found: " + msgid;
391 | if (context) {
392 | msg += " (with context: " + context + ")";
393 | }
394 | console.warn(msg);
395 | }
396 | return untranslated
397 | }
398 |
399 | // Avoid a crash when a msgid exists with and without a context, see #32.
400 | if (!(translated instanceof Array) && translated.hasOwnProperty('')) {
401 | // As things currently stand, the void key means a void context for easygettext.
402 | translated = translated[''];
403 | }
404 |
405 | if (typeof translated === 'string') {
406 | translated = [translated];
407 | }
408 |
409 | var translationIndex = plurals.getTranslationIndex(language, n);
410 |
411 | // Do not assume that the default value of n is 1 for the singular form of all languages.
412 | // E.g. Arabic, see #69.
413 | if (translated.length === 1 && n === 1) {
414 | translationIndex = 0;
415 | }
416 |
417 | return translated[translationIndex]
418 |
419 | },
420 |
421 | /*
422 | * Returns a string of the translation of the message.
423 | * Also makes the string discoverable by gettext-extract.
424 | *
425 | * @param {String} msgid - The translation key
426 | * @param {String} language - The language ID (e.g. 'fr_FR' or 'en_US')
427 | *
428 | * @return {String} The translated string
429 | */
430 | 'gettext': function (msgid, language) {
431 | if ( language === void 0 ) language = _config.language;
432 |
433 | return translate.getTranslation(msgid, 1, null, null, language)
434 | },
435 |
436 | /*
437 | * Returns a string of the translation for the given context.
438 | * Also makes the string discoverable by gettext-extract.
439 | *
440 | * @param {String} context - The context of the string to translate
441 | * @param {String} msgid - The translation key
442 | * @param {String} language - The language ID (e.g. 'fr_FR' or 'en_US')
443 | *
444 | * @return {String} The translated string
445 | */
446 | 'pgettext': function (context, msgid, language) {
447 | if ( language === void 0 ) language = _config.language;
448 |
449 | return translate.getTranslation(msgid, 1, context, null, language)
450 | },
451 |
452 | /*
453 | * Returns a string of the translation of either the singular or plural,
454 | * based on the number.
455 | * Also makes the string discoverable by gettext-extract.
456 | *
457 | * @param {String} msgid - The translation key
458 | * @param {String} plural - The plural form of the translation key
459 | * @param {Number} n - The number to switch between singular and plural
460 | * @param {String} language - The language ID (e.g. 'fr_FR' or 'en_US')
461 | *
462 | * @return {String} The translated string
463 | */
464 | 'ngettext': function (msgid, plural, n, language) {
465 | if ( language === void 0 ) language = _config.language;
466 |
467 | return translate.getTranslation(msgid, n, null, plural, language)
468 | },
469 |
470 | /*
471 | * Returns a string of the translation of either the singular or plural,
472 | * based on the number, for the given context.
473 | * Also makes the string discoverable by gettext-extract.
474 | *
475 | * @param {String} context - The context of the string to translate
476 | * @param {String} msgid - The translation key
477 | * @param {String} plural - The plural form of the translation key
478 | * @param {Number} n - The number to switch between singular and plural
479 | * @param {String} language - The language ID (e.g. 'fr_FR' or 'en_US')
480 | *
481 | * @return {String} The translated string
482 | */
483 | 'npgettext': function (context, msgid, plural, n, language) {
484 | if ( language === void 0 ) language = _config.language;
485 |
486 | return translate.getTranslation(msgid, n, context, plural, language)
487 | },
488 |
489 | /*
490 | * Initialize local state for translations and configuration
491 | * so that it works without Vue.
492 | *
493 | * @param {Object} translations - translations.json
494 | * @param {Object} config - Vue.config
495 | *
496 | */
497 | initTranslations: function (translations, config) {
498 | if (translations && typeof translations === 'object') {
499 | _translations = translations;
500 | }
501 | if (config && typeof config === 'object') {
502 | _config = config;
503 | }
504 | },
505 |
506 | /**
507 | * Allows to use interpolation outside the Vue
508 | *
509 | * @example
510 | * import {translate} from 'vue-gettext';
511 | *
512 | * const {gettext, gettextInterpolate} = translate;
513 | *
514 | * let translated = gettext('%{ n } foos', n)
515 | * let interpolated = gettextInterpolate(translated, {n: 5})
516 | */
517 | gettextInterpolate: interpolate.bind(interpolate),
518 |
519 | };
520 |
521 | // UUID v4 generator (RFC4122 compliant).
522 | //
523 | // https://gist.github.com/jcxplorer/823878
524 |
525 | function uuid () {
526 |
527 | var uuid = '';
528 | var i;
529 | var random;
530 |
531 | for (i = 0; i < 32; i++) {
532 | random = Math.random() * 16 | 0;
533 | if (i === 8 || i === 12 || i === 16 || i === 20) {
534 | uuid += '-';
535 | }
536 | uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random)).toString(16);
537 | }
538 |
539 | return uuid
540 |
541 | }
542 |
543 | /**
544 | * Translate content according to the current language.
545 | */
546 | var Component = {
547 |
548 | name: 'translate',
549 |
550 | created: function () {
551 |
552 | this.msgid = ''; // Don't crash the app with an empty component, i.e.: .
553 |
554 | // Store the raw uninterpolated string to translate.
555 | // This is currently done by looking inside a private attribute `_renderChildren`.
556 | // I haven't (yet) found a better way to access the raw content of the component.
557 | if (this.$options._renderChildren) {
558 | if (this.$options._renderChildren[0].hasOwnProperty('text')) {
559 | this.msgid = this.$options._renderChildren[0].text;
560 | } else {
561 | this.msgid = this.$options._renderChildren[0];
562 | }
563 | }
564 |
565 | this.isPlural = this.translateN !== undefined && this.translatePlural !== undefined;
566 | if (!this.isPlural && (this.translateN || this.translatePlural)) {
567 | throw new Error(("`translate-n` and `translate-plural` attributes must be used together: " + (this.msgid) + "."))
568 | }
569 |
570 | },
571 |
572 | props: {
573 | tag: {
574 | type: String,
575 | default: 'span',
576 | },
577 | // Always use v-bind for dynamically binding the `translateN` prop to data on the parent,
578 | // i.e.: `:translateN`.
579 | translateN: {
580 | type: Number,
581 | required: false,
582 | },
583 | translatePlural: {
584 | type: String,
585 | required: false,
586 | },
587 | translateContext: {
588 | type: String,
589 | required: false,
590 | },
591 | translateParams: {
592 | type: Object,
593 | required: false,
594 | },
595 | // `translateComment` is used exclusively by `easygettext`'s `gettext-extract`.
596 | translateComment: {
597 | type: String,
598 | required: false,
599 | },
600 | },
601 |
602 | computed: {
603 | translation: function () {
604 | var translation = translate.getTranslation(
605 | this.msgid,
606 | this.translateN,
607 | this.translateContext,
608 | this.isPlural ? this.translatePlural : null,
609 | this.$language.current
610 | );
611 |
612 | var context = this.$parent;
613 |
614 | if (this.translateParams) {
615 | context = Object.assign({}, this.$parent, this.translateParams);
616 | }
617 |
618 | return this.$gettextInterpolate(translation, context)
619 | },
620 | },
621 |
622 | render: function (createElement) {
623 |
624 | // Fix the problem with v-if, see #29.
625 | // Vue re-uses DOM elements for efficiency if they don't have a key attribute, see:
626 | // https://vuejs.org/v2/guide/conditional.html#Controlling-Reusable-Elements-with-key
627 | // https://vuejs.org/v2/api/#key
628 | if (_Vue.config.autoAddKeyAttributes && !this.$vnode.key) {
629 | this.$vnode.key = uuid();
630 | }
631 |
632 | // The text must be wraped inside a root HTML element, so we use a (by default).
633 | // https://github.com/vuejs/vue/blob/a4fcdb/src/compiler/parser/index.js#L209
634 | return createElement(this.tag, [this.translation])
635 |
636 | },
637 |
638 | };
639 |
640 | // Check if two values are loosely equal - that is,
641 | // if they are plain objects, do they have the same shape?
642 | // https://github.com/vuejs/vue/blob/v2.6.11/src/shared/util.js#L285
643 |
644 | function looseEqual (a, b) {
645 | if (a === b) { return true }
646 | var isObjectA = a !== null && typeof a === 'object';
647 | var isObjectB = b !== null && typeof b === 'object';
648 | if (isObjectA && isObjectB) {
649 | try {
650 | var isArrayA = Array.isArray(a);
651 | var isArrayB = Array.isArray(b);
652 | if (isArrayA && isArrayB) {
653 | return a.length === b.length && a.every(function (e, i) {
654 | return looseEqual(e, b[i])
655 | })
656 | } else if (a instanceof Date && b instanceof Date) {
657 | return a.getTime() === b.getTime()
658 | } else if (!isArrayA && !isArrayB) {
659 | var keysA = Object.keys(a);
660 | var keysB = Object.keys(b);
661 | return keysA.length === keysB.length && keysA.every(function (key) {
662 | return looseEqual(a[key], b[key])
663 | })
664 | } else {
665 | return false
666 | }
667 | } catch (e) {
668 | return false
669 | }
670 | } else if (!isObjectA && !isObjectB) {
671 | return String(a) === String(b)
672 | } else {
673 | return false
674 | }
675 | }
676 |
677 | var updateTranslation = function (el, binding, vnode) {
678 |
679 | var attrs = vnode.data.attrs || {};
680 | var msgid = el.dataset.msgid;
681 | var translateContext = attrs['translate-context'];
682 | var translateN = attrs['translate-n'];
683 | var translatePlural = attrs['translate-plural'];
684 | var isPlural = translateN !== undefined && translatePlural !== undefined;
685 | var context = vnode.context;
686 | var disableHtmlEscaping = attrs['render-html'] === 'true';
687 |
688 | if (!isPlural && (translateN || translatePlural)) {
689 | throw new Error('`translate-n` and `translate-plural` attributes must be used together:' + msgid + '.')
690 | }
691 |
692 | if (!_Vue.config.getTextPluginSilent && attrs['translate-params']) {
693 | console.warn(("`translate-params` is required as an expression for v-translate directive. Please change to `v-translate='params'`: " + msgid));
694 | }
695 |
696 | if (binding.value && typeof binding.value === 'object') {
697 | context = Object.assign({}, vnode.context, binding.value);
698 | }
699 |
700 | var translation = translate.getTranslation(
701 | msgid,
702 | translateN,
703 | translateContext,
704 | isPlural ? translatePlural : null,
705 | el.dataset.currentLanguage
706 | );
707 |
708 | var msg = interpolate(translation, context, disableHtmlEscaping);
709 |
710 | el.innerHTML = msg;
711 |
712 | };
713 |
714 | /**
715 | * A directive to translate content according to the current language.
716 | *
717 | * Use this directive instead of the component if you need to translate HTML content.
718 | * It's too tricky to support HTML content within the component because we cannot get the raw HTML to use as `msgid`.
719 | *
720 | * This directive has a similar interface to the component, supporting
721 | * `translate-comment`, `translate-context`, `translate-plural`, `translate-n`.
722 | *
723 | * `
This is Sparta!
`
724 | *
725 | * If you need interpolation, you must add an expression that outputs binding value that changes with each of the
726 | * context variable:
727 | * `
I am %{ fullName } and from %{ location }
`
728 | */
729 | var Directive = {
730 |
731 | bind: function bind (el, binding, vnode) {
732 |
733 | // Fix the problem with v-if, see #29.
734 | // Vue re-uses DOM elements for efficiency if they don't have a key attribute, see:
735 | // https://vuejs.org/v2/guide/conditional.html#Controlling-Reusable-Elements-with-key
736 | // https://vuejs.org/v2/api/#key
737 | if (_Vue.config.autoAddKeyAttributes && !vnode.key) {
738 | vnode.key = uuid();
739 | }
740 |
741 | // Get the raw HTML and store it in the element's dataset (as advised in Vue's official guide).
742 | var msgid = el.innerHTML;
743 | el.dataset.msgid = msgid;
744 |
745 | // Store the current language in the element's dataset.
746 | el.dataset.currentLanguage = _Vue.config.language;
747 |
748 | // Output an info in the console if an interpolation is required but no expression is provided.
749 | if (!_Vue.config.getTextPluginSilent) {
750 | var hasInterpolation = msgid.indexOf(interpolate.INTERPOLATION_PREFIX) !== -1;
751 | if (hasInterpolation && !binding.expression) {
752 | console.info(("No expression is provided for change detection. The translation for this key will be static:\n" + msgid));
753 | }
754 | }
755 |
756 | updateTranslation(el, binding, vnode);
757 |
758 | },
759 |
760 | update: function update (el, binding, vnode) {
761 |
762 | var doUpdate = false;
763 |
764 | // Trigger an update if the language has changed.
765 | if (el.dataset.currentLanguage !== _Vue.config.language) {
766 | el.dataset.currentLanguage = _Vue.config.language;
767 | doUpdate = true;
768 | }
769 |
770 | // Trigger an update if an optional bound expression has changed.
771 | if (!doUpdate && binding.expression && !looseEqual(binding.value, binding.oldValue)) {
772 | doUpdate = true;
773 | }
774 |
775 | if (doUpdate) {
776 | updateTranslation(el, binding, vnode);
777 | }
778 |
779 | },
780 |
781 | };
782 |
783 | function Config (Vue, languageVm, getTextPluginSilent, autoAddKeyAttributes, muteLanguages) {
784 |
785 | /*
786 | * Adds a `language` property to `Vue.config` and makes it reactive:
787 | * Vue.config.language = 'fr_FR'
788 | */
789 | Object.defineProperty(Vue.config, 'language', {
790 | enumerable: true,
791 | configurable: true,
792 | get: function () { return languageVm.current },
793 | set: function (val) { languageVm.current = val; },
794 | });
795 |
796 | /*
797 | * Adds a `getTextPluginSilent` property to `Vue.config`.
798 | * Used to enable/disable some console warnings globally.
799 | */
800 | Object.defineProperty(Vue.config, 'getTextPluginSilent', {
801 | enumerable: true,
802 | writable: true,
803 | value: getTextPluginSilent,
804 | });
805 |
806 | /*
807 | * Adds an `autoAddKeyAttributes` property to `Vue.config`.
808 | * Used to enable/disable the automatic addition of `key` attributes.
809 | */
810 | Object.defineProperty(Vue.config, 'autoAddKeyAttributes', {
811 | enumerable: true,
812 | writable: true,
813 | value: autoAddKeyAttributes,
814 | });
815 |
816 | /*
817 | * Adds a `getTextPluginMuteLanguages` property to `Vue.config`.
818 | * Used to enable/disable some console warnings for a specific set of languages.
819 | */
820 | Object.defineProperty(Vue.config, 'getTextPluginMuteLanguages', {
821 | enumerable: true,
822 | writable: true,
823 | value: muteLanguages, // Stores an array of languages for which the warnings are disabled.
824 | });
825 |
826 | }
827 |
828 | function Override (Vue, languageVm) {
829 |
830 | // Override the main init sequence. This is called for every instance.
831 | var init = Vue.prototype._init;
832 | Vue.prototype._init = function (options) {
833 | if ( options === void 0 ) options = {};
834 |
835 | var root = options._parent || options.parent || this;
836 | // Expose languageVm to every instance.
837 | this.$language = root.$language || languageVm;
838 | init.call(this, options);
839 | };
840 |
841 | // Override the main destroy sequence to destroy all languageVm watchers.
842 | var destroy = Vue.prototype._destroy;
843 | Vue.prototype._destroy = function () {
844 | this.$language = null;
845 | destroy.apply(this, arguments);
846 | };
847 |
848 | }
849 |
850 | var languageVm; // Singleton.
851 |
852 | var GetTextPlugin = function (Vue, options) {
853 | if ( options === void 0 ) options = {};
854 |
855 |
856 | var defaultConfig = {
857 | autoAddKeyAttributes: false,
858 | availableLanguages: { en_US: 'English' },
859 | defaultLanguage: 'en_US',
860 | languageVmMixin: {},
861 | muteLanguages: [],
862 | silent: Vue.config.silent,
863 | translations: null,
864 | };
865 |
866 | Object.keys(options).forEach(function (key) {
867 | if (Object.keys(defaultConfig).indexOf(key) === -1) {
868 | throw new Error((key + " is an invalid option for the translate plugin."))
869 | }
870 | });
871 |
872 | if (!options.translations) {
873 | throw new Error('No translations available.')
874 | }
875 |
876 | options = Object.assign(defaultConfig, options);
877 |
878 | languageVm = new Vue({
879 | created: function () {
880 | // Non-reactive data.
881 | this.available = options.availableLanguages;
882 | },
883 | data: {
884 | current: options.defaultLanguage,
885 | },
886 | mixins: [options.languageVmMixin],
887 | });
888 |
889 | shareVueInstance(Vue);
890 |
891 | Override(Vue, languageVm);
892 |
893 | Config(Vue, languageVm, options.silent, options.autoAddKeyAttributes, options.muteLanguages);
894 |
895 | translate.initTranslations(options.translations, Vue.config);
896 |
897 | // Makes available as a global component.
898 | Vue.component('translate', Component);
899 |
900 | // An option to support translation with HTML content: `v-translate`.
901 | Vue.directive('translate', Directive);
902 |
903 | // Exposes global properties.
904 | Vue.$translations = options.translations;
905 | // Exposes instance methods.
906 | Vue.prototype.$gettext = translate.gettext.bind(translate);
907 | Vue.prototype.$pgettext = translate.pgettext.bind(translate);
908 | Vue.prototype.$ngettext = translate.ngettext.bind(translate);
909 | Vue.prototype.$npgettext = translate.npgettext.bind(translate);
910 | Vue.prototype.$gettextInterpolate = interpolate.bind(interpolate);
911 |
912 | };
913 |
914 | exports.default = GetTextPlugin;
915 | exports.translate = translate;
916 |
917 | Object.defineProperty(exports, '__esModule', { value: true });
918 |
919 | }));
920 |
--------------------------------------------------------------------------------
/dist/vue-gettext.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * vue-gettext v2.1.12
3 | * (c) 2020 Polyconseil
4 | * @license MIT
5 | */
6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):(t=t||self,e(t.VueGettext={}))}(this,function(t){"use strict";function e(t){s=t}function n(){var t,e,n="";for(t=0;t<32;t++)e=16*Math.random()|0,8!==t&&12!==t&&16!==t&&20!==t||(n+="-"),n+=(12===t?4:16===t?3&e|8:e).toString(16);return n}function a(t,e){if(t===e)return!0;var n=null!==t&&"object"==typeof t,r=null!==e&&"object"==typeof e;if(!n||!r)return!n&&!r&&String(t)===String(e);try{var i=Array.isArray(t),s=Array.isArray(e);if(i&&s)return t.length===e.length&&t.every(function(t,n){return a(t,e[n])});if(t instanceof Date&&e instanceof Date)return t.getTime()===e.getTime();if(i||s)return!1;var o=Object.keys(t),u=Object.keys(e);return o.length===u.length&&o.every(function(n){return a(t[n],e[n])})}catch(t){return!1}}function r(t,e,n,a,r){Object.defineProperty(t.config,"language",{enumerable:!0,configurable:!0,get:function(){return e.current},set:function(t){e.current=t}}),Object.defineProperty(t.config,"getTextPluginSilent",{enumerable:!0,writable:!0,value:n}),Object.defineProperty(t.config,"autoAddKeyAttributes",{enumerable:!0,writable:!0,value:a}),Object.defineProperty(t.config,"getTextPluginMuteLanguages",{enumerable:!0,writable:!0,value:r})}function i(t,e){var n=t.prototype._init;t.prototype._init=function(t){void 0===t&&(t={});var a=t._parent||t.parent||this;this.$language=a.$language||e,n.call(this,t)};var a=t.prototype._destroy;t.prototype._destroy=function(){this.$language=null,a.apply(this,arguments)}}"function"!=typeof Object.assign&&function(){Object.assign=function(t){var e,n,a,r,i=arguments;if(void 0===t||null===t)throw new TypeError("Cannot convert undefined or null to object");for(e=Object(t),n=1;n2&&"pt_BR"!==t&&(t=t.split("_")[0]),t){case"ay":case"bo":case"cgg":case"dz":case"fa":case"id":case"ja":case"jbo":case"ka":case"kk":case"km":case"ko":case"ky":case"lo":case"ms":case"my":case"sah":case"su":case"th":case"tt":case"ug":case"vi":case"wo":case"zh":return 0;case"is":return e%10!=1||e%100==11?1:0;case"jv":return 0!==e?1:0;case"mk":return 1===e||e%10==1?0:1;case"ach":case"ak":case"am":case"arn":case"br":case"fil":case"fr":case"gun":case"ln":case"mfe":case"mg":case"mi":case"oc":case"pt_BR":case"tg":case"ti":case"tr":case"uz":case"wa":case"zh":return e>1?1:0;case"lv":return e%10==1&&e%100!=11?0:0!==e?1:2;case"lt":return e%10==1&&e%100!=11?0:e%10>=2&&(e%100<10||e%100>=20)?1:2;case"be":case"bs":case"hr":case"ru":case"sr":case"uk":return e%10==1&&e%100!=11?0:e%10>=2&&e%10<=4&&(e%100<10||e%100>=20)?1:2;case"mnk":return 0===e?0:1===e?1:2;case"ro":return 1===e?0:0===e||e%100>0&&e%100<20?1:2;case"pl":return 1===e?0:e%10>=2&&e%10<=4&&(e%100<10||e%100>=20)?1:2;case"cs":case"sk":return 1===e?0:e>=2&&e<=4?1:2;case"csb":return 1===e?0:e%10>=2&&e%10<=4&&(e%100<10||e%100>=20)?1:2;case"sl":return e%100==1?0:e%100==2?1:e%100==3||e%100==4?2:3;case"mt":return 1===e?0:0===e||e%100>1&&e%100<11?1:e%100>10&&e%100<20?2:3;case"gd":return 1===e||11===e?0:2===e||12===e?1:e>2&&e<20?2:3;case"cy":return 1===e?0:2===e?1:8!==e&&11!==e?2:3;case"kw":return 1===e?0:2===e?1:3===e?2:3;case"ga":return 1===e?0:2===e?1:e>2&&e<7?2:e>6&&e<11?3:4;case"ar":return 0===e?0:1===e?1:2===e?2:e%100>=3&&e%100<=10?3:e%100>=11?4:5;default:return 1!==e?1:0}}},u=/[[\].]{1,2}/g,l=/%\{((?:.|\n)+?)\}/g,c=/\{\{((?:.|\n)+?)\}\}/g,g=function(t,e,n){return void 0===e&&(e={}),void 0===n&&(n=!1),s&&!s.config.getTextPluginSilent&&c.test(t)&&console.warn('Mustache syntax cannot be used with vue-gettext. Please use "%{}" instead of "{{}}" in: '+t),t.replace(l,function(t,a){function r(t,e){for(var n=e.split(u).filter(function(t){return t});n.length;)t=t[n.shift()];return t}function i(t){try{s=r(this,t)}catch(t){}if(void 0===s){if(this.$parent)return i.call(this.$parent,t);console.warn("Cannot evaluate expression: "+t),s=t}var e=s.toString();return n?e:e.replace(/[&<>"']/g,function(t){return l[t]})}var s,o=a.trim(),l={"&":"&","<":"<",">":">",'"':""","'":"'"};return i.call(e,o)})};g.INTERPOLATION_RE=l,g.INTERPOLATION_PREFIX="%{";var f,d=/\s{2,}/g,p={language:"",getTextPluginSilent:!1,getTextPluginMuteLanguages:[],silent:!1},h={},v={getTranslation:function(t,e,n,a,r){if(void 0===e&&(e=1),void 0===n&&(n=null),void 0===a&&(a=null),void 0===r&&(r=p.language),!t)return"";var i=p.getTextPluginSilent||-1!==p.getTextPluginMuteLanguages.indexOf(r),s=a&&o.getTranslationIndex(r,e)>0?a:t,u=h[r]||h[r.split("_")[0]];if(!u)return i||console.warn("No translations found for "+r),s;t=t.trim();var l=u[t];if(!l&&d.test(t)&&Object.keys(u).some(function(e){if(e.replace(d," ")===t.replace(d," "))return l=u[e]}),l&&n&&(l=l[n]),!l){if(!i){var c="Untranslated "+r+" key found: "+t;n&&(c+=" (with context: "+n+")"),console.warn(c)}return s}l instanceof Array||!l.hasOwnProperty("")||(l=l[""]),"string"==typeof l&&(l=[l]);var g=o.getTranslationIndex(r,e);return 1===l.length&&1===e&&(g=0),l[g]},gettext:function(t,e){return void 0===e&&(e=p.language),v.getTranslation(t,1,null,null,e)},pgettext:function(t,e,n){return void 0===n&&(n=p.language),v.getTranslation(e,1,t,null,n)},ngettext:function(t,e,n,a){return void 0===a&&(a=p.language),v.getTranslation(t,n,null,e,a)},npgettext:function(t,e,n,a,r){return void 0===r&&(r=p.language),v.getTranslation(e,a,t,n,r)},initTranslations:function(t,e){t&&"object"==typeof t&&(h=t),e&&"object"==typeof e&&(p=e)},gettextInterpolate:g.bind(g)},y={name:"translate",created:function(){if(this.msgid="",this.$options._renderChildren&&(this.$options._renderChildren[0].hasOwnProperty("text")?this.msgid=this.$options._renderChildren[0].text:this.msgid=this.$options._renderChildren[0]),this.isPlural=void 0!==this.translateN&&void 0!==this.translatePlural,!this.isPlural&&(this.translateN||this.translatePlural))throw new Error("`translate-n` and `translate-plural` attributes must be used together: "+this.msgid+".")},props:{tag:{type:String,default:"span"},translateN:{type:Number,required:!1},translatePlural:{type:String,required:!1},translateContext:{type:String,required:!1},translateParams:{type:Object,required:!1},translateComment:{type:String,required:!1}},computed:{translation:function(){var t=v.getTranslation(this.msgid,this.translateN,this.translateContext,this.isPlural?this.translatePlural:null,this.$language.current),e=this.$parent;return this.translateParams&&(e=Object.assign({},this.$parent,this.translateParams)),this.$gettextInterpolate(t,e)}},render:function(t){return s.config.autoAddKeyAttributes&&!this.$vnode.key&&(this.$vnode.key=n()),t(this.tag,[this.translation])}},b=function(t,e,n){var a=n.data.attrs||{},r=t.dataset.msgid,i=a["translate-context"],o=a["translate-n"],u=a["translate-plural"],l=void 0!==o&&void 0!==u,c=n.context,f="true"===a["render-html"];if(!l&&(o||u))throw new Error("`translate-n` and `translate-plural` attributes must be used together:"+r+".");!s.config.getTextPluginSilent&&a["translate-params"]&&console.warn("`translate-params` is required as an expression for v-translate directive. Please change to `v-translate='params'`: "+r),e.value&&"object"==typeof e.value&&(c=Object.assign({},n.context,e.value));var d=v.getTranslation(r,o,i,l?u:null,t.dataset.currentLanguage),p=g(d,c,f);t.innerHTML=p},m={bind:function(t,e,a){s.config.autoAddKeyAttributes&&!a.key&&(a.key=n());var r=t.innerHTML;if(t.dataset.msgid=r,t.dataset.currentLanguage=s.config.language,!s.config.getTextPluginSilent){-1!==r.indexOf(g.INTERPOLATION_PREFIX)&&!e.expression&&console.info("No expression is provided for change detection. The translation for this key will be static:\n"+r)}b(t,e,a)},update:function(t,e,n){var r=!1;t.dataset.currentLanguage!==s.config.language&&(t.dataset.currentLanguage=s.config.language,r=!0),r||!e.expression||a(e.value,e.oldValue)||(r=!0),r&&b(t,e,n)}},x=function(t,n){void 0===n&&(n={});var a={autoAddKeyAttributes:!1,availableLanguages:{en_US:"English"},defaultLanguage:"en_US",languageVmMixin:{},muteLanguages:[],silent:t.config.silent,translations:null};if(Object.keys(n).forEach(function(t){if(-1===Object.keys(a).indexOf(t))throw new Error(t+" is an invalid option for the translate plugin.")}),!n.translations)throw new Error("No translations available.");n=Object.assign(a,n),f=new t({created:function(){this.available=n.availableLanguages},data:{current:n.defaultLanguage},mixins:[n.languageVmMixin]}),e(t),i(t,f),r(t,f,n.silent,n.autoAddKeyAttributes,n.muteLanguages),v.initTranslations(n.translations,t.config),t.component("translate",y),t.directive("translate",m),t.$translations=n.translations,t.prototype.$gettext=v.gettext.bind(v),t.prototype.$pgettext=v.pgettext.bind(v),t.prototype.$ngettext=v.ngettext.bind(v),t.prototype.$npgettext=v.npgettext.bind(v),t.prototype.$gettextInterpolate=g.bind(g)};t.default=x,t.translate=v,Object.defineProperty(t,"__esModule",{value:!0})});
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-gettext",
3 | "version": "2.1.12",
4 | "description": "Translate your Vue.js applications with gettext",
5 | "typings": "types/index.d.ts",
6 | "main": "dist/vue-gettext.js",
7 | "author": "Marc Hertzog",
8 | "license": "MIT",
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/Polyconseil/vue-gettext"
12 | },
13 | "publishConfig": {
14 | "registry": "https://registry.npmjs.org"
15 | },
16 | "scripts": {
17 | "build": "rollup -c build/rollup.config.js && uglifyjs dist/vue-gettext.js -cm --comments -o dist/vue-gettext.min.js",
18 | "dev": "node build/server-dev.js",
19 | "dev:dist": "rollup -w -c build/rollup.config.js",
20 | "lint": "eslint src test dev build",
21 | "test": "npm run lint && karma start build/karma.conf.js --single-run"
22 | },
23 | "engines": {
24 | "npm": ">= 3.0.0"
25 | },
26 | "devDependencies": {
27 | "babel-core": "^6.26.3",
28 | "babel-loader": "^6.2.7",
29 | "babel-plugin-transform-runtime": "^6.15.0",
30 | "babel-preset-es2015": "^6.18.0",
31 | "buble": "^0.14.2",
32 | "chai": "^3.5.0",
33 | "css-loader": "^2.1.1",
34 | "easygettext": "^2.13.0",
35 | "eslint": "^4.18.2",
36 | "eslint-config-standard": "^6.2.1",
37 | "eslint-friendly-formatter": "^2.0.6",
38 | "eslint-loader": "^1.6.1",
39 | "eslint-plugin-html": "^1.6.0",
40 | "eslint-plugin-promise": "^3.3.0",
41 | "eslint-plugin-standard": "^2.0.1",
42 | "express": "^4.16.3",
43 | "html-loader": "^0.4.4",
44 | "html-webpack-plugin": "^2.24.1",
45 | "json-loader": "^0.5.4",
46 | "karma": "^4.1.0",
47 | "karma-chrome-launcher": "^2.2.0",
48 | "karma-mocha": "^1.3.0",
49 | "karma-sinon-chai": "^1.3.4",
50 | "karma-webpack": "^2.0.13",
51 | "mocha": "^6.1.4",
52 | "moment": "^2.22.1",
53 | "postcss-cssnext": "^2.8.0",
54 | "postcss-import": "^12.0.1",
55 | "postcss-loader": "^1.1.0",
56 | "puppeteer": "^1.17.0",
57 | "rollup": "^1.16.2",
58 | "rollup-plugin-buble": "^0.19.6",
59 | "rollup-plugin-commonjs": "^10.0.0",
60 | "sinon": "^4.5.0",
61 | "sinon-chai": "^3.0.0",
62 | "style-loader": "^0.13.1",
63 | "uglify-js": "^2.7.4",
64 | "vue": "^2.5.16",
65 | "vue-loader": "^10.0.0",
66 | "vue-template-compiler": "^2.5.16",
67 | "webpack": "^1.13.3",
68 | "webpack-dev-middleware": "^1.8.4",
69 | "webpack-hot-middleware": "^2.22.1"
70 | },
71 | "files": [
72 | "dist",
73 | "src",
74 | "types/*.d.ts"
75 | ]
76 | }
77 |
--------------------------------------------------------------------------------
/src/component.js:
--------------------------------------------------------------------------------
1 | import translate from './translate'
2 | import uuid from './uuid'
3 | import { _Vue } from './localVue'
4 |
5 |
6 | /**
7 | * Translate content according to the current language.
8 | */
9 | export default {
10 |
11 | name: 'translate',
12 |
13 | created: function () {
14 |
15 | this.msgid = '' // Don't crash the app with an empty component, i.e.: .
16 |
17 | // Store the raw uninterpolated string to translate.
18 | // This is currently done by looking inside a private attribute `_renderChildren`.
19 | // I haven't (yet) found a better way to access the raw content of the component.
20 | if (this.$options._renderChildren) {
21 | if (this.$options._renderChildren[0].hasOwnProperty('text')) {
22 | this.msgid = this.$options._renderChildren[0].text
23 | } else {
24 | this.msgid = this.$options._renderChildren[0]
25 | }
26 | }
27 |
28 | this.isPlural = this.translateN !== undefined && this.translatePlural !== undefined
29 | if (!this.isPlural && (this.translateN || this.translatePlural)) {
30 | throw new Error(`\`translate-n\` and \`translate-plural\` attributes must be used together: ${this.msgid}.`)
31 | }
32 |
33 | },
34 |
35 | props: {
36 | tag: {
37 | type: String,
38 | default: 'span',
39 | },
40 | // Always use v-bind for dynamically binding the `translateN` prop to data on the parent,
41 | // i.e.: `:translateN`.
42 | translateN: {
43 | type: Number,
44 | required: false,
45 | },
46 | translatePlural: {
47 | type: String,
48 | required: false,
49 | },
50 | translateContext: {
51 | type: String,
52 | required: false,
53 | },
54 | translateParams: {
55 | type: Object,
56 | required: false,
57 | },
58 | // `translateComment` is used exclusively by `easygettext`'s `gettext-extract`.
59 | translateComment: {
60 | type: String,
61 | required: false,
62 | },
63 | },
64 |
65 | computed: {
66 | translation: function () {
67 | let translation = translate.getTranslation(
68 | this.msgid,
69 | this.translateN,
70 | this.translateContext,
71 | this.isPlural ? this.translatePlural : null,
72 | this.$language.current
73 | )
74 |
75 | let context = this.$parent
76 |
77 | if (this.translateParams) {
78 | context = Object.assign({}, this.$parent, this.translateParams)
79 | }
80 |
81 | return this.$gettextInterpolate(translation, context)
82 | },
83 | },
84 |
85 | render: function (createElement) {
86 |
87 | // Fix the problem with v-if, see #29.
88 | // Vue re-uses DOM elements for efficiency if they don't have a key attribute, see:
89 | // https://vuejs.org/v2/guide/conditional.html#Controlling-Reusable-Elements-with-key
90 | // https://vuejs.org/v2/api/#key
91 | if (_Vue.config.autoAddKeyAttributes && !this.$vnode.key) {
92 | this.$vnode.key = uuid()
93 | }
94 |
95 | // The text must be wraped inside a root HTML element, so we use a (by default).
96 | // https://github.com/vuejs/vue/blob/a4fcdb/src/compiler/parser/index.js#L209
97 | return createElement(this.tag, [this.translation])
98 |
99 | },
100 |
101 | }
102 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | export default function (Vue, languageVm, getTextPluginSilent, autoAddKeyAttributes, muteLanguages) {
2 |
3 | /*
4 | * Adds a `language` property to `Vue.config` and makes it reactive:
5 | * Vue.config.language = 'fr_FR'
6 | */
7 | Object.defineProperty(Vue.config, 'language', {
8 | enumerable: true,
9 | configurable: true,
10 | get: () => { return languageVm.current },
11 | set: (val) => { languageVm.current = val },
12 | })
13 |
14 | /*
15 | * Adds a `getTextPluginSilent` property to `Vue.config`.
16 | * Used to enable/disable some console warnings globally.
17 | */
18 | Object.defineProperty(Vue.config, 'getTextPluginSilent', {
19 | enumerable: true,
20 | writable: true,
21 | value: getTextPluginSilent,
22 | })
23 |
24 | /*
25 | * Adds an `autoAddKeyAttributes` property to `Vue.config`.
26 | * Used to enable/disable the automatic addition of `key` attributes.
27 | */
28 | Object.defineProperty(Vue.config, 'autoAddKeyAttributes', {
29 | enumerable: true,
30 | writable: true,
31 | value: autoAddKeyAttributes,
32 | })
33 |
34 | /*
35 | * Adds a `getTextPluginMuteLanguages` property to `Vue.config`.
36 | * Used to enable/disable some console warnings for a specific set of languages.
37 | */
38 | Object.defineProperty(Vue.config, 'getTextPluginMuteLanguages', {
39 | enumerable: true,
40 | writable: true,
41 | value: muteLanguages, // Stores an array of languages for which the warnings are disabled.
42 | })
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/src/directive.js:
--------------------------------------------------------------------------------
1 | import interpolate from './interpolate'
2 | import translate from './translate'
3 | import looseEqual from './looseEqual'
4 | import uuid from './uuid'
5 | import { _Vue } from './localVue'
6 |
7 |
8 | const updateTranslation = (el, binding, vnode) => {
9 |
10 | let attrs = vnode.data.attrs || {}
11 | let msgid = el.dataset.msgid
12 | let translateContext = attrs['translate-context']
13 | let translateN = attrs['translate-n']
14 | let translatePlural = attrs['translate-plural']
15 | let isPlural = translateN !== undefined && translatePlural !== undefined
16 | let context = vnode.context
17 | let disableHtmlEscaping = attrs['render-html'] === 'true'
18 |
19 | if (!isPlural && (translateN || translatePlural)) {
20 | throw new Error('`translate-n` and `translate-plural` attributes must be used together:' + msgid + '.')
21 | }
22 |
23 | if (!_Vue.config.getTextPluginSilent && attrs['translate-params']) {
24 | console.warn(`\`translate-params\` is required as an expression for v-translate directive. Please change to \`v-translate='params'\`: ${msgid}`)
25 | }
26 |
27 | if (binding.value && typeof binding.value === 'object') {
28 | context = Object.assign({}, vnode.context, binding.value)
29 | }
30 |
31 | let translation = translate.getTranslation(
32 | msgid,
33 | translateN,
34 | translateContext,
35 | isPlural ? translatePlural : null,
36 | el.dataset.currentLanguage
37 | )
38 |
39 | let msg = interpolate(translation, context, disableHtmlEscaping)
40 |
41 | el.innerHTML = msg
42 |
43 | }
44 |
45 | /**
46 | * A directive to translate content according to the current language.
47 | *
48 | * Use this directive instead of the component if you need to translate HTML content.
49 | * It's too tricky to support HTML content within the component because we cannot get the raw HTML to use as `msgid`.
50 | *
51 | * This directive has a similar interface to the component, supporting
52 | * `translate-comment`, `translate-context`, `translate-plural`, `translate-n`.
53 | *
54 | * `
This is Sparta!
`
55 | *
56 | * If you need interpolation, you must add an expression that outputs binding value that changes with each of the
57 | * context variable:
58 | * `
I am %{ fullName } and from %{ location }
`
59 | */
60 | export default {
61 |
62 | bind (el, binding, vnode) {
63 |
64 | // Fix the problem with v-if, see #29.
65 | // Vue re-uses DOM elements for efficiency if they don't have a key attribute, see:
66 | // https://vuejs.org/v2/guide/conditional.html#Controlling-Reusable-Elements-with-key
67 | // https://vuejs.org/v2/api/#key
68 | if (_Vue.config.autoAddKeyAttributes && !vnode.key) {
69 | vnode.key = uuid()
70 | }
71 |
72 | // Get the raw HTML and store it in the element's dataset (as advised in Vue's official guide).
73 | let msgid = el.innerHTML
74 | el.dataset.msgid = msgid
75 |
76 | // Store the current language in the element's dataset.
77 | el.dataset.currentLanguage = _Vue.config.language
78 |
79 | // Output an info in the console if an interpolation is required but no expression is provided.
80 | if (!_Vue.config.getTextPluginSilent) {
81 | let hasInterpolation = msgid.indexOf(interpolate.INTERPOLATION_PREFIX) !== -1
82 | if (hasInterpolation && !binding.expression) {
83 | console.info(`No expression is provided for change detection. The translation for this key will be static:\n${msgid}`)
84 | }
85 | }
86 |
87 | updateTranslation(el, binding, vnode)
88 |
89 | },
90 |
91 | update (el, binding, vnode) {
92 |
93 | let doUpdate = false
94 |
95 | // Trigger an update if the language has changed.
96 | if (el.dataset.currentLanguage !== _Vue.config.language) {
97 | el.dataset.currentLanguage = _Vue.config.language
98 | doUpdate = true
99 | }
100 |
101 | // Trigger an update if an optional bound expression has changed.
102 | if (!doUpdate && binding.expression && !looseEqual(binding.value, binding.oldValue)) {
103 | doUpdate = true
104 | }
105 |
106 | if (doUpdate) {
107 | updateTranslation(el, binding, vnode)
108 | }
109 |
110 | },
111 |
112 | }
113 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import './object-assign-polyfill'
2 |
3 | import Component from './component'
4 | import Directive from './directive'
5 | import Config from './config'
6 | import interpolate from './interpolate'
7 | import Override from './override'
8 | import translate from './translate'
9 | import { shareVueInstance } from './localVue'
10 |
11 |
12 | let languageVm // Singleton.
13 |
14 | let GetTextPlugin = function (Vue, options = {}) {
15 |
16 | let defaultConfig = {
17 | autoAddKeyAttributes: false,
18 | availableLanguages: { en_US: 'English' },
19 | defaultLanguage: 'en_US',
20 | languageVmMixin: {},
21 | muteLanguages: [],
22 | silent: Vue.config.silent,
23 | translations: null,
24 | }
25 |
26 | Object.keys(options).forEach(key => {
27 | if (Object.keys(defaultConfig).indexOf(key) === -1) {
28 | throw new Error(`${key} is an invalid option for the translate plugin.`)
29 | }
30 | })
31 |
32 | if (!options.translations) {
33 | throw new Error('No translations available.')
34 | }
35 |
36 | options = Object.assign(defaultConfig, options)
37 |
38 | languageVm = new Vue({
39 | created: function () {
40 | // Non-reactive data.
41 | this.available = options.availableLanguages
42 | },
43 | data: {
44 | current: options.defaultLanguage,
45 | },
46 | mixins: [options.languageVmMixin],
47 | })
48 |
49 | shareVueInstance(Vue)
50 |
51 | Override(Vue, languageVm)
52 |
53 | Config(Vue, languageVm, options.silent, options.autoAddKeyAttributes, options.muteLanguages)
54 |
55 | translate.initTranslations(options.translations, Vue.config)
56 |
57 | // Makes available as a global component.
58 | Vue.component('translate', Component)
59 |
60 | // An option to support translation with HTML content: `v-translate`.
61 | Vue.directive('translate', Directive)
62 |
63 | // Exposes global properties.
64 | Vue.$translations = options.translations
65 | // Exposes instance methods.
66 | Vue.prototype.$gettext = translate.gettext.bind(translate)
67 | Vue.prototype.$pgettext = translate.pgettext.bind(translate)
68 | Vue.prototype.$ngettext = translate.ngettext.bind(translate)
69 | Vue.prototype.$npgettext = translate.npgettext.bind(translate)
70 | Vue.prototype.$gettextInterpolate = interpolate.bind(interpolate)
71 |
72 | }
73 |
74 | export default GetTextPlugin
75 | export {translate}
76 |
--------------------------------------------------------------------------------
/src/interpolate.js:
--------------------------------------------------------------------------------
1 | import { _Vue } from './localVue'
2 |
3 | const EVALUATION_RE = /[[\].]{1,2}/g
4 |
5 | /* Interpolation RegExp.
6 | *
7 | * Because interpolation inside attributes are deprecated in Vue 2 we have to
8 | * use another set of delimiters to be able to use `translate-plural` etc.
9 | * We use %{ } delimiters.
10 | *
11 | * /
12 | * %\{ => Starting delimiter: `%{`
13 | * ( => Start capture
14 | * (?:.|\n) => Non-capturing group: any character or newline
15 | * +? => One or more times (ungreedy)
16 | * ) => End capture
17 | * \} => Ending delimiter: `}`
18 | * /g => Global: don't return after first match
19 | */
20 | const INTERPOLATION_RE = /%\{((?:.|\n)+?)\}/g
21 |
22 | const MUSTACHE_SYNTAX_RE = /\{\{((?:.|\n)+?)\}\}/g
23 |
24 | /**
25 | * Evaluate a piece of template string containing %{ } placeholders.
26 | * E.g.: 'Hi %{ user.name }' => 'Hi Bob'
27 | *
28 | * This is a vm.$interpolate alternative for Vue 2.
29 | * https://vuejs.org/v2/guide/migration.html#vm-interpolate-removed
30 | *
31 | * @param {String} msgid - The translation key containing %{ } placeholders
32 | * @param {Object} context - An object whose elements are put in their corresponding placeholders
33 | *
34 | * @return {String} The interpolated string
35 | */
36 | let interpolate = function (msgid, context = {}, disableHtmlEscaping = false) {
37 |
38 | if (_Vue && !_Vue.config.getTextPluginSilent && MUSTACHE_SYNTAX_RE.test(msgid)) {
39 | console.warn(`Mustache syntax cannot be used with vue-gettext. Please use "%{}" instead of "{{}}" in: ${msgid}`)
40 | }
41 |
42 | let result = msgid.replace(INTERPOLATION_RE, (match, token) => {
43 |
44 | const expression = token.trim()
45 | let evaluated
46 |
47 | let escapeHtmlMap = {
48 | '&': '&',
49 | '<': '<',
50 | '>': '>',
51 | '"': '"',
52 | '\'': ''',
53 | }
54 |
55 | // Avoid eval() by splitting `expression` and looping through its different properties if any, see #55.
56 | function getProps (obj, expression) {
57 | const arr = expression.split(EVALUATION_RE).filter(x => x)
58 | while (arr.length) {
59 | obj = obj[arr.shift()]
60 | }
61 | return obj
62 | }
63 |
64 | function evalInContext (expression) {
65 | try {
66 | evaluated = getProps(this, expression)
67 | } catch (e) {
68 | // Ignore errors, because this function may be called recursively later.
69 | }
70 | if (evaluated === undefined) {
71 | if (this.$parent) {
72 | // Recursively climb the $parent chain to allow evaluation inside nested components, see #23 and #24.
73 | return evalInContext.call(this.$parent, expression)
74 | } else {
75 | console.warn(`Cannot evaluate expression: ${expression}`)
76 | evaluated = expression
77 | }
78 | }
79 | let result = evaluated.toString()
80 | if (disableHtmlEscaping) {
81 | // Do not escape HTML, see #78.
82 | return result
83 | }
84 | // Escape HTML, see #78.
85 | return result.replace(/[&<>"']/g, function (m) { return escapeHtmlMap[m] })
86 | }
87 |
88 | return evalInContext.call(context, expression)
89 |
90 | })
91 |
92 | return result
93 |
94 | }
95 |
96 | // Store this values as function attributes for easy access elsewhere to bypass a Rollup
97 | // weak point with `export`:
98 | // https://github.com/rollup/rollup/blob/fca14d/src/utils/getExportMode.js#L27
99 | interpolate.INTERPOLATION_RE = INTERPOLATION_RE
100 | interpolate.INTERPOLATION_PREFIX = '%{'
101 |
102 | export default interpolate
103 |
--------------------------------------------------------------------------------
/src/localVue.js:
--------------------------------------------------------------------------------
1 | // Ensure to always use the same Vue instance throughout the plugin.
2 | //
3 | // This was previously done in `index.js` using both named and default exports.
4 | // However, this currently must be kept in a separate file because we are using
5 | // Rollup to build the dist files and it has a drawback when using named and
6 | // default exports together, see:
7 | // https://github.com/rollup/rollup/blob/fca14d/src/utils/getExportMode.js#L27
8 | // https://github.com/rollup/rollup/wiki/JavaScript-API#exports
9 | //
10 | // If we had kept named and default exports in `index.js`, a user would have to
11 | // do something like this to access the default export: GetTextPlugin['default']
12 |
13 | export let _Vue
14 |
15 | export function shareVueInstance (Vue) {
16 | _Vue = Vue
17 | }
18 |
--------------------------------------------------------------------------------
/src/looseEqual.js:
--------------------------------------------------------------------------------
1 | // Check if two values are loosely equal - that is,
2 | // if they are plain objects, do they have the same shape?
3 | // https://github.com/vuejs/vue/blob/v2.6.11/src/shared/util.js#L285
4 |
5 | export default function looseEqual (a, b) {
6 | if (a === b) return true
7 | const isObjectA = a !== null && typeof a === 'object'
8 | const isObjectB = b !== null && typeof b === 'object'
9 | if (isObjectA && isObjectB) {
10 | try {
11 | const isArrayA = Array.isArray(a)
12 | const isArrayB = Array.isArray(b)
13 | if (isArrayA && isArrayB) {
14 | return a.length === b.length && a.every((e, i) => {
15 | return looseEqual(e, b[i])
16 | })
17 | } else if (a instanceof Date && b instanceof Date) {
18 | return a.getTime() === b.getTime()
19 | } else if (!isArrayA && !isArrayB) {
20 | const keysA = Object.keys(a)
21 | const keysB = Object.keys(b)
22 | return keysA.length === keysB.length && keysA.every(key => {
23 | return looseEqual(a[key], b[key])
24 | })
25 | } else {
26 | return false
27 | }
28 | } catch (e) {
29 | return false
30 | }
31 | } else if (!isObjectA && !isObjectB) {
32 | return String(a) === String(b)
33 | } else {
34 | return false
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/object-assign-polyfill.js:
--------------------------------------------------------------------------------
1 | // Polyfill Object.assign for legacy browsers.
2 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
3 |
4 | if (typeof Object.assign !== 'function') {
5 | (function () {
6 | Object.assign = function (target) {
7 | 'use strict'
8 | var output
9 | var index
10 | var source
11 | var nextKey
12 | if (target === undefined || target === null) {
13 | throw new TypeError('Cannot convert undefined or null to object')
14 | }
15 | output = Object(target)
16 | for (index = 1; index < arguments.length; index++) {
17 | source = arguments[index]
18 | if (source !== undefined && source !== null) {
19 | for (nextKey in source) {
20 | if (source.hasOwnProperty(nextKey)) {
21 | output[nextKey] = source[nextKey]
22 | }
23 | }
24 | }
25 | }
26 | return output
27 | }
28 | }())
29 | }
30 |
--------------------------------------------------------------------------------
/src/override.js:
--------------------------------------------------------------------------------
1 | export default function (Vue, languageVm) {
2 |
3 | // Override the main init sequence. This is called for every instance.
4 | const init = Vue.prototype._init
5 | Vue.prototype._init = function (options = {}) {
6 | const root = options._parent || options.parent || this
7 | // Expose languageVm to every instance.
8 | this.$language = root.$language || languageVm
9 | init.call(this, options)
10 | }
11 |
12 | // Override the main destroy sequence to destroy all languageVm watchers.
13 | const destroy = Vue.prototype._destroy
14 | Vue.prototype._destroy = function () {
15 | this.$language = null
16 | destroy.apply(this, arguments)
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/plurals.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Plural Forms
3 | *
4 | * This is a list of the plural forms, as used by Gettext PO, that are appropriate to each language.
5 | * http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html
6 | *
7 | * This is a replica of angular-gettext's plural.js
8 | * https://github.com/rubenv/angular-gettext/blob/master/src/plural.js
9 | */
10 | export default {
11 |
12 | getTranslationIndex: function (languageCode, n) {
13 |
14 | n = Number(n)
15 | n = typeof n === 'number' && isNaN(n) ? 1 : n // Fallback to singular.
16 |
17 | // Extract the ISO 639 language code. The ISO 639 standard defines
18 | // two-letter codes for many languages, and three-letter codes for
19 | // more rarely used languages.
20 | // https://www.gnu.org/software/gettext/manual/html_node/Language-Codes.html#Language-Codes
21 | if (languageCode.length > 2 && languageCode !== 'pt_BR') {
22 | languageCode = languageCode.split('_')[0]
23 | }
24 |
25 | switch (languageCode) {
26 | case 'ay': // Aymará
27 | case 'bo': // Tibetan
28 | case 'cgg': // Chiga
29 | case 'dz': // Dzongkha
30 | case 'fa': // Persian
31 | case 'id': // Indonesian
32 | case 'ja': // Japanese
33 | case 'jbo': // Lojban
34 | case 'ka': // Georgian
35 | case 'kk': // Kazakh
36 | case 'km': // Khmer
37 | case 'ko': // Korean
38 | case 'ky': // Kyrgyz
39 | case 'lo': // Lao
40 | case 'ms': // Malay
41 | case 'my': // Burmese
42 | case 'sah': // Yakut
43 | case 'su': // Sundanese
44 | case 'th': // Thai
45 | case 'tt': // Tatar
46 | case 'ug': // Uyghur
47 | case 'vi': // Vietnamese
48 | case 'wo': // Wolof
49 | case 'zh': // Chinese
50 | // 1 form
51 | return 0
52 | case 'is': // Icelandic
53 | // 2 forms
54 | return (n % 10 !== 1 || n % 100 === 11) ? 1 : 0
55 | case 'jv': // Javanese
56 | // 2 forms
57 | return n !== 0 ? 1 : 0
58 | case 'mk': // Macedonian
59 | // 2 forms
60 | return n === 1 || n % 10 === 1 ? 0 : 1
61 | case 'ach': // Acholi
62 | case 'ak': // Akan
63 | case 'am': // Amharic
64 | case 'arn': // Mapudungun
65 | case 'br': // Breton
66 | case 'fil': // Filipino
67 | case 'fr': // French
68 | case 'gun': // Gun
69 | case 'ln': // Lingala
70 | case 'mfe': // Mauritian Creole
71 | case 'mg': // Malagasy
72 | case 'mi': // Maori
73 | case 'oc': // Occitan
74 | case 'pt_BR': // Brazilian Portuguese
75 | case 'tg': // Tajik
76 | case 'ti': // Tigrinya
77 | case 'tr': // Turkish
78 | case 'uz': // Uzbek
79 | case 'wa': // Walloon
80 | /* eslint-disable */
81 | /* Disable "Duplicate case label" because there are 2 forms of Chinese plurals */
82 | case 'zh': // Chinese
83 | /* eslint-enable */
84 | // 2 forms
85 | return n > 1 ? 1 : 0
86 | case 'lv': // Latvian
87 | // 3 forms
88 | return (n % 10 === 1 && n % 100 !== 11 ? 0 : n !== 0 ? 1 : 2)
89 | case 'lt': // Lithuanian
90 | // 3 forms
91 | return (n % 10 === 1 && n % 100 !== 11 ? 0 : n % 10 >= 2 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2)
92 | case 'be': // Belarusian
93 | case 'bs': // Bosnian
94 | case 'hr': // Croatian
95 | case 'ru': // Russian
96 | case 'sr': // Serbian
97 | case 'uk': // Ukrainian
98 | // 3 forms
99 | return (
100 | n % 10 === 1 && n % 100 !== 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2)
101 | case 'mnk': // Mandinka
102 | // 3 forms
103 | return (n === 0 ? 0 : n === 1 ? 1 : 2)
104 | case 'ro': // Romanian
105 | // 3 forms
106 | return (n === 1 ? 0 : (n === 0 || (n % 100 > 0 && n % 100 < 20)) ? 1 : 2)
107 | case 'pl': // Polish
108 | // 3 forms
109 | return (n === 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2)
110 | case 'cs': // Czech
111 | case 'sk': // Slovak
112 | // 3 forms
113 | return (n === 1) ? 0 : (n >= 2 && n <= 4) ? 1 : 2
114 | case 'csb': // Kashubian
115 | // 3 forms
116 | return (n === 1) ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2
117 | case 'sl': // Slovenian
118 | // 4 forms
119 | return (n % 100 === 1 ? 0 : n % 100 === 2 ? 1 : n % 100 === 3 || n % 100 === 4 ? 2 : 3)
120 | case 'mt': // Maltese
121 | // 4 forms
122 | return (n === 1 ? 0 : n === 0 || (n % 100 > 1 && n % 100 < 11) ? 1 : (n % 100 > 10 && n % 100 < 20) ? 2 : 3)
123 | case 'gd': // Scottish Gaelic
124 | // 4 forms
125 | return (n === 1 || n === 11) ? 0 : (n === 2 || n === 12) ? 1 : (n > 2 && n < 20) ? 2 : 3
126 | case 'cy': // Welsh
127 | // 4 forms
128 | return (n === 1) ? 0 : (n === 2) ? 1 : (n !== 8 && n !== 11) ? 2 : 3
129 | case 'kw': // Cornish
130 | // 4 forms
131 | return (n === 1) ? 0 : (n === 2) ? 1 : (n === 3) ? 2 : 3
132 | case 'ga': // Irish
133 | // 5 forms
134 | return n === 1 ? 0 : n === 2 ? 1 : (n > 2 && n < 7) ? 2 : (n > 6 && n < 11) ? 3 : 4
135 | case 'ar': // Arabic
136 | // 6 forms
137 | return (n === 0 ? 0 : n === 1 ? 1 : n === 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5)
138 | default: // Everything else
139 | return n !== 1 ? 1 : 0
140 | }
141 | },
142 |
143 | }
144 |
--------------------------------------------------------------------------------
/src/translate.js:
--------------------------------------------------------------------------------
1 | import plurals from './plurals'
2 | import interpolate from './interpolate'
3 |
4 | const SPACING_RE = /\s{2,}/g
5 |
6 | // Default configuration if only the translation is passed.
7 | let _config = {
8 | language: '',
9 | getTextPluginSilent: false,
10 | getTextPluginMuteLanguages: [],
11 | silent: false,
12 | }
13 | let _translations = {}
14 |
15 | const translate = {
16 |
17 | /*
18 | * Get the translated string from the translation.json file generated by easygettext.
19 | *
20 | * @param {String} msgid - The translation key
21 | * @param {Number} n - The number to switch between singular and plural
22 | * @param {String} context - The translation key context
23 | * @param {String} defaultPlural - The default plural value (optional)
24 | * @param {String} language - The language ID (e.g. 'fr_FR' or 'en_US')
25 | *
26 | * @return {String} The translated string
27 | */
28 | getTranslation: function (msgid, n = 1, context = null, defaultPlural = null, language = _config.language) {
29 |
30 | if (!msgid) {
31 | return '' // Allow empty strings.
32 | }
33 |
34 | let silent = _config.getTextPluginSilent || (_config.getTextPluginMuteLanguages.indexOf(language) !== -1)
35 |
36 | // Default untranslated string, singular or plural.
37 | let untranslated = defaultPlural && plurals.getTranslationIndex(language, n) > 0 ? defaultPlural : msgid
38 |
39 | // `easygettext`'s `gettext-compile` generates a JSON version of a .po file based on its `Language` field.
40 | // But in this field, `ll_CC` combinations denoting a language’s main dialect are abbreviated as `ll`,
41 | // for example `de` is equivalent to `de_DE` (German as spoken in Germany).
42 | // See the `Language` section in https://www.gnu.org/software/gettext/manual/html_node/Header-Entry.html
43 | // So try `ll_CC` first, or the `ll` abbreviation which can be three-letter sometimes:
44 | // https://www.gnu.org/software/gettext/manual/html_node/Language-Codes.html#Language-Codes
45 | let translations = _translations[language] || _translations[language.split('_')[0]]
46 |
47 | if (!translations) {
48 | if (!silent) {
49 | console.warn(`No translations found for ${language}`)
50 | }
51 | return untranslated
52 | }
53 |
54 | // Currently easygettext trims entries since it needs to output consistent PO translation content
55 | // even if a web template designer added spaces between lines (which are ignored in HTML or jade,
56 | // but are significant in text). See #65.
57 | // Replicate the same behaviour here.
58 | msgid = msgid.trim()
59 |
60 | let translated = translations[msgid]
61 |
62 | // Sometimes `msgid` may not have the same number of spaces than its translation key.
63 | // This could happen because we use the private attribute `_renderChildren` to access the raw uninterpolated
64 | // string to translate in the `created` hook of `component.js`: spaces are not exactly the same between the
65 | // HTML and the content of `_renderChildren`, e.g. 6 spaces becomes 4 etc. See #15, #38.
66 | // In such cases, we need to compare the translation keys and `msgid` with the same number of spaces.
67 | if (!translated && SPACING_RE.test(msgid)) {
68 | Object.keys(translations).some(key => {
69 | if (key.replace(SPACING_RE, ' ') === msgid.replace(SPACING_RE, ' ')) {
70 | translated = translations[key]
71 | return translated
72 | }
73 | })
74 | }
75 |
76 | if (translated && context) {
77 | translated = translated[context]
78 | }
79 |
80 | if (!translated) {
81 | if (!silent) {
82 | let msg = `Untranslated ${language} key found: ${msgid}`
83 | if (context) {
84 | msg += ` (with context: ${context})`
85 | }
86 | console.warn(msg)
87 | }
88 | return untranslated
89 | }
90 |
91 | // Avoid a crash when a msgid exists with and without a context, see #32.
92 | if (!(translated instanceof Array) && translated.hasOwnProperty('')) {
93 | // As things currently stand, the void key means a void context for easygettext.
94 | translated = translated['']
95 | }
96 |
97 | if (typeof translated === 'string') {
98 | translated = [translated]
99 | }
100 |
101 | let translationIndex = plurals.getTranslationIndex(language, n)
102 |
103 | // Do not assume that the default value of n is 1 for the singular form of all languages.
104 | // E.g. Arabic, see #69.
105 | if (translated.length === 1 && n === 1) {
106 | translationIndex = 0
107 | }
108 |
109 | return translated[translationIndex]
110 |
111 | },
112 |
113 | /*
114 | * Returns a string of the translation of the message.
115 | * Also makes the string discoverable by gettext-extract.
116 | *
117 | * @param {String} msgid - The translation key
118 | * @param {String} language - The language ID (e.g. 'fr_FR' or 'en_US')
119 | *
120 | * @return {String} The translated string
121 | */
122 | 'gettext': function (msgid, language = _config.language) {
123 | return translate.getTranslation(msgid, 1, null, null, language)
124 | },
125 |
126 | /*
127 | * Returns a string of the translation for the given context.
128 | * Also makes the string discoverable by gettext-extract.
129 | *
130 | * @param {String} context - The context of the string to translate
131 | * @param {String} msgid - The translation key
132 | * @param {String} language - The language ID (e.g. 'fr_FR' or 'en_US')
133 | *
134 | * @return {String} The translated string
135 | */
136 | 'pgettext': function (context, msgid, language = _config.language) {
137 | return translate.getTranslation(msgid, 1, context, null, language)
138 | },
139 |
140 | /*
141 | * Returns a string of the translation of either the singular or plural,
142 | * based on the number.
143 | * Also makes the string discoverable by gettext-extract.
144 | *
145 | * @param {String} msgid - The translation key
146 | * @param {String} plural - The plural form of the translation key
147 | * @param {Number} n - The number to switch between singular and plural
148 | * @param {String} language - The language ID (e.g. 'fr_FR' or 'en_US')
149 | *
150 | * @return {String} The translated string
151 | */
152 | 'ngettext': function (msgid, plural, n, language = _config.language) {
153 | return translate.getTranslation(msgid, n, null, plural, language)
154 | },
155 |
156 | /*
157 | * Returns a string of the translation of either the singular or plural,
158 | * based on the number, for the given context.
159 | * Also makes the string discoverable by gettext-extract.
160 | *
161 | * @param {String} context - The context of the string to translate
162 | * @param {String} msgid - The translation key
163 | * @param {String} plural - The plural form of the translation key
164 | * @param {Number} n - The number to switch between singular and plural
165 | * @param {String} language - The language ID (e.g. 'fr_FR' or 'en_US')
166 | *
167 | * @return {String} The translated string
168 | */
169 | 'npgettext': function (context, msgid, plural, n, language = _config.language) {
170 | return translate.getTranslation(msgid, n, context, plural, language)
171 | },
172 |
173 | /*
174 | * Initialize local state for translations and configuration
175 | * so that it works without Vue.
176 | *
177 | * @param {Object} translations - translations.json
178 | * @param {Object} config - Vue.config
179 | *
180 | */
181 | initTranslations: function (translations, config) {
182 | if (translations && typeof translations === 'object') {
183 | _translations = translations
184 | }
185 | if (config && typeof config === 'object') {
186 | _config = config
187 | }
188 | },
189 |
190 | /**
191 | * Allows to use interpolation outside the Vue
192 | *
193 | * @example
194 | * import {translate} from 'vue-gettext';
195 | *
196 | * const {gettext, gettextInterpolate} = translate;
197 | *
198 | * let translated = gettext('%{ n } foos', n)
199 | * let interpolated = gettextInterpolate(translated, {n: 5})
200 | */
201 | gettextInterpolate: interpolate.bind(interpolate),
202 |
203 | }
204 |
205 | export default translate
206 |
--------------------------------------------------------------------------------
/src/uuid.js:
--------------------------------------------------------------------------------
1 | // UUID v4 generator (RFC4122 compliant).
2 | //
3 | // https://gist.github.com/jcxplorer/823878
4 |
5 | export default function uuid () {
6 |
7 | let uuid = ''
8 | let i
9 | let random
10 |
11 | for (i = 0; i < 32; i++) {
12 | random = Math.random() * 16 | 0
13 | if (i === 8 || i === 12 || i === 16 || i === 20) {
14 | uuid += '-'
15 | }
16 | uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random)).toString(16)
17 | }
18 |
19 | return uuid
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | // Require all test files (files that ends with .spec.js).
2 | const testsContext = require.context('./specs', true, /\.spec$/)
3 | testsContext.keys().forEach(testsContext)
4 |
--------------------------------------------------------------------------------
/test/specs/component.spec.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | import GetTextPlugin from '../../src/'
4 | import translations from './json/component.json'
5 | import uninstallPlugin from '../testUtils'
6 |
7 |
8 | describe('translate component tests', () => {
9 |
10 | beforeEach(function () {
11 | uninstallPlugin(Vue, GetTextPlugin)
12 | Vue.use(GetTextPlugin, {
13 | availableLanguages: {
14 | en_US: 'American English',
15 | fr_FR: 'Français',
16 | },
17 | defaultLanguage: 'en_US',
18 | translations: translations,
19 | })
20 | })
21 |
22 | it('works on empty strings', () => {
23 | let vm = new Vue({template: '
'}).$mount()
24 | expect(vm.$el.innerHTML.trim()).to.equal('')
25 | })
26 |
27 | it('returns an unchanged string when no translation is available for a language', () => {
28 | console.warn = sinon.spy(console, 'warn')
29 | let vm = new Vue({template: '
Unchanged string
'}).$mount()
30 | vm.$language.current = 'fr_BE'
31 | expect(vm.$el.innerHTML.trim()).to.equal('Unchanged string')
32 | expect(console.warn).calledOnce
33 | console.warn.restore()
34 | })
35 |
36 | it('returns an unchanged string when no translation key is available', () => {
37 | console.warn = sinon.spy(console, 'warn')
38 | let vm = new Vue({template: '
Untranslated string
'}).$mount()
39 | expect(vm.$el.innerHTML.trim()).to.equal('Untranslated string')
40 | expect(console.warn).calledOnce
41 | console.warn.restore()
42 | })
43 |
44 | it('translates known strings', () => {
45 | Vue.config.language = 'fr_FR'
46 | let vm = new Vue({template: '
Pending
'}).$mount()
47 | expect(vm.$el.innerHTML.trim()).to.equal('En cours')
48 | })
49 |
50 | it('translates multiline strings no matter the number of spaces', () => {
51 | Vue.config.language = 'fr_FR'
52 | let vm = new Vue({template: `
')
79 | })
80 |
81 | it('translates known strings according to a given translation context', () => {
82 | Vue.config.language = 'en_US'
83 | let vm = new Vue({template: '
Answer
'}).$mount()
84 | expect(vm.$el.innerHTML.trim()).to.equal('Answer (verb)')
85 | vm = new Vue({template: '
',
27 | data: {count: 1},
28 | }).$mount()
29 | expect(vm.$el.innerHTML).to.equal('البرتقالي')
30 | })
31 |
32 | it('translates plural form 0', () => {
33 | let vm = new Vue({
34 | template: '
%{ count } day
',
35 | data: {count: 0},
36 | }).$mount()
37 | expect(vm.$el.innerHTML).to.equal('{ count }أقل من يوم')
38 | })
39 |
40 | it('translates plural form 1', () => {
41 | let vm = new Vue({
42 | template: '
%{ count } day
',
43 | data: {count: 1},
44 | }).$mount()
45 | expect(vm.$el.innerHTML).to.equal('{ count }يوم واحد')
46 | })
47 |
48 | it('translates plural form 2', () => {
49 | let vm = new Vue({
50 | template: '
%{ count } day
',
51 | data: {count: 2},
52 | }).$mount()
53 | expect(vm.$el.innerHTML).to.equal('{ count }يومان')
54 | })
55 |
56 | it('translates plural form 3', () => {
57 | let vm = new Vue({
58 | template: '
%{ count } day
',
59 | data: {count: 9},
60 | }).$mount()
61 | expect(vm.$el.innerHTML).to.equal('{ count } أيام')
62 | })
63 |
64 | it('translates plural form 4', () => {
65 | let vm = new Vue({
66 | template: '
%{ count } day
',
67 | data: {count: 11},
68 | }).$mount()
69 | expect(vm.$el.innerHTML).to.equal('{ count } يومًا')
70 | })
71 |
72 | it('translates plural form 5', () => {
73 | let vm = new Vue({
74 | template: '
%{ count } day
',
75 | data: {count: 3000},
76 | }).$mount()
77 | expect(vm.$el.innerHTML).to.equal('{ count } يوم')
78 | })
79 |
80 | })
81 |
--------------------------------------------------------------------------------
/test/specs/directive.spec.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | import GetTextPlugin from '../../src/'
4 | import translations from './json/directive.json'
5 | import uninstallPlugin from '../testUtils'
6 |
7 |
8 | describe('translate directive tests', () => {
9 |
10 | beforeEach(function () {
11 | uninstallPlugin(Vue, GetTextPlugin)
12 | Vue.use(GetTextPlugin, {
13 | availableLanguages: {
14 | en_US: 'American English',
15 | fr_FR: 'Français',
16 | },
17 | defaultLanguage: 'en_US',
18 | translations: translations,
19 | })
20 | })
21 |
22 | it('works on empty strings', () => {
23 | let vm = new Vue({template: ''}).$mount()
24 | expect(vm.$el.innerHTML).to.equal('')
25 | })
26 |
27 | it('returns an unchanged string when no translation is available for a language', () => {
28 | console.warn = sinon.spy(console, 'warn')
29 | let vm = new Vue({template: '
Unchanged string
'}).$mount()
30 | vm.$language.current = 'fr_BE'
31 | expect(vm.$el.innerHTML).to.equal('Unchanged string')
32 | expect(console.warn).calledOnce
33 | console.warn.restore()
34 | })
35 |
36 | it('returns an unchanged string when no translation key is available', () => {
37 | console.warn = sinon.spy(console, 'warn')
38 | let vm = new Vue({template: '
Untranslated string
'}).$mount()
39 | expect(vm.$el.innerHTML).to.equal('Untranslated string')
40 | expect(console.warn).calledOnce
41 | console.warn.restore()
42 | })
43 |
44 | it('translates known strings', () => {
45 | Vue.config.language = 'fr_FR'
46 | let vm = new Vue({template: '
Pending
'}).$mount()
47 | expect(vm.$el.innerHTML).to.equal('En cours')
48 | })
49 |
50 | it('translates known strings when surrounded by one or more tabs and spaces', () => {
51 | Vue.config.language = 'fr_FR'
52 | let vm = new Vue({template: '
\tPending\t\t \t\r\n\t\f\v
'}).$mount()
53 | expect(vm.$el.innerHTML).to.equal('En cours')
54 | })
55 |
56 | it('translates multiline strings as-is, preserving the original content', () => {
57 | Vue.config.language = 'fr_FR'
58 | let vm = new Vue({template: '
\n\nA\n\n\nlot\n\n\nof\n\nlines
'}).$mount()
59 | expect(vm.$el.innerHTML).to.equal('Plein\n\n\nde\n\nlignes')
60 | })
61 |
62 | it('translates known strings according to a given translation context', () => {
63 | Vue.config.language = 'en_US'
64 | let vm = new Vue({template: '
Answer
'}).$mount()
65 | expect(vm.$el.innerHTML).to.equal('Answer (verb)')
66 | vm = new Vue({template: '
Answer
'}).$mount()
67 | expect(vm.$el.innerHTML).to.equal('Answer (noun)')
68 | })
69 |
70 | it('works with text content', () => {
71 | let vm = new Vue({template: '
This is sparta!
'}).$mount()
72 | expect(vm.$el.innerHTML).to.equal('This is sparta!')
73 | })
74 |
75 | it('works with HTML content', () => {
76 | let vm = new Vue({template: '
This is sparta!
'}).$mount()
77 | expect(vm.$el.innerHTML).to.equal('This is sparta!')
78 | })
79 |
80 | it('allows interpolation', () => {
81 | Vue.config.language = 'fr_FR'
82 | let vm = new Vue({
83 | template: '
Hello %{ name }
',
84 | data: {name: 'John Doe'},
85 | }).$mount()
86 | expect(vm.$el.innerHTML).to.equal('Bonjour John Doe')
87 | })
88 |
89 | it('escapes HTML in variables by default', () => {
90 | Vue.config.language = 'fr_FR'
91 | let vm = new Vue({
92 | template: '
Hello %{ openingTag }%{ name }%{ closingTag }
',
93 | data: {
94 | name: 'John Doe',
95 | openingTag: '',
96 | closingTag: '',
97 | },
98 | }).$mount()
99 | expect(vm.$el.innerHTML).to.equal('Bonjour <b>John Doe</b>')
100 | })
101 |
102 | it('forces HTML rendering in variables (with the `render-html` attribute set to `true`)', () => {
103 | Vue.config.language = 'fr_FR'
104 | let vm = new Vue({
105 | template: '