├── .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 | 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 |

295 | Hello %{ openingTag }%{ name }%{ closingTag } 296 |

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 | 16 | 17 | 42 | -------------------------------------------------------------------------------- /dev/components/customTags.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /dev/components/directive.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 41 | -------------------------------------------------------------------------------- /dev/components/if/index.js: -------------------------------------------------------------------------------- 1 | import template from './template.html' 2 | 3 | 4 | export default { 5 | data: () => ({ 6 | show: true, 7 | obj: { 8 | name: 'Group1', 9 | }, 10 | }), 11 | methods: { 12 | toggleShow () { 13 | this.show = !this.show 14 | }, 15 | setName1 () { 16 | this.obj.name = 'Group1' 17 | }, 18 | setName2 () { 19 | this.obj.name = 'Group2' 20 | }, 21 | setName3 () { 22 | this.obj.name = 'Group3' 23 | }, 24 | }, 25 | template: template, 26 | } 27 | -------------------------------------------------------------------------------- /dev/components/if/template.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | 5 | Welcome %{ firstname } 6 |

7 | 8 |
9 |

10 | 11 | 12 | 13 | [{{ obj.name }}] 14 |

15 |

This is %{ obj.name }

16 |

This is %{ obj.name }

17 |

This is %{ obj.name }

18 |
19 | 20 |
21 | -------------------------------------------------------------------------------- /dev/components/languageSelect.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /dev/components/momentFilter.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 25 | -------------------------------------------------------------------------------- /dev/components/multilines.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /dev/components/plural/index.js: -------------------------------------------------------------------------------- 1 | import styles from './styles.css' 2 | import template from './template.html' 3 | 4 | 5 | export default { 6 | created: function () { 7 | this.styles = styles 8 | }, 9 | data: () => ({ 10 | n: 0, 11 | countForUntranslated: 10, 12 | }), 13 | computed: { 14 | nComputed () { 15 | return this.n 16 | }, 17 | }, 18 | methods: { 19 | decrease () { 20 | if (this.n === 0) return 21 | this.n -= 1 22 | }, 23 | increase () { 24 | this.n += 1 25 | }, 26 | }, 27 | template: template, 28 | } 29 | -------------------------------------------------------------------------------- /dev/components/plural/styles.css: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables.css'; 2 | 3 | 4 | :local(.title) { 5 | 6 | color: var(--color-blue); 7 | 8 | } 9 | -------------------------------------------------------------------------------- /dev/components/plural/template.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | In English, '0' (zero) is always plural. 5 |

6 |

7 | 8 | {{ n }} 9 | 10 |

11 |

12 | data: 13 | %{ n } book 14 |

15 |

16 | computed: 17 | 18 | %{ nComputed } book 19 | 20 |

21 | 22 |

23 | 24 | Use default singular or plural form when there is no translation. This is left untranslated on purpose. 25 | 26 |

27 |

28 | 29 | %{ countForUntranslated } item. This is left untranslated on purpose. 30 | 31 |

32 | 33 |
34 | -------------------------------------------------------------------------------- /dev/index.js: -------------------------------------------------------------------------------- 1 | import './styles/global.css' 2 | 3 | import Vue from 'vue' 4 | 5 | import GetTextPlugin from '../src/index' 6 | import translations from './translations.json' 7 | 8 | import AlertComponent from './components/alert' 9 | import CustomTags from './components/customTags' 10 | import DirectiveComponent from './components/directive' 11 | import IfComponent from './components/if' 12 | import LanguageSelectComponent from './components/languageSelect' 13 | import MomentFilterComponent from './components/momentFilter' 14 | import MultiLinesComponent from './components/multilines' 15 | import PluralComponent from './components/plural' 16 | 17 | 18 | Vue.use(GetTextPlugin, { 19 | availableLanguages: { 20 | en_GB: 'British English', 21 | fr_FR: 'Français', 22 | it_IT: 'Italiano', 23 | }, 24 | defaultLanguage: 'en_GB', 25 | translations: translations, 26 | }) 27 | 28 | export let vm = new Vue({ 29 | el: '#app', 30 | components: { 31 | 'alert': AlertComponent, 32 | 'custom-tags': CustomTags, 33 | 'directive': DirectiveComponent, 34 | 'if': IfComponent, 35 | 'language-select': LanguageSelectComponent, 36 | 'moment-filter': MomentFilterComponent, 37 | 'multilines': MultiLinesComponent, 38 | 'plural': PluralComponent, 39 | }, 40 | }) 41 | -------------------------------------------------------------------------------- /dev/index.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | vue-gettext example 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 |
14 | 15 |

[Alert]

16 | 17 | 18 |
19 | 20 |

[Plural]

21 | 22 | 23 |
24 | 25 |

[Filter]

26 | 27 | 28 |
29 | 30 |

[Custom tags]

31 | 32 | 33 |
34 | 35 |

[Multilines]

36 | 37 | 38 |
39 | 40 |

[Directive]

41 | 42 | 43 |
44 | 45 |

[If]

46 | 47 | 48 |
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: `
53 | 54 | 55 | 56 | A 57 | 58 | 59 | lot 60 | 61 | 62 | 63 | 64 | of 65 | 66 | lines 67 | 68 | 69 | 70 | 71 |
`}).$mount() 72 | expect(vm.$el.innerHTML.trim()).to.equal(`

Plein de lignes

`) 73 | }) 74 | 75 | it('renders translation in custom html tag', () => { 76 | Vue.config.language = 'fr_FR' 77 | let vm = new Vue({template: '
Pending
'}).$mount() 78 | expect(vm.$el.innerHTML.trim()).to.equal('

En cours

') 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: '
Answer
'}).$mount() 86 | expect(vm.$el.innerHTML.trim()).to.equal('Answer (noun)') 87 | }) 88 | 89 | it('allows interpolation', () => { 90 | Vue.config.language = 'fr_FR' 91 | let vm = new Vue({ 92 | template: '

Hello %{ name }

', 93 | data: {name: 'John Doe'}, 94 | }).$mount() 95 | expect(vm.$el.innerHTML.trim()).to.equal('Bonjour John Doe') 96 | }) 97 | 98 | it('allows interpolation with computed property', () => { 99 | Vue.config.language = 'fr_FR' 100 | let vm = new Vue({ 101 | template: '

Hello %{ name }

', 102 | computed: { 103 | name () { return 'John Doe' }, 104 | }, 105 | }).$mount() 106 | expect(vm.$el.innerHTML.trim()).to.equal('Bonjour John Doe') 107 | }) 108 | 109 | it('allows custom params for interpolation', () => { 110 | Vue.config.language = 'fr_FR' 111 | let vm = new Vue({ 112 | template: '

Hello %{ name }

', 113 | data: { 114 | someNewNameVar: 'John Doe', 115 | }, 116 | }).$mount() 117 | expect(vm.$el.innerHTML.trim()).to.equal('Bonjour John Doe') 118 | }) 119 | 120 | it('allows interpolation within v-for with custom params', () => { 121 | Vue.config.language = 'fr_FR' 122 | let vm = new Vue({ 123 | template: '

Hello %{ name }

', 124 | data: { 125 | names: ['John Doe', 'Chester'], 126 | }, 127 | }).$mount() 128 | expect(vm.$el.innerHTML.trim()).to.equal('Bonjour John DoeBonjour Chester') 129 | }) 130 | 131 | it('translates plurals', () => { 132 | Vue.config.language = 'fr_FR' 133 | let vm = new Vue({ 134 | template: `

135 | %{ count } car 136 |

`, 137 | data: {count: 2}, 138 | }).$mount() 139 | expect(vm.$el.innerHTML.trim()).to.equal('2 véhicules') 140 | }) 141 | 142 | it('translates plurals with computed property', () => { 143 | Vue.config.language = 'fr_FR' 144 | let vm = new Vue({ 145 | template: `

146 | %{ count } car 147 |

`, 148 | computed: { 149 | count () { return 2 }, 150 | }, 151 | }).$mount() 152 | expect(vm.$el.innerHTML.trim()).to.equal('2 véhicules') 153 | }) 154 | 155 | it('updates a plural translation after a data change', (done) => { 156 | Vue.config.language = 'fr_FR' 157 | let vm = new Vue({ 158 | template: `

159 | %{ count } car 160 |

`, 161 | data: {count: 10}, 162 | }).$mount() 163 | expect(vm.$el.innerHTML.trim()).to.equal('10 véhicules') 164 | vm.count = 8 165 | vm.$nextTick(function () { 166 | expect(vm.$el.innerHTML.trim()).to.equal('8 véhicules') 167 | done() 168 | }) 169 | }) 170 | 171 | it('updates a translation after a language change', (done) => { 172 | Vue.config.language = 'fr_FR' 173 | let vm = new Vue({template: '
Pending
'}).$mount() 174 | expect(vm.$el.innerHTML.trim()).to.equal('En cours') 175 | Vue.config.language = 'en_US' 176 | vm.$nextTick(function () { 177 | expect(vm.$el.innerHTML.trim()).to.equal('Pending') 178 | done() 179 | }) 180 | }) 181 | 182 | it('thrown errors displayed in the console if you forget to add a `translate-plural` attribute', () => { 183 | console.error = sinon.spy(console, 'error') 184 | new Vue({ 185 | template: '%{ n } car', 186 | data: {n: 2}, 187 | }).$mount() 188 | expect(console.error) 189 | .calledWith(sinon.match('`translate-n` and `translate-plural` attributes must be used together: %{ n } car.')) 190 | console.error.restore() 191 | }) 192 | 193 | it('thrown errors displayed in the console if you forget to add a `translate-n` attribute', () => { 194 | console.error = sinon.spy(console, 'error') 195 | new Vue({ 196 | template: '

%{ n } car

', 197 | }).$mount() 198 | expect(console.error) 199 | .calledWith(sinon.match('`translate-n` and `translate-plural` attributes must be used together: %{ n } car.')) 200 | console.error.restore() 201 | }) 202 | 203 | it('supports conditional rendering such as v-if, v-else-if, v-else', (done) => { 204 | Vue.config.language = 'en_US' 205 | Vue.config.autoAddKeyAttributes = true 206 | let vm = new Vue({ 207 | template: ` 208 | Pending 209 | Hello %{ name } 210 | `, 211 | data: {show: true, name: 'John Doe'}, 212 | }).$mount() 213 | expect(vm.$el.innerHTML).to.equal('Pending') 214 | vm.show = false 215 | vm.$nextTick(function () { 216 | expect(vm.$el.innerHTML).to.equal('Hello John Doe') 217 | Vue.config.autoAddKeyAttributes = false 218 | done() 219 | }) 220 | }) 221 | 222 | }) 223 | 224 | describe('translate component tests for interpolation', () => { 225 | 226 | beforeEach(function () { 227 | uninstallPlugin(Vue, GetTextPlugin) 228 | Vue.use(GetTextPlugin, { 229 | availableLanguages: { 230 | en_US: 'American English', 231 | fr_FR: 'Français', 232 | }, 233 | defaultLanguage: 'en_US', 234 | translations: translations, 235 | }) 236 | }) 237 | 238 | it('goes up the parent chain of a nested component to evaluate `name`', (done) => { 239 | Vue.config.language = 'fr_FR' 240 | let vm = new Vue({ 241 | template: `
`, 242 | data: { 243 | name: 'John Doe', 244 | }, 245 | components: { 246 | 'inner-component': { 247 | template: `

Hello %{ name }

`, 248 | }, 249 | }, 250 | }).$mount() 251 | vm.$nextTick(function () { 252 | expect(vm.$el.innerHTML.trim()).to.equal('

Bonjour John Doe

') 253 | done() 254 | }) 255 | }) 256 | 257 | it('goes up the parent chain of a nested component to evaluate `user.details.name`', (done) => { 258 | console.warn = sinon.spy(console, 'warn') 259 | Vue.config.language = 'fr_FR' 260 | let vm = new Vue({ 261 | template: `
`, 262 | data: { 263 | user: { 264 | details: { 265 | name: 'Jane Doe', 266 | }, 267 | }, 268 | }, 269 | components: { 270 | 'inner-component': { 271 | template: `

Hello %{ user.details.name }

`, 272 | }, 273 | }, 274 | }).$mount() 275 | vm.$nextTick(function () { 276 | expect(vm.$el.innerHTML.trim()).to.equal('

Bonjour Jane Doe

') 277 | expect(console.warn).notCalled 278 | console.warn.restore() 279 | done() 280 | }) 281 | }) 282 | 283 | it('goes up the parent chain of 2 nested components to evaluate `user.details.name`', (done) => { 284 | console.warn = sinon.spy(console, 'warn') 285 | Vue.config.language = 'fr_FR' 286 | let vm = new Vue({ 287 | template: `
`, 288 | data: { 289 | user: { 290 | details: { 291 | name: 'Jane Doe', 292 | }, 293 | }, 294 | }, 295 | components: { 296 | 'first-component': { 297 | template: `

`, 298 | components: { 299 | 'second-component': { 300 | template: `Hello %{ user.details.name }`, 301 | }, 302 | }, 303 | }, 304 | }, 305 | }).$mount() 306 | vm.$nextTick(function () { 307 | expect(vm.$el.innerHTML.trim()).to.equal('

Bonjour Jane Doe

') 308 | expect(console.warn).notCalled 309 | console.warn.restore() 310 | done() 311 | }) 312 | }) 313 | 314 | }) 315 | -------------------------------------------------------------------------------- /test/specs/directive.arabic.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import GetTextPlugin from '../../src/' 4 | import translations from './json/directive.arabic.json' 5 | import uninstallPlugin from '../testUtils' 6 | 7 | // https://docs.transifex.com/formats/gettext 8 | // https://www.arabeyes.org/Plural_Forms 9 | 10 | describe('translate arabic directive tests', () => { 11 | 12 | beforeEach(function () { 13 | uninstallPlugin(Vue, GetTextPlugin) 14 | Vue.use(GetTextPlugin, { 15 | availableLanguages: { 16 | en_US: 'American English', 17 | ar: 'Arabic', 18 | }, 19 | defaultLanguage: 'ar', 20 | translations: translations, 21 | }) 22 | }) 23 | 24 | it('translates singular', () => { 25 | let vm = new Vue({ 26 | template: '

Orange

', 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: '

Hello %{ openingTag }%{ name }%{ closingTag }

', 106 | data: { 107 | name: 'John Doe', 108 | openingTag: '', 109 | closingTag: '', 110 | }, 111 | }).$mount() 112 | expect(vm.$el.innerHTML).to.equal('Bonjour John Doe') 113 | }) 114 | 115 | it('allows interpolation with computed property', () => { 116 | Vue.config.language = 'fr_FR' 117 | let vm = new Vue({ 118 | template: '

Hello %{ name }

', 119 | computed: { 120 | name () { return 'John Doe' }, 121 | }, 122 | }).$mount() 123 | expect(vm.$el.innerHTML).to.equal('Bonjour John Doe') 124 | }) 125 | 126 | it('allows custom params for interpolation', () => { 127 | Vue.config.language = 'fr_FR' 128 | let vm = new Vue({ 129 | template: '

Hello %{ name }

', 130 | data: { 131 | someNewNameVar: 'John Doe', 132 | }, 133 | }).$mount() 134 | expect(vm.$el.innerHTML.trim()).to.equal('Bonjour John Doe') 135 | }) 136 | 137 | it('allows interpolation within v-for with custom params', () => { 138 | Vue.config.language = 'fr_FR' 139 | let names = ['John Doe', 'Chester'] 140 | let vm = new Vue({ 141 | template: '

Hello %{ name }

', 142 | data: { 143 | names, 144 | }, 145 | }).$mount() 146 | let html = vm.$el.innerHTML.trim() 147 | let missedName = names.some((name) => { 148 | if (html.indexOf(name) === -1) { 149 | return true 150 | } 151 | }) 152 | expect(missedName).to.equal(false) 153 | }) 154 | 155 | it('logs a warning in the console if translate-params is used', () => { 156 | console.warn = sinon.spy(console, 'warn') 157 | Vue.config.language = 'fr_FR' 158 | let vm = new Vue({ 159 | template: '

Hello %{ name }

', 160 | data: { 161 | someNewNameVar: 'John Doe', 162 | }, 163 | }).$mount() 164 | expect(vm.$el.innerHTML.trim()).to.equal('Bonjour name') 165 | expect(console.warn).called 166 | console.warn.restore() 167 | }) 168 | 169 | it('updates a translation after a data change', (done) => { 170 | Vue.config.language = 'fr_FR' 171 | let vm = new Vue({ 172 | template: '

Hello %{ name }

', 173 | data: {name: 'John Doe'}, 174 | }).$mount() 175 | expect(vm.$el.innerHTML).to.equal('Bonjour John Doe') 176 | vm.name = 'Kenny' 177 | vm.$nextTick(function () { 178 | expect(vm.$el.innerHTML).to.equal('Bonjour Kenny') 179 | done() 180 | }) 181 | }) 182 | 183 | it('logs an info in the console if an interpolation is required but an expression is not provided', () => { 184 | console.info = sinon.spy(console, 'info') 185 | Vue.config.language = 'fr_FR' 186 | let vm = new Vue({ 187 | template: '

Hello %{ name }

', 188 | data: {name: 'John Doe'}, 189 | }).$mount() 190 | expect(vm.$el.innerHTML).to.equal('Bonjour John Doe') 191 | expect(console.info).calledOnce 192 | console.info.restore() 193 | }) 194 | 195 | it('translates plurals', () => { 196 | Vue.config.language = 'fr_FR' 197 | let vm = new Vue({ 198 | template: '

%{ count } car

', 199 | data: {count: 2}, 200 | }).$mount() 201 | expect(vm.$el.innerHTML).to.equal('2 véhicules') 202 | }) 203 | 204 | it('translates plurals with computed property', () => { 205 | Vue.config.language = 'fr_FR' 206 | let vm = new Vue({ 207 | template: '

%{ count } car

', 208 | computed: { 209 | count () { return 2 }, 210 | }, 211 | }).$mount() 212 | expect(vm.$el.innerHTML).to.equal('2 véhicules') 213 | }) 214 | 215 | it('updates a plural translation after a data change', (done) => { 216 | Vue.config.language = 'fr_FR' 217 | let vm = new Vue({ 218 | template: '

%{ count } %{ brand } car

', 219 | data: {count: 1, brand: 'Toyota'}, 220 | }).$mount() 221 | expect(vm.$el.innerHTML).to.equal('1 Toyota véhicule') 222 | vm.count = 8 223 | vm.$nextTick(function () { 224 | expect(vm.$el.innerHTML).to.equal('8 Toyota véhicules') 225 | done() 226 | }) 227 | }) 228 | 229 | it('updates a translation after a language change', (done) => { 230 | Vue.config.language = 'fr_FR' 231 | let vm = new Vue({template: '
Pending
'}).$mount() 232 | expect(vm.$el.innerHTML).to.equal('En cours') 233 | Vue.config.language = 'en_US' 234 | vm.$nextTick(function () { 235 | expect(vm.$el.innerHTML).to.equal('Pending') 236 | done() 237 | }) 238 | }) 239 | 240 | it('supports conditional rendering such as v-if, v-else-if, v-else', (done) => { 241 | Vue.config.language = 'en_US' 242 | Vue.config.autoAddKeyAttributes = true 243 | let vm = new Vue({ 244 | template: ` 245 |
Pending
246 |
Hello %{ name }
247 | `, 248 | data: {show: true, name: 'John Doe'}, 249 | }).$mount() 250 | expect(vm.$el.innerHTML).to.equal('Pending') 251 | vm.show = false 252 | vm.$nextTick(function () { 253 | expect(vm.$el.innerHTML).to.equal('Hello John Doe') 254 | Vue.config.autoAddKeyAttributes = false 255 | done() 256 | }) 257 | }) 258 | 259 | it('does not trigger re-render of innerHTML when using expression with object and expression value was not changed', (done) => { 260 | let vm = new Vue({ 261 | template: ` 262 |
Hello %{ name }
263 | `, 264 | data: {someCounter: 0}, 265 | }).$mount() 266 | expect(vm.$el.innerHTML).to.equal('Hello test') 267 | 268 | let spy = sinon.spy(vm.$el, 'innerHTML', ['set']) 269 | vm.someCounter += 1 270 | vm.$nextTick(function () { 271 | vm.someCounter += 1 272 | vm.$nextTick(function () { 273 | expect(spy.set.callCount).to.equal(0) 274 | done() 275 | }) 276 | }) 277 | }) 278 | 279 | it('re-render innerHTML when using expression and only if translation data was changed', (done) => { 280 | let vm = new Vue({ 281 | template: ` 282 |
Hello %{ name.first }
283 | `, 284 | data: {someCounter: 0, varFromData: 'name'}, 285 | }).$mount() 286 | expect(vm.$el.innerHTML).to.equal('Hello name') 287 | 288 | let spy = sinon.spy(vm.$el, 'innerHTML', ['set']) 289 | vm.varFromData = 'name' 290 | vm.someCounter += 1 291 | vm.$nextTick(function () { 292 | vm.varFromData = 'otherName' 293 | vm.someCounter += 1 294 | vm.$nextTick(function () { 295 | expect(vm.$el.innerHTML).to.equal('Hello otherName') 296 | expect(spy.set).calledOnce 297 | done() 298 | }) 299 | }) 300 | }) 301 | }) 302 | -------------------------------------------------------------------------------- /test/specs/interpolate.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import GetTextPlugin from '../../src/' 4 | import interpolate from '../../src/interpolate' 5 | import translations from './json/translate.json' 6 | import uninstallPlugin from '../testUtils' 7 | 8 | 9 | describe('Interpolate tests', () => { 10 | 11 | beforeEach(function () { 12 | uninstallPlugin(Vue, GetTextPlugin) 13 | Vue.use(GetTextPlugin, { 14 | translations: translations, 15 | silent: true, 16 | }) 17 | }) 18 | 19 | it('without placeholders', () => { 20 | let msgid = 'Foo bar baz' 21 | let interpolated = interpolate(msgid) 22 | expect(interpolated).to.equal('Foo bar baz') 23 | }) 24 | 25 | it('with a placeholder', () => { 26 | let msgid = 'Foo %{ placeholder } baz' 27 | let context = { placeholder: 'bar' } 28 | let interpolated = interpolate(msgid, context) 29 | expect(interpolated).to.equal('Foo bar baz') 30 | }) 31 | 32 | it('with HTML in var (should be escaped)', () => { 33 | let msgid = 'Foo %{ placeholder } baz' 34 | let context = { placeholder: '

bar

' } 35 | let interpolated = interpolate(msgid, context) 36 | expect(interpolated).to.equal('Foo <p>bar</p> baz') 37 | }) 38 | 39 | it('with HTML in var (should NOT be escaped)', () => { 40 | let msgid = 'Foo %{ placeholder } baz' 41 | let context = { placeholder: '

bar

' } 42 | let disableHtmlEscaping = true 43 | let interpolated = interpolate(msgid, context, disableHtmlEscaping) 44 | expect(interpolated).to.equal('Foo

bar

baz') 45 | }) 46 | 47 | it('with multiple spaces in the placeholder', () => { 48 | let msgid = 'Foo %{ placeholder } baz' 49 | let context = { placeholder: 'bar' } 50 | let interpolated = interpolate(msgid, context) 51 | expect(interpolated).to.equal('Foo bar baz') 52 | }) 53 | 54 | it('with the same placeholder multiple times', () => { 55 | let msgid = 'Foo %{ placeholder } baz %{ placeholder } foo' 56 | let context = { placeholder: 'bar' } 57 | let interpolated = interpolate(msgid, context) 58 | expect(interpolated).to.equal('Foo bar baz bar foo') 59 | }) 60 | 61 | it('with multiple placeholders', () => { 62 | let msgid = '%{foo}%{bar}%{baz}%{bar}%{foo}' 63 | let context = { foo: 1, bar: 2, baz: 3 } 64 | let interpolated = interpolate(msgid, context) 65 | expect(interpolated).to.equal('12321') 66 | }) 67 | 68 | it('with new lines', () => { 69 | let msgid = '%{ \n \n\n\n\n foo} %{bar}!' 70 | let context = { foo: 'Hello', bar: 'world' } 71 | let interpolated = interpolate(msgid, context) 72 | expect(interpolated).to.equal('Hello world!') 73 | }) 74 | 75 | it('with an object', () => { 76 | let msgid = 'Foo %{ foo.bar } baz' 77 | let context = { 78 | foo: { 79 | bar: 'baz', 80 | }, 81 | } 82 | let interpolated = interpolate(msgid, context) 83 | expect(interpolated).to.equal('Foo baz baz') 84 | }) 85 | 86 | it('with an array', () => { 87 | let msgid = 'Foo %{ foo[1] } baz' 88 | let context = { 89 | foo: [ 'bar', 'baz' ], 90 | } 91 | let interpolated = interpolate(msgid, context) 92 | expect(interpolated).to.equal('Foo baz baz') 93 | }) 94 | 95 | it('with a multi level object', () => { 96 | let msgid = 'Foo %{ a.b.x } %{ a.c.y[1].title }' 97 | let context = { 98 | a: { 99 | b: { 100 | x: 'foo', 101 | }, 102 | c: { 103 | y: [ 104 | { title: 'bar' }, 105 | { title: 'baz' }, 106 | ], 107 | }, 108 | }, 109 | } 110 | let interpolated = interpolate(msgid, context) 111 | expect(interpolated).to.equal('Foo foo baz') 112 | }) 113 | 114 | it('with a failing expression', () => { 115 | let msgid = 'Foo %{ alert("foobar") } baz' 116 | let context = { 117 | foo: 'bar', 118 | } 119 | console.warn = sinon.spy(console, 'warn') 120 | interpolate(msgid, context) 121 | expect(console.warn).calledOnce 122 | expect(console.warn).calledWith('Cannot evaluate expression: alert("foobar")') 123 | console.warn.restore() 124 | }) 125 | 126 | it('should warn of the usage of mustache syntax', () => { 127 | let msgid = 'Foo {{ foo }} baz' 128 | let context = { 129 | foo: 'bar', 130 | } 131 | console.warn = sinon.spy(console, 'warn') 132 | interpolate(msgid, context) 133 | expect(console.warn).notCalled 134 | Vue.config.getTextPluginSilent = false 135 | interpolate(msgid, context) 136 | expect(console.warn).calledOnce 137 | console.warn.restore() 138 | }) 139 | 140 | }) 141 | -------------------------------------------------------------------------------- /test/specs/json/component.json: -------------------------------------------------------------------------------- 1 | { 2 | "en_US": { 3 | "Answer": { 4 | "Noun": "Answer (noun)", 5 | "Verb": "Answer (verb)" 6 | }, 7 | "Hello %{ name }": "Hello %{ name }", 8 | "Hello %{ user.details.name }": "Hello %{ user.details.name }", 9 | "Pending": "Pending", 10 | "%{ count } car": ["1 car", "%{ count } cars"], 11 | "A\n\n\n lot\n\n\n\n\n of\n\n lines": "A lot of lines" 12 | }, 13 | "fr_FR": { 14 | "Answer": { 15 | "Noun": "Réponse (nom)", 16 | "Verb": "Réponse (verbe)" 17 | }, 18 | "Hello %{ name }": "Bonjour %{ name }", 19 | "Hello %{ user.details.name }": "Bonjour %{ user.details.name }", 20 | "Pending": "En cours", 21 | "%{ count } car": ["1 véhicule", "%{ count } véhicules"], 22 | "A\n\n\n lot\n\n\n\n\n of\n\n lines": "Plein de lignes" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/specs/json/directive.arabic.json: -------------------------------------------------------------------------------- 1 | { 2 | "ar": { 3 | "Orange": "البرتقالي", 4 | "%{ count } day": [ 5 | "{ count }أقل من يوم", 6 | "{ count }يوم واحد", 7 | "{ count }يومان", 8 | "{ count } أيام", 9 | "{ count } يومًا", 10 | "{ count } يوم" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/specs/json/directive.json: -------------------------------------------------------------------------------- 1 | { 2 | "en_US": { 3 | "Answer": { 4 | "Noun": "Answer (noun)", 5 | "Verb": "Answer (verb)" 6 | }, 7 | "Hello %{ name }": "Hello %{ name }", 8 | "Hello %{ openingTag }%{ name }%{ closingTag }": "Hello %{ openingTag }%{ name }%{ closingTag }", 9 | "Pending": "Pending", 10 | "%{ count } car": ["1 car", "%{ count } cars"], 11 | "%{ count } %{ brand } car": ["1 %{ brand } car", "%{ count } %{ brand } cars"], 12 | "A\n\n\nlot\n\n\nof\n\nlines": "A\n\n\nlot\n\n\nof\n\nlines" 13 | }, 14 | "fr_FR": { 15 | "Answer": { 16 | "Noun": "Réponse (nom)", 17 | "Verb": "Réponse (verbe)" 18 | }, 19 | "Hello %{ openingTag }%{ name }%{ closingTag }": "Bonjour %{ openingTag }%{ name }%{ closingTag }", 20 | "Hello %{ name }": "Bonjour %{ name }", 21 | "Pending": "En cours", 22 | "%{ count } car": ["1 véhicule", "%{ count } véhicules"], 23 | "%{ count } %{ brand } car": ["1 %{ brand } véhicule", "%{ count } %{ brand } véhicules"], 24 | "A\n\n\nlot\n\n\nof\n\nlines": "Plein\n\n\nde\n\nlignes" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/specs/json/plugin.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "en_US": { 3 | "Foo": "Foo en_US" 4 | }, 5 | "fr_FR": { 6 | "Foo": "Foo fr_FR" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/specs/json/translate.json: -------------------------------------------------------------------------------- 1 | { 2 | "en_US": { 3 | "Answer": { 4 | "Noun": "Answer (noun)", 5 | "Verb": "Answer (verb)" 6 | }, 7 | "Hello %{ name }": "Hello %{ name }", 8 | "May": "May", 9 | "Pending": "Pending", 10 | "%{ carCount } car": [ 11 | "1 car", 12 | "%{ carCount } cars" 13 | ], 14 | "%{ carCount } car (noun)": { 15 | "Noun": [ 16 | "%{ carCount } car (noun)", 17 | "%{ carCount } cars (noun)" 18 | ] 19 | }, 20 | "%{ carCount } car (verb)": { 21 | "Verb": [ 22 | "%{ carCount } car (verb)", 23 | "%{ carCount } cars (verb)" 24 | ] 25 | }, 26 | "%{ carCount } car (multiple contexts)": { 27 | "": [ 28 | "1 car", 29 | "%{ carCount } cars" 30 | ], 31 | "Context": [ 32 | "1 car with context", 33 | "%{ carCount } cars with context" 34 | ] 35 | }, 36 | "Object": { 37 | "": "Object", 38 | "Context": "Object with context" 39 | } 40 | }, 41 | "fr": { 42 | "Answer": { 43 | "Noun": "Réponse (nom)", 44 | "Verb": "Réponse (verbe)" 45 | }, 46 | "Hello %{ name }": "Bonjour %{ name }", 47 | "May": "Pourrait", 48 | "Pending": "En cours", 49 | "%{ carCount } car": [ 50 | "1 véhicule", 51 | "%{ carCount } véhicules" 52 | ], 53 | "%{ carCount } car (noun)": { 54 | "Noun": [ 55 | "%{ carCount } véhicule (nom)", 56 | "%{ carCount } véhicules (nom)" 57 | ] 58 | }, 59 | "%{ carCount } car (verb)": { 60 | "Verb": [ 61 | "%{ carCount } véhicule (verbe)", 62 | "%{ carCount } véhicules (verbe)" 63 | ] 64 | }, 65 | "%{ carCount } car (multiple contexts)": { 66 | "": [ 67 | "1 véhicule", 68 | "%{ carCount } véhicules" 69 | ], 70 | "Context": [ 71 | "1 véhicule avec contexte", 72 | "%{ carCount } véhicules avec contexte" 73 | ] 74 | }, 75 | "Object": { 76 | "": "Objet", 77 | "Context": "Objet avec contexte" 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/specs/plugin.config.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import GetTextPlugin from '../../src/' 4 | import translate from '../../src/translate' 5 | import translations from './json/plugin.config.json' 6 | import uninstallPlugin from '../testUtils' 7 | 8 | 9 | describe('GetText plugin configuration tests', () => { 10 | 11 | beforeEach(function () { 12 | uninstallPlugin(Vue, GetTextPlugin) 13 | }) 14 | 15 | it('raises an error when an unknown option is used', () => { 16 | expect(function () { 17 | Vue.use(GetTextPlugin, { unknownOption: null, translations: {} }) 18 | }).to.throw('unknownOption is an invalid option for the translate plugin.') 19 | }) 20 | 21 | it('raises an error when there are no translations', () => { 22 | expect(function () { 23 | Vue.use(GetTextPlugin, {}) 24 | }).to.throw('No translations available.') 25 | }) 26 | 27 | it('allows to add a mixin to languageVm', () => { 28 | Vue.use(GetTextPlugin, { 29 | availableLanguages: { 30 | en_GB: 'English', 31 | fr_FR: 'Français', 32 | }, 33 | defaultLanguage: 'fr_FR', 34 | translations: {}, 35 | languageVmMixin: { 36 | computed: { 37 | currentKebabCase: function () { 38 | return this.current.toLowerCase().replace('_', '-') 39 | }, 40 | }, 41 | }, 42 | }) 43 | let vm = new Vue({template: '
Foo
'}).$mount() 44 | expect(vm.$language.currentKebabCase).to.equal('fr-fr') 45 | vm.$language.current = 'en_GB' 46 | expect(vm.$language.currentKebabCase).to.equal('en-gb') 47 | }) 48 | 49 | }) 50 | 51 | describe('GetText plugin `silent` option tests', () => { 52 | 53 | beforeEach(function () { 54 | uninstallPlugin(Vue, GetTextPlugin) 55 | }) 56 | 57 | it('warnings are ON for a missing language when `silent` is false', () => { 58 | console.warn = sinon.spy(console, 'warn') 59 | Vue.use(GetTextPlugin, { 60 | translations: translations, 61 | silent: false, 62 | }) 63 | Vue.config.language = 'pt_BR' 64 | expect(translations.hasOwnProperty('pt_BR')).to.be.false 65 | let vm = new Vue({template: '
Bar
'}).$mount() 66 | expect(vm.$el.innerHTML.trim()).to.equal('Bar') 67 | expect(console.warn).calledOnce 68 | expect(console.warn.calledWith('No translations found for pt_BR')).to.be.true 69 | console.warn.restore() 70 | }) 71 | 72 | it('warnings are OFF for a missing language when `silent` is true', () => { 73 | console.warn = sinon.spy(console, 'warn') 74 | Vue.use(GetTextPlugin, { 75 | translations: translations, 76 | silent: true, 77 | }) 78 | Vue.config.language = 'pt_BR' 79 | expect(translations.hasOwnProperty('pt_BR')).to.be.false 80 | let vm = new Vue({template: '
Bar
'}).$mount() 81 | expect(vm.$el.innerHTML.trim()).to.equal('Bar') 82 | expect(console.warn).notCalled 83 | console.warn.restore() 84 | }) 85 | 86 | it('warnings are ON for a missing translation key when `silent` is false', () => { 87 | console.warn = sinon.spy(console, 'warn') 88 | Vue.use(GetTextPlugin, { 89 | translations: translations, 90 | silent: false, 91 | }) 92 | Vue.config.language = 'fr_FR' 93 | let vm = new Vue({template: '
Bar
'}).$mount() 94 | expect(translations.fr_FR.hasOwnProperty('Bar')).to.be.false 95 | expect(vm.$el.innerHTML.trim()).to.equal('Bar') 96 | expect(console.warn).calledOnce 97 | expect(console.warn.calledWith('Untranslated fr_FR key found: Bar')).to.be.true 98 | console.warn.restore() 99 | }) 100 | 101 | it('warnings are OFF for a missing translation key when `silent` is true', () => { 102 | console.warn = sinon.spy(console, 'warn') 103 | Vue.use(GetTextPlugin, { 104 | translations: translations, 105 | silent: true, 106 | }) 107 | Vue.config.language = 'fr_FR' 108 | let vm = new Vue({template: '
Bar
'}).$mount() 109 | expect(translations.fr_FR.hasOwnProperty('Bar')).to.be.false 110 | expect(vm.$el.innerHTML.trim()).to.equal('Bar') 111 | expect(console.warn).notCalled 112 | console.warn.restore() 113 | }) 114 | 115 | }) 116 | 117 | describe('GetText plugin `muteLanguages` option tests', () => { 118 | 119 | beforeEach(function () { 120 | uninstallPlugin(Vue, GetTextPlugin) 121 | Vue.use(GetTextPlugin, { 122 | availableLanguages: { 123 | en_US: 'American English', 124 | fr_FR: 'Français', 125 | }, 126 | defaultLanguage: 'fr_FR', 127 | muteLanguages: [], 128 | silent: false, 129 | translations: translations, 130 | }) 131 | }) 132 | 133 | it('warnings are ON for all languages', () => { 134 | console.warn = sinon.spy(console, 'warn') 135 | translate.getTranslation('Untranslated key', null, null, null, 'fr_FR') 136 | expect(console.warn).calledWith('Untranslated fr_FR key found: Untranslated key') 137 | translate.getTranslation('Untranslated key', null, null, null, 'en_US') 138 | expect(console.warn).calledWith('Untranslated en_US key found: Untranslated key') 139 | console.warn.restore() 140 | }) 141 | 142 | it('warnings are OFF for fr_FR', () => { 143 | console.warn = sinon.spy(console, 'warn') 144 | Vue.config.getTextPluginMuteLanguages = ['fr_FR'] 145 | translate.getTranslation('Untranslated key', null, null, null, 'fr_FR') 146 | expect(console.warn).notCalled 147 | translate.getTranslation('Untranslated key', null, null, null, 'en_US') 148 | expect(console.warn).calledWith('Untranslated en_US key found: Untranslated key') 149 | console.warn.restore() 150 | }) 151 | 152 | it('warnings are OFF for en_US', () => { 153 | console.warn = sinon.spy(console, 'warn') 154 | Vue.config.getTextPluginMuteLanguages = ['en_US'] 155 | translate.getTranslation('Untranslated key', null, null, null, 'fr_FR') 156 | expect(console.warn).calledWith('Untranslated fr_FR key found: Untranslated key') 157 | translate.getTranslation('Untranslated key', null, null, null, 'en_US') 158 | expect(console.warn).notCalled 159 | console.warn.restore() 160 | }) 161 | 162 | it('warnings are OFF for en_US and fr_FR', () => { 163 | console.warn = sinon.spy(console, 'warn') 164 | Vue.config.getTextPluginMuteLanguages = ['fr_FR', 'en_US'] 165 | translate.getTranslation('Untranslated key', null, null, null, 'fr_FR') 166 | expect(console.warn).notCalled 167 | translate.getTranslation('Untranslated key', null, null, null, 'en_US') 168 | expect(console.warn).notCalled 169 | console.warn.restore() 170 | }) 171 | 172 | }) 173 | -------------------------------------------------------------------------------- /test/specs/plurals.spec.js: -------------------------------------------------------------------------------- 1 | import plurals from '../../src/plurals' 2 | 3 | 4 | describe('Translate plurals tests', () => { 5 | 6 | it('plural form of singular english is 0', function () { 7 | expect(plurals.getTranslationIndex('en', 1)).to.equal(0) 8 | }) 9 | 10 | it('plural form of plural english is 1', function () { 11 | expect(plurals.getTranslationIndex('en', 2)).to.equal(1) 12 | }) 13 | 14 | it('plural form of Infinity in english is 1', function () { 15 | expect(plurals.getTranslationIndex('en', Infinity)).to.equal(1) 16 | }) 17 | 18 | it('plural form of zero in english is 1', function () { 19 | expect(plurals.getTranslationIndex('en', 0)).to.equal(1) 20 | }) 21 | 22 | it('plural form of singular dutch is 0', function () { 23 | expect(plurals.getTranslationIndex('nl', 1)).to.equal(0) 24 | }) 25 | 26 | it('plural form of plural dutch is 1', function () { 27 | expect(plurals.getTranslationIndex('nl', 2)).to.equal(1) 28 | }) 29 | 30 | it('plural form of zero in dutch is 1', function () { 31 | expect(plurals.getTranslationIndex('nl', 0)).to.equal(1) 32 | }) 33 | 34 | it('plural form of singular french is 0', function () { 35 | expect(plurals.getTranslationIndex('fr', 1)).to.equal(0) 36 | }) 37 | 38 | it('plural form of plural french is 1', function () { 39 | expect(plurals.getTranslationIndex('fr', 2)).to.equal(1) 40 | }) 41 | 42 | it('plural form of zero in french is 0', function () { 43 | expect(plurals.getTranslationIndex('fr', 0)).to.equal(0) 44 | }) 45 | 46 | it('plural form of 27 in arabic is 4', function () { 47 | expect(plurals.getTranslationIndex('ar', 27)).to.equal(4) 48 | }) 49 | 50 | it('plural form of 23 in kashubian is 1', function () { 51 | expect(plurals.getTranslationIndex('csb', 23)).to.equal(1) 52 | }) 53 | 54 | }) 55 | -------------------------------------------------------------------------------- /test/specs/translate.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import GetTextPlugin from '../../src/' 4 | import translate from '../../src/translate' 5 | import translations from './json/translate.json' 6 | import uninstallPlugin from '../testUtils' 7 | 8 | 9 | describe('Translate tests', () => { 10 | 11 | beforeEach(function () { 12 | uninstallPlugin(Vue, GetTextPlugin) 13 | Vue.use(GetTextPlugin, { 14 | availableLanguages: { 15 | en_US: 'American English', 16 | fr_FR: 'Français', 17 | }, 18 | defaultLanguage: 'en_US', 19 | translations: translations, 20 | }) 21 | }) 22 | 23 | let translated 24 | 25 | it('tests the getTranslation() method', () => { 26 | 27 | translated = translate.getTranslation('', 1, null, 'fr_FR') 28 | expect(translated).to.equal('') 29 | 30 | translated = translate.getTranslation('Unexisting language', null, null, null, 'be_FR') 31 | expect(translated).to.equal('Unexisting language') 32 | 33 | translated = translate.getTranslation('Untranslated key', null, null, null, 'fr_FR') 34 | expect(translated).to.equal('Untranslated key') 35 | 36 | translated = translate.getTranslation('Pending', 1, null, null, 'fr_FR') 37 | expect(translated).to.equal('En cours') 38 | 39 | translated = translate.getTranslation('%{ carCount } car', 2, null, null, 'fr_FR') 40 | expect(translated).to.equal('%{ carCount } véhicules') 41 | 42 | translated = translate.getTranslation('Answer', 1, 'Verb', null, 'fr_FR') 43 | expect(translated).to.equal('Réponse (verbe)') 44 | 45 | translated = translate.getTranslation('Answer', 1, 'Noun', null, 'fr_FR') 46 | expect(translated).to.equal('Réponse (nom)') 47 | 48 | translated = translate.getTranslation('Pending', 1, null, null, 'en_US') 49 | expect(translated).to.equal('Pending') 50 | 51 | // If no translation exists, display the default singular form (if n < 2). 52 | translated = translate.getTranslation('Untranslated %{ n } item', 0, null, 'Untranslated %{ n } items', 'fr_FR') 53 | expect(translated).to.equal('Untranslated %{ n } item') 54 | 55 | // If no translation exists, display the default plural form (if n > 1). 56 | translated = translate.getTranslation('Untranslated %{ n } item', 10, null, 'Untranslated %{ n } items', 'fr_FR') 57 | expect(translated).to.equal('Untranslated %{ n } items') 58 | 59 | // Test that it works when a msgid exists with and without a context, see #32. 60 | translated = translate.getTranslation('Object', null, null, null, 'fr_FR') 61 | expect(translated).to.equal('Objet') 62 | translated = translate.getTranslation('Object', null, 'Context', null, 'fr_FR') 63 | expect(translated).to.equal('Objet avec contexte') 64 | 65 | // Ensure that pluralization is right in English when there are no English translations. 66 | translated = translate.getTranslation('Untranslated %{ n } item', 0, null, 'Untranslated %{ n } items', 'en_US') 67 | expect(translated).to.equal('Untranslated %{ n } items') 68 | translated = translate.getTranslation('Untranslated %{ n } item', 1, null, 'Untranslated %{ n } items', 'en_US') 69 | expect(translated).to.equal('Untranslated %{ n } item') 70 | translated = translate.getTranslation('Untranslated %{ n } item', 2, null, 'Untranslated %{ n } items', 'en_US') 71 | expect(translated).to.equal('Untranslated %{ n } items') 72 | 73 | // Test plural message with multiple contexts (default context and 'Context'') 74 | translated = translate.getTranslation('%{ carCount } car (multiple contexts)', 1, null, null, 'en_US') 75 | expect(translated).to.equal('1 car') 76 | translated = translate.getTranslation('%{ carCount } car (multiple contexts)', 2, null, null, 'en_US') 77 | expect(translated).to.equal('%{ carCount } cars') 78 | translated = translate.getTranslation('%{ carCount } car (multiple contexts)', 1, 'Context', null, 'en_US') 79 | expect(translated).to.equal('1 car with context') 80 | translated = translate.getTranslation('%{ carCount } car (multiple contexts)', 2, 'Context', null, 'en_US') 81 | expect(translated).to.equal('%{ carCount } cars with context') 82 | 83 | }) 84 | 85 | it('tests the gettext() method', () => { 86 | 87 | let undetectableGettext = translate.gettext.bind(translate) // Hide from gettext-extract. 88 | 89 | Vue.config.language = 'fr_FR' 90 | expect(undetectableGettext('Pending')).to.equal('En cours') 91 | 92 | Vue.config.language = 'en_US' 93 | expect(undetectableGettext('Pending')).to.equal('Pending') 94 | 95 | expect(undetectableGettext('Pending', 'fr_FR')).to.equal('En cours') 96 | }) 97 | 98 | it('tests the pgettext() method', () => { 99 | 100 | let undetectablePgettext = translate.pgettext.bind(translate) // Hide from gettext-extract. 101 | 102 | Vue.config.language = 'fr_FR' 103 | expect(undetectablePgettext('Noun', 'Answer')).to.equal('Réponse (nom)') 104 | 105 | Vue.config.language = 'en_US' 106 | expect(undetectablePgettext('Noun', 'Answer')).to.equal('Answer (noun)') 107 | 108 | expect(undetectablePgettext('Noun', 'Answer', 'fr_FR')).to.equal('Réponse (nom)') 109 | }) 110 | 111 | it('tests the ngettext() method', () => { 112 | 113 | let undetectableNgettext = translate.ngettext.bind(translate) // Hide from gettext-extract. 114 | 115 | Vue.config.language = 'fr_FR' 116 | expect(undetectableNgettext('%{ carCount } car', '%{ carCount } cars', 2)).to.equal('%{ carCount } véhicules') 117 | 118 | Vue.config.language = 'en_US' 119 | expect(undetectableNgettext('%{ carCount } car', '%{ carCount } cars', 2)).to.equal('%{ carCount } cars') 120 | 121 | expect(undetectableNgettext('%{ carCount } car', '%{ carCount } cars', 2, 'fr_FR')).to.equal('%{ carCount } véhicules') 122 | 123 | // If no translation exists, display the default singular form (if n < 2). 124 | Vue.config.language = 'fr_FR' 125 | expect(undetectableNgettext('Untranslated %{ n } item', 'Untranslated %{ n } items', -1)) 126 | .to.equal('Untranslated %{ n } item') 127 | 128 | // If no translation exists, display the default plural form (if n > 1). 129 | Vue.config.language = 'fr_FR' 130 | expect(undetectableNgettext('Untranslated %{ n } item', 'Untranslated %{ n } items', 2)) 131 | .to.equal('Untranslated %{ n } items') 132 | 133 | }) 134 | 135 | it('tests the npgettext() method', () => { 136 | 137 | let undetectableNpgettext = translate.npgettext.bind(translate) // Hide from gettext-extract 138 | 139 | Vue.config.language = 'fr_FR' 140 | expect(undetectableNpgettext('Noun', '%{ carCount } car (noun)', '%{ carCount } cars (noun)', 2)) 141 | .to.equal('%{ carCount } véhicules (nom)') 142 | 143 | Vue.config.language = 'en_US' 144 | expect(undetectableNpgettext('Verb', '%{ carCount } car (verb)', '%{ carCount } cars (verb)', 2)) 145 | .to.equal('%{ carCount } cars (verb)') 146 | 147 | Vue.config.language = 'fr_FR' 148 | expect(undetectableNpgettext('Noun', '%{ carCount } car (noun)', '%{ carCount } cars (noun)', 1)) 149 | .to.equal('%{ carCount } véhicule (nom)') 150 | 151 | Vue.config.language = 'en_US' 152 | expect(undetectableNpgettext('Verb', '%{ carCount } car (verb)', '%{ carCount } cars (verb)', 1)) 153 | .to.equal('%{ carCount } car (verb)') 154 | 155 | expect(undetectableNpgettext('Noun', '%{ carCount } car (noun)', '%{ carCount } cars (noun)', 2, 'fr_FR')) 156 | .to.equal('%{ carCount } véhicules (nom)') 157 | 158 | // If no translation exists, display the default singular form (if n < 2). 159 | Vue.config.language = 'fr_FR' 160 | expect(undetectableNpgettext('Noun', 'Untranslated %{ n } item (noun)', 'Untranslated %{ n } items (noun)', 1)) 161 | .to.equal('Untranslated %{ n } item (noun)') 162 | 163 | // If no translation exists, display the default plural form (if n > 1). 164 | Vue.config.language = 'fr_FR' 165 | expect(undetectableNpgettext('Noun', 'Untranslated %{ n } item (noun)', 'Untranslated %{ n } items (noun)', 2)) 166 | .to.equal('Untranslated %{ n } items (noun)') 167 | 168 | }) 169 | 170 | it('works when a msgid exists with and without a context, but the one with the context has not been translated', () => { 171 | 172 | expect(Vue.config.silent).to.equal(false) 173 | console.warn = sinon.spy(console, 'warn') 174 | 175 | translated = translate.getTranslation('May', null, null, null, 'fr_FR') 176 | expect(translated).to.equal('Pourrait') 177 | 178 | translated = translate.getTranslation('May', null, 'Month name', null, 'fr_FR') 179 | expect(translated).to.equal('May') 180 | 181 | expect(console.warn).calledOnce 182 | expect(console.warn).calledWith('Untranslated fr_FR key found: May (with context: Month name)') 183 | 184 | console.warn.restore() 185 | 186 | }) 187 | 188 | }) 189 | 190 | describe('Translate tests without Vue', () => { 191 | let config 192 | 193 | beforeEach(function () { 194 | config = { 195 | language: 'en_US', 196 | getTextPluginSilent: false, 197 | getTextPluginMuteLanguages: [], 198 | silent: false, 199 | } 200 | translate.initTranslations(translations, config) 201 | }) 202 | 203 | let translated 204 | 205 | it('tests the getTranslation() method', () => { 206 | 207 | translated = translate.getTranslation('', 1, null, 'fr_FR') 208 | expect(translated).to.equal('') 209 | 210 | translated = translate.getTranslation('Unexisting language', null, null, null, 'be_FR') 211 | expect(translated).to.equal('Unexisting language') 212 | 213 | translated = translate.getTranslation('Untranslated key', null, null, null, 'fr_FR') 214 | expect(translated).to.equal('Untranslated key') 215 | 216 | translated = translate.getTranslation('Pending', 1, null, null, 'fr_FR') 217 | expect(translated).to.equal('En cours') 218 | 219 | translated = translate.getTranslation('%{ carCount } car', 2, null, null, 'fr_FR') 220 | expect(translated).to.equal('%{ carCount } véhicules') 221 | 222 | translated = translate.getTranslation('Answer', 1, 'Verb', null, 'fr_FR') 223 | expect(translated).to.equal('Réponse (verbe)') 224 | 225 | translated = translate.getTranslation('Answer', 1, 'Noun', null, 'fr_FR') 226 | expect(translated).to.equal('Réponse (nom)') 227 | 228 | translated = translate.getTranslation('Pending', 1, null, null, 'en_US') 229 | expect(translated).to.equal('Pending') 230 | 231 | // If no translation exists, display the default singular form (if n < 2). 232 | translated = translate.getTranslation('Untranslated %{ n } item', 0, null, 'Untranslated %{ n } items', 'fr_FR') 233 | expect(translated).to.equal('Untranslated %{ n } item') 234 | 235 | // If no translation exists, display the default plural form (if n > 1). 236 | translated = translate.getTranslation('Untranslated %{ n } item', 10, null, 'Untranslated %{ n } items', 'fr_FR') 237 | expect(translated).to.equal('Untranslated %{ n } items') 238 | 239 | // Test that it works when a msgid exists with and without a context, see #32. 240 | translated = translate.getTranslation('Object', null, null, null, 'fr_FR') 241 | expect(translated).to.equal('Objet') 242 | translated = translate.getTranslation('Object', null, 'Context', null, 'fr_FR') 243 | expect(translated).to.equal('Objet avec contexte') 244 | 245 | // Ensure that pluralization is right in English when there are no English translations. 246 | translated = translate.getTranslation('Untranslated %{ n } item', 0, null, 'Untranslated %{ n } items', 'en_US') 247 | expect(translated).to.equal('Untranslated %{ n } items') 248 | translated = translate.getTranslation('Untranslated %{ n } item', 1, null, 'Untranslated %{ n } items', 'en_US') 249 | expect(translated).to.equal('Untranslated %{ n } item') 250 | translated = translate.getTranslation('Untranslated %{ n } item', 2, null, 'Untranslated %{ n } items', 'en_US') 251 | expect(translated).to.equal('Untranslated %{ n } items') 252 | 253 | // Test plural message with multiple contexts (default context and 'Context'') 254 | translated = translate.getTranslation('%{ carCount } car (multiple contexts)', 1, null, null, 'en_US') 255 | expect(translated).to.equal('1 car') 256 | translated = translate.getTranslation('%{ carCount } car (multiple contexts)', 2, null, null, 'en_US') 257 | expect(translated).to.equal('%{ carCount } cars') 258 | translated = translate.getTranslation('%{ carCount } car (multiple contexts)', 1, 'Context', null, 'en_US') 259 | expect(translated).to.equal('1 car with context') 260 | translated = translate.getTranslation('%{ carCount } car (multiple contexts)', 2, 'Context', null, 'en_US') 261 | expect(translated).to.equal('%{ carCount } cars with context') 262 | 263 | }) 264 | 265 | it('tests the gettext() method', () => { 266 | 267 | let undetectableGettext = translate.gettext.bind(translate) // Hide from gettext-extract. 268 | 269 | config.language = 'fr_FR' 270 | expect(undetectableGettext('Pending')).to.equal('En cours') 271 | 272 | config.language = 'en_US' 273 | expect(undetectableGettext('Pending')).to.equal('Pending') 274 | 275 | expect(undetectableGettext('Pending', 'fr_FR')).to.equal('En cours') 276 | }) 277 | 278 | it('tests the pgettext() method', () => { 279 | 280 | let undetectablePgettext = translate.pgettext.bind(translate) // Hide from gettext-extract. 281 | 282 | config.language = 'fr_FR' 283 | expect(undetectablePgettext('Noun', 'Answer')).to.equal('Réponse (nom)') 284 | 285 | config.language = 'en_US' 286 | expect(undetectablePgettext('Noun', 'Answer')).to.equal('Answer (noun)') 287 | 288 | expect(undetectablePgettext('Noun', 'Answer', 'fr_FR')).to.equal('Réponse (nom)') 289 | }) 290 | 291 | it('tests the ngettext() method', () => { 292 | 293 | let undetectableNgettext = translate.ngettext.bind(translate) // Hide from gettext-extract. 294 | 295 | config.language = 'fr_FR' 296 | expect(undetectableNgettext('%{ carCount } car', '%{ carCount } cars', 2)).to.equal('%{ carCount } véhicules') 297 | 298 | config.language = 'en_US' 299 | expect(undetectableNgettext('%{ carCount } car', '%{ carCount } cars', 2)).to.equal('%{ carCount } cars') 300 | 301 | expect(undetectableNgettext('%{ carCount } car', '%{ carCount } cars', 2, 'fr_FR')).to.equal('%{ carCount } véhicules') 302 | 303 | // If no translation exists, display the default singular form (if n < 2). 304 | config.language = 'fr_FR' 305 | expect(undetectableNgettext('Untranslated %{ n } item', 'Untranslated %{ n } items', -1)) 306 | .to.equal('Untranslated %{ n } item') 307 | 308 | // If no translation exists, display the default plural form (if n > 1). 309 | config.language = 'fr_FR' 310 | expect(undetectableNgettext('Untranslated %{ n } item', 'Untranslated %{ n } items', 2)) 311 | .to.equal('Untranslated %{ n } items') 312 | 313 | }) 314 | 315 | it('tests the npgettext() method', () => { 316 | 317 | let undetectableNpgettext = translate.npgettext.bind(translate) // Hide from gettext-extract 318 | 319 | config.language = 'fr_FR' 320 | expect(undetectableNpgettext('Noun', '%{ carCount } car (noun)', '%{ carCount } cars (noun)', 2)) 321 | .to.equal('%{ carCount } véhicules (nom)') 322 | 323 | config.language = 'en_US' 324 | expect(undetectableNpgettext('Verb', '%{ carCount } car (verb)', '%{ carCount } cars (verb)', 2)) 325 | .to.equal('%{ carCount } cars (verb)') 326 | 327 | config.language = 'fr_FR' 328 | expect(undetectableNpgettext('Noun', '%{ carCount } car (noun)', '%{ carCount } cars (noun)', 1)) 329 | .to.equal('%{ carCount } véhicule (nom)') 330 | 331 | config.language = 'en_US' 332 | expect(undetectableNpgettext('Verb', '%{ carCount } car (verb)', '%{ carCount } cars (verb)', 1)) 333 | .to.equal('%{ carCount } car (verb)') 334 | 335 | expect(undetectableNpgettext('Noun', '%{ carCount } car (noun)', '%{ carCount } cars (noun)', 1, 'fr_FR')) 336 | .to.equal('%{ carCount } véhicule (nom)') 337 | 338 | // If no translation exists, display the default singular form (if n < 2). 339 | config.language = 'fr_FR' 340 | expect(undetectableNpgettext('Noun', 'Untranslated %{ n } item (noun)', 'Untranslated %{ n } items (noun)', 1)) 341 | .to.equal('Untranslated %{ n } item (noun)') 342 | 343 | // If no translation exists, display the default plural form (if n > 1). 344 | config.language = 'fr_FR' 345 | expect(undetectableNpgettext('Noun', 'Untranslated %{ n } item (noun)', 'Untranslated %{ n } items (noun)', 2)) 346 | .to.equal('Untranslated %{ n } items (noun)') 347 | 348 | }) 349 | 350 | it('works when a msgid exists with and without a context, but the one with the context has not been translated', () => { 351 | 352 | expect(config.silent).to.equal(false) 353 | console.warn = sinon.spy(console, 'warn') 354 | 355 | translated = translate.getTranslation('May', null, null, null, 'fr_FR') 356 | expect(translated).to.equal('Pourrait') 357 | 358 | translated = translate.getTranslation('May', null, 'Month name', null, 'fr_FR') 359 | expect(translated).to.equal('May') 360 | 361 | expect(console.warn).calledOnce 362 | expect(console.warn).calledWith('Untranslated fr_FR key found: May (with context: Month name)') 363 | 364 | console.warn.restore() 365 | 366 | }) 367 | 368 | }) 369 | -------------------------------------------------------------------------------- /test/testUtils.js: -------------------------------------------------------------------------------- 1 | // Provide a way to uninstall the GetTextPlugin between each unit test. 2 | 3 | let uninstallPlugin = function (Vue, Plugin) { 4 | if (Vue.hasOwnProperty('_installedPlugins')) { 5 | // The way to do this has changed over time, see: 6 | // https://github.com/vuejs/vue/commit/049f31#diff-df137982016aef85f3594216a4c9a295 7 | Vue._installedPlugins = [] 8 | } else { 9 | // Could also be `._installed` for some Vue versions 10 | // https://github.com/vuejs/vue/commit/b4dd0b#diff-df137982016aef85f3594216a4c9a295 11 | Plugin.installed = false 12 | } 13 | } 14 | 15 | export default uninstallPlugin 16 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import VueGettext from './vue-gettext'; 2 | 3 | export default VueGettext; 4 | 5 | export * from './vue'; 6 | export * from './vue-gettext'; 7 | -------------------------------------------------------------------------------- /types/vue-gettext.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { ComponentOptions, VueConstructor} from 'vue'; 2 | 3 | interface IVueGettextOptions { 4 | availableLanguages: { 5 | [key: string]: string; 6 | }; 7 | defaultLanguage: string; 8 | languageVmMixin: ComponentOptions | typeof Vue; 9 | muteLanguages: string[]; 10 | silent: boolean; 11 | translations: object; 12 | } 13 | 14 | export class VueGettext { 15 | constructor(Vue: VueConstructor, options?: IVueGettextOptions); 16 | } 17 | 18 | export function install(vue: typeof Vue): void; 19 | 20 | declare const _default: { 21 | VueGettext: typeof VueGettext, 22 | install: typeof install, 23 | }; 24 | 25 | export default _default; 26 | -------------------------------------------------------------------------------- /types/vue.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VueConfiguration } from 'vue/types/vue'; 2 | 3 | declare module 'vue/types/vue' { 4 | interface ILanguageComponent extends Vue { 5 | available: { 6 | [key: string]: string; 7 | }; 8 | current: string; 9 | } 10 | 11 | interface Vue { 12 | $translations: object; 13 | $language: ILanguageComponent; 14 | $gettext: (msgid: string) => string; 15 | $pgettext: (context: string, msgid: string) => string; 16 | $ngettext: (msgid: string, plural: string, n: number) => string; 17 | $npgettext: (context: string, msgid: string, plural: string, n: number) => string; 18 | $gettextInterpolate: (msgid: string, context: object, disableHtmlEscaping?: boolean) => string; 19 | } 20 | 21 | interface VueConfiguration { 22 | language: string; 23 | } 24 | } 25 | --------------------------------------------------------------------------------