waha-shared 1.0.509 → 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
@@ -1885,6 +1885,38 @@
1885
1885
  "f.012": "ماداغ تعلام لقيصت اد ف انسكار امحضارن؟",
1886
1886
  "f.013": "غيلاد، ران سفلد ا لحق اماينو غواوال ن ربي"
1887
1887
  },
1888
+ "skr": {
1889
+ "a.001": "ہُݨ ساڈے ءچوں کوئی اِیں حوالے کُوں آپنڑے لفظاں ءچ اِینویں بیان کرے جِینویں او کہِیں ایجھے دوست کُوں سُݨیندا پیا ہووے جئیں ایہ پہلے کڈھائیں نہ سُنڑیا ہووے۔ جے او غلطی نال کوئی ڳالھ چھوڑ ݙیوے یا کُجھ ودھا ݙیوے تاں اساں مِل تے اُوندی مدد کریسُوں۔ ایجھی صورت ءچ اساں پُچھ سڳدے ہیں، \"ایہ ڳالھ کہانڑی ءچ کِتھاں لِکھی ہے؟",
1890
+ "a.002": "اِیں کہانڑی کنُوں اساں خُدا، اُوندیاں صِفتاں تے اُوندے کماں دے بارے ءچ کیا سِکھدے ہیں؟",
1891
+ "a.003": "اِیں کہانڑی کنُوں اساں آپنڑے آپ دے بلکہ سبھ لوکاں دے بارے ءچ کیا سِکھدے ہیں؟",
1892
+ "a.004": "اِیں کہانڑی ءچوں سِکھے ہوئے سبق کُوں تُساں اِیں ہفتے آپنڑی زندگی اُتے کِینویں لاگُو کریسو؟ تُساں عملی طور تے کیا کریسو؟",
1893
+ "a.005": "ساڈے ولا مِلنڑ کنُوں پہلے تُساں اِیں کہانڑی ءچوں سِکھی ہوئی کہیں گالھ کُوں کِیندے نال شیئر کریسو؟ کیا تُساں ایجھے لوکاں کُوں جانڑدے ہیوے جیہڑے ساڈے آلی کار اِیں ایپ ءچوں خُدا دے کلام دے بارے ءچ سِکھنڑا چاہسن؟",
1894
+ "a.006": "اچھا دوستو! ساڈی مِیٹنگ ختم تھیندی پئی ہے، آؤ اساں فیصلہ کرُوں جو اساں ولا کڈݨ مِلسُوں تے ساڈی اگلی مِیٹنگ دی راہنمائی کوݨ کریسی؟",
1895
+ "a.007": "اَج دی مِیٹنگ بہُوں چنگی رِیہی۔ تُساں جیہڑا کُجھ کرݨ کیتے آکھیا ہا، اُونکُوں ذرا لِکھ گِھنو تے ساڈی اگلی مُلاقات کنُوں پہلے اِیں کہانڑی کُوں ولا سُنڑو۔ جے کہیں دے کولھ اِیں کہانڑی دی تحریر یا آڈیؤ کائنی تاں راہنمائی کرݨ آلا اِینکُوں شیئر کر ڈیسی۔ آؤ، ونجݨ تُوں پہلے خُدا تُوں مدد کیتے دُعا کریندے ہیں۔",
1896
+ "a.008": "شاگرد ہووݨ دے بارے ءچ اساں اِیں کہانڑی کنُوں کیا سِکھدے ہیں؟",
1897
+ "a.009": "اِیں کہانڑی تُوں اساں اِیمانداراں دی جماعت ہووݨ دے بارے ءچ کیا سِکھدے ہیں؟",
1898
+ "a.010": "اِیں کہانڑی تُوں اساں جماعت دے راہنما ہووݨ دے بارے ءچ کیا سِکھدے ہیں؟",
1899
+ "a.011": "شاگرد بنڑاوݨ دے بارے ءچ اساں اِیں کہانڑی تُوں کیا سِکھدے ہیں؟",
1900
+ "a.012": "اِیں کہانڑی ءچوں خُدا جیہڑی گالھ تُہاڈے اُتے ظاہر کیتی ہے، تُساں اُونکُوں اِیں ہفتے آپنڑی زندگی اُتے کِینویں لاگُو کریسو؟ تُساں عملی طور تے کیا کریسو؟",
1901
+ "a.013": "آؤ ہُنڑ اَج دی ٹریننگ دی وِڈیؤ ڈیکُھوں۔۔۔۔۔",
1902
+ "a.014": "جیہڑا کُجھ اساں سِکھے، آؤ اُوندے بارے ءچ گالھ مہاڑ کرُوں تے ایہ وی جو شاگرد بݨاوݨ ویلے اساں اُوندے اُتے کِینویں عمل کر سگدے ہیں۔",
1903
+ "a.015": "ساڈی اگلی مُلاقات تُوں پہلے تُساں اِیں مِیٹنگ تُوں سِکھی ہوئی کہیں گالھ کُوں کِیندے نال شیئر کریسو؟",
1904
+ "a.016": "ہُݨ آؤ گُم تھئے ہوئے لوکاں نال رابطہ کرݨ دے بارے ءچ ڳالھ کرُوں۔ ساڈی پِچھلی مُلاقات تُوں بعد اساں کِنہاں لوکاں نال گالھ مہاڑ کیتی، تے اساں اُنہاں کیتے کِینویں دُعا کر سڳدے ہیں؟",
1905
+ "a.017": "اِیں ہفتے، اساں اُنہاں لوکاں ناں تے ڈُوجھیاں نال کِینویں رابطہ کریسُوں تاں جو ساڈی اُنہاں نال بامقصد گفتگو تھی سگے؟",
1906
+ "f.001": "خدا دے بارے ءچ سِکھنڑ دے نویں سیشن ءچ خوشآمدید! آؤ، ہِک بئے دا حال چال پُچھ تے شروع کریندے ہیں۔ ذرا سوچو، ساڈی آخری مُلاقات دے بعد تُوں تُہادے نال کیا کیا تھیّا، اُوندے ءچوں تُساں کیہڑی گالھ دے بارے ءچ خُدا دے شُکر گُزار ہیوے؟",
1907
+ "f.002": "اِیں ہفتے کیہڑی ڳالھ تُہاڈی پریشانی دا سبب بنڑی، تے حالات بہتر کرݨ کیتے کیا کیتا ونج سگدے؟",
1908
+ "f.003": "تُہاڈی جماعت دے لوکاں دِیاں ضرورتاں کیا ہِن، تے جِنہاں ضرورتاں دا اساں ذکر کیتے، اُنہاں کُوں پُورا کرݨ ءچ اساں ہِک ڈُوجھے دی کِینویں مدد کر سڳدے ہیں؟",
1909
+ "f.004": "ساڈی پِچھلی مُلاقات ءچ اساں کیہڑی کہانڑی سِکھی؟ اساں خُدا تے لوکاں دے بارے ءچ کیا سِکھیا؟",
1910
+ "f.005": "ساڈی پِچھلی مِیٹنگ ءچ تُساں سِکھے ہوئے گالھیں اُتے عمل کرݨ دا فیصلہ کیتا ہا۔ اِیں سِلسلے ءچ تُساں کیا کیتا، تے اِیندا کیا نتیجہ نِکلیا؟",
1911
+ "f.006": "پِچھلی کہانڑی دی کوئی گالھ تُساں کِیندے نال شیئر کیتی؟ اُوندا کیا نتیجہ نِکلیا؟",
1912
+ "f.007": "پِچھلی مُلاقات ءچ اساں لوکاں دیاں کئی ضرورتاں دے بارے ءچ گالھ کیتی ہئی تے اساں اُنہاں ضرورتاں کُوں پُورا کرݨ دا منصوبہ بݨایا ہا۔ اِیں سلسلے ءچ اساں کیا کیتے؟",
1913
+ "f.008": "آؤ، ہُنڑ خُدا دے کلام ءچوں ہِک نوِیں کہانڑی سُنڑدے ہیں۔۔۔۔",
1914
+ "f.009": "ساڈی شاگرد بݨاوݨ دی جماعت دے نویں سیشن ءچ خوشآمدید! آؤ، ہِک بئے دا حال چال پُچھ تے شروع کریندے ہیں۔ ذرا سوچو، ساڈی آخری مُلاقات دے بعد تُوں تُہادے نال کیا کیا تھیّا، اُوندے ءچوں تُساں کیہڑی گالھ دے بارے ءچ خُدا شُکر گُزار ہیوے؟",
1915
+ "f.010": "تُہاڈے علاقے دے گُم تھئے ہوئے لوکاں دِیاں ضرورتاں کیا ہِن، تے جیہڑیاں ضرورتاں دا اساں ذِکر کیتے اُنہاں کُوں پُورا کرݨ ءچ اساں ہِک ڈُوجھے دی کِینویں مدد کر سڳدے ہیں؟",
1916
+ "f.011": "دُعا کُوں فروغ ڈیوݨ دیاں تُہاڈیاں کوششاں دے ذریعے خُدا کِینویں کم کریندا پئے؟ کیا دُعا دی قبولیت دے کوئی واقعات ہِن جِنہاں کیتے اساں اُوندا شکر ادا کر سگوں؟",
1917
+ "f.012": "ساڈی پِچھلی مِیٹنگ ءچ اساں شاگرد بنڑاوݨ دے بارے ءچ کیا سِکھیا؟",
1918
+ "f.013": "آؤ، ہُݨ خُدا دے کلام ءچوں ہِک نویں ڳالھ سُݨدے ہیں۔۔۔۔"
1919
+ },
1888
1920
  "som": {
1889
1921
  "a.001": "Haddaba midkeen ka mid ah ha inoogu sheego casharkan sheeko ahaan sidii isagoo u sheegaya saaxiib aan weligii maqal. Aan uga caawinno hadduu wax ku daro ama wax ka tago si khalad ah. Hadday taasi dhacdo aan weydiinno, \"Xaggee bay taasi kaga jirtaa qisada?\"",
1890
1922
  "a.002": "Maxay sheekadani inaga baraysaa Ilaahay, dabeecadiisa iyo waxa uu sameeyo?",
@@ -1949,6 +1981,38 @@
1949
1981
  "f.012": "¿Qué aprendimos en nuestra última reunión acerca de hacer discípulos?",
1950
1982
  "f.013": "Ahora, escuchemos una nueva historia de parte de Dios..."
1951
1983
  },
1984
+ "sqi": {
1985
+ "a.001": "Tani, le ta tregojë dikush sërish këtë pjesë me fjalët e veta, sikur t'ia tregonte një shoku që nuk e ka dëgjuar kurrë. Le ta ndihmojmë nëse lë diçka pa thënë ose shton diçka gabimisht. Nëse ndodh kështu, mund të pyesim: \"Ku e gjen këtë në histori?\"",
1986
+ "a.002": "Çfarë na mëson kjo histori për Perëndinë, karakterin e Tij dhe atë që bën Ai?",
1987
+ "a.003": "Çfarë mësojmë për njerëzit, duke përfshirë edhe veten tonë, nga kjo histori?",
1988
+ "a.004": "Si do ta zbatosh të vërtetën e Perëndisë që del nga kjo histori në jetën tënde këtë javë? Cili është ai veprim ose gjë specifike që do të bësh?",
1989
+ "a.005": "Me kë do të ndash një nga të vërtetat e kësaj historie para se të takohemi përsëri? A njeh njerëz të tjerë që do të donin ta zbulonin fjalën e Perëndisë në këtë aplikacion, ashtu si po e zbulojmë ne?",
1990
+ "a.006": "Ndërsa takimi ynë po vjen drejt fundit, le të vendosim se kur do të takohemi përsëri dhe kush do ta drejtojë takimin tonë të ardhshëm.",
1991
+ "a.007": "Ka qenë një kohë e mirë së bashku. Ju inkurajojmë të mbani shënim atë që thatë se do të bëni dhe ta dëgjoni përsëri këtë histori në ditët vijuese, para se të takohemi sërish. Moderatori mund të shpërndajë tekstin ose audion e historisë nëse dikush nuk e ka. Ndërsa vazhdojmë, le t'i kërkojmë Zotit të na ndihmojë.",
1992
+ "a.008": "Çfarë mësojmë nga kjo histori për të qenë dishepuj?",
1993
+ "a.009": "Çfarë mësojmë nga kjo histori për të qenë kishë?",
1994
+ "a.010": "Çfarë mësojmë nga kjo histori për të qenë udhëheqës?",
1995
+ "a.011": "Çfarë mësojmë nga kjo histori për bërjen e dishepujve?",
1996
+ "a.012": "Si do ta zbatosh në jetën tënde këtë javë, atë që Perëndia të ka treguar nga kjo histori ? Cili është një veprim ose gjë specifike që do të bësh?",
1997
+ "a.013": "Tani, le të shohim videon e trajnimit të sotëm...",
1998
+ "a.014": "Le të diskutojmë atë që sapo mësuam dhe si mund ta zbatojmë atë ndërsa bëjmë dishepuj.",
1999
+ "a.015": "Me kë do të ndani disa nga gjërat që keni mësuar nga ky takim, para se të takohemi përsëri?",
2000
+ "a.016": "Tani, le të flasim për angazhimin me të humburit. Me kë kemi pasur biseda që kur u takuam për herë të fundit dhe si mund të lutemi për ta?",
2001
+ "a.017": "Si do të angazhohemi me këta njerëz dhe me të tjerë, në mënyrë që të zhvillojmë biseda të mira me ta këtë javë?",
2002
+ "f.001": "Mirë se vini në një sesion të ri të studimit \"Të zbulojmë Perëndinë\". Le të fillojmë duke treguar se si po na shkojnë gjërat. Duke u nisur nga ajo që të ka ndodhur që nga hera e fundit që u takuam, për çfarë je mirënjohës?",
2003
+ "f.002": "Çfarë të ka munduar këtë javë dhe çfarë të nevojitet që gjërat të përmirësohen?",
2004
+ "f.003": "Cilat janë nevojat e njerëzve në komunitetin tënd dhe si mund ta ndihmojmë njëri-tjetrin për të përmbushur nevojat që kemi shprehur?",
2005
+ "f.004": "Cila ishte historia e herës së fundit që u takuam? Çfarë mësuam për Perëndinë dhe njerëzit?",
2006
+ "f.005": "Në takimin tonë të fundit, vendosëm që do të zbatonit atë që mësuat. Çfarë ke bërë dhe si shkoi?",
2007
+ "f.006": "Me kë keni ndarë diçka nga historia e fundit? Si reagoi ai person?",
2008
+ "f.007": "Herën e fundit që u takuam, kemi identifikuar disa nevoja dhe planifikuam t’i plotësonim ato. Si shkoi?",
2009
+ "f.008": "Tani, le të dëgjojmë një histori të re nga Perëndia...",
2010
+ "f.009": "Mirë se vini në një sesion të ri të Komunitetit tonë të Dishepull - Bërjes. Le të fillojmë duke treguar se si po na shkojnë gjërat. Duke u nisur nga ajo që të ka ndodhur që nga hera e fundit që u takuam, për çfarë je mirënjohës?",
2011
+ "f.010": "Cilat janë nevojat e të humburve në komunitetin tënd dhe si mund ta ndihmojmë njëri-tjetrin për të përmbushur nevojat që kemi shprehur?",
2012
+ "f.011": "Si ka vepruar Perëndia përmes përpjekjeve tuaja për ta shumëfishuar lutjen? A kemi ndonjë histori të lutjeve që kanë marrë përgjigje, për të cilën mund ta lavdërojmë Atë?",
2013
+ "f.012": "Çfarë mësuam në takimin tonë të fundit rreth bërjes së dishepujve?",
2014
+ "f.013": "Tani, le të dëgjojmë një të vërtetë të re nga Perëndia..."
2015
+ },
1952
2016
  "swe": {
1953
2017
  "a.001": "Låt nu någon återberätta detta avsnitt med sina egna ord, som om de berättade för en vän som aldrig har hört det. Låt oss hjälpa dem om de utelämnar något eller lägger till något av misstag. Om det händer kan vi fråga: \"Var hittar du det i texten?\"",
1954
2018
  "a.002": "Vad lär oss denna berättelse om Gud, Hans karaktär och vad Han gör?",
@@ -4,6 +4,12 @@ declare const listNumbers: {
4
4
  'dm-course': number;
5
5
  'in-app-donation-sequence': number;
6
6
  };
7
+ /**
8
+ * The ActiveCampaign API URL that looks up a contact by email. The email is
9
+ * encoded, since a raw `+` in a query string reads as a space and
10
+ * `name+tag@x.com` would never match.
11
+ */
12
+ export declare function contactLookupUrl(apiUrl: string, email: string): string;
7
13
  export declare function addToACList({ baseUrl, email, list, token, name, }: {
8
14
  token: string;
9
15
  baseUrl: string;
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.contactLookupUrl = contactLookupUrl;
6
7
  exports.addToACList = addToACList;
7
8
  exports.getContactsInList = getContactsInList;
8
9
  const signale_1 = __importDefault(require("signale"));
@@ -12,10 +13,18 @@ const listNumbers = {
12
13
  'dm-course': 9,
13
14
  'in-app-donation-sequence': 17,
14
15
  };
16
+ /**
17
+ * The ActiveCampaign API URL that looks up a contact by email. The email is
18
+ * encoded, since a raw `+` in a query string reads as a space and
19
+ * `name+tag@x.com` would never match.
20
+ */
21
+ function contactLookupUrl(apiUrl, email) {
22
+ return `${apiUrl}/contacts?email=${encodeURIComponent(email)}`;
23
+ }
15
24
  async function addToACList({ baseUrl, email, list, token, name, }) {
16
25
  const headers = { 'Api-Token': token, 'Content-Type': 'application/json' };
17
26
  const url = `${baseUrl}/api/3`;
18
- const findContact = await fetch(`${url}/contacts?email=${email}`, {
27
+ const findContact = await fetch(contactLookupUrl(url, email), {
19
28
  method: 'GET',
20
29
  headers,
21
30
  });
@@ -12,5 +12,10 @@ export interface VerseRangeInfo {
12
12
  chapterAudioFileName: string;
13
13
  chapterJsonFileName: string;
14
14
  }
15
- /** Parses a verse range string like "GEN.1.1-GEN.1.25" or "JHN.3.16". */
15
+ /**
16
+ * Parses a verse range string like "GEN.1.1-GEN.1.25" or "JHN.3.16". A range
17
+ * must stay within one chapter: the chapter is read from its start, so
18
+ * "JHN.1.50-JHN.2.3" would come back empty. `bibleBooks.test.ts` checks that
19
+ * no bundled passage crosses chapters.
20
+ */
16
21
  export declare function parseVerseRange(passageId: string): VerseRangeInfo;
@@ -74,7 +74,12 @@ exports.NEW_TESTAMENT_BOOKS = new Set([
74
74
  'JUD',
75
75
  'REV',
76
76
  ]);
77
- /** Parses a verse range string like "GEN.1.1-GEN.1.25" or "JHN.3.16". */
77
+ /**
78
+ * Parses a verse range string like "GEN.1.1-GEN.1.25" or "JHN.3.16". A range
79
+ * must stay within one chapter: the chapter is read from its start, so
80
+ * "JHN.1.50-JHN.2.3" would come back empty. `bibleBooks.test.ts` checks that
81
+ * no bundled passage crosses chapters.
82
+ */
78
83
  function parseVerseRange(passageId) {
79
84
  // Handle single verse: "GEN.1.1" -> "GEN.1.1-GEN.1.1"
80
85
  const parts = passageId.includes('-')
@@ -1,21 +1,30 @@
1
- import type { BibleModifier, Language, SetCategory } from '../data/languages/languages.zod';
1
+ import type { BibleModifier, SetCategory } from '../data/languages/languages.zod';
2
2
  import { BibleInfo, LanguageInfo } from '../types/languages';
3
+ /**
4
+ * Joins a bible's text, audio, language, and available chapters. Cached, since
5
+ * lesson screens look up the same bibles many times.
6
+ */
3
7
  export declare function getBibleInfo(bibleId: string): BibleInfo | undefined;
4
8
  export declare function replaceBibleWith({ modifiers, lessonCategory, passageId, }: {
5
9
  modifiers: BibleModifier[];
6
10
  lessonCategory: SetCategory | undefined;
7
11
  passageId: string;
8
12
  }): string | undefined;
9
- /**
10
- * Determines whether a lesson/passage meet the requirements for concatenating.
11
- */
12
- export declare function shouldConcatBibleWith({ modifier, lessonCategory, passageId, }: {
13
+ export interface ShouldConcatBibleWithParams {
13
14
  modifier: BibleModifier;
14
15
  lessonCategory: SetCategory;
15
16
  passageId: string;
16
- }): boolean;
17
+ }
18
+ /**
19
+ * Determines whether a lesson/passage meet the requirements for concatenating.
20
+ */
21
+ export declare function shouldConcatBibleWith({ modifier, lessonCategory, passageId, }: ShouldConcatBibleWithParams): boolean;
17
22
  /** Returns a deduplicated list of all bible IDs referenced by a language. */
18
- export declare function getAllBibleIds(language: LanguageInfo | Language): string[];
23
+ export declare function getAllBibleIds(language: {
24
+ bible?: string | Pick<BibleInfo, 'bibleTextId'>;
25
+ bibleFallbacks?: (string | Pick<BibleInfo, 'bibleTextId'>)[];
26
+ bibleModifiers?: Pick<BibleModifier, 'bibleId'>[];
27
+ }): string[];
19
28
  /**
20
29
  * Chapters that can be missing without affecting book/testament completeness.
21
30
  * MAL.4 is optional because the Hebrew Bible includes its content within
@@ -37,13 +46,18 @@ export declare function getAvailableChapters(bibleId: string | undefined): {
37
46
  timings: string[];
38
47
  all: string[];
39
48
  };
49
+ /**
50
+ * The percentage of a complete bible that `bibleId` has for `type`. Counts
51
+ * distinct chapters, so overlapping entries (e.g. `OT` and `GEN`) and optional
52
+ * chapters can't push it past 100.
53
+ */
40
54
  export declare function getPercentage(bibleId: string, type: 'text' | 'audio' | 'timings'): number;
41
55
  /**
42
56
  * Determines the appropriate bible for a given passage based on the language's
43
57
  * bible, bible modifiers, and fallback bible.
44
58
  */
45
59
  export declare function getBibleForPassage({ languageInfo, lessonCategory, passageId, }: {
46
- languageInfo: LanguageInfo;
60
+ languageInfo: Pick<LanguageInfo, 'bible' | 'bibleFallbacks' | 'bibleModifiers'>;
47
61
  lessonCategory: SetCategory | undefined;
48
62
  passageId: string;
49
63
  }): string;
@@ -15,47 +15,60 @@ const bibleTexts_1 = require("../data/bibleTexts");
15
15
  const languages_1 = require("../data/languages");
16
16
  const scripts_1 = require("../data/scripts");
17
17
  const bibleBooks_1 = require("./bibleBooks");
18
- const bibleInfoCache = new Map();
18
+ /**
19
+ * {@link getBibleInfo} results and the data they were built from. The bundled
20
+ * data never changes at runtime, but tests swap it, and results built from the
21
+ * old data would be stale.
22
+ */
23
+ let bibleInfoCache;
24
+ /**
25
+ * Joins a bible's text, audio, language, and available chapters. Cached, since
26
+ * lesson screens look up the same bibles many times.
27
+ */
19
28
  function getBibleInfo(bibleId) {
20
- const cached = bibleInfoCache.get(bibleId);
21
- if (cached !== undefined)
22
- return cached;
23
- if (bibleInfoCache.has(bibleId))
24
- return undefined;
29
+ const builtFrom = [
30
+ bibleStatuses_1.bibleStatuses,
31
+ bibleTexts_1.bibleTexts,
32
+ bibleAudios_1.bibleAudios,
33
+ languages_1.languages,
34
+ scripts_1.scripts,
35
+ ];
36
+ if (!bibleInfoCache?.builtFrom.every((data, i) => data === builtFrom[i]))
37
+ bibleInfoCache = { builtFrom, infos: new Map() };
38
+ const { infos } = bibleInfoCache;
39
+ if (!infos.has(bibleId))
40
+ infos.set(bibleId, buildBibleInfo(bibleId));
41
+ return infos.get(bibleId);
42
+ }
43
+ function buildBibleInfo(bibleId) {
25
44
  const bibleStatus = bibleStatuses_1.bibleStatuses[bibleId];
26
- if (!bibleStatus) {
27
- bibleInfoCache.set(bibleId, undefined);
45
+ if (!bibleStatus)
28
46
  return undefined;
29
- }
30
47
  const bibleText = bibleTexts_1.bibleTexts.find((t) => t.bibleTextId === bibleId);
31
48
  const bibleAudio = bibleAudios_1.bibleAudios.find((t) => t.bibleAudioId === bibleText?.audioId ||
32
49
  t.bibleAudioId === bibleText?.bibleTextId);
33
50
  const language = languages_1.languages.find((l) => l.languageId === bibleText?.languageId);
34
51
  const script = scripts_1.scripts.find((s) => s.name === language?.script) ?? scripts_1.scripts[0];
35
52
  if (!bibleText || !bibleAudio || !language)
36
- return;
53
+ return undefined;
37
54
  const availableChapters = getAvailableChapters(bibleText.bibleTextId);
38
- const result = bibleText && bibleAudio && language
39
- ? {
40
- ...bibleText,
41
- ...bibleAudio,
42
- language,
43
- script,
44
- availableTextChapters: availableChapters.text,
45
- availableAudioChapters: availableChapters.audio,
46
- availableTimingsChapters: availableChapters.timings,
47
- availableChapters: availableChapters.all,
48
- bookNames: {
49
- ...(bibleText.bookNameFallback
50
- ? (bibleStatuses_1.bibleStatuses[bibleText.bookNameFallback]?.bookNames ?? {})
51
- : {}),
52
- ...bibleStatus.bookNames,
53
- },
54
- fullCopyright: (bibleText.copyright ?? '') + '\n\n' + (bibleAudio.copyright ?? ''),
55
- }
56
- : undefined;
57
- bibleInfoCache.set(bibleId, result);
58
- return result;
55
+ return {
56
+ ...bibleText,
57
+ ...bibleAudio,
58
+ language,
59
+ script,
60
+ availableTextChapters: availableChapters.text,
61
+ availableAudioChapters: availableChapters.audio,
62
+ availableTimingsChapters: availableChapters.timings,
63
+ availableChapters: availableChapters.all,
64
+ bookNames: {
65
+ ...(bibleText.bookNameFallback
66
+ ? (bibleStatuses_1.bibleStatuses[bibleText.bookNameFallback]?.bookNames ?? {})
67
+ : {}),
68
+ ...bibleStatus.bookNames,
69
+ },
70
+ fullCopyright: (bibleText.copyright ?? '') + '\n\n' + (bibleAudio.copyright ?? ''),
71
+ };
59
72
  }
60
73
  function replaceBibleWith({ modifiers, lessonCategory, passageId, }) {
61
74
  for (const modifier of modifiers) {
@@ -114,74 +127,6 @@ exports.OPTIONAL_CHAPTERS = new Set(['MAL.4']);
114
127
  var bibleBooks_2 = require("./bibleBooks");
115
128
  Object.defineProperty(exports, "NEW_TESTAMENT_BOOKS", { enumerable: true, get: function () { return bibleBooks_2.NEW_TESTAMENT_BOOKS; } });
116
129
  Object.defineProperty(exports, "OLD_TESTAMENT_BOOKS", { enumerable: true, get: function () { return bibleBooks_2.OLD_TESTAMENT_BOOKS; } });
117
- const bookLengths = new Map([
118
- ['GEN', 50],
119
- ['EXO', 40],
120
- ['LEV', 27],
121
- ['NUM', 36],
122
- ['DEU', 34],
123
- ['JOS', 24],
124
- ['JDG', 21],
125
- ['RUT', 4],
126
- ['1SA', 31],
127
- ['2SA', 24],
128
- ['1KI', 22],
129
- ['2KI', 25],
130
- ['1CH', 29],
131
- ['2CH', 36],
132
- ['EZR', 10],
133
- ['NEH', 13],
134
- ['EST', 10],
135
- ['JOB', 42],
136
- ['PSA', 150],
137
- ['PRO', 31],
138
- ['ECC', 12],
139
- ['SNG', 8],
140
- ['ISA', 66],
141
- ['JER', 52],
142
- ['LAM', 5],
143
- ['EZK', 48],
144
- ['DAN', 12],
145
- ['HOS', 14],
146
- ['JOL', 3],
147
- ['AMO', 9],
148
- ['OBA', 1],
149
- ['JON', 4],
150
- ['MIC', 7],
151
- ['NAM', 3],
152
- ['HAB', 3],
153
- ['ZEP', 3],
154
- ['HAG', 2],
155
- ['ZEC', 14],
156
- ['MAL', 3],
157
- ['MAT', 28],
158
- ['MRK', 16],
159
- ['LUK', 24],
160
- ['JHN', 21],
161
- ['ACT', 28],
162
- ['ROM', 16],
163
- ['1CO', 16],
164
- ['2CO', 13],
165
- ['GAL', 6],
166
- ['EPH', 6],
167
- ['PHP', 4],
168
- ['COL', 4],
169
- ['1TH', 5],
170
- ['2TH', 3],
171
- ['1TI', 6],
172
- ['2TI', 4],
173
- ['TIT', 3],
174
- ['PHM', 1],
175
- ['HEB', 13],
176
- ['JAS', 5],
177
- ['1PE', 5],
178
- ['2PE', 3],
179
- ['1JN', 5],
180
- ['2JN', 1],
181
- ['3JN', 1],
182
- ['JUD', 1],
183
- ['REV', 22],
184
- ]);
185
130
  /**
186
131
  * Expands an optimized chapter list back to individual chapters for a single
187
132
  * type. Converts book names (e.g., "GEN") back to all chapters (e.g., ["GEN.1",
@@ -265,28 +210,17 @@ function getAvailableChapters(bibleId) {
265
210
  const all = bibleChaptersList_1.bibleChaptersList.filter((c) => allSet.has(c));
266
211
  return { text, audio, timings, all };
267
212
  }
213
+ /** Chapters in a complete bible, not counting optional ones like MAL.4. */
214
+ const REQUIRED_CHAPTER_COUNT = bibleChaptersList_1.bibleChaptersList.length - exports.OPTIONAL_CHAPTERS.size;
215
+ /**
216
+ * The percentage of a complete bible that `bibleId` has for `type`. Counts
217
+ * distinct chapters, so overlapping entries (e.g. `OT` and `GEN`) and optional
218
+ * chapters can't push it past 100.
219
+ */
268
220
  function getPercentage(bibleId, type) {
269
- const optimizedChapters = bibleStatuses_1.bibleStatuses[bibleId]?.[type] || [];
270
- let total = 0;
271
- optimizedChapters.forEach((entry) => {
272
- if (entry === 'OT') {
273
- // All Old Testament books (excluding optional chapters like MAL.4)
274
- total += 928;
275
- }
276
- else if (entry === 'NT') {
277
- // All New Testament books
278
- total += 260;
279
- }
280
- else if (entry.includes('.')) {
281
- // It's already a chapter ID, count as 1
282
- total += 1;
283
- }
284
- else {
285
- // It's a book ID, add its chapter count
286
- total += bookLengths.get(entry) || 0;
287
- }
288
- });
289
- return (total / 1188) * 100;
221
+ const chapters = expandOptimizedChapters(bibleStatuses_1.bibleStatuses[bibleId]?.[type] ?? []);
222
+ const required = chapters.filter((c) => !exports.OPTIONAL_CHAPTERS.has(c));
223
+ return (required.length / REQUIRED_CHAPTER_COUNT) * 100;
290
224
  }
291
225
  /**
292
226
  * Determines the appropriate bible for a given passage based on the language's
@@ -16,6 +16,19 @@ export declare const crowdinProjectIds: {
16
16
  * is why translations download straight into `crowdin/{wahaLanguageId}`.
17
17
  */
18
18
  export declare const CROWDIN_TRANSLATION_PATTERN = "%three_letters_code%/%original_file_name%";
19
+ /**
20
+ * Derives the Crowdin-relative path (what's shown in the dashboard, e.g.
21
+ * `eng/app_interface_text.json`) from an absolute filesystem path. Every
22
+ * Waha project keeps its Crowdin sources under a `crowdin/` directory, so
23
+ * that directory's name is the anchor: everything after the last `crowdin`
24
+ * segment is the path Crowdin knows about.
25
+ *
26
+ * This indirection exists because callers pass an absolute path rather than
27
+ * one relative to this file. `shared/` ships as a published npm package, so
28
+ * this file can end up executing from inside a consumer's `node_modules`,
29
+ * where a base path hardcoded relative to `__dirname` points nowhere useful.
30
+ */
31
+ export declare function crowdinRelativePath(absolutePath: string): string;
19
32
  interface DownloadTranslationsParams {
20
33
  /**
21
34
  * The id of the project to download translations for. Can either be taken
@@ -59,11 +72,47 @@ interface UploadSourceParams {
59
72
  */
60
73
  label?: string;
61
74
  /** Parser and import settings, for files needing more than the defaults. */
62
- fileSettings?: {
63
- type?: SourceFilesModel.FileType;
64
- importOptions?: SourceFilesModel.ImportOptions;
65
- };
75
+ fileSettings?: FileSettings;
66
76
  }
77
+ interface FileSettings {
78
+ type?: SourceFilesModel.FileType;
79
+ importOptions?: SourceFilesModel.ImportOptions;
80
+ }
81
+ /**
82
+ * The request that replaces an existing file. Built separately from
83
+ * {@link createFileRequest} rather than sharing a spread: a file's parser
84
+ * `type` is fixed at creation and the replace endpoint rejects it outright,
85
+ * and spreading into a typed parameter skips TypeScript's excess property
86
+ * check — so a shared object hides the mistake until Crowdin returns "Field
87
+ * 'type' is unexpected".
88
+ */
89
+ export declare function replaceFileRequest({ storageId, fileSettings, attachLabelIds, }: {
90
+ storageId: number;
91
+ fileSettings?: FileSettings;
92
+ attachLabelIds?: number[];
93
+ }): SourceFilesModel.ReplaceFileFromStorageRequest;
94
+ /** The request that creates a new file. See {@link replaceFileRequest}. */
95
+ export declare function createFileRequest({ storageId, name, fileSettings, attachLabelIds, directoryId, }: {
96
+ storageId: number;
97
+ name: string;
98
+ fileSettings?: FileSettings;
99
+ attachLabelIds?: number[];
100
+ directoryId?: number;
101
+ }): SourceFilesModel.CreateFileRequest;
67
102
  /** Uploads a source file to Crowdin, creating it or replacing its contents. */
68
103
  export declare function uploadSource({ projectId, source, token, label, fileSettings, }: UploadSourceParams): Promise<void>;
104
+ /**
105
+ * The directory a new file at `relativePath` belongs in, or undefined for a
106
+ * file at the project root.
107
+ *
108
+ * A missing directory is an error rather than something to create: a
109
+ * misspelled path that gets created silently uploads to a new file and
110
+ * orphans every translation attached to the real one.
111
+ */
112
+ /** A Crowdin project directory, as {@link findDirectoryId} reads it. */
113
+ export interface CrowdinDirectory {
114
+ id: number;
115
+ path: string;
116
+ }
117
+ export declare function findDirectoryId(directories: CrowdinDirectory[], projectId: number, relativePath: string): number | undefined;
69
118
  export {};
@@ -4,8 +4,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.CROWDIN_TRANSLATION_PATTERN = exports.crowdinProjectIds = void 0;
7
+ exports.crowdinRelativePath = crowdinRelativePath;
7
8
  exports.downloadTranslations = downloadTranslations;
9
+ exports.replaceFileRequest = replaceFileRequest;
10
+ exports.createFileRequest = createFileRequest;
8
11
  exports.uploadSource = uploadSource;
12
+ exports.findDirectoryId = findDirectoryId;
9
13
  const crowdin_api_client_1 = __importDefault(require("@crowdin/crowdin-api-client"));
10
14
  const fs_1 = __importDefault(require("fs"));
11
15
  const node_child_process_1 = require("node:child_process");
@@ -66,6 +70,41 @@ function downloadTranslations({ projectId, translationsFolder, token, sourcePatt
66
70
  stdio: 'inherit',
67
71
  });
68
72
  }
73
+ const exportOptions = {
74
+ exportPattern: `/${exports.CROWDIN_TRANSLATION_PATTERN}`,
75
+ };
76
+ /**
77
+ * The request that replaces an existing file. Built separately from
78
+ * {@link createFileRequest} rather than sharing a spread: a file's parser
79
+ * `type` is fixed at creation and the replace endpoint rejects it outright,
80
+ * and spreading into a typed parameter skips TypeScript's excess property
81
+ * check — so a shared object hides the mistake until Crowdin returns "Field
82
+ * 'type' is unexpected".
83
+ */
84
+ function replaceFileRequest({ storageId, fileSettings, attachLabelIds, }) {
85
+ return {
86
+ storageId,
87
+ exportOptions,
88
+ // Keeps translations of revised strings but drops their approvals, so a
89
+ // reviewer looks again rather than stale copy staying green.
90
+ updateOption: 'keep_translations',
91
+ ...(fileSettings?.importOptions && {
92
+ importOptions: fileSettings.importOptions,
93
+ }),
94
+ ...(attachLabelIds && { attachLabelIds }),
95
+ };
96
+ }
97
+ /** The request that creates a new file. See {@link replaceFileRequest}. */
98
+ function createFileRequest({ storageId, name, fileSettings, attachLabelIds, directoryId, }) {
99
+ return {
100
+ storageId,
101
+ name,
102
+ exportOptions,
103
+ ...fileSettings,
104
+ ...(attachLabelIds && { attachLabelIds }),
105
+ ...(directoryId !== undefined && { directoryId }),
106
+ };
107
+ }
69
108
  /** Uploads a source file to Crowdin, creating it or replacing its contents. */
70
109
  async function uploadSource({ projectId, source, token, label, fileSettings, }) {
71
110
  const client = new crowdin_api_client_1.default({ token });
@@ -74,6 +113,7 @@ async function uploadSource({ projectId, source, token, label, fileSettings, })
74
113
  const fileName = relativePath.split('/').pop();
75
114
  const contents = fs_1.default.readFileSync(source, 'utf8');
76
115
  const storage = await client.uploadStorageApi.addStorage(fileName, contents);
116
+ const storageId = storage.data.id;
77
117
  /*
78
118
  * Replacing a file needs its numeric id, and the API has no by-path
79
119
  * lookup, so this listing is how the id is obtained rather than a
@@ -85,41 +125,22 @@ async function uploadSource({ projectId, source, token, label, fileSettings, })
85
125
  limit: 500,
86
126
  });
87
127
  const existing = files.data.find((file) => file.data.path === `/${relativePath}`);
88
- const exportOptions = {
89
- exportPattern: `/${exports.CROWDIN_TRANSLATION_PATTERN}`,
90
- };
91
128
  const attachLabelIds = label ? [await labelId(client, id, label)] : undefined;
92
- /*
93
- * The two requests are built separately rather than sharing a spread. A
94
- * file's parser `type` is fixed at creation and the replace endpoint
95
- * rejects it outright, and spreading into a typed parameter skips
96
- * TypeScript's excess property check — so a shared object hides the
97
- * mistake until Crowdin returns "Field 'type' is unexpected".
98
- */
99
129
  if (existing) {
100
- const request = {
101
- storageId: storage.data.id,
102
- exportOptions,
103
- // Keeps translations of revised strings but drops their approvals, so
104
- // a reviewer looks again rather than stale copy staying green.
105
- updateOption: 'keep_translations',
106
- ...(fileSettings?.importOptions && {
107
- importOptions: fileSettings.importOptions,
108
- }),
109
- ...(attachLabelIds && { attachLabelIds }),
110
- };
111
- await client.sourceFilesApi.updateOrRestoreFile(id, existing.data.id, request);
130
+ await client.sourceFilesApi.updateOrRestoreFile(id, existing.data.id, replaceFileRequest({ storageId, fileSettings, attachLabelIds }));
112
131
  return;
113
132
  }
114
- const request = {
115
- storageId: storage.data.id,
133
+ // Root-level files have no directory to look up.
134
+ const directories = relativePath.includes('/')
135
+ ? await client.sourceFilesApi.listProjectDirectories(id, { limit: 500 })
136
+ : { data: [] };
137
+ await client.sourceFilesApi.createFile(id, createFileRequest({
138
+ storageId,
116
139
  name: fileName,
117
- exportOptions,
118
- ...fileSettings,
119
- ...(attachLabelIds && { attachLabelIds }),
120
- ...(await directoryId(client, id, relativePath)),
121
- };
122
- await client.sourceFilesApi.createFile(id, request);
140
+ fileSettings,
141
+ attachLabelIds,
142
+ directoryId: findDirectoryId(directories.data.map((entry) => entry.data), id, relativePath),
143
+ }));
123
144
  }
124
145
  /** Resolves a label by title, creating it if Crowdin does not have it. */
125
146
  async function labelId(client, projectId, title) {
@@ -129,23 +150,14 @@ async function labelId(client, projectId, title) {
129
150
  return existing.data.id;
130
151
  return (await client.labelsApi.addLabel(projectId, { title })).data.id;
131
152
  }
132
- /**
133
- * Resolves the directory a new file belongs in, or nothing for projects that
134
- * keep sources at the root.
135
- *
136
- * A missing directory is an error rather than something to create: a
137
- * misspelled path that gets created silently uploads to a new file and
138
- * orphans every translation attached to the real one.
139
- */
140
- async function directoryId(client, projectId, relativePath) {
153
+ function findDirectoryId(directories, projectId, relativePath) {
141
154
  const segments = relativePath.split('/').slice(0, -1);
142
155
  if (segments.length === 0)
143
- return {};
156
+ return undefined;
144
157
  const wanted = `/${segments.join('/')}`;
145
- const directories = await client.sourceFilesApi.listProjectDirectories(projectId, { limit: 500 });
146
- const match = directories.data.find((entry) => entry.data.path === wanted);
158
+ const match = directories.find((directory) => directory.path === wanted);
147
159
  if (!match) {
148
160
  throw new Error(`Crowdin project ${projectId} has no ${wanted} directory. Create it before uploading.`);
149
161
  }
150
- return { directoryId: match.data.id };
162
+ return match.id;
151
163
  }