@helloao/cli 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -125
- package/actions.d.ts +6 -1
- package/actions.js +129 -39
- package/cli.js +56 -8
- package/db.js +133 -133
- package/files.d.ts +2 -2
- package/files.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,125 +1,132 @@
|
|
|
1
|
-
## Hello AO CLI
|
|
2
|
-
|
|
3
|
-
A Command Line Interface (CLI) that makes it easy to generate and manage your own [Free Use Bible API](https://bible.helloao.org/).
|
|
4
|
-
|
|
5
|
-
Additionally, it includes many functions and utilities that can make working with commonly formatted Bible data much easier.
|
|
6
|
-
|
|
7
|
-
### Features
|
|
8
|
-
|
|
9
|
-
- Supports [USFM](https://ubsicap.github.io/usfm/), [USX](https://ubsicap.github.io/usx/), and Codex (A JSON format).
|
|
10
|
-
- Download over 1000 Bible translations from [fetch.bible](https://fetch.bible/).
|
|
11
|
-
- Import Bible translations into a SQLite database.
|
|
12
|
-
- Upload to S3, a zip file, or a local directory.
|
|
13
|
-
|
|
14
|
-
### Usage
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
Usage: helloao [options] [command]
|
|
18
|
-
|
|
19
|
-
A CLI for managing a Free Use Bible API.
|
|
20
|
-
|
|
21
|
-
Options:
|
|
22
|
-
-V, --version output the version number
|
|
23
|
-
-h, --help display help for command
|
|
24
|
-
|
|
25
|
-
Commands:
|
|
26
|
-
init [options] [path] Initialize a new Bible API DB.
|
|
27
|
-
|
|
28
|
-
import-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
//
|
|
125
|
-
|
|
1
|
+
## Hello AO CLI
|
|
2
|
+
|
|
3
|
+
A Command Line Interface (CLI) that makes it easy to generate and manage your own [Free Use Bible API](https://bible.helloao.org/).
|
|
4
|
+
|
|
5
|
+
Additionally, it includes many functions and utilities that can make working with commonly formatted Bible data much easier.
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- Supports [USFM](https://ubsicap.github.io/usfm/), [USX](https://ubsicap.github.io/usx/), and Codex (A JSON format).
|
|
10
|
+
- Download over 1000 Bible translations from [fetch.bible](https://fetch.bible/).
|
|
11
|
+
- Import Bible translations into a SQLite database.
|
|
12
|
+
- Upload to S3, a zip file, or a local directory.
|
|
13
|
+
|
|
14
|
+
### Usage
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
Usage: helloao [options] [command]
|
|
18
|
+
|
|
19
|
+
A CLI for managing a Free Use Bible API.
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
-V, --version output the version number
|
|
23
|
+
-h, --help display help for command
|
|
24
|
+
|
|
25
|
+
Commands:
|
|
26
|
+
init [options] [path] Initialize a new Bible API DB.
|
|
27
|
+
generate-translation-metadata Generates a metadata file for a translation.
|
|
28
|
+
import-translation [options] <dir> [dirs...] Imports a translation from the given directory into the database.
|
|
29
|
+
import-translations [options] <dir> Imports all translations from the given directory into the database.
|
|
30
|
+
upload-test-translation [options] <input> Uploads a translation to the HelloAO Free Bible API test S3 bucket.
|
|
31
|
+
Requires access to the HelloAO Free Bible API test S3 bucket.
|
|
32
|
+
For inquiries, please contact hello@helloao.org.
|
|
33
|
+
upload-test-translations [options] <input> Uploads all the translations in the given input directory to the HelloAO Free Bible API test S3 bucket.
|
|
34
|
+
Requires access to the HelloAO Free Bible API test S3 bucket.
|
|
35
|
+
For inquiries, please contact hello@helloao.org.
|
|
36
|
+
generate-translation-files [options] <input> <dir> Generates API files from the given input translation.
|
|
37
|
+
generate-translations-files [options] <input> <dir> Generates API files from the given input translations.
|
|
38
|
+
upload-api-files [options] <dest> Uploads API files to the specified destination. For S3, use the format s3://bucket-name/path/to/folder.
|
|
39
|
+
fetch-translations [options] <dir> [translations...] Fetches the specified translations from fetch.bible and places them in the given directory.
|
|
40
|
+
fetch-audio [options] <dir> [translations...] Fetches the specified audio translations and places them in the given directory.
|
|
41
|
+
Translations should be in the format "translationId/audioId". e.g. "BSB/gilbert"
|
|
42
|
+
fetch-bible-metadata <dir> Fetches the Theographic bible metadata and places it in the given directory.
|
|
43
|
+
help [command] display help for command
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The `@helloao/cli` package can also be used as a library.
|
|
47
|
+
|
|
48
|
+
The library exports a variety of actions, utilities, and supporting classes designed to assist with generating and managing a Free Use Bible API.
|
|
49
|
+
|
|
50
|
+
There are 6 main exports:
|
|
51
|
+
|
|
52
|
+
- `actions` - This export contains function versions of the CLI commands. They make it easy to call a CLI command from a script.
|
|
53
|
+
- `db` - This export contains functions that make working with a database easier. It supports operations like importing translations into a database, inserting chapters, verses, etc. and getting an updated database instance from a path.
|
|
54
|
+
- `downloads` - This export contains functions that make downloading files easier.
|
|
55
|
+
- `files` - This export contains functions that make working with files easier. It has functions to load files from a translation, discover translation metadata from the filesystem, and classes that support uploading API files to the local file system or to a zip archive.
|
|
56
|
+
- `uploads` - This export contains functions that make it easy to upload an API to a destination like S3, the local filesystem, or a zip archive.
|
|
57
|
+
- `s3` - This export contains a class that can upload files to S3.
|
|
58
|
+
|
|
59
|
+
Here are some common operations that you might want to perform:
|
|
60
|
+
|
|
61
|
+
#### Get a SQL Database
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { db } from '@helloao/cli';
|
|
65
|
+
|
|
66
|
+
const pathToDb = './bible-database.db';
|
|
67
|
+
const database = await db.getDb(pathToDb);
|
|
68
|
+
|
|
69
|
+
// do work on the database
|
|
70
|
+
|
|
71
|
+
// Close it when you are done.
|
|
72
|
+
database.close();
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### Import a translation into a database from a directory
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { db } from '@helloao/cli';
|
|
79
|
+
|
|
80
|
+
const pathToDb = './bible-database.db';
|
|
81
|
+
const database = await db.getDb(pathToDb);
|
|
82
|
+
|
|
83
|
+
// Get a DOMParser for parsing USX.
|
|
84
|
+
// On Node.js, you may have to import jsdom or linkedom.
|
|
85
|
+
const parser = new DOMParser();
|
|
86
|
+
|
|
87
|
+
const pathToTranslation = './path/to/translation';
|
|
88
|
+
|
|
89
|
+
// Whether to overwrite files that already exist in the database.
|
|
90
|
+
// The system will automatically determine the hashes of the input files and overwrite changed files if needed, so this is only needed
|
|
91
|
+
// when you know that they need to be overwritten.
|
|
92
|
+
const overwrite = false;
|
|
93
|
+
await db.importTranslations(database, pathToTranslation, parser, overwrite);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### Generate an API from a translation
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { files, uploads } from '@helloao/cli';
|
|
100
|
+
import { generation } from '@helloao/tools';
|
|
101
|
+
import { toAsyncIterable } from '@helloao/tools/parser/iterators';
|
|
102
|
+
|
|
103
|
+
const translationPath = './path/to/translation';
|
|
104
|
+
const translationFiles = await files.loadTranslationFiles(translationPath);
|
|
105
|
+
|
|
106
|
+
// Used to parse XML
|
|
107
|
+
const domParser = new DOMParser();
|
|
108
|
+
|
|
109
|
+
// Generate a dataset from the files
|
|
110
|
+
// Datasets organize all the files and their content
|
|
111
|
+
// by translation, book, chapter, and verse
|
|
112
|
+
const dataset = generation.dataset.generateDataset(files, parser);
|
|
113
|
+
|
|
114
|
+
// You can optionally specifiy a prefix that should be added to all API
|
|
115
|
+
// links
|
|
116
|
+
const pathPrefix = '';
|
|
117
|
+
|
|
118
|
+
// Generate an API representation from the files
|
|
119
|
+
// This adds links between chapters and additional metadata.
|
|
120
|
+
const api = generation.api.generateApiForDataset(dataset, {
|
|
121
|
+
pathPrefix,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Generate output files from the API representation.
|
|
125
|
+
// This will give us a list of files and file paths that represent
|
|
126
|
+
// the entire API.
|
|
127
|
+
const outputFiles = generation.api.generateFilesForApi(api);
|
|
128
|
+
|
|
129
|
+
// Optionally upload files by using:
|
|
130
|
+
// const dest = 's3://my-bucket';
|
|
131
|
+
// await uploads.serializeAndUploadDatasets(dest, toAsyncIterable(outputFiles));
|
|
132
|
+
```
|
package/actions.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { InputTranslationMetadata } from '@helloao/tools/generation';
|
|
1
2
|
import { UploadApiFromDatabaseOptions, UploadApiOptions } from './uploads';
|
|
2
3
|
export interface InitDbOptions {
|
|
3
4
|
/**
|
|
@@ -117,5 +118,9 @@ export declare function uploadTestTranslations(input: string, options: UploadTes
|
|
|
117
118
|
* @param input The input directory that the translations are stored in.
|
|
118
119
|
* @param options The options to use for the upload.
|
|
119
120
|
*/
|
|
120
|
-
export declare function uploadTestTranslation(input: string, options: UploadTestTranslationOptions): Promise<UploadTestTranslationResult>;
|
|
121
|
+
export declare function uploadTestTranslation(input: string, options: UploadTestTranslationOptions): Promise<UploadTestTranslationResult | undefined>;
|
|
122
|
+
/**
|
|
123
|
+
* Asks the user for the metadata for the translation.
|
|
124
|
+
*/
|
|
125
|
+
export declare function askForMetadata(defaultId?: string): Promise<InputTranslationMetadata>;
|
|
121
126
|
//# sourceMappingURL=actions.d.ts.map
|
package/actions.js
CHANGED
|
@@ -35,6 +35,7 @@ exports.generateTranslationsFiles = generateTranslationsFiles;
|
|
|
35
35
|
exports.generateTranslationFiles = generateTranslationFiles;
|
|
36
36
|
exports.uploadTestTranslations = uploadTestTranslations;
|
|
37
37
|
exports.uploadTestTranslation = uploadTestTranslation;
|
|
38
|
+
exports.askForMetadata = askForMetadata;
|
|
38
39
|
const node_path_1 = __importStar(require("node:path"));
|
|
39
40
|
const database = __importStar(require("./db"));
|
|
40
41
|
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
@@ -51,6 +52,8 @@ const files_1 = require("./files");
|
|
|
51
52
|
const dataset_1 = require("@helloao/tools/generation/dataset");
|
|
52
53
|
const uploads_1 = require("./uploads");
|
|
53
54
|
const s3_1 = require("./s3");
|
|
55
|
+
const prompts_1 = require("@inquirer/prompts");
|
|
56
|
+
const all_iso_language_codes_1 = require("all-iso-language-codes");
|
|
54
57
|
/**
|
|
55
58
|
* Initializes a new Bible API DB.
|
|
56
59
|
* @param dbPath The path to the database. If null or empty, then the "bible-api.db" will be used from the current working directory.
|
|
@@ -68,46 +71,46 @@ async function initDb(dbPath, options) {
|
|
|
68
71
|
const languages = `(${options.language
|
|
69
72
|
.map((l) => `'${l}'`)
|
|
70
73
|
.join(', ')})`;
|
|
71
|
-
db.exec(`
|
|
72
|
-
ATTACH DATABASE "${sourcePath}" AS source;
|
|
73
|
-
|
|
74
|
-
CREATE TABLE "_prisma_migrations" AS SELECT * FROM source._prisma_migrations;
|
|
75
|
-
|
|
76
|
-
CREATE TABLE "Translation" AS SELECT * FROM source.Translation
|
|
77
|
-
WHERE language IN ${languages};
|
|
78
|
-
|
|
79
|
-
CREATE TABLE "Book" AS SELECT * FROM source.Book
|
|
80
|
-
INNER JOIN source.Translation ON source.Translation.id = source.Book.translationId
|
|
81
|
-
WHERE source.Translation.language IN ${languages};
|
|
82
|
-
|
|
83
|
-
CREATE TABLE "Chapter" AS SELECT * FROM source.Chapter
|
|
84
|
-
INNER JOIN source.Translation ON source.Translation.id = source.Chapter.translationId
|
|
85
|
-
WHERE source.Translation.language IN ${languages};
|
|
86
|
-
|
|
87
|
-
CREATE TABLE "ChapterVerse" AS SELECT * FROM source.ChapterVerse
|
|
88
|
-
INNER JOIN source.Translation ON source.Translation.id = source.ChapterVerse.translationId
|
|
89
|
-
WHERE source.Translation.language IN ${languages};
|
|
90
|
-
|
|
91
|
-
CREATE TABLE "ChapterFootnote" AS SELECT * FROM source.ChapterFootnote
|
|
92
|
-
INNER JOIN source.Translation ON source.Translation.id = source.ChapterFootnote.translationId
|
|
93
|
-
WHERE source.Translation.language IN ${languages};
|
|
94
|
-
|
|
95
|
-
CREATE TABLE "ChapterAudioUrl" AS SELECT * FROM source.ChapterAudioUrl
|
|
96
|
-
INNER JOIN source.Translation ON source.Translation.id = source.ChapterAudioUrl.translationId
|
|
97
|
-
WHERE source.Translation.language IN ${languages};
|
|
74
|
+
db.exec(`
|
|
75
|
+
ATTACH DATABASE "${sourcePath}" AS source;
|
|
76
|
+
|
|
77
|
+
CREATE TABLE "_prisma_migrations" AS SELECT * FROM source._prisma_migrations;
|
|
78
|
+
|
|
79
|
+
CREATE TABLE "Translation" AS SELECT * FROM source.Translation
|
|
80
|
+
WHERE language IN ${languages};
|
|
81
|
+
|
|
82
|
+
CREATE TABLE "Book" AS SELECT * FROM source.Book
|
|
83
|
+
INNER JOIN source.Translation ON source.Translation.id = source.Book.translationId
|
|
84
|
+
WHERE source.Translation.language IN ${languages};
|
|
85
|
+
|
|
86
|
+
CREATE TABLE "Chapter" AS SELECT * FROM source.Chapter
|
|
87
|
+
INNER JOIN source.Translation ON source.Translation.id = source.Chapter.translationId
|
|
88
|
+
WHERE source.Translation.language IN ${languages};
|
|
89
|
+
|
|
90
|
+
CREATE TABLE "ChapterVerse" AS SELECT * FROM source.ChapterVerse
|
|
91
|
+
INNER JOIN source.Translation ON source.Translation.id = source.ChapterVerse.translationId
|
|
92
|
+
WHERE source.Translation.language IN ${languages};
|
|
93
|
+
|
|
94
|
+
CREATE TABLE "ChapterFootnote" AS SELECT * FROM source.ChapterFootnote
|
|
95
|
+
INNER JOIN source.Translation ON source.Translation.id = source.ChapterFootnote.translationId
|
|
96
|
+
WHERE source.Translation.language IN ${languages};
|
|
97
|
+
|
|
98
|
+
CREATE TABLE "ChapterAudioUrl" AS SELECT * FROM source.ChapterAudioUrl
|
|
99
|
+
INNER JOIN source.Translation ON source.Translation.id = source.ChapterAudioUrl.translationId
|
|
100
|
+
WHERE source.Translation.language IN ${languages};
|
|
98
101
|
`);
|
|
99
102
|
}
|
|
100
103
|
else {
|
|
101
|
-
db.exec(`
|
|
102
|
-
ATTACH DATABASE "${sourcePath}" AS source;
|
|
103
|
-
|
|
104
|
-
CREATE TABLE "_prisma_migrations" AS SELECT * FROM source._prisma_migrations;
|
|
105
|
-
CREATE TABLE "Translation" AS SELECT * FROM source.Translation;
|
|
106
|
-
CREATE TABLE "Book" AS SELECT * FROM source.Book;
|
|
107
|
-
CREATE TABLE "Chapter" AS SELECT * FROM source.Chapter;
|
|
108
|
-
CREATE TABLE "ChapterVerse" AS SELECT * FROM source.ChapterVerse;
|
|
109
|
-
CREATE TABLE "ChapterFootnote" AS SELECT * FROM source.ChapterFootnote;
|
|
110
|
-
CREATE TABLE "ChapterAudioUrl" AS SELECT * FROM source.ChapterAudioUrl;
|
|
104
|
+
db.exec(`
|
|
105
|
+
ATTACH DATABASE "${sourcePath}" AS source;
|
|
106
|
+
|
|
107
|
+
CREATE TABLE "_prisma_migrations" AS SELECT * FROM source._prisma_migrations;
|
|
108
|
+
CREATE TABLE "Translation" AS SELECT * FROM source.Translation;
|
|
109
|
+
CREATE TABLE "Book" AS SELECT * FROM source.Book;
|
|
110
|
+
CREATE TABLE "Chapter" AS SELECT * FROM source.Chapter;
|
|
111
|
+
CREATE TABLE "ChapterVerse" AS SELECT * FROM source.ChapterVerse;
|
|
112
|
+
CREATE TABLE "ChapterFootnote" AS SELECT * FROM source.ChapterFootnote;
|
|
113
|
+
CREATE TABLE "ChapterAudioUrl" AS SELECT * FROM source.ChapterAudioUrl;
|
|
111
114
|
`);
|
|
112
115
|
}
|
|
113
116
|
console.log('Done.');
|
|
@@ -306,7 +309,11 @@ async function generateTranslationFiles(input, dest, options) {
|
|
|
306
309
|
globalThis.DOMParser = linkedom_1.DOMParser;
|
|
307
310
|
globalThis.Element = linkedom_1.Element;
|
|
308
311
|
globalThis.Node = linkedom_1.Node;
|
|
309
|
-
const files = await (
|
|
312
|
+
const files = await loadTranslationFilesOrAskForMetadata(node_path_1.default.resolve(input));
|
|
313
|
+
if (!files) {
|
|
314
|
+
console.log('No translation files found.');
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
310
317
|
const dataset = (0, dataset_1.generateDataset)(files, parser);
|
|
311
318
|
await (0, uploads_1.serializeAndUploadDatasets)(dest, (0, iterators_1.toAsyncIterable)([dataset]), options);
|
|
312
319
|
}
|
|
@@ -354,7 +361,12 @@ async function uploadTestTranslation(input, options) {
|
|
|
354
361
|
globalThis.DOMParser = linkedom_1.DOMParser;
|
|
355
362
|
globalThis.Element = linkedom_1.Element;
|
|
356
363
|
globalThis.Node = linkedom_1.Node;
|
|
357
|
-
const
|
|
364
|
+
const inputPath = node_path_1.default.resolve(input);
|
|
365
|
+
const files = await loadTranslationFilesOrAskForMetadata(inputPath);
|
|
366
|
+
if (!files || files.length <= 0) {
|
|
367
|
+
console.log('No translation files found.');
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
358
370
|
const hash = (0, files_1.hashInputFiles)(files);
|
|
359
371
|
const dataset = (0, dataset_1.generateDataset)(files, parser);
|
|
360
372
|
const url = options.s3Url || 's3://ao-bible-api-public-uploads';
|
|
@@ -376,3 +388,81 @@ function getUrls(dest) {
|
|
|
376
388
|
url: url,
|
|
377
389
|
};
|
|
378
390
|
}
|
|
391
|
+
async function loadTranslationFilesOrAskForMetadata(dir) {
|
|
392
|
+
let files = await (0, files_1.loadTranslationFiles)(dir);
|
|
393
|
+
if (!files) {
|
|
394
|
+
console.log(`No metadata found for the translation in ${dir}`);
|
|
395
|
+
const enterMetadata = await (0, prompts_1.confirm)({
|
|
396
|
+
message: 'Do you want to enter the metadata for the translation?',
|
|
397
|
+
});
|
|
398
|
+
if (!enterMetadata) {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
const defaultId = (0, node_path_1.basename)(dir);
|
|
402
|
+
const metadata = await askForMetadata(defaultId);
|
|
403
|
+
const saveMetadata = await (0, prompts_1.confirm)({
|
|
404
|
+
message: 'Do you want to save this metadata?',
|
|
405
|
+
});
|
|
406
|
+
if (saveMetadata) {
|
|
407
|
+
await (0, promises_1.writeFile)(node_path_1.default.resolve(dir, 'metadata.json'), JSON.stringify(metadata, null, 2));
|
|
408
|
+
}
|
|
409
|
+
files = await (0, files_1.loadTranslationFiles)(dir);
|
|
410
|
+
}
|
|
411
|
+
return files;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Asks the user for the metadata for the translation.
|
|
415
|
+
*/
|
|
416
|
+
async function askForMetadata(defaultId) {
|
|
417
|
+
const id = await (0, prompts_1.input)({
|
|
418
|
+
message: 'Enter the translation ID',
|
|
419
|
+
default: defaultId,
|
|
420
|
+
});
|
|
421
|
+
const language = await (0, prompts_1.input)({
|
|
422
|
+
message: 'Enter the ISO 639 translation language',
|
|
423
|
+
validate: (input) => {
|
|
424
|
+
return (0, all_iso_language_codes_1.isValid)(input) ? true : 'Invalid language code.';
|
|
425
|
+
},
|
|
426
|
+
required: true,
|
|
427
|
+
});
|
|
428
|
+
const direction = await (0, prompts_1.select)({
|
|
429
|
+
message: 'Enter the text direction of the language',
|
|
430
|
+
choices: [
|
|
431
|
+
{ name: 'Left-to-right', value: 'ltr' },
|
|
432
|
+
{ name: 'Right-to-left', value: 'rtl' },
|
|
433
|
+
],
|
|
434
|
+
default: 'ltr',
|
|
435
|
+
});
|
|
436
|
+
const shortName = await (0, prompts_1.input)({
|
|
437
|
+
message: 'Enter the short name of the translation',
|
|
438
|
+
default: id,
|
|
439
|
+
required: false,
|
|
440
|
+
});
|
|
441
|
+
const name = await (0, prompts_1.input)({
|
|
442
|
+
message: 'Enter the name of the translation',
|
|
443
|
+
required: true,
|
|
444
|
+
});
|
|
445
|
+
const englishName = await (0, prompts_1.input)({
|
|
446
|
+
message: 'Enter the English name of the translation',
|
|
447
|
+
default: name,
|
|
448
|
+
});
|
|
449
|
+
const licenseUrl = await (0, prompts_1.input)({
|
|
450
|
+
message: 'Enter the license URL for the translation',
|
|
451
|
+
required: true,
|
|
452
|
+
});
|
|
453
|
+
const website = await (0, prompts_1.input)({
|
|
454
|
+
message: 'Enter the website URL for the translation',
|
|
455
|
+
required: true,
|
|
456
|
+
default: licenseUrl,
|
|
457
|
+
});
|
|
458
|
+
return {
|
|
459
|
+
id,
|
|
460
|
+
language,
|
|
461
|
+
direction: direction,
|
|
462
|
+
shortName,
|
|
463
|
+
name,
|
|
464
|
+
englishName,
|
|
465
|
+
licenseUrl,
|
|
466
|
+
website,
|
|
467
|
+
};
|
|
468
|
+
}
|
package/cli.js
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
var
|
|
4
|
-
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
20
|
+
if (mod && mod.__esModule) return mod;
|
|
21
|
+
var result = {};
|
|
22
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
23
|
+
__setModuleDefault(result, mod);
|
|
24
|
+
return result;
|
|
5
25
|
};
|
|
6
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
27
|
const commander_1 = require("commander");
|
|
8
|
-
const path_1 =
|
|
28
|
+
const path_1 = __importStar(require("path"));
|
|
9
29
|
const promises_1 = require("fs/promises");
|
|
10
30
|
const linkedom_1 = require("linkedom");
|
|
11
31
|
const downloads_1 = require("./downloads");
|
|
@@ -31,6 +51,32 @@ async function start() {
|
|
|
31
51
|
.action(async (dbPath, options) => {
|
|
32
52
|
await (0, actions_1.initDb)(dbPath, options);
|
|
33
53
|
});
|
|
54
|
+
program
|
|
55
|
+
.command('generate-translation-metadata')
|
|
56
|
+
.description('Generates a metadata file for a translation.')
|
|
57
|
+
.action(async () => {
|
|
58
|
+
const meta = await (0, actions_1.askForMetadata)();
|
|
59
|
+
console.log('Your metadata:', meta);
|
|
60
|
+
const save = await (0, prompts_1.confirm)({
|
|
61
|
+
message: 'Do you want to save this metadata?',
|
|
62
|
+
});
|
|
63
|
+
if (save) {
|
|
64
|
+
let location = await (0, prompts_1.input)({
|
|
65
|
+
message: 'Where would you like to save the metadata?',
|
|
66
|
+
});
|
|
67
|
+
const ext = (0, path_1.extname)(location);
|
|
68
|
+
if (!ext) {
|
|
69
|
+
if (!location.endsWith('/')) {
|
|
70
|
+
location += '/';
|
|
71
|
+
}
|
|
72
|
+
location += 'metadata.json';
|
|
73
|
+
}
|
|
74
|
+
console.log('Saving metadata to:', location);
|
|
75
|
+
const dir = path_1.default.dirname(location);
|
|
76
|
+
await (0, promises_1.mkdir)(dir, { recursive: true });
|
|
77
|
+
await (0, promises_1.writeFile)(location, JSON.stringify(meta, null, 2));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
34
80
|
program
|
|
35
81
|
.command('import-translation <dir> [dirs...]')
|
|
36
82
|
.description('Imports a translation from the given directory into the database.')
|
|
@@ -69,11 +115,13 @@ async function start() {
|
|
|
69
115
|
return;
|
|
70
116
|
}
|
|
71
117
|
const result = await (0, actions_1.uploadTestTranslation)(input, options);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
118
|
+
if (result) {
|
|
119
|
+
console.log('\n');
|
|
120
|
+
console.log('Version: ', result.version);
|
|
121
|
+
console.log('Uploaded to: ', result.uploadS3Url);
|
|
122
|
+
console.log('URL: ', result.url);
|
|
123
|
+
console.log('Available Translations:', result.availableTranslationsUrl);
|
|
124
|
+
}
|
|
77
125
|
});
|
|
78
126
|
program
|
|
79
127
|
.command('upload-test-translations <input>')
|
package/db.js
CHANGED
|
@@ -83,7 +83,7 @@ async function importTranslationBatch(db, dirs, parser, overwrite) {
|
|
|
83
83
|
const promises = [];
|
|
84
84
|
for (let dir of dirs) {
|
|
85
85
|
const fullPath = path_1.default.resolve(dir);
|
|
86
|
-
promises.push((0, files_1.loadTranslationFiles)(fullPath));
|
|
86
|
+
promises.push((0, files_1.loadTranslationFiles)(fullPath).then((files) => files ?? []));
|
|
87
87
|
}
|
|
88
88
|
const allFiles = await Promise.all(promises);
|
|
89
89
|
const files = allFiles.flat();
|
|
@@ -130,22 +130,22 @@ function getChangedOrNewInputFiles(db, files) {
|
|
|
130
130
|
});
|
|
131
131
|
}
|
|
132
132
|
function insertFileMetadata(db, files) {
|
|
133
|
-
const fileUpsert = db.prepare(`INSERT INTO InputFile(
|
|
134
|
-
translationId,
|
|
135
|
-
name,
|
|
136
|
-
format,
|
|
137
|
-
sha256,
|
|
138
|
-
sizeInBytes
|
|
139
|
-
) VALUES (
|
|
140
|
-
@translationId,
|
|
141
|
-
@name,
|
|
142
|
-
@format,
|
|
143
|
-
@sha256,
|
|
144
|
-
@sizeInBytes
|
|
145
|
-
) ON CONFLICT(translationId, name) DO
|
|
146
|
-
UPDATE SET
|
|
147
|
-
format=excluded.format,
|
|
148
|
-
sha256=excluded.sha256,
|
|
133
|
+
const fileUpsert = db.prepare(`INSERT INTO InputFile(
|
|
134
|
+
translationId,
|
|
135
|
+
name,
|
|
136
|
+
format,
|
|
137
|
+
sha256,
|
|
138
|
+
sizeInBytes
|
|
139
|
+
) VALUES (
|
|
140
|
+
@translationId,
|
|
141
|
+
@name,
|
|
142
|
+
@format,
|
|
143
|
+
@sha256,
|
|
144
|
+
@sizeInBytes
|
|
145
|
+
) ON CONFLICT(translationId, name) DO
|
|
146
|
+
UPDATE SET
|
|
147
|
+
format=excluded.format,
|
|
148
|
+
sha256=excluded.sha256,
|
|
149
149
|
sizeInBytes=excluded.sizeInBytes;`);
|
|
150
150
|
const insertManyFiles = db.transaction((files) => {
|
|
151
151
|
for (let file of files) {
|
|
@@ -161,32 +161,32 @@ function insertFileMetadata(db, files) {
|
|
|
161
161
|
insertManyFiles(files);
|
|
162
162
|
}
|
|
163
163
|
function insertTranslations(db, translations) {
|
|
164
|
-
const translationUpsert = db.prepare(`INSERT INTO Translation(
|
|
165
|
-
id,
|
|
166
|
-
name,
|
|
167
|
-
language,
|
|
168
|
-
shortName,
|
|
169
|
-
textDirection,
|
|
170
|
-
licenseUrl,
|
|
171
|
-
website,
|
|
172
|
-
englishName
|
|
173
|
-
) VALUES (
|
|
174
|
-
@id,
|
|
175
|
-
@name,
|
|
176
|
-
@language,
|
|
177
|
-
@shortName,
|
|
178
|
-
@textDirection,
|
|
179
|
-
@licenseUrl,
|
|
180
|
-
@website,
|
|
181
|
-
@englishName
|
|
182
|
-
) ON CONFLICT(id) DO
|
|
183
|
-
UPDATE SET
|
|
184
|
-
name=excluded.name,
|
|
185
|
-
language=excluded.language,
|
|
186
|
-
shortName=excluded.shortName,
|
|
187
|
-
textDirection=excluded.textDirection,
|
|
188
|
-
licenseUrl=excluded.licenseUrl,
|
|
189
|
-
website=excluded.website,
|
|
164
|
+
const translationUpsert = db.prepare(`INSERT INTO Translation(
|
|
165
|
+
id,
|
|
166
|
+
name,
|
|
167
|
+
language,
|
|
168
|
+
shortName,
|
|
169
|
+
textDirection,
|
|
170
|
+
licenseUrl,
|
|
171
|
+
website,
|
|
172
|
+
englishName
|
|
173
|
+
) VALUES (
|
|
174
|
+
@id,
|
|
175
|
+
@name,
|
|
176
|
+
@language,
|
|
177
|
+
@shortName,
|
|
178
|
+
@textDirection,
|
|
179
|
+
@licenseUrl,
|
|
180
|
+
@website,
|
|
181
|
+
@englishName
|
|
182
|
+
) ON CONFLICT(id) DO
|
|
183
|
+
UPDATE SET
|
|
184
|
+
name=excluded.name,
|
|
185
|
+
language=excluded.language,
|
|
186
|
+
shortName=excluded.shortName,
|
|
187
|
+
textDirection=excluded.textDirection,
|
|
188
|
+
licenseUrl=excluded.licenseUrl,
|
|
189
|
+
website=excluded.website,
|
|
190
190
|
englishName=excluded.englishName;`);
|
|
191
191
|
const insertManyTranslations = db.transaction((translations) => {
|
|
192
192
|
for (let translation of translations) {
|
|
@@ -208,27 +208,27 @@ function insertTranslations(db, translations) {
|
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
function insertTranslationBooks(db, translation, translationBooks) {
|
|
211
|
-
const bookUpsert = db.prepare(`INSERT INTO Book(
|
|
212
|
-
id,
|
|
213
|
-
translationId,
|
|
214
|
-
title,
|
|
215
|
-
name,
|
|
216
|
-
commonName,
|
|
217
|
-
numberOfChapters,
|
|
218
|
-
\`order\`
|
|
219
|
-
) VALUES (
|
|
220
|
-
@id,
|
|
221
|
-
@translationId,
|
|
222
|
-
@title,
|
|
223
|
-
@name,
|
|
224
|
-
@commonName,
|
|
225
|
-
@numberOfChapters,
|
|
226
|
-
@bookOrder
|
|
227
|
-
) ON CONFLICT(id,translationId) DO
|
|
228
|
-
UPDATE SET
|
|
229
|
-
title=excluded.title,
|
|
230
|
-
name=excluded.name,
|
|
231
|
-
commonName=excluded.commonName,
|
|
211
|
+
const bookUpsert = db.prepare(`INSERT INTO Book(
|
|
212
|
+
id,
|
|
213
|
+
translationId,
|
|
214
|
+
title,
|
|
215
|
+
name,
|
|
216
|
+
commonName,
|
|
217
|
+
numberOfChapters,
|
|
218
|
+
\`order\`
|
|
219
|
+
) VALUES (
|
|
220
|
+
@id,
|
|
221
|
+
@translationId,
|
|
222
|
+
@title,
|
|
223
|
+
@name,
|
|
224
|
+
@commonName,
|
|
225
|
+
@numberOfChapters,
|
|
226
|
+
@bookOrder
|
|
227
|
+
) ON CONFLICT(id,translationId) DO
|
|
228
|
+
UPDATE SET
|
|
229
|
+
title=excluded.title,
|
|
230
|
+
name=excluded.name,
|
|
231
|
+
commonName=excluded.commonName,
|
|
232
232
|
numberOfChapters=excluded.numberOfChapters;`);
|
|
233
233
|
const insertMany = db.transaction((books) => {
|
|
234
234
|
for (let book of books) {
|
|
@@ -252,69 +252,69 @@ function insertTranslationBooks(db, translation, translationBooks) {
|
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
function insertTranslationContent(db, translation, book, chapters) {
|
|
255
|
-
const chapterUpsert = db.prepare(`INSERT INTO Chapter(
|
|
256
|
-
translationId,
|
|
257
|
-
bookId,
|
|
258
|
-
number,
|
|
259
|
-
json
|
|
260
|
-
) VALUES (
|
|
261
|
-
@translationId,
|
|
262
|
-
@bookId,
|
|
263
|
-
@number,
|
|
264
|
-
@json
|
|
265
|
-
) ON CONFLICT(translationId,bookId,number) DO
|
|
266
|
-
UPDATE SET
|
|
255
|
+
const chapterUpsert = db.prepare(`INSERT INTO Chapter(
|
|
256
|
+
translationId,
|
|
257
|
+
bookId,
|
|
258
|
+
number,
|
|
259
|
+
json
|
|
260
|
+
) VALUES (
|
|
261
|
+
@translationId,
|
|
262
|
+
@bookId,
|
|
263
|
+
@number,
|
|
264
|
+
@json
|
|
265
|
+
) ON CONFLICT(translationId,bookId,number) DO
|
|
266
|
+
UPDATE SET
|
|
267
267
|
json=excluded.json;`);
|
|
268
|
-
const verseUpsert = db.prepare(`INSERT INTO ChapterVerse(
|
|
269
|
-
translationId,
|
|
270
|
-
bookId,
|
|
271
|
-
chapterNumber,
|
|
272
|
-
number,
|
|
273
|
-
text,
|
|
274
|
-
contentJson
|
|
275
|
-
) VALUES (
|
|
276
|
-
@translationId,
|
|
277
|
-
@bookId,
|
|
278
|
-
@chapterNumber,
|
|
279
|
-
@number,
|
|
280
|
-
@text,
|
|
281
|
-
@contentJson
|
|
282
|
-
) ON CONFLICT(translationId,bookId,chapterNumber,number) DO
|
|
283
|
-
UPDATE SET
|
|
284
|
-
text=excluded.text,
|
|
268
|
+
const verseUpsert = db.prepare(`INSERT INTO ChapterVerse(
|
|
269
|
+
translationId,
|
|
270
|
+
bookId,
|
|
271
|
+
chapterNumber,
|
|
272
|
+
number,
|
|
273
|
+
text,
|
|
274
|
+
contentJson
|
|
275
|
+
) VALUES (
|
|
276
|
+
@translationId,
|
|
277
|
+
@bookId,
|
|
278
|
+
@chapterNumber,
|
|
279
|
+
@number,
|
|
280
|
+
@text,
|
|
281
|
+
@contentJson
|
|
282
|
+
) ON CONFLICT(translationId,bookId,chapterNumber,number) DO
|
|
283
|
+
UPDATE SET
|
|
284
|
+
text=excluded.text,
|
|
285
285
|
contentJson=excluded.contentJson;`);
|
|
286
|
-
const footnoteUpsert = db.prepare(`INSERT INTO ChapterFootnote(
|
|
287
|
-
translationId,
|
|
288
|
-
bookId,
|
|
289
|
-
chapterNumber,
|
|
290
|
-
id,
|
|
291
|
-
verseNumber,
|
|
292
|
-
text
|
|
293
|
-
) VALUES (
|
|
294
|
-
@translationId,
|
|
295
|
-
@bookId,
|
|
296
|
-
@chapterNumber,
|
|
297
|
-
@id,
|
|
298
|
-
@verseNumber,
|
|
299
|
-
@text
|
|
300
|
-
) ON CONFLICT(translationId,bookId,chapterNumber,id) DO
|
|
301
|
-
UPDATE SET
|
|
302
|
-
verseNumber=excluded.verseNumber,
|
|
286
|
+
const footnoteUpsert = db.prepare(`INSERT INTO ChapterFootnote(
|
|
287
|
+
translationId,
|
|
288
|
+
bookId,
|
|
289
|
+
chapterNumber,
|
|
290
|
+
id,
|
|
291
|
+
verseNumber,
|
|
292
|
+
text
|
|
293
|
+
) VALUES (
|
|
294
|
+
@translationId,
|
|
295
|
+
@bookId,
|
|
296
|
+
@chapterNumber,
|
|
297
|
+
@id,
|
|
298
|
+
@verseNumber,
|
|
299
|
+
@text
|
|
300
|
+
) ON CONFLICT(translationId,bookId,chapterNumber,id) DO
|
|
301
|
+
UPDATE SET
|
|
302
|
+
verseNumber=excluded.verseNumber,
|
|
303
303
|
text=excluded.text;`);
|
|
304
|
-
const chapterAudioUpsert = db.prepare(`INSERT INTO ChapterAudioUrl(
|
|
305
|
-
translationId,
|
|
306
|
-
bookId,
|
|
307
|
-
number,
|
|
308
|
-
reader,
|
|
309
|
-
url
|
|
310
|
-
) VALUES (
|
|
311
|
-
@translationId,
|
|
312
|
-
@bookId,
|
|
313
|
-
@number,
|
|
314
|
-
@reader,
|
|
315
|
-
@url
|
|
316
|
-
) ON CONFLICT(translationId,bookId,number,reader) DO
|
|
317
|
-
UPDATE SET
|
|
304
|
+
const chapterAudioUpsert = db.prepare(`INSERT INTO ChapterAudioUrl(
|
|
305
|
+
translationId,
|
|
306
|
+
bookId,
|
|
307
|
+
number,
|
|
308
|
+
reader,
|
|
309
|
+
url
|
|
310
|
+
) VALUES (
|
|
311
|
+
@translationId,
|
|
312
|
+
@bookId,
|
|
313
|
+
@number,
|
|
314
|
+
@reader,
|
|
315
|
+
@url
|
|
316
|
+
) ON CONFLICT(translationId,bookId,number,reader) DO
|
|
317
|
+
UPDATE SET
|
|
318
318
|
url=excluded.url;`);
|
|
319
319
|
const insertChaptersAndVerses = db.transaction(() => {
|
|
320
320
|
for (let chapter of chapters) {
|
|
@@ -516,15 +516,15 @@ async function getDbFromDir(dir) {
|
|
|
516
516
|
}
|
|
517
517
|
async function getDb(dbPath) {
|
|
518
518
|
const db = new better_sqlite3_1.default(dbPath, {});
|
|
519
|
-
db.exec(`CREATE TABLE IF NOT EXISTS "_prisma_migrations" (
|
|
520
|
-
"id" TEXT PRIMARY KEY NOT NULL,
|
|
521
|
-
"checksum" TEXT NOT NULL,
|
|
522
|
-
"finished_at" DATETIME,
|
|
523
|
-
"migration_name" TEXT NOT NULL,
|
|
524
|
-
"logs" TEXT,
|
|
525
|
-
"rolled_back_at" DATETIME,
|
|
526
|
-
"started_at" DATETIME NOT NULL DEFAULT current_timestamp,
|
|
527
|
-
"applied_steps_count" INTEGER UNSIGNED NOT NULL DEFAULT 0
|
|
519
|
+
db.exec(`CREATE TABLE IF NOT EXISTS "_prisma_migrations" (
|
|
520
|
+
"id" TEXT PRIMARY KEY NOT NULL,
|
|
521
|
+
"checksum" TEXT NOT NULL,
|
|
522
|
+
"finished_at" DATETIME,
|
|
523
|
+
"migration_name" TEXT NOT NULL,
|
|
524
|
+
"logs" TEXT,
|
|
525
|
+
"rolled_back_at" DATETIME,
|
|
526
|
+
"started_at" DATETIME NOT NULL DEFAULT current_timestamp,
|
|
527
|
+
"applied_steps_count" INTEGER UNSIGNED NOT NULL DEFAULT 0
|
|
528
528
|
);`);
|
|
529
529
|
const migrations = await (0, fs_extra_1.readdir)(migrationsPath);
|
|
530
530
|
const appliedMigrations = db
|
package/files.d.ts
CHANGED
|
@@ -65,9 +65,9 @@ export declare function loadTranslationsFiles(dirs: string[]): Promise<InputFile
|
|
|
65
65
|
/**
|
|
66
66
|
* Loads the files for the given translation.
|
|
67
67
|
* @param translation The directory that the translation exists in.
|
|
68
|
-
* @returns
|
|
68
|
+
* @returns The list of files that were loaded, or null if the translation has no metadata.
|
|
69
69
|
*/
|
|
70
|
-
export declare function loadTranslationFiles(translation: string): Promise<InputFile[]>;
|
|
70
|
+
export declare function loadTranslationFiles(translation: string): Promise<InputFile[] | null>;
|
|
71
71
|
export interface CollectionTranslationMetadata {
|
|
72
72
|
name: {
|
|
73
73
|
local: string;
|
package/files.js
CHANGED
|
@@ -152,7 +152,7 @@ async function loadTranslationsFiles(dirs) {
|
|
|
152
152
|
const promises = [];
|
|
153
153
|
for (let dir of dirs) {
|
|
154
154
|
const fullPath = path.resolve(dir);
|
|
155
|
-
promises.push(loadTranslationFiles(fullPath));
|
|
155
|
+
promises.push(loadTranslationFiles(fullPath).then((files) => files ?? []));
|
|
156
156
|
}
|
|
157
157
|
const allFiles = await Promise.all(promises);
|
|
158
158
|
const files = allFiles.flat();
|
|
@@ -161,13 +161,13 @@ async function loadTranslationsFiles(dirs) {
|
|
|
161
161
|
/**
|
|
162
162
|
* Loads the files for the given translation.
|
|
163
163
|
* @param translation The directory that the translation exists in.
|
|
164
|
-
* @returns
|
|
164
|
+
* @returns The list of files that were loaded, or null if the translation has no metadata.
|
|
165
165
|
*/
|
|
166
166
|
async function loadTranslationFiles(translation) {
|
|
167
167
|
const metadata = await loadTranslationMetadata(translation);
|
|
168
168
|
if (!metadata) {
|
|
169
169
|
console.error('Could not load metadata for translation!', translation);
|
|
170
|
-
return
|
|
170
|
+
return null;
|
|
171
171
|
}
|
|
172
172
|
let files = await (0, promises_1.readdir)(translation);
|
|
173
173
|
let usfmFiles = files.filter((f) => (0, path_1.extname)(f) === '.usfm' ||
|