mirror of
https://github.com/frappe/books.git
synced 2024-12-22 10:58:59 +00:00
196 lines
4.9 KiB
TypeScript
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;
|
|
}
|
|
}
|