Skip to content

Commit

Permalink
V2 translate (#6569)
Browse files Browse the repository at this point in the history
* translations atom

* migrate Translate WIP

* handle inline edit mode

* stop event

* adds update translations endpoint

* wip modal form

* get translations V2 WIP

* Translate component using translationsAtom and loading correct values in to the trtanslation form

* test new endpoint

* correctly set form

* context to post request

* update translate atom after update

* uses context by language

* look & feel + socket events

* ci fixes

* migrate t and update cache reset strategy

* update affected files

* adjust zindex and use t function for texts inside

* migrate I18Nmenu

* remove icon dependency

* cleanup

* update import

* fix issue with dropdown

* update t function and move to v2 folder

* update imports

* remove unused afterEach

* stories cleanup

* remove redux provider from pdf unit test

* remove commented

* locale sensitve i18n link

* remove legacy store

* use new i18n link for breadcrumbs

* reorganize files

* unit test snapshot update

* update import

* avoid undefined when no classname and update snapshots

* revert some unnecessary changes

* do not return component in notification

* fix type + wip fix test

* fix export

* fix multiple unit test by updating selectors, stores or mocking translate

* more fixes

* revert removal of redux translations

* fix test and eslint errors

* update snapshot

* update selectors and e2e

* more specific selector

* wait for modal

* attempt to stabilize

* I18NMenu test update wip

* use actual store for test

* use real events

* close modal after a11y check

* more attempts at fixing e2e

* more explicit steps

* continued attempt at fixing test

* install function handles modal closing

* more specific install selector

* restore workflow file

* account for new languages in socket emit

* wip update api

* set translation modal's zindex to 10000

* propagation of thesaurus translations

* socket events test update

* update translations on language delete

* udpate how the test updates component

* translateModal test wip

* test modal closes properly

* update submit test

* update response type

* validate form and notify

* fix lint error

* stabilze e2e

* disable modal elements while saving

* update test description

* update snapshot

* more attempts at e2e stabiliation

* cleanup

* fix route duplication

---------

Co-authored-by: mfacar <[email protected]>
Co-authored-by: A happy cat <[email protected]>
Co-authored-by: JoshuaD <[email protected]>
Co-authored-by: Joan Gallego Girona <[email protected]>
  • Loading branch information
5 people authored Jan 28, 2025
1 parent 691ded0 commit 062a26f
Show file tree
Hide file tree
Showing 150 changed files with 3,114 additions and 2,281 deletions.
1 change: 1 addition & 0 deletions app/api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default (app, server) => {
require('./files/ocrRoutes').ocrRoutes(app);
require('./settings/routes').default(app);
require('./i18n/routes').default(app);
require('./i18n.v2/routes').translationsRoutes(app);
require('./sync/routes').default(app);
require('./tasks/routes').default(app);
require('./usergroups/routes').default(app);
Expand Down
54 changes: 54 additions & 0 deletions app/api/i18n.v2/routes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Application, Request } from 'express';
import { needsAuthorization } from 'api/auth';
import { validation } from 'api/utils';
import translations from 'api/i18n';
import { getTranslationsEntriesV2 } from 'api/i18n/v2_support';

const translationsRoutes = (app: Application) => {
app.get('/api/v2/translations', async (_req: Request, res) => {
const translationsV2 = await getTranslationsEntriesV2();
const translationList = await translationsV2.all();
res.json(translationList);
});

app.post(
'/api/v2/translations',
needsAuthorization(),
validation.validateRequest({
type: 'object',
properties: {
body: {
type: 'array',
items: {
type: 'object',
properties: {
_id: { type: 'string' },
language: { type: 'string' },
key: { type: 'string' },
value: { type: 'string' },
context: {
type: 'object',
properties: {
id: { type: 'string' },
label: { type: 'string' },
type: { type: 'string' },
},
required: ['id', 'label', 'type'],
},
},
required: ['language', 'key', 'value', 'context'],
},
},
},
required: ['body'],
}),
async (req, res) => {
await translations.v2StructureSave(req.body);
req.sockets.emitToCurrentTenant('translationKeysChange', req.body);
res.status(200);
res.json({ success: true });
}
);
};

export { translationsRoutes };
102 changes: 102 additions & 0 deletions app/api/i18n.v2/routes/specs/routes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import 'isomorphic-fetch';
import request from 'supertest';

import { TranslationDBO } from 'api/i18n.v2/schemas/TranslationDBO';
import { getFixturesFactory } from 'api/utils/fixturesFactory';
import { testingEnvironment } from 'api/utils/testingEnvironment';
import { TestEmitSources, iosocket, setUpApp } from 'api/utils/testingRoutes';
import { UserRole } from 'shared/types/userSchema';
import { translationsRoutes } from '..';

describe('i18n translations V2 routes', () => {
const createTranslationDBO = getFixturesFactory().v2.database.translationDBO;
const app = setUpApp(translationsRoutes, (req, _res, next) => {
req.user = {
username: 'admin',
role: UserRole.ADMIN,
email: '[email protected]',
};
// @ts-ignore
req.file = { path: 'filder/filename.ext' };
next();
});

beforeEach(async () => {
const translationsV2: TranslationDBO[] = [
createTranslationDBO('Search', 'Buscar', 'es', {
id: 'System',
type: 'Entity',
label: 'User Interface',
}),
createTranslationDBO('Search', 'Search', 'en', {
id: 'System',
type: 'Uwazi UI',
label: 'User Interface',
}),
];
await testingEnvironment.setUp(
{
settings: [
{
languages: [
{ key: 'en', label: 'English', default: true },
{ key: 'es', label: 'Spanish', default: false },
],
},
],
translationsV2,
},
'index_i18n_v2_routes'
);
});

afterEach(() => {
iosocket.emit.mockReset();
});

afterAll(async () => {
await testingEnvironment.tearDown();
});

describe('/api/v2/translations', () => {
it('should update the translations and emit translationKeysChange event', async () => {
const response = await request(app)
.post('/api/v2/translations')
.send([
{
language: 'es',
key: 'Search',
value: 'Búsqueda',
context: {
id: 'System',
label: 'User Interface',
type: 'Uwazi UI',
},
},
]);
expect(response.status).toEqual(200);
expect(iosocket.emit).toHaveBeenCalledWith(
'translationKeysChange',
TestEmitSources.currentTenant,
[
{
context: { id: 'System', label: 'User Interface', type: 'Uwazi UI' },
key: 'Search',
language: 'es',
value: 'Búsqueda',
},
]
);
});

it('should handle invalid POST request payload', async () => {
const response = await request(app)
.post('/api/v2/translations')
.send({ invalidKey: 'value' }); // Invalid payload
expect(response.status).toBe(400);
expect(response.body).toEqual(
expect.objectContaining({ prettyMessage: 'validation failed' })
);
});
});
});
2 changes: 2 additions & 0 deletions app/api/i18n/routes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable max-lines */
import { createError, validation } from 'api/utils';
import settings from 'api/settings';
import entities from 'api/entities';
Expand Down Expand Up @@ -59,6 +60,7 @@ async function deleteLanguage(key: LanguageISO6391, req: Request) {

type TranslationsRequest = Request & { query: { context: string } };

// eslint-disable-next-line max-statements
export default (app: Application) => {
app.get(
'/api/translations',
Expand Down
22 changes: 22 additions & 0 deletions app/api/i18n/specs/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,13 @@ const fixtures: DBFixture = {
{
_id: entityTemplateId,
type: 'template',
properties: [
{
type: 'select',
name: 'Dictionary',
content: dictionaryId.toString(),
},
],
},
{
_id: documentTemplateId,
Expand All @@ -214,6 +221,21 @@ const fixtures: DBFixture = {
published: false,
metadata: {},
},
{
language: 'es',
sharedId: 'entity1',
title: '1',
template: entityTemplateId,
published: false,
metadata: {
Dictionary: [
{
value: '1',
label: 'Password',
},
],
},
},
],
pages: [
{
Expand Down
34 changes: 33 additions & 1 deletion app/api/i18n/specs/translations.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import thesauri from 'api/thesauri/thesauri.js';
import { ContextType } from 'shared/translationSchema';
// eslint-disable-next-line node/no-restricted-import
import * as fs from 'fs';
import { TranslationSyO } from 'api/i18n.v2/schemas/TranslationSyO';
import { UITranslationNotAvailable } from '../defaultTranslations';
import translations from '../translations';
import fixtures, { dictionaryId } from './fixtures';
import { sortByLocale } from './sortByLocale';
import { addLanguage } from '../routes';
import { getTranslationsV2ByContext } from '../v2_support';

describe('translations', () => {
beforeEach(async () => {
Expand All @@ -29,7 +31,7 @@ describe('translations', () => {
expect(result).toMatchObject({
contexts: [
{
type: 'Thesaurus',
type: 'Thesaurus' as 'Thesaurus',
values: {
Account: 'Account',
Age: 'Age',
Expand All @@ -56,6 +58,36 @@ describe('translations', () => {
});
});

describe('v2StructureSave', () => {
it('should save changed translations and propagate the changes', async () => {
const initialTranslations = await getTranslationsV2ByContext(dictionaryId.toString());
const initialEntity = (await entities.get({ language: 'es', sharedId: 'entity1' }))[0];
const translationsToSave = [
{
_id: '1',
language: initialTranslations[0].locale!,
key: 'Password',
value: 'Changed Password ES',
context: {
id: dictionaryId.toString(),
type: 'Thesaurus' as TranslationSyO['context']['type'],
label: '',
},
},
];

await translations.v2StructureSave(translationsToSave);
const updatedTranslations = await getTranslationsV2ByContext(dictionaryId.toString());
initialTranslations![0]!.contexts![0]!.values!.find(v => v.key === 'Password')!.value =
'Changed Password ES';
expect(updatedTranslations).toEqual(initialTranslations);

const updatedEntity = (await entities.get({ language: 'es', sharedId: 'entity1' }))[0];
initialEntity.metadata.Dictionary[0].label = 'Changed Password ES';
expect(updatedEntity).toEqual(initialEntity);
});
});

describe('save()', () => {
it('should save the translation and return it', async () => {
const result = await translations.save({ locale: 'fr' });
Expand Down
Loading

0 comments on commit 062a26f

Please sign in to comment.