import * as gu from 'test/nbrowser/gristUtils'; import {server, setupTestSuite} from 'test/nbrowser/testUtils'; import {assert, driver} from 'mocha-webdriver'; import * as testUtils from 'test/server/testUtils'; import {getAppRoot} from 'app/server/lib/places'; import fetch from "node-fetch"; import fs from "fs"; import os from "os"; import path from 'path'; // We only support those formats for now: // en.client.json // en_US.client.json // en_US.server.json // zh_Hant.client.json // {lang code (+ maybe with underscore and country code}.{namespace}.json // // Only this format was tested and is known to work. const VALID_LOCALE_FORMAT = /^[a-z]{2,}(_\w+)?\.(\w+)\.json$/; describe("Localization", function() { this.timeout(60000); setupTestSuite(); before(async function() { const session = await gu.session().personalSite.anon.login(); await session.loadRelPath("/"); }); it("uses default options for English language", async function() { // Currently, there is not much translated, so test just what we have. assert.equal(await driver.findWait('.test-welcome-title', 3000).getText(), 'Welcome to Grist!'); // Grist config should contain the list of supported languages; const gristConfig: any = await driver.executeScript("return window.gristConfig"); // client and en is required. assert.isTrue(gristConfig.namespaces.includes("client")); assert.isTrue(gristConfig.supportedLngs.includes("en")); }); it("loads all files from resource folder", async function() { if (server.isExternalServer()) { this.skip(); } // Grist config should contain the list of supported languages; const gristConfig: any = await driver.executeScript("return window.gristConfig"); // Should report all supported languages and namespaces. const localeDirectory = path.join(getAppRoot(), 'static', 'locales'); // Read all file names from localeDirectory const langs: Set = new Set(); const namespaces: Set = new Set(); for (const file of fs.readdirSync(localeDirectory)) { // Make sure we see only valid files. assert.match(file, VALID_LOCALE_FORMAT); const langRaw = file.split('.')[0]; const lang = langRaw?.replace(/_/g, '-'); const ns = file.split('.')[1]; const clientFile = path.join(localeDirectory, `${langRaw}.client.json`); const clientText = fs.readFileSync(clientFile, { encoding: 'utf8' }); if (!clientText.includes('Translators: please translate this only when')) { // Translation not ready if this key is not present. continue; } langs.add(lang); namespaces.add(ns); } assert.deepEqual(gristConfig.supportedLngs.sort(), [...langs].sort()); assert.deepEqual(gristConfig.namespaces.sort(), [...namespaces].sort()); assert.isAbove(gristConfig.supportedLngs.length, 9); }); // Now make a uz-UZ language file, and test that it is used. describe("with uz-UZ language file", function() { let oldEnv: testUtils.EnvironmentSnapshot; let tempLocale: string; let existingLocales: string[]; before(async function() { if (server.isExternalServer()) { this.skip(); } const gristConfig: any = await driver.executeScript("return window.gristConfig"); existingLocales = gristConfig.supportedLngs; oldEnv = new testUtils.EnvironmentSnapshot(); // Add another language to the list of supported languages. tempLocale = makeCopy(); createLanguage(tempLocale, "uz"); process.env.GRIST_LOCALES_DIR = tempLocale; await server.restart(); }); after(async () => { oldEnv.restore(); await server.restart(); }); it("detects correct language from client headers", async function() { const homeUrl = `${server.getHost()}/o/docs`; // Read response from server, and check that it contains the correct language. const enResponse = await (await fetch(homeUrl)).text(); const uzResponse = await (await fetch(homeUrl, {headers: {"Accept-Language": "uz-UZ,uz;q=1"}})).text(); const ptResponse = await (await fetch(homeUrl, {headers: {"Accept-Language": "pt-PR,pt;q=1"}})).text(); // We have file with nb_NO code, but still this should be preloaded. const noResponse = await (await fetch(homeUrl, {headers: {"Accept-Language": "nb-NO,nb;q=1"}})).text(); function present(response: string, ...langs: string[]) { for (const lang of langs) { assert.include(response, `href="locales/${lang}.client.json"`); } } function notPresent(response: string, ...langs: string[]) { for (const lang of langs) { assert.notInclude(response, `href="locales/${lang}.client.json"`); } } // English locale is preloaded always. present(enResponse, "en"); present(uzResponse, "en"); present(ptResponse, "en"); present(noResponse, "en"); // Other locales are not preloaded for English. notPresent(enResponse, "uz", "un-UZ", "en-US"); // For uz-UZ we have additional uz locale. present(uzResponse, "uz"); // But only uz code is preloaded. notPresent(uzResponse, "uz-UZ"); notPresent(ptResponse, "pt-PR", "uz", "en-US"); // For no-NO we have nb_NO file. present(noResponse, "nb_NO"); }); it("loads correct languages from file system", async function() { modifyByCode(tempLocale, "en", {HomeIntro: {'Welcome to Grist!': 'TestMessage'}}); await driver.navigate().refresh(); assert.equal(await driver.findWait('.test-welcome-title', 3000).getText(), 'TestMessage'); const gristConfig: any = await driver.executeScript("return window.gristConfig"); assert.sameDeepMembers(gristConfig.supportedLngs, [...existingLocales, 'uz']); }); }); it("breaks the server if something is wrong with resource files", async function() { if (server.isExternalServer()) { this.skip(); } const oldEnv = new testUtils.EnvironmentSnapshot(); try { // Wrong path to locales. process.env.GRIST_LOCALES_DIR = __filename; await assert.isRejected(server.restart(false, true)); // Empty folder. const tempDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'grist_test_')); process.env.GRIST_LOCALES_DIR = tempDirectory; await assert.isRejected(server.restart(false, true)); // Wrong file format. fs.writeFileSync(path.join(tempDirectory, 'dummy.json'), 'invalid json'); await assert.isRejected(server.restart(false, true)); } finally { oldEnv.restore(); await server.restart(); } }); /** * Creates a new language by coping existing "en" resources. */ function createLanguage(localesPath: string, code: string) { for (const file of fs.readdirSync(localesPath)) { if (file.startsWith('en.')) { const newFile = file.replace('en', code); fs.copyFileSync(path.join(localesPath, file), path.join(localesPath, newFile)); } } } /** * Makes a copy of all resource files and returns path to the temporary directory. */ function makeCopy() { const tempDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'grist_test_')); const localeDirectory = path.join(getAppRoot(), 'static', 'locales'); // Copy all files from localeDirectory to tempDirectory. fs.readdirSync(localeDirectory).forEach(file => { fs.copyFileSync(path.join(localeDirectory, file), path.join(tempDirectory, file)); }); return tempDirectory; } function modifyByCode(localeDir: string, code: string, obj: any) { // Read current client localization file. const filePath = path.join(localeDir, `${code}.client.json`); const resources = JSON.parse(fs.readFileSync(filePath).toString()); const newResource = Object.assign(resources, obj); fs.writeFileSync(filePath, JSON.stringify(newResource)); } });