waha-shared 1.0.510 → 1.0.511

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.
Files changed (52) hide show
  1. package/README.md +12 -12
  2. package/dist/data/bibleAudios/bibleAudios.json +12 -0
  3. package/dist/data/bibleStatuses/bibleStatuses.json +852 -0
  4. package/dist/data/bibleTexts/bibleTexts.json +11 -0
  5. package/dist/data/countriesAndLanguages/countriesAndLanguages.json +25 -20
  6. package/dist/data/countriesAndLanguages/countriesAndLanguages.schema.json +1 -1
  7. package/dist/data/countriesAndLanguages/countriesAndLanguages.zod.js +1 -1
  8. package/dist/data/languages/index.d.ts +1 -1
  9. package/dist/data/languages/languages.json +18 -11
  10. package/dist/data/languages/languages.schema.json +3 -1
  11. package/dist/data/languages/languages.zod.d.ts +1 -1
  12. package/dist/data/languages/languages.zod.js +6 -1
  13. package/dist/data/mediaDurations/mediaDurations.json +1090 -0
  14. package/dist/data/notification/index.d.ts +2 -2
  15. package/dist/data/notification/notification.schema.json +9 -3
  16. package/dist/data/notification/notification.zod.d.ts +3 -3
  17. package/dist/data/notification/notification.zod.js +4 -6
  18. package/dist/data/phoneLanguages/phoneLanguages.json +32 -0
  19. package/dist/data/releaseNotes/releaseNotes.json +9 -2
  20. package/dist/data/translationsApp/translationsApp.json +878 -8
  21. package/dist/data/translationsFtb/translationsFtb.json +136 -0
  22. package/dist/data/translationsIntroduction/translationsIntroduction.json +31 -31
  23. package/dist/data/translationsQuestion/translationsQuestion.json +64 -0
  24. package/dist/data/translationsSet/translationsSet.json +1661 -83
  25. package/dist/data/translationsSpokenQuestion/translationsSpokenQuestion.json +64 -0
  26. package/dist/functions/activeCampaign.d.ts +6 -0
  27. package/dist/functions/activeCampaign.js +10 -1
  28. package/dist/functions/bibleBooks.d.ts +6 -1
  29. package/dist/functions/bibleBooks.js +6 -1
  30. package/dist/functions/bibles.d.ts +22 -8
  31. package/dist/functions/bibles.js +54 -120
  32. package/dist/functions/crowdin.d.ts +53 -4
  33. package/dist/functions/crowdin.js +55 -43
  34. package/dist/functions/donations.d.ts +40 -0
  35. package/dist/functions/donations.js +135 -0
  36. package/dist/functions/ffmpeg.d.ts +21 -0
  37. package/dist/functions/ffmpeg.js +37 -9
  38. package/dist/functions/languages.d.ts +2 -2
  39. package/dist/functions/languages.js +26 -44
  40. package/dist/functions/scripturePassages.d.ts +9 -5
  41. package/dist/functions/scripturePassages.js +6 -10
  42. package/dist/functions/sets.d.ts +15 -7
  43. package/dist/functions/sets.js +34 -28
  44. package/dist/functions/upload.d.ts +16 -6
  45. package/dist/functions/upload.js +15 -6
  46. package/dist/functions/utils.d.ts +0 -2
  47. package/dist/functions/utils.js +1 -9
  48. package/dist/types/logs.d.ts +2 -2
  49. package/dist/types/notifications.d.ts +3 -3
  50. package/dist/types/sets.d.ts +14 -0
  51. package/dist/types/users.d.ts +1 -1
  52. package/package.json +4 -4
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Currencies Stripe takes in whole units rather than minor units, from
3
+ * https://docs.stripe.com/currencies#zero-decimal. HUF, IDR, and COP look
4
+ * zero-decimal but aren't: Stripe takes them in minor units like USD.
5
+ */
6
+ export declare const ZERO_DECIMAL_CURRENCIES: Set<string>;
7
+ /**
8
+ * Currencies donors enter as whole numbers. ISK and UGX have no fractional
9
+ * amounts, but Stripe still takes them as two-decimal values ending in `00`
10
+ * (https://docs.stripe.com/currencies#special-cases), so they aren't in
11
+ * {@link ZERO_DECIMAL_CURRENCIES}.
12
+ */
13
+ export declare const WHOLE_AMOUNT_CURRENCIES: Set<string>;
14
+ export declare const toStripeAmount: (amount: number, currency: string) => number;
15
+ export declare const fromStripeAmount: (amount: number, currency: string) => number;
16
+ /**
17
+ * Cleans typed custom-amount text down to digits and at most one `.` with
18
+ * two decimal places, or digits only when `wholeOnly`. The first `.` or `,`
19
+ * is the decimal point, since comma-decimal locales type `5,50` on the
20
+ * decimal pad; any later separators are dropped.
21
+ */
22
+ export declare function normalizeAmountInput(text: string, wholeOnly: boolean): string;
23
+ /**
24
+ * The smallest and largest donation in `currency`, in major units. Both
25
+ * scale from the currency's first preset so they track its value — 1 to
26
+ * 100,000 for USD — and the minimum stays above Stripe's per-currency charge
27
+ * minimums (https://docs.stripe.com/currencies#minimum-and-maximum-charge-amounts).
28
+ */
29
+ export declare function getDonationLimits(currency: string): {
30
+ min: number;
31
+ max: number;
32
+ };
33
+ /** Validates that a custom amount string is within `currency`'s limits. */
34
+ export declare function isValidDonationAmount(amount: string, currency: string): boolean;
35
+ /**
36
+ * Validates an amount already in Stripe's units (from
37
+ * {@link toStripeAmount}), for the server that creates the PaymentIntent.
38
+ */
39
+ export declare function isValidStripeDonationAmount(amount: number, currency: string): boolean;
40
+ export declare const DONATION_PRESETS: Record<string, number[]>;
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DONATION_PRESETS = exports.fromStripeAmount = exports.toStripeAmount = exports.WHOLE_AMOUNT_CURRENCIES = exports.ZERO_DECIMAL_CURRENCIES = void 0;
4
+ exports.normalizeAmountInput = normalizeAmountInput;
5
+ exports.getDonationLimits = getDonationLimits;
6
+ exports.isValidDonationAmount = isValidDonationAmount;
7
+ exports.isValidStripeDonationAmount = isValidStripeDonationAmount;
8
+ /**
9
+ * Currencies Stripe takes in whole units rather than minor units, from
10
+ * https://docs.stripe.com/currencies#zero-decimal. HUF, IDR, and COP look
11
+ * zero-decimal but aren't: Stripe takes them in minor units like USD.
12
+ */
13
+ exports.ZERO_DECIMAL_CURRENCIES = new Set([
14
+ 'BIF',
15
+ 'CLP',
16
+ 'DJF',
17
+ 'GNF',
18
+ 'JPY',
19
+ 'KMF',
20
+ 'KRW',
21
+ 'MGA',
22
+ 'PYG',
23
+ 'RWF',
24
+ 'VND',
25
+ 'VUV',
26
+ 'XAF',
27
+ 'XOF',
28
+ 'XPF',
29
+ ]);
30
+ /**
31
+ * Currencies donors enter as whole numbers. ISK and UGX have no fractional
32
+ * amounts, but Stripe still takes them as two-decimal values ending in `00`
33
+ * (https://docs.stripe.com/currencies#special-cases), so they aren't in
34
+ * {@link ZERO_DECIMAL_CURRENCIES}.
35
+ */
36
+ exports.WHOLE_AMOUNT_CURRENCIES = new Set([
37
+ ...exports.ZERO_DECIMAL_CURRENCIES,
38
+ 'ISK',
39
+ 'UGX',
40
+ ]);
41
+ // Rounds because float math makes 19.99 * 100 = 1998.9999999999998, and
42
+ // Stripe only accepts integer amounts.
43
+ const toStripeAmount = (amount, currency) => exports.ZERO_DECIMAL_CURRENCIES.has(currency.toUpperCase())
44
+ ? amount
45
+ : Math.round(amount * 100);
46
+ exports.toStripeAmount = toStripeAmount;
47
+ const fromStripeAmount = (amount, currency) => exports.ZERO_DECIMAL_CURRENCIES.has(currency.toUpperCase()) ? amount : amount / 100;
48
+ exports.fromStripeAmount = fromStripeAmount;
49
+ /**
50
+ * Cleans typed custom-amount text down to digits and at most one `.` with
51
+ * two decimal places, or digits only when `wholeOnly`. The first `.` or `,`
52
+ * is the decimal point, since comma-decimal locales type `5,50` on the
53
+ * decimal pad; any later separators are dropped.
54
+ */
55
+ function normalizeAmountInput(text, wholeOnly) {
56
+ const digits = (s) => s.replace(/[^0-9]/g, '');
57
+ if (wholeOnly)
58
+ return digits(text);
59
+ const match = text.match(/[.,]/);
60
+ if (!match || match.index === undefined)
61
+ return digits(text);
62
+ const whole = digits(text.slice(0, match.index));
63
+ const fraction = digits(text.slice(match.index + 1)).slice(0, 2);
64
+ return `${whole}.${fraction}`;
65
+ }
66
+ /**
67
+ * The smallest and largest donation in `currency`, in major units. Both
68
+ * scale from the currency's first preset so they track its value — 1 to
69
+ * 100,000 for USD — and the minimum stays above Stripe's per-currency charge
70
+ * minimums (https://docs.stripe.com/currencies#minimum-and-maximum-charge-amounts).
71
+ */
72
+ function getDonationLimits(currency) {
73
+ const [firstPreset] = exports.DONATION_PRESETS[currency.toUpperCase()] ?? exports.DONATION_PRESETS.USD;
74
+ return { min: firstPreset / 5, max: firstPreset * 20000 };
75
+ }
76
+ /** Validates that a custom amount string is within `currency`'s limits. */
77
+ function isValidDonationAmount(amount, currency) {
78
+ const parsed = parseFloat(amount);
79
+ const { min, max } = getDonationLimits(currency);
80
+ return !isNaN(parsed) && parsed >= min && parsed <= max;
81
+ }
82
+ /**
83
+ * Validates an amount already in Stripe's units (from
84
+ * {@link toStripeAmount}), for the server that creates the PaymentIntent.
85
+ */
86
+ function isValidStripeDonationAmount(amount, currency) {
87
+ const { min, max } = getDonationLimits(currency);
88
+ return (Number.isInteger(amount) &&
89
+ amount >= (0, exports.toStripeAmount)(min, currency) &&
90
+ amount <= (0, exports.toStripeAmount)(max, currency));
91
+ }
92
+ exports.DONATION_PRESETS = {
93
+ // North America
94
+ USD: [5, 10, 25], // US Dollar
95
+ CAD: [5, 10, 25], // Canadian Dollar
96
+ MXN: [50, 100, 250], // Mexican Peso
97
+ // Europe
98
+ EUR: [5, 10, 25], // Euro
99
+ GBP: [5, 10, 25], // British Pound
100
+ CHF: [5, 10, 25], // Swiss Franc
101
+ SEK: [50, 100, 250], // Swedish Krona
102
+ NOK: [50, 100, 250], // Norwegian Krone
103
+ DKK: [50, 100, 250], // Danish Krone
104
+ PLN: [20, 50, 100], // Polish Zloty
105
+ CZK: [100, 250, 500], // Czech Koruna
106
+ HUF: [1000, 2000, 5000], // Hungarian Forint
107
+ RON: [25, 50, 100], // Romanian Leu
108
+ // Asia Pacific
109
+ JPY: [500, 1000, 2500], // Japanese Yen (zero-decimal)
110
+ CNY: [35, 70, 175], // Chinese Yuan
111
+ HKD: [40, 80, 200], // Hong Kong Dollar
112
+ SGD: [7, 15, 35], // Singapore Dollar
113
+ AUD: [5, 10, 25], // Australian Dollar
114
+ NZD: [8, 15, 30], // New Zealand Dollar
115
+ KRW: [5000, 10000, 25000], // South Korean Won (zero-decimal)
116
+ INR: [100, 250, 500], // Indian Rupee
117
+ IDR: [25000, 50000, 150000], // Indonesian Rupiah
118
+ MYR: [20, 45, 110], // Malaysian Ringgit
119
+ THB: [150, 350, 850], // Thai Baht
120
+ PHP: [250, 500, 1250], // Philippine Peso
121
+ VND: [100000, 250000], // Vietnamese Dong (zero-decimal)
122
+ TWD: [150, 300, 750], // Taiwan Dollar
123
+ // Middle East & Africa
124
+ AED: [20, 35, 90], // UAE Dirham
125
+ SAR: [20, 40, 95], // Saudi Riyal
126
+ ILS: [20, 35, 90], // Israeli Shekel
127
+ ZAR: [90, 180, 450], // South African Rand
128
+ NGN: [2000, 5000], // Nigerian Naira
129
+ // Latin America
130
+ BRL: [25, 50, 125], // Brazilian Real
131
+ ARS: [1000, 2500, 5000], // Argentine Peso
132
+ CLP: [2500, 5000, 12500], // Chilean Peso (zero-decimal)
133
+ COP: [10000, 25000, 50000], // Colombian Peso
134
+ PEN: [20, 40, 100], // Peruvian Sol
135
+ };
@@ -1,3 +1,9 @@
1
+ /**
2
+ * The contents of a concat demuxer list file, one `file '<path>'` line per
3
+ * input. Single quotes in a path are escaped the way the demuxer expects
4
+ * (`'\''`); unescaped, they would end the path early.
5
+ */
6
+ export declare function concatListFile(files: string[]): string;
1
7
  /** Concatenate audio files using ffmpeg's concat demuxer. */
2
8
  export declare function concatAudio(files: string[], output: string): Promise<void>;
3
9
  /** Generate a silence MP3 of the given duration in seconds. */
@@ -11,6 +17,11 @@ export declare function extractAudioSegment({ endTime, input, output, startTime,
11
17
  }): Promise<void>;
12
18
  /** Extract the audio track from a video file and re-encode to MP3. */
13
19
  export declare function extractVideoAudio(input: string, output: string): Promise<void>;
20
+ /**
21
+ * Where {@link normalizeAudio} writes: a sibling `.normalized.mp3`, whatever
22
+ * the input's extension, so the output never overwrites the input.
23
+ */
24
+ export declare function normalizedPath(input: string): string;
14
25
  /**
15
26
  * Normalize audio to EBU R128 standard using ffmpeg-normalize. Returns the path
16
27
  * to the normalized file. Skips if already exists.
@@ -90,6 +101,16 @@ export declare function compressVideo({ input, output, audio, }: {
90
101
  output: string;
91
102
  audio?: string;
92
103
  }): Promise<void>;
104
+ /**
105
+ * The filter graph for {@link mixAudioWithMusic}. Expects narration as input 0
106
+ * and music as input 1: narration plays at full volume, music at 10% trimmed
107
+ * to the shorter of the two, and the mix is padded to `targetDuration`.
108
+ */
109
+ export declare function musicMixFilter({ narrationDuration, musicDuration, targetDuration, }: {
110
+ narrationDuration: number;
111
+ musicDuration: number;
112
+ targetDuration: number;
113
+ }): string;
93
114
  /**
94
115
  * Mix narration audio with background music at a fixed volume ratio. Music is
95
116
  * trimmed to the shorter of the two inputs; if `targetDuration` is provided,
@@ -1,12 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VIDEO_REMOTION_OVERRIDE_ARGS = exports.VIDEO_AUDIO_ENCODE_ARGS = exports.VIDEO_ENCODE_ARGS = exports.VIDEO_ENCODE_SETTINGS = void 0;
4
+ exports.concatListFile = concatListFile;
4
5
  exports.concatAudio = concatAudio;
5
6
  exports.generateSilence = generateSilence;
6
7
  exports.extractAudioSegment = extractAudioSegment;
7
8
  exports.extractVideoAudio = extractVideoAudio;
9
+ exports.normalizedPath = normalizedPath;
8
10
  exports.normalizeAudio = normalizeAudio;
9
11
  exports.compressVideo = compressVideo;
12
+ exports.musicMixFilter = musicMixFilter;
10
13
  exports.mixAudioWithMusic = mixAudioWithMusic;
11
14
  exports.getDurationFromFile = getDurationFromFile;
12
15
  const child_process_1 = require("child_process");
@@ -35,11 +38,19 @@ const BITRATE = ['-b:a', '64k'];
35
38
  const SAMPLE_RATE = ['-ar', '22050'];
36
39
  /** Audio channels: stereo (2 channels). */
37
40
  const CHANNELS = ['-ac', '2'];
41
+ /**
42
+ * The contents of a concat demuxer list file, one `file '<path>'` line per
43
+ * input. Single quotes in a path are escaped the way the demuxer expects
44
+ * (`'\''`); unescaped, they would end the path early.
45
+ */
46
+ function concatListFile(files) {
47
+ return files.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join('\n');
48
+ }
38
49
  /** Concatenate audio files using ffmpeg's concat demuxer. */
39
50
  async function concatAudio(files, output) {
40
51
  const dir = await (0, promises_1.mkdtemp)((0, path_1.join)((0, os_1.tmpdir)(), 'ffconcat-'));
41
52
  const listFile = (0, path_1.join)(dir, 'list.txt');
42
- await (0, promises_1.writeFile)(listFile, files.map((f) => `file '${f}'`).join('\n'));
53
+ await (0, promises_1.writeFile)(listFile, concatListFile(files));
43
54
  await exec('ffmpeg', [
44
55
  // Use the concat demuxer to join multiple input files
45
56
  '-f',
@@ -131,12 +142,19 @@ async function extractVideoAudio(input, output) {
131
142
  output,
132
143
  ]);
133
144
  }
145
+ /**
146
+ * Where {@link normalizeAudio} writes: a sibling `.normalized.mp3`, whatever
147
+ * the input's extension, so the output never overwrites the input.
148
+ */
149
+ function normalizedPath(input) {
150
+ return input.replace(/\.[^./]+$/, '') + '.normalized.mp3';
151
+ }
134
152
  /**
135
153
  * Normalize audio to EBU R128 standard using ffmpeg-normalize. Returns the path
136
154
  * to the normalized file. Skips if already exists.
137
155
  */
138
156
  async function normalizeAudio(input) {
139
- const output = input.replace(/\.mp3$/, '.normalized.mp3');
157
+ const output = normalizedPath(input);
140
158
  if ((0, fs_1.existsSync)(output))
141
159
  return output;
142
160
  await exec('ffmpeg-normalize', [
@@ -266,15 +284,13 @@ async function compressVideo({ input, output, audio, }) {
266
284
  await exec('ffmpeg', args);
267
285
  }
268
286
  /**
269
- * Mix narration audio with background music at a fixed volume ratio. Music is
270
- * trimmed to the shorter of the two inputs; if `targetDuration` is provided,
271
- * the output is padded with silence to match exactly.
287
+ * The filter graph for {@link mixAudioWithMusic}. Expects narration as input 0
288
+ * and music as input 1: narration plays at full volume, music at 10% trimmed
289
+ * to the shorter of the two, and the mix is padded to `targetDuration`.
272
290
  */
273
- async function mixAudioWithMusic({ music, narration, output, targetDuration, }) {
274
- const narrationDuration = await getDurationFromFile(narration);
275
- const musicDuration = await getDurationFromFile(music);
291
+ function musicMixFilter({ narrationDuration, musicDuration, targetDuration, }) {
276
292
  const minDuration = Math.min(narrationDuration, musicDuration);
277
- const filterComplex = [
293
+ return [
278
294
  `[0:a]volume=1.0[a1]`,
279
295
  `[1:a]atrim=0:${minDuration},volume=0.1[a2]`,
280
296
  /**
@@ -283,6 +299,18 @@ async function mixAudioWithMusic({ music, narration, output, targetDuration, })
283
299
  */
284
300
  `[a1][a2]amix=inputs=2:duration=first:dropout_transition=0:normalize=0,apad=whole_dur=${targetDuration}`,
285
301
  ].join(';');
302
+ }
303
+ /**
304
+ * Mix narration audio with background music at a fixed volume ratio. Music is
305
+ * trimmed to the shorter of the two inputs; if `targetDuration` is provided,
306
+ * the output is padded with silence to match exactly.
307
+ */
308
+ async function mixAudioWithMusic({ music, narration, output, targetDuration, }) {
309
+ const filterComplex = musicMixFilter({
310
+ narrationDuration: await getDurationFromFile(narration),
311
+ musicDuration: await getDurationFromFile(music),
312
+ targetDuration,
313
+ });
286
314
  await exec('ffmpeg', [
287
315
  '-i',
288
316
  narration,
@@ -5,11 +5,11 @@ export declare function getLanguageInfo(languageId: LanguageId, options?: {
5
5
  includeInProgressDmcSets?: boolean;
6
6
  }): LanguageInfo;
7
7
  export declare function getInitialAppInterfaceLanguage(userPhoneLanguages: Array<string | null>): Language['languageId'];
8
- export declare function isLanguageAvailable(status: Language['status'], isDev?: boolean): boolean;
8
+ export declare function isLanguageAvailable(status: Pick<Language['status'], 'dbs'>, isDev?: boolean): boolean;
9
9
  export declare function numerals(text: string, scriptName: string): string;
10
10
  /**
11
11
  * These values are not the name of the font necessarily, but the identifier of
12
12
  * the font which can be found by inspecting the font in an app like Font Book.
13
13
  */
14
14
  export declare function getAppTranslations(appLanguageId: LanguageId): TranslationsApp[string];
15
- export declare function getMeetTranslations(meetLanguageInfo: LanguageInfo): MeetTranslations;
15
+ export declare function getMeetTranslations(meetLanguageInfo: Pick<LanguageInfo, 'contentLanguages'>): MeetTranslations;
@@ -6,8 +6,6 @@ exports.isLanguageAvailable = isLanguageAvailable;
6
6
  exports.numerals = numerals;
7
7
  exports.getAppTranslations = getAppTranslations;
8
8
  exports.getMeetTranslations = getMeetTranslations;
9
- const bibleAudios_1 = require("../data/bibleAudios");
10
- const bibleTexts_1 = require("../data/bibleTexts");
11
9
  const curriculumFoundations_1 = require("../data/curriculumFoundations");
12
10
  const curriculumOnboarding_1 = require("../data/curriculumOnboarding");
13
11
  const curriculumQuestions_1 = require("../data/curriculumQuestions");
@@ -25,48 +23,28 @@ const translationsQuestion_1 = require("../data/translationsQuestion");
25
23
  const translationsSet_1 = require("../data/translationsSet");
26
24
  const translationsSpokenQuestion_1 = require("../data/translationsSpokenQuestion");
27
25
  const bibles_1 = require("./bibles");
26
+ /**
27
+ * What an unknown language, or a part of one that can't be resolved, falls back
28
+ * to: English, matching the English content languages. English always resolves
29
+ * (`languages.test.ts` checks it), so this never recurses more than once, and
30
+ * if it didn't, failing loudly beats a half-resolved language. Not cached, so a
31
+ * test that swaps the data gets a fresh result.
32
+ */
33
+ function getEnglishFallback(languageId) {
34
+ if (languageId === 'eng')
35
+ throw new Error("English can't be resolved, so there's nothing to fall back to");
36
+ return getLanguageInfo('eng');
37
+ }
28
38
  function getLanguageInfo(languageId, options) {
29
- const defaultReturn = {
30
- ...languages_1.languages[0],
31
- setIds: [...curriculumFoundations_1.curriculumFoundations[0].sets],
32
- questionSets: curriculumQuestions_1.curriculumQuestions[0].questionSets,
33
- bible: {
34
- ...bibleTexts_1.bibleTexts[0],
35
- ...bibleAudios_1.bibleAudios[0],
36
- availableTextChapters: [],
37
- availableAudioChapters: [],
38
- availableTimingsChapters: [],
39
- availableChapters: [],
40
- language: languages_1.languages[0],
41
- fullCopyright: '',
42
- bookNames: {},
43
- script: scripts_1.scripts[0],
44
- },
45
- bibleFallbacks: [],
46
- contentLanguages: {
47
- ftbs: 'eng',
48
- intros: 'eng',
49
- titles: 'eng',
50
- trainingVideos: 'eng',
51
- questionAudio: 'eng',
52
- questionText: 'eng',
53
- onboardingPassages: 'eng',
54
- },
55
- script: scripts_1.scripts[0],
56
- lessonPauses: lessonPauses_1.lessonPauses,
57
- allBibleIds: [],
58
- onboardingPassageIds: curriculumOnboarding_1.curriculumOnboarding[0].passages,
59
- status: { ...languages_1.languages[0].status, dmCourse: 'notStarted' },
60
- };
61
39
  const language = languages_1.languages.find((language) => language.languageId === languageId);
62
40
  if (!language) {
63
41
  console.error(`Can't find language with id ${languageId}`);
64
- return defaultReturn;
42
+ return getEnglishFallback(languageId);
65
43
  }
66
44
  const script = scripts_1.scripts.find((s) => s.name === language.script);
67
45
  if (!script) {
68
46
  console.error(`Can't find script with name ${language.script}`);
69
- return defaultReturn;
47
+ return getEnglishFallback(languageId);
70
48
  }
71
49
  const questionsCurriculum = curriculumQuestions_1.curriculumQuestions.find((questionCurriculum) => questionCurriculum.curriculumId === language.curriculum.questions);
72
50
  const foundationsCurriculum = curriculumFoundations_1.curriculumFoundations.find((foundationsCurriculum) => foundationsCurriculum.curriculumId === language.curriculum.foundations);
@@ -110,12 +88,12 @@ function getLanguageInfo(languageId, options) {
110
88
  const toReturn = {
111
89
  ...language,
112
90
  setIds: setIds,
113
- questionSets: questionsCurriculum?.questionSets ?? defaultReturn.questionSets,
114
- bible: language.bible
115
- ? (0, bibles_1.getBibleInfo)(language.bible) || defaultReturn.bible
116
- : defaultReturn.bible,
91
+ questionSets: questionsCurriculum?.questionSets ??
92
+ getEnglishFallback(languageId).questionSets,
93
+ bible: (language.bible && (0, bibles_1.getBibleInfo)(language.bible)) ||
94
+ getEnglishFallback(languageId).bible,
117
95
  bibleFallbacks: (language.bibleFallbacks ?? [])
118
- .map(bibles_1.getBibleInfo)
96
+ .map((id) => (0, bibles_1.getBibleInfo)(id))
119
97
  .filter((b) => b != null),
120
98
  contentLanguages: {
121
99
  trainingVideos: language.bridgeLanguages?.trainingVideos ?? language.languageId,
@@ -129,7 +107,8 @@ function getLanguageInfo(languageId, options) {
129
107
  script,
130
108
  lessonPauses: lessonPauses_1.lessonPauses,
131
109
  allBibleIds: (0, bibles_1.getAllBibleIds)(language),
132
- onboardingPassageIds: onboardingCurriculum?.passages ?? defaultReturn.onboardingPassageIds,
110
+ onboardingPassageIds: onboardingCurriculum?.passages ??
111
+ getEnglishFallback(languageId).onboardingPassageIds,
133
112
  status: { ...language.status, dmCourse: dmCourseStatus },
134
113
  };
135
114
  return toReturn;
@@ -138,15 +117,18 @@ function getInitialAppInterfaceLanguage(userPhoneLanguages) {
138
117
  for (const userPhoneLanguage of userPhoneLanguages) {
139
118
  if (!userPhoneLanguage)
140
119
  continue;
120
+ // The language subtag of a locale like `en-US`. Not just the first two
121
+ // letters, since Filipino is `fil`.
122
+ const languageCode = userPhoneLanguage.split(/[-_]/)[0];
141
123
  for (const phoneLanguage of phoneLanguages_1.phoneLanguages) {
142
124
  // Step 1: find the matching phone language.
143
- if (phoneLanguage.twoLetterId !== userPhoneLanguage.slice(0, 2))
125
+ if (phoneLanguage.twoLetterId !== languageCode)
144
126
  continue;
145
127
  // Step 2: search through alternatives to find the best option.
146
128
  for (const alternative of phoneLanguage.alternatives) {
147
129
  // If the alternative is a valid waha app interface language, we've
148
130
  // found a match.
149
- if (languages_1.languages.some((wahaLanguage) => wahaLanguage.appInterface !== undefined &&
131
+ if (languages_1.languages.some((wahaLanguage) => wahaLanguage.appInterface &&
150
132
  isLanguageAvailable(wahaLanguage.status) &&
151
133
  wahaLanguage.languageId === alternative.threeLetterId))
152
134
  return alternative.threeLetterId;
@@ -1,6 +1,6 @@
1
1
  import { BibleChapter } from '../types/bibleChapters';
2
2
  import { ScripturePassage } from '../types/scripturePassages';
3
- import { DbsInfo, EnrichedSection, LessonInfo, Section, VideoInfo } from '../types/sets';
3
+ import { DbsInfo, EnrichedSection, LessonInfo, Section } from '../types/sets';
4
4
  export { parseVerseRange } from './bibleBooks';
5
5
  export type { VerseRangeInfo } from './bibleBooks';
6
6
  type GetChapterUrlParams = {
@@ -62,7 +62,7 @@ interface GetScripturePassageParams {
62
62
  export declare function getScripturePassage({ bibleId, passageId, getChapter, noTextAvailableString, }: GetScripturePassageParams): Promise<ScripturePassage | undefined>;
63
63
  /** Gets all scripture passages for a lesson. */
64
64
  export declare function getLessonScripture({ getChapter, lessonInfo, noTextAvailableString, }: {
65
- lessonInfo: LessonInfo;
65
+ lessonInfo: Pick<LessonInfo, 'sections'>;
66
66
  getChapter: (params: GetChapterParams) => Promise<BibleChapter | undefined>;
67
67
  noTextAvailableString: string;
68
68
  }): Promise<Array<ScripturePassage | undefined>>;
@@ -75,17 +75,21 @@ export declare function getLessonScripture({ getChapter, lessonInfo, noTextAvail
75
75
  * @returns The superscript string representation of the verse number.
76
76
  */
77
77
  export declare function verseToSuperscript(num: string | undefined): string;
78
+ /** The part of a lesson's pauses that an FTB clip's duration depends on. */
79
+ export type FtbPauses = {
80
+ lessonPauses: Pick<DbsInfo['lessonPauses'], 'afterFtb'>;
81
+ };
78
82
  /**
79
83
  * Returns the FTB audio duration + afterFtb pause for a section, or 0 if the
80
84
  * section has no FTB.
81
85
  */
82
- export declare function getFtbDuration(section: Section, lessonInfo: DbsInfo | VideoInfo): number;
86
+ export declare function getFtbDuration(section: Pick<Section, 'ftbPath' | 'ftbDuration'>, lessonInfo: FtbPauses): number;
83
87
  /**
84
88
  * Fills in `length` (for verse-timed story sections) and `startTime` (for all
85
89
  * sections) by accumulating forward through the lesson. After this, every
86
90
  * section has both fields set.
87
91
  */
88
- export declare function enrichSections(lessonInfo: DbsInfo | VideoInfo, scripture: Array<ScripturePassage | undefined>): EnrichedSection[];
92
+ export declare function enrichSections(lessonInfo: Pick<LessonInfo, 'sections' | 'mediaDuration'> & FtbPauses, scripture: Array<ScripturePassage | undefined>): EnrichedSection[];
89
93
  /**
90
94
  * Converts verse timings from chapter-relative to lesson-relative using each
91
95
  * section's `startTime` (set by {@link enrichSections}).
@@ -93,5 +97,5 @@ export declare function enrichSections(lessonInfo: DbsInfo | VideoInfo, scriptur
93
97
  export declare function normalizeVerseTimings({ enrichedSections, scripture, lessonInfo, }: {
94
98
  enrichedSections: EnrichedSection[];
95
99
  scripture: Array<ScripturePassage | undefined>;
96
- lessonInfo: DbsInfo | VideoInfo;
100
+ lessonInfo: FtbPauses;
97
101
  }): Array<ScripturePassage | undefined>;
@@ -10,13 +10,11 @@ exports.verseToSuperscript = verseToSuperscript;
10
10
  exports.getFtbDuration = getFtbDuration;
11
11
  exports.enrichSections = enrichSections;
12
12
  exports.normalizeVerseTimings = normalizeVerseTimings;
13
- const bibleStatuses_1 = require("../data/bibleStatuses");
14
13
  const firebase_1 = require("../data/firebase");
15
14
  const languages_1 = require("../functions/languages");
16
15
  const sets_1 = require("../types/sets");
17
16
  const bibleBooks_1 = require("./bibleBooks");
18
17
  const bibles_1 = require("./bibles");
19
- const media_1 = require("./media");
20
18
  const utils_1 = require("./utils");
21
19
  var bibleBooks_2 = require("./bibleBooks");
22
20
  Object.defineProperty(exports, "parseVerseRange", { enumerable: true, get: function () { return bibleBooks_2.parseVerseRange; } });
@@ -215,11 +213,10 @@ function verseToSuperscript(num) {
215
213
  function getFtbDuration(section, lessonInfo) {
216
214
  if (!section.ftbPath)
217
215
  return 0;
218
- const ftbDuration = (0, media_1.getCachedDuration)(section.ftbPath);
219
- if (ftbDuration == null) {
216
+ if (section.ftbDuration == null) {
220
217
  console.warn(`FTB duration not found for ${(0, utils_1.basename)(section.ftbPath)}`);
221
218
  }
222
- return (ftbDuration ?? 0) + lessonInfo.lessonPauses.afterFtb;
219
+ return (section.ftbDuration ?? 0) + lessonInfo.lessonPauses.afterFtb;
223
220
  }
224
221
  /**
225
222
  * Fills in `length` (for verse-timed story sections) and `startTime` (for all
@@ -245,12 +242,12 @@ function enrichSections(lessonInfo, scripture) {
245
242
  const ignoreTimings = isStory && priorStoryLengthUnknown ? true : undefined;
246
243
  const startTime = currentTime;
247
244
  if (!isStory) {
248
- length = (0, media_1.getCachedDuration)(section.path);
245
+ length = section.duration;
249
246
  }
250
247
  else {
251
248
  currentTime += getFtbDuration(section, lessonInfo);
252
- const [bibleId, passageId] = section.id.split(';');
253
- length = bibleStatuses_1.bibleStatuses[bibleId]?.customPassages?.[passageId];
249
+ // Set only for custom passage recordings.
250
+ length = section.duration;
254
251
  if (!length) {
255
252
  const passage = scripture[storySections.indexOf(section)];
256
253
  if (passage) {
@@ -275,8 +272,7 @@ function enrichSections(lessonInfo, scripture) {
275
272
  * lesson file, so they are always correct even if story durations are
276
273
  * slightly off.
277
274
  */
278
- const path = lessonInfo.type === 'dbs' ? lessonInfo.fullPath : lessonInfo.videoPath;
279
- const totalDuration = (0, media_1.getCachedDuration)(path);
275
+ const totalDuration = lessonInfo.mediaDuration;
280
276
  if (totalDuration) {
281
277
  let timeFromEnd = totalDuration;
282
278
  const applicationSections = enriched.filter((s) => s.chapter === sets_1.Chapter.APPLICATION);
@@ -1,11 +1,19 @@
1
1
  import { type LanguageId } from '../data/languages/languages.zod';
2
- import { TranslationsApp } from '../data/translationsApp/translationsApp.zod';
2
+ import { type FtbPauses } from '../functions/scripturePassages';
3
3
  import type { AppTranslations, LanguageInfo, MeetTranslations } from '../types/languages';
4
- import { DbsInfo, EnrichedSection, Lesson, LessonInfo, SetInfo, VideoInfo, type Section } from '../types/sets';
4
+ import { DbsInfo, EnrichedSection, EqInfo, Lesson, LessonInfo, SetInfo, VideoInfo, type Section } from '../types/sets';
5
+ /** The translations set and lesson assembly read. */
6
+ export type LessonTranslations = Pick<MeetTranslations, 'sets' | 'questions' | 'questionsSpoken'> & {
7
+ introductions: Pick<MeetTranslations['introductions'], 'introduction'> & {
8
+ introductions: Partial<MeetTranslations['introductions']['introductions']>;
9
+ };
10
+ } & Pick<AppTranslations, 'foundations' | 'topics' | 'training' | 'fellowship' | 'application' | 'training_video'>;
11
+ /** The translations {@link getSetInfo} reads. */
12
+ export type SetInfoTranslations = Pick<LessonTranslations, 'sets' | 'foundations' | 'topics' | 'training'>;
5
13
  export declare function getSetInfo({ setId, languageId, setIds, t, }: {
6
14
  setId: string;
7
15
  languageId: LanguageId;
8
- t: MeetTranslations & TranslationsApp[string];
16
+ t: SetInfoTranslations;
9
17
  setIds: string[];
10
18
  }): SetInfo | undefined;
11
19
  /** Assembles all of the audio sections for a lesson. */
@@ -13,7 +21,7 @@ export declare function assembleLessonSections({ languageInfo, setInfo, lesson,
13
21
  languageInfo: LanguageInfo;
14
22
  setInfo: SetInfo;
15
23
  lesson: Lesson;
16
- t: MeetTranslations & AppTranslations;
24
+ t: LessonTranslations;
17
25
  useSpokenQuestions: boolean | undefined;
18
26
  }): {
19
27
  sections: Section[];
@@ -24,11 +32,11 @@ export declare function getLessonInfo({ lessonId, languageInfo, setInfo, t, useS
24
32
  lessonId: Lesson['lessonId'] | undefined;
25
33
  languageInfo: LanguageInfo;
26
34
  setInfo?: SetInfo;
27
- t: MeetTranslations & TranslationsApp[string];
35
+ t: LessonTranslations;
28
36
  useSpokenQuestions?: boolean;
29
37
  }): LessonInfo | undefined;
30
38
  /** Determines whether a lesson should be visible in Waha. */
31
- export declare function shouldShowLesson(lessonInfo: LessonInfo | undefined): boolean;
39
+ export declare function shouldShowLesson(lessonInfo: Pick<DbsInfo | EqInfo, 'type' | 'lessonId' | 'lessonTitle' | 'mediaDuration'> | Pick<VideoInfo, 'type' | 'lessonId' | 'lessonTitle' | 'mediaDuration' | 'videoPath'> | undefined): boolean;
32
40
  /**
33
41
  * Validates that the computed lesson duration (from section lengths) matches
34
42
  * the actual audio file duration. Sections must be enriched (all have `length`)
@@ -38,5 +46,5 @@ export declare function shouldShowLesson(lessonInfo: LessonInfo | undefined): bo
38
46
  */
39
47
  export declare function getExpectedLessonDuration({ enrichedSections, lessonInfo, }: {
40
48
  enrichedSections: EnrichedSection[];
41
- lessonInfo: DbsInfo | VideoInfo;
49
+ lessonInfo: FtbPauses;
42
50
  }): number;