2
0
mirror of https://github.com/frappe/books.git synced 2024-09-19 19:19:02 +00:00
books/main/getLanguageMap.ts
2023-06-30 03:04:16 -07:00

196 lines
4.9 KiB
TypeScript

/**
* 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<LanguageMap> {
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<string> {
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<Date> {
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;
}
}