etherpad-lite/src/node/hooks/i18n.ts

157 lines
5.3 KiB
TypeScript
Raw Normal View History

2024-04-17 21:29:15 +02:00
"use strict";
2021-01-21 21:06:52 +00:00
2024-04-17 21:29:15 +02:00
import type { MapArrayType } from "../types/MapType";
import { I18nPluginDefs } from "../types/I18nPluginDefs";
2024-04-17 21:29:15 +02:00
const languages = require("languages4translatewiki");
const fs = require("fs");
const path = require("path");
const _ = require("underscore");
const pluginDefs = require("../../static/js/pluginfw/plugin_defs.js");
const existsSync = require("../utils/path_exists");
const settings = require("../utils/Settings");
2012-12-23 22:43:32 +01:00
// returns all existing messages merged together and grouped by langcode
// {es: {"foo": "string"}, en:...}
2021-01-21 21:06:52 +00:00
const getAllLocales = () => {
2024-04-17 21:29:15 +02:00
const locales2paths: MapArrayType<string[]> = {};
// Puts the paths of all locale files contained in a given directory
// into `locales2paths` (files from various dirs are grouped by lang code)
// (only json files with valid language code as name)
const extractLangs = (dir: string) => {
if (!existsSync(dir)) return;
let stat = fs.lstatSync(dir);
if (!stat.isDirectory() || stat.isSymbolicLink()) return;
fs.readdirSync(dir).forEach((file: string) => {
file = path.resolve(dir, file);
stat = fs.lstatSync(file);
if (stat.isDirectory() || stat.isSymbolicLink()) return;
const ext = path.extname(file);
const locale = path.basename(file, ext).toLowerCase();
if (ext === ".json" && languages.isValid(locale)) {
if (!locales2paths[locale]) locales2paths[locale] = [];
locales2paths[locale].push(file);
}
});
};
// add core supported languages first
extractLangs(path.join(settings.root, "src/locales"));
// add plugins languages (if any)
for (const {
package: { path: pluginPath },
} of Object.values<I18nPluginDefs>(pluginDefs.plugins)) {
// plugin locales should overwrite etherpad's core locales
if (pluginPath.endsWith("/ep_etherpad-lite")) continue;
extractLangs(path.join(pluginPath, "locales"));
}
// Build a locale index (merge all locale data other than user-supplied overrides)
const locales: MapArrayType<any> = {};
_.each(locales2paths, (files: string[], langcode: string) => {
locales[langcode] = {};
files.forEach((file) => {
let fileContents;
try {
fileContents = JSON.parse(fs.readFileSync(file, "utf8"));
} catch (err) {
console.error(`failed to read JSON file ${file}: ${err}`);
throw err;
}
_.extend(locales[langcode], fileContents);
});
});
// Add custom strings from settings.json
// Since this is user-supplied, we'll do some extra sanity checks
const wrongFormatErr = Error(
"customLocaleStrings in wrong format. See documentation " +
"for Customization for Administrators, under Localization.",
);
if (settings.customLocaleStrings) {
if (typeof settings.customLocaleStrings !== "object") throw wrongFormatErr;
_.each(
settings.customLocaleStrings,
(overrides: MapArrayType<string>, langcode: string) => {
if (typeof overrides !== "object") throw wrongFormatErr;
_.each(overrides, (localeString: string | object, key: string) => {
if (typeof localeString !== "string") throw wrongFormatErr;
const locale = locales[langcode];
// Handles the error if an unknown language code is entered
if (locale === undefined) {
const possibleMatches = [];
let strippedLangcode = "";
if (langcode.includes("-")) {
strippedLangcode = langcode.split("-")[0];
}
for (const localeInEtherPad of Object.keys(locales)) {
if (localeInEtherPad.includes(strippedLangcode)) {
possibleMatches.push(localeInEtherPad);
}
}
throw new Error(
`Language code ${langcode} is unknown. ` +
`Maybe you meant: ${possibleMatches}`,
);
}
locales[langcode][key] = localeString;
});
},
);
}
return locales;
2021-01-21 21:06:52 +00:00
};
2012-12-17 18:18:07 +01:00
// returns a hash of all available languages availables with nativeName and direction
// e.g. { es: {nativeName: "español", direction: "ltr"}, ... }
2024-04-17 21:29:15 +02:00
const getAvailableLangs = (locales: MapArrayType<any>) => {
const result: MapArrayType<string> = {};
for (const langcode of Object.keys(locales)) {
result[langcode] = languages.getLanguageInfo(langcode);
}
return result;
2021-01-21 21:06:52 +00:00
};
2012-12-17 18:18:07 +01:00
// returns locale index that will be served in /locales.json
2024-04-17 21:29:15 +02:00
const generateLocaleIndex = (locales: MapArrayType<string>) => {
const result = _.clone(locales); // keep English strings
for (const langcode of Object.keys(locales)) {
if (langcode !== "en") result[langcode] = `locales/${langcode}.json`;
}
return JSON.stringify(result);
2020-11-23 13:24:19 -05:00
};
2012-12-04 12:12:58 +01:00
2024-04-17 21:29:15 +02:00
exports.expressPreSession = async (hookName: string, { app }: any) => {
// regenerate locales on server restart
const locales = getAllLocales();
const localeIndex = generateLocaleIndex(locales);
exports.availableLangs = getAvailableLangs(locales);
app.get("/locales/:locale", (req: any, res: any) => {
// works with /locale/en and /locale/en.json requests
const locale = req.params.locale.split(".")[0];
if (Object.prototype.hasOwnProperty.call(exports.availableLangs, locale)) {
res.setHeader("Cache-Control", `public, max-age=${settings.maxAge}`);
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.send(`{"${locale}":${JSON.stringify(locales[locale])}}`);
} else {
res.status(404).send("Language not available");
}
});
app.get("/locales.json", (req: any, res: any) => {
res.setHeader("Cache-Control", `public, max-age=${settings.maxAge}`);
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.send(localeIndex);
});
2020-11-23 13:24:19 -05:00
};