mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-12 10:53:51 +00:00
feat: add user defined translations (#1756)
* feat: add user defined translations * add refetch key for translation (#1757) --------- Co-authored-by: Alex Christoffer Rasmussen <ac@omnivox.dk>
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import express from 'express';
|
||||
import type { Request, Response } from 'express';
|
||||
import type { ErrorResponse } from 'ontime-types';
|
||||
import { validatePostCss } from './assets.validation.js';
|
||||
import { readCssFile, writeCssFile } from './assets.service.js';
|
||||
import { RefetchKey, type ErrorResponse } from 'ontime-types';
|
||||
import { validatePostCss, validatePostTranslation } from './assets.validation.js';
|
||||
import { readCssFile, writeCssFile, writeUserTranslation } from './assets.service.js';
|
||||
import { getErrorMessage } from 'ontime-utils';
|
||||
import { defaultCss } from '../../user/styles/bundledCss.js';
|
||||
import { sendRefetch } from '../../adapters/WebsocketAdapter.js';
|
||||
|
||||
export const router = express.Router();
|
||||
|
||||
@@ -38,3 +39,21 @@ router.post('/css/restore', async (_req: Request, res: Response<string | ErrorRe
|
||||
res.status(500).send({ message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/translations', validatePostTranslation, async (req: Request, res: Response<never | ErrorResponse>) => {
|
||||
const { translation } = req.body;
|
||||
|
||||
if (!translation) {
|
||||
res.status(400).send({ message: 'translation payload is required ' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await writeUserTranslation(translation);
|
||||
sendRefetch(RefetchKey.Translation);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
res.status(500).send({ message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { publicFiles } from '../../setup/index.js';
|
||||
import type { TranslationObject } from 'ontime-types';
|
||||
|
||||
import { existsSync } from 'node:fs';
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
|
||||
import { publicFiles } from '../../setup/index.js';
|
||||
import { defaultCss } from '../../user/styles/bundledCss.js';
|
||||
|
||||
/**
|
||||
@@ -31,3 +34,13 @@ export async function writeCssFile(css: string) {
|
||||
|
||||
await writeFile(path, css, { encoding: 'utf8' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the user's custom translation file
|
||||
* @param translations the updated translations to write to file
|
||||
*/
|
||||
export async function writeUserTranslation(translations: TranslationObject) {
|
||||
const path = publicFiles.translationsFile;
|
||||
const translationsString = JSON.stringify(translations, null, 2);
|
||||
await writeFile(path, translationsString, { encoding: 'utf8' });
|
||||
}
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
import { body } from 'express-validator';
|
||||
|
||||
import { requestValidationFunction } from '../validation-utils/validationFunction.js';
|
||||
|
||||
export const validatePostCss = [body('css').isString().trim(), requestValidationFunction];
|
||||
|
||||
export const validatePostTranslation = [
|
||||
body('translation')
|
||||
.custom((v) => v != null && typeof v === 'object' && !Array.isArray(v))
|
||||
.withMessage('translation must be an object (key -> string)')
|
||||
.bail(),
|
||||
body('translation.*').isString().trim().notEmpty(),
|
||||
requestValidationFunction,
|
||||
];
|
||||
|
||||
@@ -29,12 +29,13 @@ import { getDataProvider } from './classes/data-provider/DataProvider.js';
|
||||
|
||||
// Services
|
||||
import { logger } from './classes/Logger.js';
|
||||
import { populateDemo } from './setup/loadDemo.js';
|
||||
import { populateTranslation } from './setup/loadTranslations.js';
|
||||
import { populateStyles } from './setup/loadStyles.js';
|
||||
import { eventStore } from './stores/EventStore.js';
|
||||
import { runtimeService } from './services/runtime-service/runtime.service.js';
|
||||
import { RestorePoint, restoreService } from './services/RestoreService.js';
|
||||
import * as messageService from './services/message-service/message.service.js';
|
||||
import { populateDemo } from './setup/loadDemo.js';
|
||||
import { getState } from './stores/runtimeState.js';
|
||||
import { initialiseProject } from './services/project-service/ProjectService.js';
|
||||
import { getShowWelcomeDialog } from './services/app-state-service/AppStateService.js';
|
||||
@@ -157,6 +158,7 @@ export const initAssets = async (escalateErrorFn?: (error: string, unrecoverable
|
||||
|
||||
await clearUploadfolder();
|
||||
populateStyles();
|
||||
populateTranslation();
|
||||
await populateDemo();
|
||||
const project = await initialiseProject();
|
||||
logger.info(LogOrigin.Server, `Initialised Ontime with ${project}`);
|
||||
|
||||
@@ -33,4 +33,8 @@ export const config = {
|
||||
},
|
||||
uploads: 'uploads',
|
||||
logo: 'logo',
|
||||
translations: {
|
||||
directory: 'translations',
|
||||
filename: 'translations.json',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -81,12 +81,16 @@ export const srcFiles = {
|
||||
clientIndexHtml: join(srcDir.clientDir, 'index.html'),
|
||||
/** Path to bundled CSS */
|
||||
cssOverride: join(srcDir.root, config.user, config.styles.directory, config.styles.filename),
|
||||
/** Path to bundled translation */
|
||||
translationsFile: join(srcDir.root, config.user, config.translations.directory, config.translations.filename),
|
||||
/** Path to bundled external readme */
|
||||
externalReadme: join(srcDir.root, config.external, 'README.md'),
|
||||
/** Path to bundled user readme */
|
||||
userReadme: join(srcDir.root, config.user, 'README.md'),
|
||||
/** Path to bundled CSS readme */
|
||||
cssReadme: join(srcDir.root, config.user, config.styles.directory, 'README.md'),
|
||||
/** Path to bundled translation readme */
|
||||
translationReadme: join(srcDir.root, config.user, config.translations.directory, 'README.md'),
|
||||
/** Path to login */
|
||||
login: join(srcDir.root, 'html/login.html'),
|
||||
};
|
||||
@@ -136,6 +140,8 @@ export const publicDir = {
|
||||
/** path to external styles override */
|
||||
stylesDir: join(resolvePublicDirectory, config.user, config.styles.directory),
|
||||
logoDir: join(resolvePublicDirectory, config.user, config.logo),
|
||||
/** path to translations folder */
|
||||
translationsDir: join(resolvePublicDirectory, config.user, config.translations.directory),
|
||||
} as const;
|
||||
|
||||
/**
|
||||
@@ -148,10 +154,14 @@ export const publicFiles = {
|
||||
restoreFile: join(publicDir.root, config.restoreFile),
|
||||
/** path to CSS override file */
|
||||
cssOverride: join(publicDir.stylesDir, config.styles.filename),
|
||||
/** path to custom translation file */
|
||||
translationsFile: join(publicDir.translationsDir, config.translations.filename),
|
||||
/** path to external readme file */
|
||||
externalReadme: join(publicDir.externalDir, 'README.md'),
|
||||
/** path to user readme file */
|
||||
userReadme: join(publicDir.userDir, 'README.md'),
|
||||
/** path to CSS readme file */
|
||||
cssReadme: join(publicDir.stylesDir, 'README.md'),
|
||||
/** path to translation readme file */
|
||||
translationReadme: join(publicDir.translationsDir, 'README.md'),
|
||||
} as const;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { copyFileSync, existsSync, writeFileSync } from 'fs';
|
||||
|
||||
import { ensureDirectory } from '../utils/fileManagement.js';
|
||||
import { defaultTranslation } from '../user/translations/bundledTranslations.js';
|
||||
|
||||
import { publicDir, publicFiles, srcFiles } from './index.js';
|
||||
|
||||
/**
|
||||
* ensures directories exist and populates translation
|
||||
*/
|
||||
export const populateTranslation = () => {
|
||||
ensureDirectory(publicDir.translationsDir);
|
||||
// if translations doesn't exist we want to use startup translation
|
||||
try {
|
||||
copyFileSync(srcFiles.translationReadme, publicFiles.translationReadme);
|
||||
if (!existsSync(publicFiles.translationsFile)) {
|
||||
// copy the startup translation only if user doesnt have one
|
||||
writeFileSync(publicFiles.translationsFile, defaultTranslation, { encoding: 'utf-8' });
|
||||
}
|
||||
} catch (_) {
|
||||
/* we do not handle this */
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
# translations
|
||||
|
||||
The translations folder contain the user's custom translations which is used for custom translations in Ontime.
|
||||
The file should be named `translations.json`.
|
||||
@@ -0,0 +1,3 @@
|
||||
import { langEn } from 'ontime-types';
|
||||
|
||||
export const defaultTranslation = JSON.stringify(langEn, null, 2);
|
||||
Reference in New Issue
Block a user