diff --git a/docs/guide/directive.md b/docs/guide/directive.md
index 2efc5e2..a63d8ba 100644
--- a/docs/guide/directive.md
+++ b/docs/guide/directive.md
@@ -1,4 +1,6 @@
-# Directive
+# Directives
+
+## v-t
Full Featured properties:
@@ -33,3 +35,30 @@ Vue.component("app", {
template: `
`
});
```
+
+
+## v-waitForT
+
+Wait for the i18next fot be initialized. If not initialized it sets the element to `hidden = true` and wait
+for i18next to be initialized.
+
+```javascript
+const locales = {
+ en: {
+ hello: "Hello"
+ }
+};
+
+i18next.init({
+ lng: "en",
+ resources: {
+ en: { translation: locales.en }
+ }
+});
+
+const i18n = new VueI18next(i18next);
+
+Vue.component("app", {
+ template: `$t("hello")
`
+});
+```
diff --git a/examples/app.js b/examples/app.js
index 0d982af..9ad7082 100644
--- a/examples/app.js
+++ b/examples/app.js
@@ -31,28 +31,32 @@ const i18n = new VueI18next(i18next);
Vue.component('app', {
template: `
+
-
Translation
$t: {{ $t("message.hello") }}
-
-
-
-
Prefix
-
-
-
-
Interpolation
-
-
-
`,
+
+
+
+
Prefix
+
+
+
+
Inline translations
+
+
+
+
Directive
+
+
+ `,
});
Vue.component('language-changer', {
@@ -113,6 +117,11 @@ Vue.component('inline-translations', {
`,
});
+Vue.component('with-directive', {
+ template: `
+ `,
+});
+
new Vue({
i18n,
}).$mount('#app');
diff --git a/src/directive.js b/src/directive.js
index 5f944ef..17bd9dd 100644
--- a/src/directive.js
+++ b/src/directive.js
@@ -86,3 +86,8 @@ export function update(el, binding, vnode, oldVNode) {
t(el, binding, vnode);
}
+
+export default {
+ bind,
+ update,
+};
diff --git a/src/install.js b/src/install.js
index 18c4c58..6229280 100644
--- a/src/install.js
+++ b/src/install.js
@@ -1,7 +1,8 @@
/* eslint-disable import/no-mutable-exports */
import deepmerge from 'deepmerge';
import component from './component';
-import { bind, update } from './directive';
+import directive from './directive';
+import waitDirective from './wait';
export let Vue;
@@ -134,5 +135,6 @@ export function install(_Vue) {
};
Vue.component(component.name, component);
- Vue.directive('t', { bind, update });
+ Vue.directive('t', directive);
+ Vue.directive('waitForT', waitDirective);
}
diff --git a/src/wait.js b/src/wait.js
new file mode 100644
index 0000000..055a19a
--- /dev/null
+++ b/src/wait.js
@@ -0,0 +1,51 @@
+/* eslint-disable no-param-reassign, no-unused-vars */
+
+import { warn } from './utils';
+
+function assert(vnode) {
+ const vm = vnode.context;
+
+ if (!vm.$i18n) {
+ warn('No VueI18Next instance found in the Vue instance');
+ return false;
+ }
+
+ return true;
+}
+
+function waitForIt(el, vnode) {
+ if (vnode.context.$i18n.i18next.isInitialized) {
+ el.hidden = false;
+ } else {
+ el.hidden = true;
+ const initialized = () => {
+ vnode.context.$forceUpdate();
+ // due to emitter removing issue in i18next we need to delay remove
+ setTimeout(() => {
+ if (vnode.context && vnode.context.$i18n) {
+ vnode.context.$i18n.i18next.off('initialized', initialized);
+ }
+ }, 1000);
+ };
+ vnode.context.$i18n.i18next.on('initialized', initialized);
+ }
+}
+
+export function bind(el, binding, vnode) {
+ if (!assert(vnode)) {
+ return;
+ }
+
+ waitForIt(el, vnode);
+}
+
+export function update(el, binding, vnode, oldVNode) {
+ if (vnode.context.$i18n.i18next.isInitialized) {
+ el.hidden = false;
+ }
+}
+
+export default {
+ bind,
+ update,
+};
diff --git a/test/unit/component.test.js b/test/unit/component.test.js
index f4a571c..247dc72 100644
--- a/test/unit/component.test.js
+++ b/test/unit/component.test.js
@@ -311,6 +311,15 @@ describe('Components with backend', () => {
expect(root.textContent).to.equal('dev__common__test');
});
+
+ it('should wait for translation to be ready', async () => {
+ const root = vm.$refs.hello;
+ expect(root.textContent).to.equal('key1');
+ backend.flush();
+ await nextTick();
+
+ expect(root.textContent).to.equal('dev__common__test');
+ });
});
describe('Nested namespaces', () => {
diff --git a/test/unit/wait.test.js b/test/unit/wait.test.js
new file mode 100644
index 0000000..44b3cf2
--- /dev/null
+++ b/test/unit/wait.test.js
@@ -0,0 +1,206 @@
+import BackendMock from '../helpers/backendMock';
+import { bind, update } from '../../src/wait';
+import sinon from 'sinon';
+
+const backend = new BackendMock();
+
+class I18nextMock {
+
+ constructor() {
+ this.events = { on: [], off: [] };
+ this.isInitialized = undefined;
+ }
+
+ off(event, options) {
+ this.events.off.push({ event, options });
+ }
+
+ on(event, options) {
+ this.events.on.push({ event, options });
+ }
+
+ mockFireEvent(e) {
+ this.events.on
+ .filter(({ event }) => e === event)
+ .map(({ options }) => options());
+ }
+}
+
+function nextTick() {
+ return new Promise(resolve => Vue.nextTick(resolve));
+}
+
+function sleep(time = 50) {
+ return new Promise(resolve => setTimeout(() => resolve(), time));
+}
+
+describe('wait directive', () => {
+ describe('with already loaded resources', () => {
+ const i18next1 = i18next.createInstance();
+ let vueI18Next;
+ beforeEach(() => {
+ i18next1.init({
+ lng: 'en',
+ fallbackLng: 'en',
+ resources: {
+ en: {
+ translation: { hello: 'Hello' },
+ },
+ de: {
+ translation: { hello: 'Hallo' },
+ },
+ },
+ });
+ vueI18Next = new VueI18Next(i18next1);
+ });
+
+ it('should not wait if translations are already ready', async () => {
+ const el = document.createElement('div');
+ const vm = new Vue({
+ i18n: vueI18Next,
+ render(h) {
+ //
+ return h('p', {
+ ref: 'text',
+ directives: [
+ {
+ name: 'waitForT',
+ rawName: 'v-waitForT',
+ },
+ ],
+ });
+ },
+ }).$mount(el);
+
+ await nextTick();
+ expect(vm.$el.hidden).to.equal(false);
+ });
+
+ it('vuei18Next instance warning', async () => {
+ const el = document.createElement('div');
+ const spy = sinon.spy(console, 'warn');
+ new Vue({
+ render(h) {
+ //
+ return h('p', {
+ ref: 'text',
+ directives: [
+ {
+ name: 'waitForT',
+ rawName: 'v-waitForT',
+ },
+ ],
+ });
+ },
+ }).$mount(el);
+
+ await nextTick();
+ expect(spy.notCalled).to.equal(false);
+ expect(spy.callCount).to.equal(1);
+ spy.restore();
+ });
+
+ it('resets i18n listener workaround', async () => {
+ const i18next = new I18nextMock();
+ const vm = {
+ context: {
+ $forceUpdate: () => undefined,
+ $i18n: {
+ i18next,
+ },
+ },
+ };
+
+ const spy = sinon.spy(vm.context, '$forceUpdate');
+
+ bind({}, null, vm);
+
+ expect(i18next.events.on.length).to.equal(1);
+ expect(i18next.events.off.length).to.equal(0);
+
+ i18next.mockFireEvent('initialized');
+ expect(spy.called).to.equal(true);
+
+ await sleep(1500);
+
+ expect(i18next.events.off.length).to.equal(1);
+ });
+
+ it('resets i18n listener workaround and does it only if context is still valid', async () => {
+ const i18next = new I18nextMock();
+ const vm = {
+ context: {
+ $forceUpdate: () => undefined,
+ $i18n: {
+ i18next,
+ },
+ },
+ };
+
+ const spy = sinon.spy(vm.context, '$forceUpdate');
+
+ bind({}, null, vm);
+
+ expect(i18next.events.on.length).to.equal(1);
+ expect(i18next.events.off.length).to.equal(0);
+
+ i18next.mockFireEvent('initialized');
+ expect(spy.called).to.equal(true);
+ vm.context = undefined;
+
+ await sleep(1500);
+
+ expect(i18next.events.off.length).to.equal(0);
+ });
+
+ it('do not show on update if it is not initialized', async () => {
+ const el = { hidden: true };
+ update(el, null, { context: { $i18n: { i18next: { isInitialized: false } } } });
+ expect(el.hidden).to.equal(true);
+ });
+
+ it('do not show on update if it is not initialized', async () => {
+ const el = { hidden: true };
+ update(el, null, { context: { $i18n: { i18next: { isInitialized: true } } } });
+ expect(el.hidden).to.equal(false);
+ });
+ });
+
+ describe('withBackend', () => {
+ const i18next1 = i18next.createInstance();
+ let vueI18Next;
+ beforeEach(async () => {
+ i18next1.use(backend).init({
+ lng: 'en',
+ });
+ vueI18Next = new VueI18Next(i18next1);
+
+ await sleep(50);
+ });
+
+ it('should wait for translation to be ready', async () => {
+ const el = document.createElement('div');
+ const vm = new Vue({
+ i18n: vueI18Next,
+ render(h) {
+ //
+ return h('p', {
+ ref: 'text',
+ directives: [
+ {
+ name: 'waitForT',
+ rawName: 'v-waitForT',
+ },
+ ],
+ });
+ },
+ }).$mount(el);
+
+ await nextTick();
+ expect(vm.$el.hidden).to.equal(true);
+ backend.flush();
+ await nextTick();
+ expect(vm.$el.hidden).to.equal(false);
+ });
+ });
+});