Skip to content
Open
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 apps/meteor/app/api/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import './v1/mailer';
import './v1/teams';
import './v1/moderation';
import './v1/uploads';
import './v1/i18n';

// This has to come last so all endpoints are registered before generating the OpenAPI documentation
import './default/openApi';
Expand Down
86 changes: 86 additions & 0 deletions apps/meteor/app/api/server/v1/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { Logger } from '@rocket.chat/logger';
import { ajv, validateUnauthorizedErrorResponse, validateBadRequestErrorResponse } from '@rocket.chat/rest-typings';

import { API } from '../api';

const logger = new Logger('AppsTranslations');

API.v1.get(
'apps.translations',
{
authRequired: true,
query: ajv.compile<{ language?: string }>({
type: 'object',
properties: {
language: { type: 'string', minLength: 2 },
},
additionalProperties: false,
}),
response: {
200: ajv.compile<{
language: string;
translations: { [key: string]: string };
success: true;
}>({
type: 'object',
properties: {
language: { type: 'string' },
translations: { type: 'object', additionalProperties: { type: 'string' } },
success: { type: 'boolean', enum: [true] },
},
required: ['language', 'translations', 'success'],
additionalProperties: false,
}),
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
},
},
async function action() {
const language = (this.queryParams.language ?? 'en').split('-')[0].toLowerCase();
logger.debug({ msg: 'Fetching app translations', language, userId: this.userId });

// lazy import inside the function — avoids circular init crash
const { Apps } = await import('../../../../ee/server/apps/orchestrator');

const manager = (Apps as any).getManager();
if (!manager) {
logger.error({ msg: 'Apps manager unavailable' });
return API.v1.failure('Apps manager unavailable.');
}

const mergedTranslations: { [key: string]: string } = {};

const apps = await manager.get({ enabled: true });
logger.debug({ msg: 'Found enabled apps', count: apps.length });

for (const app of apps) {
try {
const storageItem = app.getStorageItem() as {
languageContent?: {
[lang: string]: {
[key: string]: string;
};
};
};

const langContent = storageItem.languageContent?.[language] ?? storageItem.languageContent?.en;

if (!langContent) {
logger.debug({ msg: 'No translations found for app', appId: app.getID(), language });
continue;
}

const appId = app.getID();

for (const [key, value] of Object.entries(langContent)) {
mergedTranslations[`app-${appId}.${key}`] = value;
}
} catch (err) {
logger.warn({ msg: 'Failed to get translations for app', appId: app.getID(), err });
}
}

return API.v1.success({ language, translations: mergedTranslations });
},
);