i18n-gs
Version:
Support Google Sheets push and fetch into i18n json file.
262 lines • 14.6 kB
JavaScript
;
var __asyncValues = (this && this.__asyncValues) || function (o) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var m = o[Symbol.asyncIterator], i;
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
Object.defineProperty(exports, "__esModule", { value: true });
const google_spreadsheet_1 = require("google-spreadsheet");
const i18nGSConfig_1 = require("../types/i18nGSConfig");
const path = require("path");
const fs = require("fs-extra");
const log_1 = require("../utils/log");
const ora = require("ora");
const { unflatten, flatten } = require("flat");
class i18nGS {
constructor(config) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
this.config = config;
this.doc = new google_spreadsheet_1.GoogleSpreadsheet(this.config.spreadsheet.sheetId);
this.spinner = ora({
isSilent: ((_a = config === null || config === void 0 ? void 0 : config.logging) === null || _a === void 0 ? void 0 : _a.level) === i18nGSConfig_1.LogLevel.Silent,
});
if ((_d = (_c = (_b = this.config) === null || _b === void 0 ? void 0 : _b.i18n) === null || _c === void 0 ? void 0 : _c.namespaces) === null || _d === void 0 ? void 0 : _d.excludes)
log_1.default.debug(`Excluding namespaces:`, (_g = (_f = (_e = this.config) === null || _e === void 0 ? void 0 : _e.i18n) === null || _f === void 0 ? void 0 : _f.namespaces) === null || _g === void 0 ? void 0 : _g.excludes);
if ((_k = (_j = (_h = this.config) === null || _h === void 0 ? void 0 : _h.i18n) === null || _j === void 0 ? void 0 : _j.locales) === null || _k === void 0 ? void 0 : _k.excludes)
log_1.default.debug(`Excluding locales:`, (_o = (_m = (_l = this.config) === null || _l === void 0 ? void 0 : _l.i18n) === null || _m === void 0 ? void 0 : _m.locales) === null || _o === void 0 ? void 0 : _o.excludes);
}
failSpinner() {
if (this.spinner.isSpinning)
this.spinner.fail();
}
async connect() {
switch (this.config.spreadsheet.credential.type) {
case "serviceAccount":
await this.connectWithServiceAccount();
}
}
async connectWithServiceAccount() {
const pathname = path.resolve(this.config.spreadsheet.credential.path);
let credential = null;
try {
credential = require(pathname);
}
catch (_a) {
this.failSpinner();
(0, log_1.exit)(`Credential file is not defined at: '${pathname}'`);
}
await this.doc.useServiceAccountAuth(credential);
await this.doc.loadInfo();
log_1.default.debug("Service account credential verified");
}
async readSheet(namespace) {
var _a, _b, _c, _d, _e;
const sheet = this.doc.sheetsByTitle[namespace];
if (!sheet) {
log_1.default.warn(`Sheet '${namespace}' not found`);
return undefined;
}
this.spinner.start(`Loading sheet '${namespace}'`);
const rows = await sheet.getRows();
const locales = ((_e = (_d = (_c = (_b = (_a = this.config) === null || _a === void 0 ? void 0 : _a.i18n) === null || _b === void 0 ? void 0 : _b.locales) === null || _c === void 0 ? void 0 : _c.includes) !== null && _d !== void 0 ? _d : sheet.headerValues.slice(1)) !== null && _e !== void 0 ? _e : []).filter((locale) => { var _a, _b, _c, _d; return !((_d = (_c = (_b = (_a = this.config) === null || _a === void 0 ? void 0 : _a.i18n) === null || _b === void 0 ? void 0 : _b.locales) === null || _c === void 0 ? void 0 : _c.excludes) === null || _d === void 0 ? void 0 : _d.includes(locale)); });
if (locales.length === 0) {
this.spinner.fail();
log_1.default.warn(`No locale available in ${namespace}`);
return undefined;
}
let namespaceData = {};
rows.forEach((row) => {
locales.forEach((langKey) => {
var _a;
namespaceData[langKey] = namespaceData[langKey] || {};
namespaceData[langKey][row.key] = (_a = row[langKey]) !== null && _a !== void 0 ? _a : "";
});
});
if (this.config.logging.level === i18nGSConfig_1.LogLevel.Debug)
this.spinner.succeed(`Loaded sheet '${namespace}' with locale '${locales}'`);
else
this.spinner.stop();
return namespaceData;
}
async readSheets() {
var _a, _b, _c, _d, _e;
const namespaces = ((_e = (_d = (_c = (_b = (_a = this.config) === null || _a === void 0 ? void 0 : _a.i18n) === null || _b === void 0 ? void 0 : _b.namespaces) === null || _c === void 0 ? void 0 : _c.includes) !== null && _d !== void 0 ? _d : Object.keys(this.doc.sheetsByTitle)) !== null && _e !== void 0 ? _e : []).filter((namespace) => { var _a, _b, _c, _d; return !((_d = (_c = (_b = (_a = this.config) === null || _a === void 0 ? void 0 : _a.i18n) === null || _b === void 0 ? void 0 : _b.namespaces) === null || _c === void 0 ? void 0 : _c.excludes) === null || _d === void 0 ? void 0 : _d.includes(namespace)); });
log_1.default.debug("Selected namespaces:", namespaces);
if (namespaces.length === 0)
(0, log_1.exit)("There is no selected namespace!");
const sheetsData = {};
for (const namespace of namespaces) {
const sheet = await this.readSheet(namespace);
if (sheet)
sheetsData[namespace] = sheet;
}
return sheetsData;
}
writeFile(namespaceData, namespace) {
Object.keys(namespaceData).forEach((locale) => {
var _a, _b;
const keyStyle = (_b = (_a = this.config) === null || _a === void 0 ? void 0 : _a.i18n) === null || _b === void 0 ? void 0 : _b.keyStyle;
let i18n = undefined;
switch (keyStyle) {
case "flat":
i18n = namespaceData[locale];
break;
case "nested":
default:
i18n = unflatten(namespaceData[locale], { object: true });
break;
}
const path = this.config.i18n.path;
// if no folder, make directory
if (!fs.existsSync(`${path}/${locale}`))
fs.mkdirSync(`${path}/${locale}`, { recursive: true });
// update namespace file, overwrite all data
fs.writeJSONSync(`${path}/${locale}/${namespace}.json`, i18n, {
spaces: 2,
});
log_1.default.debug(`Updated ${path}/${locale}/${namespace}.json`);
});
}
writeFiles(data) {
Object.entries(data).forEach(([namespace, namespaceData]) => this.writeFile(namespaceData, namespace));
}
readFile(path) {
try {
const data = fs.readJsonSync(path);
return flatten(data);
}
catch (err) {
this.failSpinner();
(0, log_1.exit)(`File ${path} does not exists!`);
}
}
async readFiles() {
const { i18n: { path, namespaces: { includes: _namespacesIncludes, excludes: _namespacesExcludes, }, locales: { includes: _localesIncludes, excludes: _localesExcludes }, }, } = this.config;
if (!fs.existsSync(path))
(0, log_1.exit)(`Path '${path}' does not exist`);
const sheetsData = {};
const locales = (_localesIncludes !== null && _localesIncludes !== void 0 ? _localesIncludes : (await fs.readdirSync(path).filter((file) => !file.startsWith(".")))).filter((locale) => !(_localesExcludes === null || _localesExcludes === void 0 ? void 0 : _localesExcludes.includes(locale)));
if (locales.length === 0)
(0, log_1.exit)("No locale available!");
locales.forEach((locale) => {
const extensionRegExp = /\.json+/g;
const files = fs.readdirSync(`${path}/${locale}`).filter((file) => {
const isHiddenFile = file.startsWith(".");
const isValidExtension = file.match(extensionRegExp);
if (isHiddenFile)
log_1.default.warn(`File '${file}' is hidden, it will be ignored`);
if (!isValidExtension)
log_1.default.warn(`File '${file}' will be ignored. Only extension '.json' is allowed`);
return !isHiddenFile && isValidExtension;
});
const namespaces = (_namespacesIncludes !== null && _namespacesIncludes !== void 0 ? _namespacesIncludes : files.map((filename) => filename.replace(extensionRegExp, ""))).filter((namespace) => !(_namespacesExcludes === null || _namespacesExcludes === void 0 ? void 0 : _namespacesExcludes.includes(namespace)));
log_1.default.debug(`Selected namespaces in '${locale}':`, namespaces);
if (namespaces.length === 0)
(0, log_1.exit)(`There is no available namespace in '${locale}'`);
namespaces.forEach((namespace) => {
log_1.default.debug(`Loading namespace '${namespace}' in '${locale}'`);
const data = this.readFile(`${path}/${locale}/${namespace}.json`);
if (!data)
return;
sheetsData[namespace] = sheetsData[namespace] || {};
Object.entries(data).forEach(([key, value]) => {
sheetsData[namespace][locale] = sheetsData[namespace][locale] || {};
sheetsData[namespace][locale][key] = value;
});
});
});
return sheetsData;
}
async upsertSheets(i18n) {
var e_1, _a;
var _b;
function getKeyOrientedNamespaceData(data) {
return Object.entries(data).reduce((acc, [locale, record]) => {
Object.entries(record).forEach(([key, value]) => {
acc[key] = acc[key] || {};
acc[key][locale] = value;
});
return acc;
}, {});
}
async function updateExistKey(sheet, data) {
var _a;
const rows = await sheet.getRows();
const clone = JSON.parse(JSON.stringify(data));
let updatedCount = 0;
for (const row of rows) {
if (!(clone === null || clone === void 0 ? void 0 : clone[row.key]))
continue;
for (const locale in clone[row.key]) {
const columnIndex = sheet.headerValues.findIndex((header) => header === locale);
if (columnIndex === -1)
continue;
const cell = sheet.getCell(row.rowIndex - 1, columnIndex);
if (cell.value !== clone[row.key][locale]) {
if (cell.value === null && !clone[row.key][locale])
continue;
log_1.default.debug(`Updating ${sheet.title}/${row.key}/${locale}`);
cell.value = (_a = clone[row.key][locale]) !== null && _a !== void 0 ? _a : "";
updatedCount++;
}
}
delete clone[row.key];
}
if (updatedCount > 0)
await sheet.saveUpdatedCells();
return { remainingData: clone, updatedCount };
}
async function appendNonExistKey(sheet, data) {
const appendRows = Object.entries(data).map(([key, value]) => (Object.assign({ key }, value)));
if (appendRows.length > 0)
await sheet.addRows(appendRows).then(() => {
if (log_1.default.getLevel() <= log_1.default.levels.DEBUG)
appendRows.forEach((col) => log_1.default.debug(`Appended row '${col.key}' to '${sheet.title}'`));
});
return { appendedCount: appendRows.length };
}
try {
for (var _c = __asyncValues(Object.entries(i18n)), _d; _d = await _c.next(), !_d.done;) {
const [namespace, data] = _d.value;
const locales = Object.keys(data);
const defaultHeaderRow = ["key", ...locales];
let sheet = (_b = this.doc.sheetsByTitle) === null || _b === void 0 ? void 0 : _b[namespace];
if (!sheet) {
sheet = await this.doc.addSheet({
title: namespace,
headerValues: defaultHeaderRow,
});
log_1.default.debug(`Created sheet '${namespace}'`);
}
this.spinner.start(`Uploading sheet '${namespace}'`);
await sheet.loadHeaderRow().catch(async () => {
// if no header row, assume sheet is empty and insert default header
await sheet.setHeaderRow(defaultHeaderRow);
});
if (!sheet.headerValues[0]) {
(0, log_1.exit)(`Header is invalid! Please set cell A1 to 'key'`);
}
const headerNotFound = defaultHeaderRow.filter((col) => !sheet.headerValues.includes(col));
if (headerNotFound.length > 0) {
log_1.default.warn(`Header '${headerNotFound.join(",")}' not found! These locales will be skipped`);
}
await sheet.loadCells();
const keyData = getKeyOrientedNamespaceData(data);
const { remainingData, updatedCount } = await updateExistKey(sheet, keyData);
const { appendedCount } = await appendNonExistKey(sheet, remainingData);
this.spinner.succeed(`Uploaded sheet '${namespace}': updated ${updatedCount} cells, appended ${appendedCount} rows`);
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (_d && !_d.done && (_a = _c.return)) await _a.call(_c);
}
finally { if (e_1) throw e_1.error; }
}
}
}
exports.default = i18nGS;
//# sourceMappingURL=I18nGS.js.map