/** * Language files are packaged into the binary, if * newer files are available (if internet available) * then those will replace the current file. * * Language files are fetched from the frappe/books repo * the language files before storage have a ISO timestamp * prepended to the file. * * This timestamp denotes the commit datetime, update of the file * takes place only if a new update has been pushed. */ import { constants } from 'fs'; import fs from 'fs/promises'; import path from 'path'; import { parseCSV } from 'utils/csvParser'; import { LanguageMap } from 'utils/types'; import fetch from 'node-fetch'; const VALENTINES_DAY = 1644796800000; export async function getLanguageMap(code: string): Promise { const contents = await getContents(code); return getMapFromCsv(contents); } function getMapFromCsv(csv: string): LanguageMap { const matrix = parseCSV(csv); const languageMap: LanguageMap = {}; for (const row of matrix) { /** * Ignore lines that have no translations */ if (!row[0] || !row[1]) { continue; } const source = row[0]; const translation = row[1]; const context = row[3]; languageMap[source] = { translation }; if (context?.length) { languageMap[source].context = context; } } return languageMap; } async function getContents(code: string) { let contents = await getContentsIfExists(code); if (contents.length === 0) { contents = (await fetchAndStoreFile(code)) || contents; } else { contents = (await getUpdatedContent(code, contents)) || contents; } if (!contents || contents.length === 0) { throwCouldNotFetchFile(code); } return contents; } async function getContentsIfExists(code: string): Promise { const filePath = await getTranslationFilePath(code); if (!filePath) { return ''; } return await fs.readFile(filePath, { encoding: 'utf-8' }); } async function fetchAndStoreFile(code: string, date?: Date) { let contents = await fetchContentsFromApi(code); if (!contents) { contents = await fetchContentsFromRaw(code); } if (!date && contents) { date = await getLastUpdated(code); } if (contents) { contents = [date!.toISOString(), contents].join('\n'); await storeFile(code, contents); } return contents ?? ''; } async function fetchContentsFromApi(code: string) { const url = `https://api.github.com/repos/frappe/books/contents/translations/${code}.csv`; const res = await errorHandledFetch(url); if (res === null || res.status !== 200) { return null; } const resJson = (await res.json()) as { content: string }; return Buffer.from(resJson.content, 'base64').toString(); } async function fetchContentsFromRaw(code: string) { const url = `https://raw.githubusercontent.com/frappe/books/master/translations/${code}.csv`; const res = await errorHandledFetch(url); if (res === null || res.status !== 200) { return null; } return await res.text(); } async function getUpdatedContent(code: string, contents: string) { const { shouldUpdate, date } = await shouldUpdateFile(code, contents); if (!shouldUpdate) { return contents; } return await fetchAndStoreFile(code, date); } async function shouldUpdateFile(code: string, contents: string) { const date = await getLastUpdated(code); const oldDate = new Date(contents.split('\n')[0]); const shouldUpdate = date > oldDate || +oldDate === VALENTINES_DAY; return { shouldUpdate, date }; } async function getLastUpdated(code: string): Promise { const url = `https://api.github.com/repos/frappe/books/commits?path=translations%2F${code}.csv&page=1&per_page=1`; const res = await errorHandledFetch(url); if (res === null || res.status !== 200) { return new Date(VALENTINES_DAY); } const resJson = (await res.json()) as { commit: { author: { date: string } }; }[]; try { return new Date(resJson[0].commit.author.date); } catch { return new Date(VALENTINES_DAY); } } async function getTranslationFilePath(code: string) { let filePath = path.join( process.resourcesPath, `../translations/${code}.csv` ); try { await fs.access(filePath, constants.R_OK); } catch { /** * This will be used for in Development mode */ filePath = path.join(__dirname, `../translations/${code}.csv`); } try { await fs.access(filePath, constants.R_OK); } catch { return ''; } return filePath; } function throwCouldNotFetchFile(code: string) { throw new Error(`Could not fetch translations for '${code}'.`); } async function storeFile(code: string, contents: string) { const filePath = await getTranslationFilePath(code); if (!filePath) { return; } const dirname = path.dirname(filePath); await fs.mkdir(dirname, { recursive: true }); await fs.writeFile(filePath, contents, { encoding: 'utf-8' }); } async function errorHandledFetch(url: string) { try { return await fetch(url); } catch { return null; } }