diff --git a/src/main/configStore.ts b/src/main/configStore.ts index c360b05d..a1833ccc 100644 --- a/src/main/configStore.ts +++ b/src/main/configStore.ts @@ -9,6 +9,7 @@ import { copyFile } from 'node:fs/promises'; import { KeyBinding, Config } from '../../types.js'; import logger from './logger.js'; import { isWindows } from './util.js'; +import { fallbackLng } from './i18nCommon.js'; const { app } = electron; @@ -117,7 +118,7 @@ const defaults: Config = { autoSaveProjectFile: true, wheelSensitivity: 0.2, waveformHeight: 40, - language: undefined, + language: fallbackLng, ffmpegExperimental: false, preserveChapters: true, preserveMetadata: 'default', diff --git a/src/main/i18n.ts b/src/main/i18n.ts index 12417aab..3053f5b5 100644 --- a/src/main/i18n.ts +++ b/src/main/i18n.ts @@ -1,5 +1,7 @@ import i18n from 'i18next'; import Backend from 'i18next-fs-backend'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { app } from 'electron'; import { commonI18nOptions, loadPath, addPath } from './i18nCommon.js'; @@ -8,12 +10,7 @@ import { commonI18nOptions, loadPath, addPath } from './i18nCommon.js'; // https://github.com/i18next/i18next/issues/869 export default i18n .use(Backend) - // detect user language - // learn more: https://github.com/i18next/i18next-browser-languageDetector - // TODO disabled for now because translations need more reviewing https://github.com/mifi/lossless-cut/issues/346 - // .use(LanguageDetector) - // init i18next - // for all options read: https://www.i18next.com/overview/configuration-options + .use({ type: 'languageDetector', async: false, detect: () => app.getLocale() }) // See also i18next-parser.config.mjs .init({ ...commonI18nOptions, diff --git a/src/main/i18nCommon.ts b/src/main/i18nCommon.ts index 11a06c60..dbb866e8 100644 --- a/src/main/i18nCommon.ts +++ b/src/main/i18nCommon.ts @@ -4,6 +4,7 @@ import { app } from 'electron'; import { join } from 'node:path'; import { InitOptions } from 'i18next'; +import { ElectronLanguageKey, SupportedLanguage } from '../../types'; let customLocalesPath: string | undefined; @@ -20,22 +21,47 @@ function getLangPath(subPath: string) { // Weblate hardcodes different lang codes than electron // https://www.electronjs.org/docs/api/app#appgetlocale // https://source.chromium.org/chromium/chromium/src/+/master:ui/base/l10n/l10n_util.cc -const mapLang = (lng: string) => ({ - nb: 'nb_NO', - no: 'nb_NO', - zh: 'zh_Hans', - // https://source.chromium.org/chromium/chromium/src/+/master:ui/base/l10n/l10n_util.cc;l=354 - 'zh-CN': 'zh_Hans', // Chinese simplified (mainland China) - 'zh-TW': 'zh_Hant', // Chinese traditional (Taiwan) - fr: 'fr', - 'fr-CA': 'fr', - 'fr-CH': 'fr', - 'fr-FR': 'fr', - it: 'it', - 'it-CH': 'it', - 'it-IT': 'it', - 'ru-RU': 'ru', -}[lng] || lng); +function mapLang(lng: ElectronLanguageKey) { + const map: Partial> = { + 'de-AT': 'de', + 'de-CH': 'de', + 'de-DE': 'de', + 'de-LI': 'de', + + 'es-419': 'es', + 'es-AR': 'es', + 'es-CL': 'es', + 'es-CO': 'es', + 'es-CR': 'es', + 'es-ES': 'es', + 'es-HN': 'es', + 'es-MX': 'es', + 'es-PE': 'es', + 'es-US': 'es', + 'es-UY': 'es', + 'es-VE': 'es', + + 'fr-CA': 'fr', + 'fr-CH': 'fr', + 'fr-FR': 'fr', + + nb: 'nb_NO', + no: 'nb_NO', + + zh: 'zh_Hans', + 'zh-CN': 'zh_Hans', + 'zh-TW': 'zh_Hant', + 'zh-HK': 'zh_Hant', + + 'pt-BR': 'pt_BR', + 'pt-PT': 'pt', + + 'it-CH': 'it', + 'it-IT': 'it', + }; + + return map[lng] ?? lng; +} export const fallbackLng = 'en'; @@ -53,5 +79,5 @@ export const commonI18nOptions: InitOptions = { nsSeparator: false, }; -export const loadPath = (lng: string, ns: string) => getLangPath(`${mapLang(lng)}/${ns}.json`); -export const addPath = (lng: string, ns: string) => getLangPath(`${mapLang(lng)}/${ns}.missing.json`); +export const loadPath = (lng: SupportedLanguage, ns: string) => getLangPath(`${mapLang(lng)}/${ns}.json`); +export const addPath = (lng: SupportedLanguage, ns: string) => getLangPath(`${mapLang(lng)}/${ns}.missing.json`); diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 6e799f17..3ed0882a 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -80,7 +80,6 @@ import { adjustRate } from './util/rate-calculator'; import { askExtractFramesAsImages } from './dialogs/extractFrames'; import { askForOutDir, askForImportChapters, askForFileOpenAction, showDiskFull, showExportFailedDialog, showConcatFailedDialog, openYouTubeChaptersDialog, showRefuseToOverwrite, showOpenDialog, showMuxNotSupported, promptDownloadMediaUrl, CleanupChoicesType, showOutputNotWritable, deleteFiles, mustDisallowVob, toastError } from './dialogs'; import { openSendReportDialog } from './reporting'; -import { fallbackLng } from './i18n'; import { sortSegments, convertSegmentsToChaptersWithGaps, hasAnySegmentOverlap, isDurationValid, getPlaybackAction, getSegmentTags, filterNonMarkers } from './segments'; import { generateCutFileNames as generateCutFileNamesRaw, generateCutMergedFileNames as generateCutMergedFileNamesRaw, generateMergedFileNames as generateMergedFileNamesRaw, defaultCutFileTemplate, defaultCutMergedFileTemplate, defaultMergedFileTemplate, GenerateMergedOutFileNamesParams, GeneratedOutFileNames } from './util/outputNameTemplate'; import { rightBarWidth, leftBarWidth, ffmpegExtractWindow, zoomMax } from './util/constants'; @@ -204,9 +203,8 @@ function App() { const mergedFileTemplateOrDefault = mergedFileTemplate || defaultMergedFileTemplate; useEffect(() => { - const l = language || fallbackLng; - i18n.changeLanguage(l).catch(console.error); - electron.ipcRenderer.send('setLanguage', l); + i18n.changeLanguage(language).catch(console.error); + electron.ipcRenderer.send('setLanguage', language); }, [language]); diff --git a/src/renderer/src/components/ConcatSheet.tsx b/src/renderer/src/components/ConcatSheet.tsx index eaa50a58..ab738e47 100644 --- a/src/renderer/src/components/ConcatSheet.tsx +++ b/src/renderer/src/components/ConcatSheet.tsx @@ -103,7 +103,6 @@ function ConcatSheet({ isShown, onHide, paths, mergedFileTemplate, generateMerge useEffect(() => { const abortController = new AbortController(); - console.log('wat', fileFormat); (async () => { // in simple mode, set merged file template to a generated name based on first file, so they don't *have to* deal with variables diff --git a/src/renderer/src/components/Settings.tsx b/src/renderer/src/components/Settings.tsx index 3c6ccef4..857c4d7e 100644 --- a/src/renderer/src/components/Settings.tsx +++ b/src/renderer/src/components/Settings.tsx @@ -10,7 +10,7 @@ import Switch from './Switch'; import useUserSettings from '../hooks/useUserSettings'; import { askForFfPath } from '../dialogs'; import { isMasBuild, isStoreBuild } from '../util'; -import { Config, LanguageKey, ModifierKey, TimecodeFormat, langNames } from '../../../../types'; +import { Config, SupportedLanguage, ModifierKey, TimecodeFormat, langNames } from '../../../../types'; import styles from './Settings.module.css'; import SelectRaw, { SelectProps } from './Select'; import ButtonRaw, { ButtonProps } from './Button'; @@ -74,7 +74,7 @@ function Settings({ const onLangChange = useCallback>((e) => { const { value } = e.target; const l = value !== '' ? value : undefined; - setLanguage(l as LanguageKey | undefined); + setLanguage(l as SupportedLanguage | undefined); }, [setLanguage]); const timecodeFormatOptions = useMemo>(() => ({ diff --git a/src/renderer/src/i18n.ts b/src/renderer/src/i18n.ts index dbad35f7..dc91fdb4 100644 --- a/src/renderer/src/i18n.ts +++ b/src/renderer/src/i18n.ts @@ -3,29 +3,18 @@ import { initReactI18next } from 'react-i18next'; const Backend = window.require('i18next-fs-backend'); -const { i18n: { commonI18nOptions, fallbackLng, loadPath, addPath } } = window.require('@electron/remote').require('./index.js'); +const remote = window.require('@electron/remote'); +const { i18n: { commonI18nOptions, loadPath, addPath } } = remote.require('./index.js'); -// https://www.i18next.com/overview/typescript#argument-of-type-defaulttfuncreturn-is-not-assignable-to-parameter-of-type-xyz -// todo This should not be necessary anymore since v23.0.0 -declare module 'i18next' { - interface CustomTypeOptions { - returnNull: false; - } -} -export { fallbackLng }; + +// See also main // https://github.com/i18next/react-i18next/blob/master/example/react/src/i18n.js // https://github.com/i18next/i18next/issues/869 i18n .use(Backend) - // detect user language - // learn more: https://github.com/i18next/i18next-browser-languageDetector - // LanguageDetector is disabled because many users are used to english, and I cannot guarantee the status of all the translations so it's best to default to engligh https://github.com/mifi/lossless-cut/issues/346 - // .use(LanguageDetector) - // pass the i18n instance to react-i18next. + .use({ type: 'languageDetector', async: false, detect: () => remote.app.getLocale() }) .use(initReactI18next) - // init i18next - // for all options read: https://www.i18next.com/overview/configuration-options // See also i18next-parser.config.mjs .init({ ...commonI18nOptions, diff --git a/types.ts b/types.ts index f432a85d..355ad4bb 100644 --- a/types.ts +++ b/types.ts @@ -7,7 +7,197 @@ export interface KeyBinding { export type CaptureFormat = 'jpeg' | 'png' | 'webp'; -// https://www.electronjs.org/docs/api/locales +// returned from app.getLocale() +// https://www.electronjs.org/docs/api/app#appgetlocale +// https://source.chromium.org/chromium/chromium/src/+/master:ui/base/l10n/l10n_util.cc +export type ElectronLanguageKey = +'af' // Afrikaans +| 'ak' // Twi +| 'am' // Amharic +| 'an' // Aragonese +| 'ar' // Arabic +| 'as' // Assamese +| 'ast' // Asturian +| 'ay' // Aymara +| 'az' // Azerbaijani +| 'be' // Belarusian +| 'bg' // Bulgarian +| 'bho' // Bhojpuri +| 'bm' // Bambara +| 'bn' // Bengali +| 'br' // Breton +| 'bs' // Bosnian +| 'ca' // Catalan +| 'ceb' // Cebuano +| 'chr' // Cherokee +| 'ckb' // Kurdish (Arabic), Sorani +| 'co' // Corsican +| 'cs' // Czech +| 'cy' // Welsh +| 'da' // Danish +| 'de' // German +| 'de-AT' // German (Austria) +| 'de-CH' // German (Switzerland) +| 'de-DE' // German (Germany) +| 'de-LI' // German (Liechtenstein) +| 'doi' // Dogri +| 'dv' // Dhivehi +| 'ee' // Ewe +| 'el' // Greek +| 'en' // English +| 'en-AU' // English (Australia) +| 'en-CA' // English (Canada) +| 'en-GB' // English (UK) +| 'en-GB-oxendict' // English (UK, OED spelling) +| 'en-IE' // English (Ireland) +| 'en-IN' // English (India) +| 'en-NZ' // English (New Zealand) +| 'en-US' // English (US) +| 'en-ZA' // English (South Africa) +| 'eo' // Esperanto +| 'es' // Spanish +| 'es-419' // Spanish (Latin America) +| 'es-AR' // Spanish (Argentina) +| 'es-CL' // Spanish (Chile) +| 'es-CO' // Spanish (Colombia) +| 'es-CR' // Spanish (Costa Rica) +| 'es-ES' // Spanish (Spain) +| 'es-HN' // Spanish (Honduras) +| 'es-MX' // Spanish (Mexico) +| 'es-PE' // Spanish (Peru) +| 'es-US' // Spanish (US) +| 'es-UY' // Spanish (Uruguay) +| 'es-VE' // Spanish (Venezuela) +| 'et' // Estonian +| 'eu' // Basque +| 'fa' // Persian +| 'fi' // Finnish +| 'fil' // Filipino +| 'fo' // Faroese +| 'fr' // French +| 'fr-CA' // French (Canada) +| 'fr-CH' // French (Switzerland) +| 'fr-FR' // French (France) +| 'fy' // Frisian +| 'ga' // Irish +| 'gd' // Scots Gaelic +| 'gl' // Galician +| 'gn' // Guarani +| 'gu' // Gujarati +| 'ha' // Hausa +| 'haw' // Hawaiian +| 'he' // Hebrew +| 'hi' // Hindi +| 'hmn' // Hmong +| 'hr' // Croatian +| 'ht' // Haitian Creole +| 'hu' // Hungarian +| 'hy' // Armenian +| 'ia' // Interlingua +| 'id' // Indonesian +| 'ig' // Igbo +| 'ilo' // Ilocano +| 'is' // Icelandic +| 'it' // Italian +| 'it-CH' // Italian (Switzerland) +| 'it-IT' // Italian (Italy) +| 'ja' // Japanese +| 'jv' // Javanese +| 'ka' // Georgian +| 'kk' // Kazakh +| 'km' // Cambodian +| 'kn' // Kannada +| 'ko' // Korean +| 'kok' // Konkani +| 'kri' // Krio +| 'ku' // Kurdish +| 'ky' // Kyrgyz +| 'la' // Latin +| 'lb' // Luxembourgish +| 'lg' // Luganda +| 'ln' // Lingala +| 'lo' // Laothian +| 'lt' // Lithuanian +| 'lus' // Mizo +| 'lv' // Latvian +| 'mai' // Maithili +| 'mg' // Malagasy +| 'mi' // Maori +| 'mk' // Macedonian +| 'ml' // Malayalam +| 'mn' // Mongolian +| 'mni-Mtei' // Manipuri (Meitei Mayek) +| 'mo' // Moldavian +| 'mr' // Marathi +| 'ms' // Malay +| 'mt' // Maltese +| 'my' // Burmese +| 'nb' // Norwegian (Bokmal) +| 'ne' // Nepali +| 'nl' // Dutch +| 'nn' // Norwegian (Nynorsk) +| 'no' // Norwegian +| 'nso' // Sepedi +| 'ny' // Nyanja +| 'oc' // Occitan +| 'om' // Oromo +| 'or' // Odia (Oriya) +| 'pa' // Punjabi +| 'pl' // Polish +| 'ps' // Pashto +| 'pt' // Portuguese +| 'pt-BR' // Portuguese (Brazil) +| 'pt-PT' // Portuguese (Portugal) +| 'qu' // Quechua +| 'rm' // Romansh +| 'ro' // Romanian +| 'ru' // Russian +| 'rw' // Kinyarwanda +| 'sa' // Sanskrit +| 'sd' // Sindhi +| 'sh' // Serbo-Croatian +| 'si' // Sinhalese +| 'sk' // Slovak +| 'sl' // Slovenian +| 'sm' // Samoan +| 'sn' // Shona +| 'so' // Somali +| 'sq' // Albanian +| 'sr' // Serbian +| 'st' // Sesotho +| 'su' // Sundanese +| 'sv' // Swedish +| 'sw' // Swahili +| 'ta' // Tamil +| 'te' // Telugu +| 'tg' // Tajik +| 'th' // Thai +| 'ti' // Tigrinya +| 'tk' // Turkmen +| 'tn' // Tswana +| 'to' // Tonga +| 'tr' // Turkish +| 'ts' // Tsonga +| 'tt' // Tatar +| 'tw' // Twi +| 'ug' // Uyghur +| 'uk' // Ukrainian +| 'ur' // Urdu +| 'uz' // Uzbek +| 'vi' // Vietnamese +| 'wa' // Walloon +| 'wo' // Wolof +| 'xh' // Xhosa +| 'yi' // Yiddish +| 'yo' // Yoruba +| 'zh' // Chinese +| 'zh-CN' // Chinese (China) +| 'zh-HK' // Chinese (Hong Kong) +| 'zh-TW' // Chinese (Taiwan) +| 'zu' // Zulu + +// https://www.electronjs.org/docs/api/app#appgetlocale +// https://source.chromium.org/chromium/chromium/src/+/master:ui/base/l10n/l10n_util.cc // See i18n.js export const langNames = { en: 'English', @@ -21,23 +211,25 @@ export const langNames = { nn: 'Norsk (nynorsk)', pl: 'Polski', pt: 'Português', - pt_BR: 'Português do Brasil', + 'pt-BR': 'Português do Brasil', sl: 'Slovenščina', - sk: 'slovenčina', + sk: 'Slovenčina', fi: 'Suomi', ru: 'Русский', - uk: 'українська', + uk: 'Українська', // sr: 'Cрпски', tr: 'Türkçe', vi: 'Tiếng Việt', ja: '日本語', zh: '中文', - zh_Hant: '繁體中文', - zh_Hans: '简体中文', + 'zh-TW': '繁體中文', + 'zh-CN': '简体中文', ko: '한국어', -}; +} satisfies Partial>; + +type ExtraLanguages = 'no'; -export type LanguageKey = keyof typeof langNames; +export type SupportedLanguage = (keyof typeof langNames) | ExtraLanguages; export type TimecodeFormat = 'timecodeWithDecimalFraction' | 'frameCount' | 'seconds' | 'timecodeWithFramesFraction'; @@ -73,7 +265,7 @@ export interface Config { autoSaveProjectFile: boolean, wheelSensitivity: number, waveformHeight: number, - language: LanguageKey | undefined, + language: SupportedLanguage | undefined, ffmpegExperimental: boolean, preserveChapters: boolean, preserveMetadata: PreserveMetadata,