"use strict"; import type { MapArrayType } from "../types/MapType"; import { I18nPluginDefs } from "../types/I18nPluginDefs"; 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"); // returns all existing messages merged together and grouped by langcode // {es: {"foo": "string"}, en:...} const getAllLocales = () => { const locales2paths: MapArrayType = {}; // 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(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 = {}; _.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, 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; }; // returns a hash of all available languages availables with nativeName and direction // e.g. { es: {nativeName: "espaƱol", direction: "ltr"}, ... } const getAvailableLangs = (locales: MapArrayType) => { const result: MapArrayType = {}; for (const langcode of Object.keys(locales)) { result[langcode] = languages.getLanguageInfo(langcode); } return result; }; // returns locale index that will be served in /locales.json const generateLocaleIndex = (locales: MapArrayType) => { 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); }; 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); }); };