Files
church-translate/app/server/public/languages.js
T
christianandClaude Opus 5 c9eef0672c Church Translate, as it runs at the campuses
Live sermon translation into 100+ languages, running entirely on one Mac per
campus: audio in, voice detection, Whisper on the GPU, NLLB-200 in its own
process, and a built-in web server the congregation reads from on their
phones. No audio leaves the building.

This is the source only. What used to sit beside it — 14 GB of speech and
translation models, 8 GB of voice bundles, 6 GB of natural voices, the Electron
caches, and the app data holding the operator PIN and every service transcript
— is machine state and downloaded assets, and stays in the NAS backup where it
belongs. The vendored node, cloudflared and Windows ffmpeg binaries are out too
(533 MB, re-fetched for packaging); the browser-side runtime beside them is
kept, so a checkout still serves the congregation page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 15:41:21 -04:00

187 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* languages.js — the language catalog, shared by the NAS server, the listener
* page, and the Mac engine.
*
* `code` is a FLORES-200 code, which is what NLLB-200 expects. `tts` is the
* BCP-47 tag we hand to the phone's speechSynthesis; it is a best effort, and a
* phone that has no matching voice simply shows captions without speaking.
*
* `native` is what the listener actually sees in the picker. Never show someone
* the English name of their own language when you can help it.
*/
(function (root, factory) {
if (typeof module === 'object' && module.exports) module.exports = factory();
else root.Languages = factory();
}(typeof self !== 'undefined' ? self : this, function () {
// [ FLORES code, English name, native name, BCP-47 for TTS, rtl ]
var RAW = [
['eng_Latn', 'English', 'English', 'en-US'],
['spa_Latn', 'Spanish (Latin America)', 'Español (América Latina)', 'es-MX'],
['zho_Hans', 'Chinese (Simplified)', '简体中文', 'zh-CN'],
['zho_Hant', 'Chinese (Traditional)','繁體中文', 'zh-TW'],
['yue_Hant', 'Cantonese', '廣東話', 'zh-HK'],
['fra_Latn', 'French', 'Français', 'fr-FR'],
/* One NLLB Portuguese, two voices: the Brazilian and European variants
share their translation (por_PT is aliased to por_Latn at the MT
boundary in engine.js) and differ in the voice that reads it. */
['por_Latn', 'Portuguese (Brazil)', 'Português (Brasil)', 'pt-BR'],
['por_PT', 'Portuguese (Portugal)', 'Português (Portugal)', 'pt-PT'],
/* Same NLLB Spanish, read in a Castilian voice. There is no grammatical
transform between them: converting ustedes->vosotros needs to know
whether "you" was one person or a room, and neither the English nor
the Spanish carries that — see spa-variants.js for the measurements. */
['spa_ES', 'Spanish (Spain)', 'Español (España)', 'es-ES'],
/* Mexico shares Latin America's grammar exactly (RAE: vosotros "carecen
de uso en América", and Mexico is tuteo-exclusive), so the text is
identical — this exists so a Mexican listener sees their own country
named, and so the operator can give it its own voice. */
['spa_MX', 'Spanish (Mexico)', 'Español (México)', 'es-MX'],
['kor_Hang', 'Korean', '한국어', 'ko-KR'],
['vie_Latn', 'Vietnamese', 'Tiếng Việt', 'vi-VN'],
['hat_Latn', 'Haitian Creole', 'Kreyòl Ayisyen', 'ht-HT'],
['arb_Arab', 'Arabic', 'العربية', 'ar-SA', true],
['rus_Cyrl', 'Russian', 'Русский', 'ru-RU'],
['ukr_Cyrl', 'Ukrainian', 'Українська', 'uk-UA'],
['ron_Latn', 'Romanian', 'Română', 'ro-RO'],
['pol_Latn', 'Polish', 'Polski', 'pl-PL'],
['deu_Latn', 'German', 'Deutsch', 'de-DE'],
['ita_Latn', 'Italian', 'Italiano', 'it-IT'],
['nld_Latn', 'Dutch', 'Nederlands', 'nl-NL'],
['tgl_Latn', 'Tagalog', 'Tagalog', 'fil-PH'],
['ceb_Latn', 'Cebuano', 'Cebuano', 'fil-PH'],
['ind_Latn', 'Indonesian', 'Bahasa Indonesia', 'id-ID'],
['zsm_Latn', 'Malay', 'Bahasa Melayu', 'ms-MY'],
['tha_Thai', 'Thai', 'ไทย', 'th-TH'],
['khm_Khmr', 'Khmer', 'ភាសាខ្មែរ', 'km-KH'],
['lao_Laoo', 'Lao', 'ລາວ', 'lo-LA'],
['mya_Mymr', 'Burmese', 'မြန်မာ', 'my-MM'],
['shn_Mymr', 'Shan', ' လိၵ်ႈတႆး', 'shn'],
['kac_Latn', 'Kachin (Jingpho)', 'Jinghpaw', 'kac'],
['npi_Deva', 'Nepali', 'नेपाली', 'ne-NP'],
['hin_Deva', 'Hindi', 'हिन्दी', 'hi-IN'],
['urd_Arab', 'Urdu', 'اردو', 'ur-PK', true],
['ben_Beng', 'Bengali', 'বাংলা', 'bn-BD'],
['pan_Guru', 'Punjabi', 'ਪੰਜਾਬੀ', 'pa-IN'],
['guj_Gujr', 'Gujarati', 'ગુજરાતી', 'gu-IN'],
['tam_Taml', 'Tamil', 'தமிழ்', 'ta-IN'],
['tel_Telu', 'Telugu', 'తెలుగు', 'te-IN'],
['mal_Mlym', 'Malayalam', 'മലയാളം', 'ml-IN'],
['kan_Knda', 'Kannada', 'ಕನ್ನಡ', 'kn-IN'],
['mar_Deva', 'Marathi', 'मराठी', 'mr-IN'],
['sin_Sinh', 'Sinhala', 'සිංහල', 'si-LK'],
['pes_Arab', 'Persian (Farsi)', 'فارسی', 'fa-IR', true],
['prs_Arab', 'Dari', 'دری', 'fa-AF', true],
['pbt_Arab', 'Pashto', 'پښتو', 'ps-AF', true],
['ckb_Arab', 'Kurdish (Sorani)', 'کوردی', 'ckb', true],
['kmr_Latn', 'Kurdish (Kurmanji)', 'Kurmancî', 'ku'],
['heb_Hebr', 'Hebrew', 'עברית', 'he-IL', true],
['tur_Latn', 'Turkish', 'Türkçe', 'tr-TR'],
['amh_Ethi', 'Amharic', 'አማርኛ', 'am-ET'],
['tir_Ethi', 'Tigrinya', 'ትግርኛ', 'ti-ET'],
['som_Latn', 'Somali', 'Soomaali', 'so-SO'],
['swh_Latn', 'Swahili', 'Kiswahili', 'sw-KE'],
['yor_Latn', 'Yoruba', 'Yorùbá', 'yo-NG'],
['ibo_Latn', 'Igbo', 'Igbo', 'ig-NG'],
['hau_Latn', 'Hausa', 'Hausa', 'ha-NG'],
['lin_Latn', 'Lingala', 'Lingála', 'ln'],
['kin_Latn', 'Kinyarwanda', 'Ikinyarwanda', 'rw-RW'],
['run_Latn', 'Kirundi', 'Ikirundi', 'rn'],
['lug_Latn', 'Luganda', 'Luganda', 'lg'],
['nya_Latn', 'Chichewa', 'Chichewa', 'ny'],
['sna_Latn', 'Shona', 'chiShona', 'sn'],
['zul_Latn', 'Zulu', 'isiZulu', 'zu-ZA'],
['xho_Latn', 'Xhosa', 'isiXhosa', 'xh-ZA'],
['afr_Latn', 'Afrikaans', 'Afrikaans', 'af-ZA'],
['jpn_Jpan', 'Japanese', '日本語', 'ja-JP'],
['ell_Grek', 'Greek', 'Ελληνικά', 'el-GR'],
['hun_Latn', 'Hungarian', 'Magyar', 'hu-HU'],
['ces_Latn', 'Czech', 'Čeština', 'cs-CZ'],
['slk_Latn', 'Slovak', 'Slovenčina', 'sk-SK'],
['bul_Cyrl', 'Bulgarian', 'Български', 'bg-BG'],
['srp_Cyrl', 'Serbian', 'Српски', 'sr-RS'],
['hrv_Latn', 'Croatian', 'Hrvatski', 'hr-HR'],
['bos_Latn', 'Bosnian', 'Bosanski', 'bs-BA'],
['slv_Latn', 'Slovenian', 'Slovenščina', 'sl-SI'],
['als_Latn', 'Albanian', 'Shqip', 'sq-AL'],
['lit_Latn', 'Lithuanian', 'Lietuvių', 'lt-LT'],
['lvs_Latn', 'Latvian', 'Latviešu', 'lv-LV'],
['est_Latn', 'Estonian', 'Eesti', 'et-EE'],
['fin_Latn', 'Finnish', 'Suomi', 'fi-FI'],
['swe_Latn', 'Swedish', 'Svenska', 'sv-SE'],
['nob_Latn', 'Norwegian', 'Norsk', 'nb-NO'],
['dan_Latn', 'Danish', 'Dansk', 'da-DK'],
['isl_Latn', 'Icelandic', 'Íslenska', 'is-IS'],
['kat_Geor', 'Georgian', 'ქართული', 'ka-GE'],
['hye_Armn', 'Armenian', 'Հայերեն', 'hy-AM'],
['azj_Latn', 'Azerbaijani', 'Azərbaycan', 'az-AZ'],
['uzn_Latn', 'Uzbek', 'Oʻzbek', 'uz-UZ'],
['kaz_Cyrl', 'Kazakh', 'Қазақша', 'kk-KZ'],
['kir_Cyrl', 'Kyrgyz', 'Кыргызча', 'ky-KG'],
['tgk_Cyrl', 'Tajik', 'Тоҷикӣ', 'tg-TJ'],
['khk_Cyrl', 'Mongolian', 'Монгол', 'mn-MN'],
['bod_Tibt', 'Tibetan', 'བོད་སྐད་', 'bo'],
['gle_Latn', 'Irish', 'Gaeilge', 'ga-IE'],
['cym_Latn', 'Welsh', 'Cymraeg', 'cy-GB'],
['eus_Latn', 'Basque', 'Euskara', 'eu-ES'],
['cat_Latn', 'Catalan', 'Català', 'ca-ES'],
['glg_Latn', 'Galician', 'Galego', 'gl-ES'],
['quy_Latn', 'Quechua', 'Runasimi', 'qu'],
['ayr_Latn', 'Aymara', 'Aymar aru', 'ay'],
['grn_Latn', 'Guarani', "Avañe'ẽ", 'gn'],
['mri_Latn', 'Maori', 'Te Reo Māori', 'mi-NZ'],
['smo_Latn', 'Samoan', 'Gagana Samoa', 'sm'],
['fij_Latn', 'Fijian', 'Na Vosa Vakaviti', 'fj'],
['tpi_Latn', 'Tok Pisin', 'Tok Pisin', 'tpi']
];
var LANGS = RAW.map(function (r) {
return { code: r[0], name: r[1], native: r[2], tts: r[3], rtl: !!r[4] };
});
var BY_CODE = {};
LANGS.forEach(function (l) { BY_CODE[l.code] = l; });
// A sane opening set for a US church. The operator can change this at any time.
var DEFAULT_FEATURED = [
'spa_Latn', 'zho_Hans', 'por_Latn', 'fra_Latn',
'kor_Hang', 'vie_Latn', 'hat_Latn', 'arb_Arab'
];
function get(code) { return BY_CODE[code] || null; }
function label(code) {
var l = BY_CODE[code];
if (!l) return code;
return l.native === l.name ? l.native : l.native + ' · ' + l.name;
}
/* Case- and accent-insensitive search across both names and the code, so
"espanol", "Español", "spanish" and "spa" all find Spanish. */
function normalize(s) {
return String(s || '')
.toLowerCase()
.normalize('NFD')
.replace(/[̀-ͯ]/g, '');
}
function search(query) {
var q = normalize(query).trim();
if (!q) return LANGS.slice();
return LANGS.filter(function (l) {
return normalize(l.name).indexOf(q) !== -1 ||
normalize(l.native).indexOf(q) !== -1 ||
normalize(l.code).indexOf(q) !== -1;
});
}
return {
LANGS: LANGS,
DEFAULT_FEATURED: DEFAULT_FEATURED,
get: get,
label: label,
search: search,
codes: function () { return LANGS.map(function (l) { return l.code; }); }
};
}));