Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stepangranat/vue ssr test #3374

Closed
wants to merge 11 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"develop:react-i18next": "npm run develop -- --scope=@tolgee/react-i18next-testapp",
"develop:vue-i18next": "npm run develop -- --scope=@tolgee/vue-i18next-testapp",
"develop:next-app": "npm run develop -- --scope=@tolgee/next-app-testapp",
"develop:vue-ssr": "npm run develop -- --scope=@tolgee/vue-ssr-testapp",
"build:e2e": "turbo run build:e2e --cache-dir='.turbo'",
"test:e2e": "pnpm run build:e2e && pnpm --prefix e2e run start",
"clean": "turbo run clean --cache-dir='.turbo'",
Expand Down
1 change: 1 addition & 0 deletions packages/vue/src/GlobalContextPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ let globalContext: TolgeeVueContext | undefined;
export const GlobalContextPlugin = (): TolgeePlugin => (tolgee) => {
globalContext = {
tolgee,
isInitialRender: false,
};
return tolgee;
};
Expand Down
3 changes: 2 additions & 1 deletion packages/vue/src/T.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
TranslateProps,
TranslationKey,
} from '@tolgee/web';
import { defineComponent, PropType } from 'vue';
import { defineComponent } from 'vue';
import type { PropType } from 'vue';
import { useTranslateInternal } from './useTranslateInternal';

export const T = defineComponent({
Expand Down
6 changes: 6 additions & 0 deletions packages/vue/src/TolgeeProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { render, screen, waitFor } from '@testing-library/vue';
import ProviderComponent from './mocks/ProviderComponent.vue';
import ProviderComponentSlot from './mocks/ProviderComponentSlot.vue';
import { TolgeeInstance, Tolgee } from '@tolgee/web';
import { VueTolgee } from '.';

describe('Tolgee Provider Component', function () {
let mockedTolgee: TolgeeInstance;
Expand All @@ -23,6 +24,7 @@ describe('Tolgee Provider Component', function () {
getLanguage: () => 'mocked-lang',
},
},
global: { plugins: [[VueTolgee, { tolgee: mockedTolgee }]] },
});
await waitFor(() => {
screen.getByText("It's rendered!");
Expand All @@ -33,13 +35,15 @@ describe('Tolgee Provider Component', function () {
test('runs tolgee', async () => {
render(ProviderComponent, {
props: { tolgee: mockedTolgee },
global: { plugins: [[VueTolgee, { tolgee: mockedTolgee }]] },
});
expect(mockedTolgee.run).toHaveBeenCalledTimes(1);
});

test('stops tolgee', () => {
const { unmount } = render(ProviderComponent, {
props: { tolgee: mockedTolgee },
global: { plugins: [[VueTolgee, { tolgee: mockedTolgee }]] },
});
unmount();
expect(mockedTolgee.stop).toHaveBeenCalledTimes(1);
Expand All @@ -48,6 +52,7 @@ describe('Tolgee Provider Component', function () {
test('renders fallback with slot', async () => {
render(ProviderComponentSlot, {
props: { tolgee: mockedTolgee },
global: { plugins: [[VueTolgee, { tolgee: mockedTolgee }]] },
});
await waitFor(() => {
screen.getByText('loading');
Expand All @@ -62,6 +67,7 @@ describe('Tolgee Provider Component', function () {
tolgee: { ...mockedTolgee, isLoaded: () => true },
fallback: 'loading',
},
global: { plugins: [[VueTolgee, { tolgee: mockedTolgee }]] },
});
await waitFor(async () => {
screen.getByText("It's rendered!");
Expand Down
69 changes: 59 additions & 10 deletions packages/vue/src/TolgeeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
import {
defineComponent,
PropType,
getCurrentInstance,
provide,
onBeforeMount,
onUnmounted,
ref,
inject,
onMounted,
computed,
} from 'vue';
import { TolgeeInstance } from '@tolgee/web';
import type { Ref, ComputedRef } from 'vue';
import { TolgeeInstance, TolgeeStaticData } from '@tolgee/web';
import { TolgeeVueContext } from './types';

export const TolgeeProvider = defineComponent({
Expand All @@ -18,28 +20,75 @@ export const TolgeeProvider = defineComponent({
fallback: {
type: [Object, String] as PropType<JSX.Element | string>,
},
staticData: {
type: Object as PropType<TolgeeStaticData>,
required: false,
},
language: {
type: String as PropType<string>,
required: false,
},
},

setup(props) {
const tolgee: TolgeeInstance | undefined =
props.tolgee || getCurrentInstance().proxy.$tolgee;
const tolgeeContext: Ref<TolgeeVueContext> = inject('tolgeeContext');

// for backward compatibility
if (props.tolgee) {
tolgeeContext.value.tolgee = props.tolgee;
}

const tolgee: ComputedRef<TolgeeInstance> = computed(
() => tolgeeContext.value.tolgee
);

if (!tolgee) {
if (!tolgee.value) {
throw new Error('Tolgee instance not provided');
}

provide('tolgeeContext', { tolgee } as TolgeeVueContext);
if (tolgeeContext.value.isInitialRender) {
if (!props.staticData || !props.language) {
throw new Error(
'TolgeeProvider: "staticData" and "language" props are required for SSR.'
);
}

tolgee.value.setEmitterActive(false);
tolgee.value.addStaticData(props.staticData);
tolgee.value.changeLanguage(props.language);
tolgee.value.setEmitterActive(true);

if (!tolgee.value.isLoaded()) {
// warning user, that static data provided are not sufficient
// for proper SSR render
const missingRecords = tolgee.value
.getRequiredRecords(props.language)
.map(({ namespace, language }) =>
namespace ? `${namespace}:${language}` : language
)
.filter((key) => !props.staticData?.[key]);

// eslint-disable-next-line no-console
console.warn(
`Tolgee: Missing records in "staticData" for proper SSR functionality: ${missingRecords.map((key) => `"${key}"`).join(', ')}`
);
}
}

onMounted(() => {
tolgeeContext.value.isInitialRender = false;
});

const isLoading = ref(!tolgee.isLoaded());
const isLoading = ref(!tolgee.value.isLoaded());

onBeforeMount(() => {
tolgee.run().finally(() => {
tolgee.value.run().finally(() => {
isLoading.value = false;
});
});

onUnmounted(() => {
tolgee.stop();
tolgee.value.stop();
});
return { isLoading };
},
Expand Down
67 changes: 47 additions & 20 deletions packages/vue/src/VueTolgee.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import type { App } from 'vue';
import { ref } from 'vue';
import { ref, watch } from 'vue';
import {
getTranslateProps,
TolgeeInstance,
TFnType,
DefaultParamType,
TranslationKey,
} from '@tolgee/web';
import { TolgeeVueContext } from './types';

type Options = {
tolgee?: TolgeeInstance;
enableSSR?: boolean;
};

type TolgeeT = TolgeeInstance['t'];

export const VueTolgee = {
install(app: App, options?: Options) {
const tolgee = options?.tolgee;
Expand All @@ -20,27 +24,50 @@ export const VueTolgee = {
throw new Error('Tolgee instance not passed in options');
}

const createTFunc = () => {
return (...props) => {
// @ts-ignore
const params = getTranslateProps(...props);
return tolgee.t(params);
};
};

const tFunc = ref(createTFunc());
tolgee.on('update', () => {
tFunc.value = createTFunc();
});
const isSsrEnabled = Boolean(options?.enableSSR);

app.mixin({
computed: {
$t() {
return tFunc.value;
},
},
const reactiveContext = ref<TolgeeVueContext>({
tolgee: tolgee,
isInitialRender: isSsrEnabled,
});
app.config.globalProperties.$tolgee = tolgee;

app.provide('tolgeeContext', reactiveContext);

if (isSsrEnabled) {
const getOriginalTolgeeInstance = (): TolgeeInstance => ({
...reactiveContext.value.tolgee,
t: ((...args: Parameters<TolgeeT>) => {
const props = getTranslateProps(...args);
return tolgee.t({ ...props });
}) as TolgeeT,
});
const getTolgeeInstanceWithDeactivatedWrapper = (): TolgeeInstance => ({
...reactiveContext.value.tolgee,
t: ((...args: Parameters<TolgeeT>) => {
const props = getTranslateProps(...args);
return tolgee.t({ ...props, noWrap: true });
}) as TolgeeT,
});

reactiveContext.value.tolgee = getTolgeeInstanceWithDeactivatedWrapper();

watch(
() => reactiveContext.value.isInitialRender,
(isInitialRender) => {
if (!isInitialRender) {
reactiveContext.value.tolgee = getOriginalTolgeeInstance();
}
}
);
}

app.config.globalProperties.$t = ((...args: Parameters<TolgeeT>) =>
reactiveContext.value.tolgee.t(...args)) as TolgeeT;

// keep it for backward compatibility
// but it is not reactive
// not recommended to use it
app.config.globalProperties.$tolgee = reactiveContext.value.tolgee;
},
};

Expand Down
11 changes: 9 additions & 2 deletions packages/vue/src/__integration/T.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import '@testing-library/jest-dom';
import { render, screen, waitFor } from '@testing-library/vue';

import { TolgeeProvider, T, TolgeeInstance, Tolgee, DevTools } from '..';
import {
TolgeeProvider,
T,
TolgeeInstance,
Tolgee,
DevTools,
VueTolgee,
} from '..';
import { FormatIcu } from '@tolgee/format-icu';
import { mockCoreFetch } from '@tolgee/testing/fetchMock';

Expand All @@ -11,7 +18,6 @@ const API_KEY = 'dummyApiKey';
const fetch = mockCoreFetch();

const TestComponent = {
inject: ['tolgeeContext'],
components: { T },
template: `
<div>
Expand Down Expand Up @@ -68,6 +74,7 @@ describe('T component integration', () => {
tolgee,
fallback: 'Loading...',
},
global: { plugins: [[VueTolgee, { tolgee }]] },
});

await waitFor(() => {
Expand Down
3 changes: 2 additions & 1 deletion packages/vue/src/__integration/useTranslate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Tolgee,
DevTools,
useTranslate,
VueTolgee,
} from '..';
import { FormatIcu } from '@tolgee/format-icu';
import { mockCoreFetch } from '@tolgee/testing/fetchMock';
Expand All @@ -20,7 +21,6 @@ const fetch = mockCoreFetch();
const setTitle = jest.fn();

const TestComponent = {
inject: ['tolgeeContext'],
components: { T },
setup() {
const { t } = useTranslate();
Expand Down Expand Up @@ -76,6 +76,7 @@ describe('T component integration', () => {
tolgee,
fallback: 'Loading...',
},
global: { plugins: [[VueTolgee, { tolgee }]] },
});

await waitFor(() => {
Expand Down
9 changes: 4 additions & 5 deletions packages/vue/src/mocks/ComponentUsingProvider.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
</div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
<script setup lang="ts">
import { inject } from 'vue';
import { TolgeeVueContext } from '@tolgee/vue';

export default defineComponent({
inject: ['tolgeeContext'],
});
const tolgeeContext = inject<TolgeeVueContext>('tolgeeContext');
</script>
1 change: 1 addition & 0 deletions packages/vue/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ import { TolgeeInstance } from '@tolgee/web';

export type TolgeeVueContext = {
tolgee: TolgeeInstance;
isInitialRender: boolean;
};
23 changes: 12 additions & 11 deletions packages/vue/src/useTolgee.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { TolgeeEvent } from '@tolgee/web';
import { getCurrentInstance, inject, onUnmounted, ref } from 'vue';
import { TolgeeEvent, TolgeeInstance } from '@tolgee/web';
import { inject, computed, onUnmounted, getCurrentInstance } from 'vue';
import { TolgeeVueContext } from './types';
import type { Ref, ComputedRef } from 'vue';

export const useTolgee = (events?: TolgeeEvent[]) => {
const instance = getCurrentInstance();
const tolgeeContext = inject('tolgeeContext', {
tolgee: instance.proxy.$tolgee,
}) as TolgeeVueContext;
const tolgeeContext: Ref<TolgeeVueContext> = inject('tolgeeContext');

const tolgee = ref(tolgeeContext.tolgee);
const tolgee: ComputedRef<TolgeeInstance> = computed(
() => tolgeeContext.value.tolgee
);

const listeners = events?.map((e) =>
tolgee.value.on(e, () => {
tolgee.value = Object.freeze({ ...tolgee.value });
const listeners = events?.map((e) => {
return tolgee.value.on(e, () => {
tolgeeContext.value.tolgee = Object.freeze({ ...tolgee.value });
instance.proxy.$forceUpdate();
})
);
});
});

onUnmounted(() => {
listeners?.forEach((listener) => listener.unsubscribe());
Expand Down
Loading