feat: fetch remote i18n

This commit is contained in:
Mohamed Marrouchi
2024-10-18 17:50:35 +01:00
parent 08b1deae50
commit 85cc85e4db
50 changed files with 1823 additions and 1553 deletions

View File

@@ -0,0 +1,28 @@
/*
* Copyright © 2024 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
import { I18nService } from '../services/i18n.service';
@UseInterceptors(CsrfInterceptor)
@Controller('i18n')
export class I18nController {
constructor(private readonly i18nService: I18nService) {}
/**
* Retrieves translations of all the installed extensions.
* @returns An nested object that holds the translations grouped by language and extension name.
*/
@Get()
getTranslations() {
return this.i18nService.getExtensionI18nTranslations();
}
}

View File

@@ -46,10 +46,10 @@ export class LanguageController extends BaseController<Language> {
}
/**
* Retrieves a paginated list of categories based on provided filters and pagination settings.
* Retrieves a paginated list of languages based on provided filters and pagination settings.
* @param pageQuery - The pagination settings.
* @param filters - The filters to apply to the language search.
* @returns A Promise that resolves to a paginated list of categories.
* @returns A Promise that resolves to a paginated list of languages.
*/
@Get()
async findPage(
@@ -61,8 +61,8 @@ export class LanguageController extends BaseController<Language> {
}
/**
* Counts the filtered number of categories.
* @returns A promise that resolves to an object representing the filtered number of categories.
* Counts the filtered number of languages.
* @returns A promise that resolves to an object representing the filtered number of languages.
*/
@Get('count')
async filterCount(

View File

@@ -26,6 +26,7 @@ import { Observable } from 'rxjs';
import { ChatModule } from '@/chat/chat.module';
import { I18nController } from './controllers/i18n.controller';
import { LanguageController } from './controllers/language.controller';
import { TranslationController } from './controllers/translation.controller';
import { LanguageRepository } from './repositories/language.repository';
@@ -62,6 +63,7 @@ export class I18nModule extends NativeI18nModule {
controllers: (controllers || []).concat([
LanguageController,
TranslationController,
I18nController,
]),
providers: providers.concat([
I18nService,

View File

@@ -6,8 +6,13 @@
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import { Injectable } from '@nestjs/common';
import { promises as fs } from 'fs';
import * as path from 'path';
import { Injectable, OnModuleInit } from '@nestjs/common';
import {
I18nJsonLoader,
I18nTranslation,
I18nService as NativeI18nService,
Path,
PathValue,
@@ -19,11 +24,22 @@ import { config } from '@/config';
import { Translation } from '@/i18n/schemas/translation.schema';
@Injectable()
export class I18nService<
K = Record<string, unknown>,
> extends NativeI18nService<K> {
export class I18nService<K = Record<string, unknown>>
extends NativeI18nService<K>
implements OnModuleInit
{
private dynamicTranslations: Record<string, Record<string, string>> = {};
private extensionTranslations: I18nTranslation = {};
onModuleInit() {
this.loadExtensionI18nTranslations();
}
getExtensionI18nTranslations() {
return this.extensionTranslations;
}
t<P extends Path<K> = any, R = PathValue<K, P>>(
key: P,
options?: TranslateOptions,
@@ -66,4 +82,48 @@ export class I18nService<
return acc;
}, this.dynamicTranslations);
}
async loadExtensionI18nTranslations() {
const extensionsDir = path.join(
__dirname,
'..',
'..',
'extensions',
'channels',
);
try {
const extensionFolders = await fs.readdir(extensionsDir, {
withFileTypes: true,
});
for (const folder of extensionFolders) {
if (folder.isDirectory()) {
const i18nPath = path.join(extensionsDir, folder.name, 'i18n');
const extensionName = folder.name.replaceAll('-', '_');
try {
// Check if the i18n directory exists
await fs.access(i18nPath);
// Load and merge translations
const i18nLoader = new I18nJsonLoader({ path: i18nPath });
const translations = await i18nLoader.load();
for (const lang in translations) {
if (!this.extensionTranslations[lang]) {
this.extensionTranslations[lang] = {
[extensionName]: translations[lang],
};
} else {
this.extensionTranslations[lang][extensionName] =
translations[lang];
}
}
} catch (error) {
// If the i18n folder does not exist or error in reading, skip this folder
}
}
}
} catch (error) {
throw new Error(`Failed to read extensions directory: ${error.message}`);
}
}
}

View File

@@ -10,7 +10,6 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
import { Test, TestingModule } from '@nestjs/testing';
import { I18nService } from '@/i18n/services/i18n.service';
import { Settings } from '@/setting/schemas/types';
import { SettingService } from '@/setting/services/setting.service';
import { Block } from '../../chat/schemas/block.schema';
@@ -64,7 +63,7 @@ describe('TranslationService', () => {
global_fallback: true,
fallback_message: ['Global fallback message'],
},
} as Settings),
}),
},
},
{