Add server translation (#9847)

First proof of concept for server-side translation.

The goal was to translate one metadata item:

<img width="939" alt="Screenshot 2025-01-26 at 08 18 41"
src="https://github.com/user-attachments/assets/e42a3f7f-f5e3-4ee7-9be5-272a2adccb23"
/>
This commit is contained in:
Félix Malfait
2025-01-27 21:07:49 +01:00
committed by GitHub
parent 2a911b4305
commit 549c3faf71
35 changed files with 412 additions and 131 deletions

View File

@ -1,7 +1,7 @@
import { InMemoryCache, NormalizedCacheObject } from '@apollo/client';
import { useMemo, useRef } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
@ -29,6 +29,7 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
currentWorkspaceState,
);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const setCurrentUser = useSetRecoilState(currentUserState);
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
@ -61,6 +62,7 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
connectToDevTools: isDebugMode,
// We don't want to re-create the client on token change or it will cause infinite loop
initialTokenPair: tokenPair,
currentWorkspaceMember: currentWorkspaceMember,
onTokenPairChange: (tokenPair) => {
setTokenPair(tokenPair);
},
@ -105,5 +107,11 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
}
}, [tokenPair]);
useUpdateEffect(() => {
if (isDefined(apolloRef.current)) {
apolloRef.current.updateWorkspaceMember(currentWorkspaceMember);
}
}, [currentWorkspaceMember]);
return apolloClient;
};

View File

@ -21,12 +21,22 @@ jest.mock('@/auth/services/AuthService', () => {
const mockOnError = jest.fn();
const mockOnNetworkError = jest.fn();
const mockWorkspaceMember = {
id: 'workspace-member-id',
locale: 'en',
name: {
firstName: 'John',
lastName: 'Doe',
},
};
const createMockOptions = (): Options<any> => ({
uri: 'http://localhost:3000',
initialTokenPair: {
accessToken: { token: 'mockAccessToken', expiresAt: '' },
refreshToken: { token: 'mockRefreshToken', expiresAt: '' },
},
currentWorkspaceMember: mockWorkspaceMember,
cache: new InMemoryCache(),
isDebugMode: true,
onError: mockOnError,
@ -50,13 +60,21 @@ const makeRequest = async () => {
});
};
describe('xApolloFactory', () => {
describe('ApolloFactory', () => {
it('should create an instance of ApolloFactory', () => {
const options = createMockOptions();
const apolloFactory = new ApolloFactory(options);
expect(apolloFactory).toBeInstanceOf(ApolloFactory);
});
it('should initialize with the correct workspace member', () => {
const options = createMockOptions();
const apolloFactory = new ApolloFactory(options);
expect(apolloFactory['currentWorkspaceMember']).toEqual(
mockWorkspaceMember,
);
});
it('should call onError when encountering "Unauthorized" error', async () => {
const errors = [{ message: 'Unauthorized' }];
fetchMock.mockResponse(() =>
@ -138,4 +156,21 @@ describe('xApolloFactory', () => {
expect(mockOnNetworkError).toHaveBeenCalledWith(mockError);
}
}, 10000);
it('should update workspace member when calling updateWorkspaceMember', () => {
const options = createMockOptions();
const apolloFactory = new ApolloFactory(options);
const newWorkspaceMember = {
id: 'new-workspace-member-id',
locale: 'fr',
name: {
firstName: 'John',
lastName: 'Doe',
},
};
apolloFactory.updateWorkspaceMember(newWorkspaceMember);
expect(apolloFactory['currentWorkspaceMember']).toEqual(newWorkspaceMember);
});
});

View File

@ -12,6 +12,7 @@ import { RetryLink } from '@apollo/client/link/retry';
import { createUploadLink } from 'apollo-upload-client';
import { renewToken } from '@/auth/services/AuthService';
import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
import { AuthTokenPair } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
import { logDebug } from '~/utils/logDebug';
@ -28,6 +29,7 @@ export interface Options<TCacheShape> extends ApolloClientOptions<TCacheShape> {
onTokenPairChange?: (tokenPair: AuthTokenPair) => void;
onUnauthenticatedError?: () => void;
initialTokenPair: AuthTokenPair | null;
currentWorkspaceMember: CurrentWorkspaceMember | null;
extraLinks?: ApolloLink[];
isDebugMode?: boolean;
}
@ -35,6 +37,7 @@ export interface Options<TCacheShape> extends ApolloClientOptions<TCacheShape> {
export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
private client: ApolloClient<TCacheShape>;
private tokenPair: AuthTokenPair | null = null;
private currentWorkspaceMember: CurrentWorkspaceMember | null = null;
constructor(opts: Options<TCacheShape>) {
const {
@ -44,12 +47,14 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
onTokenPairChange,
onUnauthenticatedError,
initialTokenPair,
currentWorkspaceMember,
extraLinks,
isDebugMode,
...options
} = opts;
this.tokenPair = initialTokenPair;
this.currentWorkspaceMember = currentWorkspaceMember;
const buildApolloLink = (): ApolloLink => {
const httpLink = createUploadLink({
@ -64,6 +69,9 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
authorization: this.tokenPair?.accessToken.token
? `Bearer ${this.tokenPair?.accessToken.token}`
: '',
...(this.currentWorkspaceMember?.locale
? { 'x-locale': this.currentWorkspaceMember.locale }
: {}),
},
};
});
@ -157,6 +165,10 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
this.tokenPair = tokenPair;
}
updateWorkspaceMember(workspaceMember: CurrentWorkspaceMember | null) {
this.currentWorkspaceMember = workspaceMember;
}
getClient() {
return this.client;
}

View File

@ -1,8 +1,10 @@
import { ApolloClient } from '@apollo/client';
import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
import { AuthTokenPair } from '~/generated/graphql';
export interface ApolloManager<TCacheShape> {
getClient(): ApolloClient<TCacheShape>;
updateTokenPair(tokenPair: AuthTokenPair | null): void;
updateWorkspaceMember(workspaceMember: CurrentWorkspaceMember | null): void;
}