upgrade i18next

also include more locales
and refactor a bit
pull/2575/head
Mikael Finstad 8 months ago
parent 2511117808
commit 2511114302
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -1,3 +1,4 @@
/dist
/out
/ts-dist
/main-ts-dist
/common-ts-dist

3
.gitignore vendored

@ -17,6 +17,7 @@ node_modules
/doc
/ffmpeg
/app*.log
/ts-dist
/main-ts-dist
/common-ts-dist
tsconfig.*.tsbuildinfo

@ -1,22 +0,0 @@
// eslint-disable-line unicorn/filename-case
export default {
input: ['src/renderer/**/*.{js,jsx,ts,tsx}', 'src/main/*.{js,ts}'],
output: 'locales/$LOCALE/$NAMESPACE.json',
indentation: 4,
sort: true,
locales: ['en'],
lexers: {
js: ['JavascriptLexer'],
jsx: ['JsxLexer'],
},
defaultValue: (lng, ns, key) => key,
// Keep in sync between i18next-parser.config.js and i18nCommon.js:
keySeparator: false,
namespaceSeparator: false,
};

@ -0,0 +1,37 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { defineConfig } from 'i18next-cli';
import { langNames, mapLang, SupportedLanguage } from './src/common/i18n.js';
export default defineConfig({
locales: Object.keys(langNames).map((lng) => mapLang(lng as SupportedLanguage)),
extract: {
input: [
'src/renderer/**/*.{ts,tsx}',
'src/main/**/*.ts',
'src/common/**/*.ts',
'src/preload/**/*.ts',
],
output: 'locales/{{language}}/{{namespace}}.json',
defaultNS: 'translation',
functions: [
't',
'*.t',
],
transComponents: [
'Trans',
],
ignoredAttributes: ['data-testid', 'aria-label', 'role'],
// Keep in sync between i18next-parser.config.js and i18nCommon.js:
keySeparator: false,
nsSeparator: false,
},
types: {
input: [
'locales/{{language}}/{{namespace}}.json',
],
output: 'src/types/i18next.d.ts',
},
});

@ -7,7 +7,7 @@
"main": "./out/main/index.js",
"homepage": "./",
"scripts": {
"clean": "rimraf dist out ts-dist build-resources icon-build",
"clean": "rimraf dist out common-ts-dist main-ts-dist build-resources icon-build",
"start": "electron-vite preview",
"dev": "electron-vite dev -w",
"icon-gen": "mkdirp icon-build build-resources/appx && tsx script/icon-gen.mts",
@ -25,7 +25,7 @@
"postinstall": "electron-builder install-app-deps",
"version": "tsx script/postversion.mts && git add no.mifi.losslesscut.appdata.xml",
"pack-linux": "yarn build && electron-builder --linux",
"scan-i18n": "i18next --config i18next-parser.config.mjs",
"scan-i18n": "i18next-cli extract",
"generate-licenses": "yarn licenses generate-disclaimer > licenses.txt && echo '\n\nffmpeg is licensed under GPL v2+:\n\nhttp://www.gnu.org/licenses/old-licenses/gpl-2.0.html' >> licenses.txt"
},
"author": {
@ -96,6 +96,7 @@
"eslint-plugin-unicorn": "^51.0.1",
"fast-xml-parser": "^4.4.1",
"framer-motion": "^9.0.3",
"i18next-cli": "^1.22.2",
"i18next-parser": "^9.0.1",
"icon-gen": "^4.0.0",
"immer": "^10.0.2",
@ -111,7 +112,7 @@
"pretty-bytes": "^6.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^15.0.0",
"react-i18next": "^16.2.4",
"react-icons": "^4.1.0",
"react-leaflet": "^4.2.1",
"react-lottie-player": "^1.5.0",
@ -147,7 +148,7 @@
"express-async-handler": "^1.2.0",
"file-type": "patch:file-type@npm%3A19.4.1#~/.yarn/patches/file-type-npm-19.4.1-d18086444c.patch",
"fs-extra": "^8.1.0",
"i18next": "^23.12.2",
"i18next": "^25.6.1",
"i18next-fs-backend": "^2.3.2",
"json5": "^2.2.2",
"lodash.debounce": "^4.0.8",

@ -0,0 +1,268 @@
// 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',
cs: 'Čeština',
de: 'Deutsch',
es: 'Español',
fr: 'Français',
it: 'Italiano',
nl: 'Nederlands',
// nb: 'Norsk (bokmål)',
nn: 'Norsk (nynorsk)',
// sv: 'Svenska',
pl: 'Polski',
pt: 'Português',
'pt-BR': 'Português do Brasil',
sl: 'Slovenščina',
sk: 'Slovenčina',
fi: 'Suomi',
ru: 'Русский',
uk: 'Українська',
tr: 'Türkçe',
vi: 'Tiếng Việt',
ja: '日本語',
'zh-TW': '繁體中文',
'zh-CN': '简体中文',
ko: '한국어',
ta: 'தமிழ்',
lt: 'Lietuvių',
hu: 'Magyar',
} satisfies Partial<Record<ElectronLanguageKey, string>>;
export type SupportedLanguage = (keyof typeof langNames);
// 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
export function mapLang(lng: ElectronLanguageKey) {
const map: Partial<Record<ElectronLanguageKey, string>> = {
'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;
}

@ -1,3 +1,5 @@
import { SupportedLanguage } from './i18n';
export type KeyboardAction = 'addSegment' | 'togglePlayResetSpeed' | 'togglePlayNoResetSpeed' | 'reducePlaybackRate' | 'reducePlaybackRateMore' | 'increasePlaybackRate' | 'increasePlaybackRateMore' | 'timelineToggleComfortZoom' | 'seekPreviousFrame' | 'seekNextFrame' | 'captureSnapshot' | 'setCutStart' | 'setCutEnd' | 'removeCurrentSegment' | 'removeCurrentCutpoint' | 'cleanupFilesDialog' | 'splitCurrentSegment' | 'focusSegmentAtCursor' | 'selectSegmentsAtCursor' | 'increaseRotation' | 'goToTimecode' | 'seekBackwards' | 'seekBackwards2' | 'seekBackwards3' | 'seekBackwardsPercent' | 'seekBackwardsPercent' | 'seekBackwardsKeyframe' | 'jumpCutStart' | 'seekForwards' | 'seekForwards2' | 'seekForwards3' | 'seekForwardsPercent' | 'seekForwardsPercent' | 'seekForwardsKeyframe' | 'jumpCutEnd' | 'jumpTimelineStart' | 'jumpTimelineEnd' | 'jumpFirstSegment' | 'jumpPrevSegment' | 'jumpSeekFirstSegment' | 'jumpSeekPrevSegment' | 'timelineZoomIn' | 'timelineZoomIn' | 'batchPreviousFile' | 'jumpLastSegment' | 'jumpNextSegment' | 'jumpSeekLastSegment' | 'jumpSeekNextSegment' | 'timelineZoomOut' | 'timelineZoomOut' | 'batchNextFile' | 'batchOpenSelectedFile' | 'batchOpenPreviousFile' | 'batchOpenNextFile' | 'undo' | 'undo' | 'redo' | 'redo' | 'copySegmentsToClipboard' | 'copySegmentsToClipboard' | 'toggleFullscreenVideo' | 'labelCurrentSegment' | 'export' | 'toggleKeyboardShortcuts' | 'increaseVolume' | 'decreaseVolume' | 'toggleMuted' | 'detectBlackScenes' | 'detectSilentScenes' | 'detectSceneChanges' | 'toggleLastCommands' | 'play' | 'pause' | 'reloadFile' | 'html5ify' | 'makeCursorTimeZero' | 'togglePlayOnlyCurrentSegment' | 'toggleLoopOnlyCurrentSegment' | 'toggleLoopStartEndOnlyCurrentSegment' | 'togglePlaySelectedSegments' | 'toggleLoopSelectedSegments' | 'editCurrentSegmentTags' | 'duplicateCurrentSegment' | 'reorderSegsByStartTime' | 'invertAllSegments' | 'fillSegmentsGaps' | 'shiftAllSegmentTimes' | 'alignSegmentTimesToKeyframes' | 'readAllKeyframes' | 'createSegmentsFromKeyframes' | 'createFixedDurationSegments' | 'createNumSegments' | 'createFixedByteSizedSegments' | 'createRandomSegments' | 'shuffleSegments' | 'combineOverlappingSegments' | 'combineSelectedSegments' | 'clearSegments' | 'toggleSegmentsList' | 'selectOnlyCurrentSegment' | 'deselectAllSegments' | 'selectAllSegments' | 'toggleCurrentSegmentSelected' | 'invertSelectedSegments' | 'removeSelectedSegments' | 'toggleStreamsSelector' | 'extractAllStreams' | 'showStreamsSelector' | 'showIncludeExternalStreamsDialog' | 'captureSnapshotAsCoverArt' | 'extractCurrentSegmentFramesAsImages' | 'extractSelectedSegmentsFramesAsImages' | 'convertFormatBatch' | 'convertFormatCurrentFile' | 'fixInvalidDuration' | 'closeBatch' | 'concatBatch' | 'toggleKeyframeCutMode' | 'toggleCaptureFormat' | 'toggleStripAudio' | 'toggleStripVideo' | 'toggleStripSubtitle' | 'toggleStripThumbnail' | 'toggleStripCurrentFilter' | 'toggleStripAll' | 'toggleDarkMode' | 'setStartTimeOffset' | 'toggleWaveformMode' | 'toggleShowThumbnails' | 'toggleShowKeyframes' | 'toggleSettings' | 'openSendReportDialog' | 'openFilesDialog' | 'openDirDialog' | 'exportYouTube' | 'closeCurrentFile' | 'quit' | 'selectAllMarkers';
export interface KeyBinding {
@ -7,230 +9,6 @@ export interface KeyBinding {
export type CaptureFormat = 'jpeg' | 'png' | 'webp';
// 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',
cs: 'Čeština',
de: 'Deutsch',
es: 'Español',
fr: 'Français',
it: 'Italiano',
nl: 'Nederlands',
nb: 'Norsk (bokmål)',
nn: 'Norsk (nynorsk)',
pl: 'Polski',
pt: 'Português',
'pt-BR': 'Português do Brasil',
sl: 'Slovenščina',
sk: 'Slovenčina',
fi: 'Suomi',
ru: 'Русский',
uk: 'Українська',
// sr: 'Cрпски',
tr: 'Türkçe',
vi: 'Tiếng Việt',
ja: '日本語',
zh: '中文',
'zh-TW': '繁體中文',
'zh-CN': '简体中文',
ko: '한국어',
} satisfies Partial<Record<ElectronLanguageKey, string>>;
type ExtraLanguages = 'no';
export type SupportedLanguage = (keyof typeof langNames) | ExtraLanguages;
export type TimecodeFormat = 'timecodeWithDecimalFraction' | 'frameCount' | 'seconds' | 'timecodeWithFramesFraction';
export type AvoidNegativeTs = 'make_zero' | 'auto' | 'make_non_negative' | 'disabled';
@ -243,7 +21,6 @@ export type WaveformMode = 'big-waveform' | 'waveform';
export type FixCodecTagOption = 'always' | 'never' | 'auto';
export interface Config {
version: number,
lastAppVersion: string,

@ -6,7 +6,7 @@ import { pathExists } from 'fs-extra';
import assert from 'node:assert';
import { copyFile } from 'node:fs/promises';
import { KeyBinding, Config } from '../../types.js';
import { KeyBinding, Config } from '../common/types.js';
import logger from './logger.js';
import { isWindows } from './util.js';
import { fallbackLng } from './i18nCommon.js';

@ -8,7 +8,7 @@ import { Readable } from 'node:stream';
import { app } from 'electron';
import { platform, arch, isWindows, isLinux } from './util.js';
import { CaptureFormat, Waveform } from '../../types.js';
import { CaptureFormat, Waveform } from '../common/types.js';
import isDev from './isDev.js';
import logger from './logger.js';
import { parseFfmpegProgressLine } from './progress.js';

@ -4,7 +4,8 @@
import { app } from 'electron';
import { join } from 'node:path';
import { InitOptions } from 'i18next';
import { ElectronLanguageKey, SupportedLanguage } from '../../types';
import { mapLang, SupportedLanguage } from '../common/i18n';
let customLocalesPath: string | undefined;
@ -18,51 +19,6 @@ function getLangPath(subPath: string) {
return join('locales', subPath);
}
// 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
function mapLang(lng: ElectronLanguageKey) {
const map: Partial<Record<ElectronLanguageKey, string>> = {
'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';
export const commonI18nOptions: InitOptions = {

@ -30,7 +30,7 @@ import { checkNewVersion } from './updateChecker.js';
import * as i18nCommon from './i18nCommon.js';
import './i18n.js';
import { ApiActionRequest } from '../../types.js';
import { ApiActionRequest } from '../common/types.js';
export * as ffmpeg from './ffmpeg.js';

@ -86,8 +86,8 @@ import { rightBarWidth, leftBarWidth, ffmpegExtractWindow, zoomMax } from './uti
import BigWaveform from './components/BigWaveform';
import { BatchFile, Chapter, CustomTagsByFile, EdlExportType, EdlFileType, EdlImportType, FfmpegCommandLog, FilesMeta, goToTimecodeDirectArgsSchema, openFilesActionArgsSchema, ParamsByStreamId, PlaybackMode, SegmentBase, SegmentColorIndex, SegmentTags, StateSegment, TunerType } from './types';
import { CaptureFormat, KeyboardAction, ApiActionRequest } from '../../../types';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe';
import { CaptureFormat, KeyboardAction, ApiActionRequest } from '../../common/types.js';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../common/ffprobe.js';
import useLoading from './hooks/useLoading';
import useVideo from './hooks/useVideo';
import useTimecode from './hooks/useTimecode';

@ -25,7 +25,7 @@ import { isExactDurationMatch } from './util/duration';
import useUserSettings from './hooks/useUserSettings';
import { askForPlaybackRate, checkAppPath } from './dialogs';
import { FormatTimecode, ParseTimecode, PlaybackMode, SegmentColorIndex, SegmentToExport, StateSegment } from './types';
import { WaveformMode } from '../../../types';
import { WaveformMode } from '../../common/types';
import { Frame } from './ffmpeg';
const { clipboard } = window.require('electron');

@ -5,7 +5,7 @@ import { FaVideo } from 'react-icons/fa';
import isDev from './isDev';
import { ChromiumHTMLVideoElement } from './types';
import { FFprobeStream } from '../../../ffprobe';
import { FFprobeStream } from '../../common/ffprobe';
import { getFrameDuration } from './util';
const { compatPlayer: { createMediaSourceStream } } = window.require('@electron/remote').require('./index.js');

@ -7,7 +7,7 @@ import SetCutpointButton from './components/SetCutpointButton';
import SimpleModeButton from './components/SimpleModeButton';
import useUserSettings from './hooks/useUserSettings';
import { StateSegment } from './types';
import { KeyBinding } from '../../../types';
import { KeyBinding } from '../../common/types';
import { splitKeyboardKeys } from './util';
import { getModifier } from './hooks/useTimelineScroll';
import Kbd from './components/Kbd';

@ -14,7 +14,7 @@ import { FileStream, getStreamFps } from './ffmpeg';
import { deleteDispositionValue } from './util';
import { getActiveDisposition, attachedPicDisposition, isGpsStream } from './util/streams';
import TagEditor from './components/TagEditor';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../common/ffprobe';
import { CustomTagsByFile, FilesMeta, FormatTimecode, ParamsByStreamId, StreamParams } from './types';
import Button, { DialogButton } from './components/Button';
import Checkbox from './components/Checkbox';
@ -149,7 +149,7 @@ const EditStreamDialog = memo(({ editingStream: { streamId: editingStreamId, pat
<Dialog.Content style={{ width: '40em' }} aria-describedby={undefined}>
<Dialog.Title>{t('Edit track {{trackNum}} metadata', { trackNum: editingStream && (editingStream.index + 1) })}</Dialog.Title>
<h2>Parameters</h2>
<h2>{t('Parameters')}</h2>
{editingStream != null && <StreamParametersEditor stream={editingStream} streamParams={streamParams} updateStreamParams={(setter) => updateStreamParams(editingFile, editingStreamId, setter)} />}
<h2>Tags</h2>

@ -1,5 +1,5 @@
import { FaBolt } from 'react-icons/fa';
import { KeyboardAction } from '../../../../types';
import { KeyboardAction } from '../../../common/types';
export default function Action({ name }: { name: KeyboardAction }) {

@ -11,7 +11,7 @@ import OutputFormatSelect from './OutputFormatSelect';
import useUserSettings from '../hooks/useUserSettings';
import { isMov } from '../util/streams';
import { getOutDir } from '../util';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../../ffprobe';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../common/ffprobe';
import Button, { DialogButton } from './Button';
import { defaultMergedFileTemplate, GeneratedOutFileNames, GenerateMergedOutFileNames } from '../util/outputNameTemplate';
import { primaryColor, saveColor, warningColor } from '../colors';

@ -20,8 +20,8 @@ import useUserSettings from '../hooks/useUserSettings';
import styles from './ExportConfirm.module.css';
import { SegmentToExport } from '../types';
import { defaultCutFileTemplate, defaultCutMergedFileTemplate, GenerateOutFileNames } from '../util/outputNameTemplate';
import { FFprobeStream } from '../../../../ffprobe';
import { AvoidNegativeTs, FixCodecTagOption, PreserveMetadata } from '../../../../types';
import { FFprobeStream } from '../../../common/ffprobe';
import { AvoidNegativeTs, FixCodecTagOption, PreserveMetadata } from '../../../common/types';
import TextInput from './TextInput';
import { UseSegments } from '../hooks/useSegments';
import ExportSheet from './ExportSheet';
@ -619,7 +619,7 @@ function ExportConfirm({
{lossyMode != null && (
<AnimatedTr>
<td>
Lossy mode
{t('Lossy mode')}
</td>
<td>
<Switch disabled checked={lossyMode != null} />

@ -10,7 +10,7 @@ import useUserSettings from '../hooks/useUserSettings';
import SetCutpointButton from './SetCutpointButton';
import SegmentCutpointButton from './SegmentCutpointButton';
import { getModifier } from '../hooks/useTimelineScroll';
import { KeyBinding, KeyboardAction, ModifierKey } from '../../../../types';
import { KeyBinding, KeyboardAction, ModifierKey } from '../../../common/types';
import { StateSegment } from '../types';
import { allModifiers, altModifiers, controlModifiers, metaModifiers, shiftModifiers, splitKeyboardKeys } from '../util';
import * as Dialog from './Dialog';

@ -6,7 +6,7 @@ import { motion } from 'framer-motion';
import Select from './Select';
import Switch from './Switch';
import styles from './PlaybackStreamSelector.module.css';
import { FFprobeStream } from '../../../../ffprobe';
import { FFprobeStream } from '../../../common/ffprobe';
function PlaybackStreamSelector({

@ -10,7 +10,8 @@ import Switch from './Switch';
import useUserSettings from '../hooks/useUserSettings';
import { askForFfPath } from '../dialogs';
import { isMasBuild, isStoreBuild } from '../util';
import { Config, SupportedLanguage, ModifierKey, TimecodeFormat, langNames } from '../../../../types';
import { SupportedLanguage, langNames } from '../../../common/i18n';
import { Config, ModifierKey, TimecodeFormat } from '../../../common/types.js';
import styles from './Settings.module.css';
import SelectRaw, { SelectProps } from './Select';
import ButtonRaw, { ButtonProps } from './Button';

@ -8,7 +8,7 @@ import invariant from 'tiny-invariant';
import { pcmAudioCodecs, isMov } from './util/streams';
import { isExecaError } from './util';
import { isDurationValid } from './segments';
import { FFprobeChapter, FFprobeFormat, FFprobeProbeResult, FFprobeStream } from '../../../ffprobe';
import { FFprobeChapter, FFprobeFormat, FFprobeProbeResult, FFprobeStream } from '../../common/ffprobe';
import { parseSrt, parseSrtToSegments } from './edlFormats';
import { UnsupportedFileError, UserFacingError } from '../errors';

@ -1,6 +1,6 @@
// This code is for future use (e.g. creating black video to fill in using same codec parameters)
import { FFprobeStream } from '../../../ffprobe';
import { FFprobeStream } from '../../common/ffprobe';
export function parseLevel(videoStream: FFprobeStream) {
const { level: levelNumeric, codec_name: videoCodec } = videoStream;

@ -10,8 +10,8 @@ import { isCuttingStart, isCuttingEnd, runFfmpegWithProgress, getFfCommandLine,
import { getMapStreamsArgs, getStreamIdsToCopy } from '../util/streams';
import { needsSmartCut, getCodecParams } from '../smartcut';
import { getGuaranteedSegments, isDurationValid } from '../segments';
import { FFprobeStream } from '../../../../ffprobe';
import { AvoidNegativeTs, FixCodecTagOption, Html5ifyMode, PreserveMetadata } from '../../../../types';
import { FFprobeStream } from '../../../common/ffprobe';
import { AvoidNegativeTs, FixCodecTagOption, Html5ifyMode, PreserveMetadata } from '../../../common/types';
import { AllFilesMeta, Chapter, CopyfileStreams, CustomTagsByFile, LiteFFprobeStream, ParamsByStreamId, SegmentToExport } from '../types';
import { LossyMode } from '../../../main';
import { UserFacingError } from '../../errors';

@ -7,7 +7,7 @@ import { getNumDigits } from '../segments';
import * as ffmpeg from '../ffmpeg';
import { FormatTimecode } from '../types';
import { CaptureFormat } from '../../../../types';
import { CaptureFormat } from '../../../common/types';
const mime = window.require('mime-types');
const { rename, readdir, writeFile } = window.require('fs/promises');

@ -2,7 +2,7 @@ import { useState, useCallback, ChangeEventHandler } from 'react';
import i18n from 'i18next';
import { useTranslation } from 'react-i18next';
import { Html5ifyMode } from '../../../../types';
import { Html5ifyMode } from '../../../common/types';
import { DirectoryAccessDeclinedError } from '../../errors';
import getSwal from '../swal';
import Checkbox from '../components/Checkbox';

@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { KeyBinding, KeyboardAction } from '../../../../types';
import { KeyBinding, KeyboardAction } from '../../../common/types';
import { allModifiers, altModifiers, controlModifiers, metaModifiers, shiftModifiers } from '../util';
import { KeyboardLayoutMap } from '../types';
import isDev from '../isDev';

@ -4,7 +4,7 @@ import useDebounceOld from 'react-use/lib/useDebounce'; // Want to phase out thi
import { useTranslation } from 'react-i18next';
import { readFramesAroundTime, findNearestKeyFrameTime as ffmpegFindNearestKeyFrameTime, Frame, readFrames } from '../ffmpeg';
import { FFprobeStream } from '../../../../ffprobe';
import { FFprobeStream } from '../../../common/ffprobe';
import { getFrameCountRaw } from '../edlFormats';
import { HandleError } from '../contexts';

@ -17,8 +17,8 @@ import { parameters as allFfmpegParameters, FfmpegDialog, getHint, getLabel } fr
import { maxSegmentsAllowed } from '../util/constants';
import { DefiniteSegmentBase, ParseTimecode, SegmentBase, segmentTagsSchema, SegmentToExport, StateSegment, UpdateSegAtIndex } from '../types';
import safeishEval from '../worker/eval';
import { ScopeSegment } from '../../../../types';
import { FFprobeFormat, FFprobeStream } from '../../../../ffprobe';
import { ScopeSegment } from '../../../common/types';
import { FFprobeFormat, FFprobeStream } from '../../../common/ffprobe';
import { HandleError } from '../contexts';
import { ShowGenericDialog, useGenericDialogContext } from '../components/GenericDialog';
import ExpressionDialog from '../components/ExpressionDialog';
@ -257,7 +257,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
<AlertDialog.Description>{description}</AlertDialog.Description>
{docUrl && <p><Button onClick={() => shell.openExternal(docUrl)}><FaLink style={{ fontSize: '.8em' }} /> Read more</Button></p>}
{docUrl && <p><Button onClick={() => shell.openExternal(docUrl)}><FaLink style={{ fontSize: '.8em' }} /> {t('Read more')}</Button></p>}
<form onSubmit={handleSubmit}>
{Object.entries(parametersIn).map(([key, parameter], i) => {

@ -5,7 +5,7 @@ import { Trans, useTranslation } from 'react-i18next';
import { isStreamThumbnail, shouldCopyStreamByDefault } from '../util/streams';
import StreamsSelector from '../StreamsSelector';
import { FFprobeStream } from '../../../../ffprobe';
import { FFprobeStream } from '../../../common/ffprobe';
import { FilesMeta } from '../types';
import safeishEval from '../worker/eval';
import i18n from '../i18n';

@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { extractSubtitleTrackVtt } from '../ffmpeg';
import { FFprobeStream } from '../../../../ffprobe';
import { FFprobeStream } from '../../../common/ffprobe';
export default () => {

@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { FormatTimecode, ParseTimecode } from '../types';
import { getFrameCountRaw } from '../edlFormats';
import { getFrameDuration } from '../util';
import { TimecodeFormat } from '../../../../types';
import { TimecodeFormat } from '../../../common/types';
import { formatDuration, parseDuration } from '../util/duration';
import { ShowGenericDialog, useGenericDialogContext } from '../components/GenericDialog';
import * as AlertDialog from '../components/AlertDialog';

@ -2,7 +2,7 @@ import { WheelEventHandler, useCallback } from 'react';
import { t } from 'i18next';
import normalizeWheel from './normalizeWheel';
import { ModifierKey } from '../../../../types';
import { ModifierKey } from '../../../common/types';
import { getMetaKeyName } from '../util';
export const keyMap = {

@ -2,7 +2,7 @@ import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
import i18n from 'i18next';
import { Transition } from 'framer-motion';
import { Config } from '../../../../types';
import { Config } from '../../../common/types.js';
import { errorToast } from '../swal';
import isDev from '../isDev';

@ -4,7 +4,7 @@ import invariant from 'tiny-invariant';
import { renderWaveformPng, safeCreateBlob } from '../ffmpeg';
import { OverviewWaveform, WaveformSlice } from '../types';
import { FFprobeStream } from '../../../../ffprobe';
import { FFprobeStream } from '../../../common/ffprobe';
const maxWaveforms = 100;

@ -3,7 +3,7 @@ import i18n from 'i18next';
import { getRealVideoStreams, getVideoTimebase } from './util/streams';
import { readKeyframesAroundTime, findNextKeyframe, findKeyframeAtExactTime } from './ffmpeg';
import { FFprobeStream } from '../../../ffprobe';
import { FFprobeStream } from '../../common/ffprobe';
import { UserFacingError } from '../errors';
const { stat } = window.require('fs-extra');

@ -1,6 +1,6 @@
import type { MenuItem, MenuItemConstructorOptions } from 'electron';
import { z } from 'zod';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../common/ffprobe';
import type { FileStream } from './ffmpeg';

@ -9,7 +9,7 @@ import invariant from 'tiny-invariant';
import { ffmpegExtractWindow } from './util/constants';
import { appName } from '../../main/common';
import { Html5ifyMode } from '../../../types';
import { Html5ifyMode } from '../../common/types';
import { UserFacingError } from '../errors';
const { dirname, parse: parsePath, join, extname, isAbsolute, resolve, basename } = window.require('path');

@ -1,7 +1,7 @@
import { test, expect } from 'vitest';
import { getMapStreamsArgs, getStreamIdsToCopy } from './streams';
import { FFprobeStreamDisposition } from '../../../../ffprobe';
import { FFprobeStreamDisposition } from '../../../common/ffprobe';
import { LiteFFprobeStream } from '../types';

@ -1,8 +1,8 @@
import invariant from 'tiny-invariant';
import { FFprobeStream, FFprobeStreamDisposition } from '../../../../ffprobe';
import { FFprobeStream, FFprobeStreamDisposition } from '../../../common/ffprobe';
import { AllFilesMeta, ChromiumHTMLAudioElement, ChromiumHTMLVideoElement, CopyfileStreams, LiteFFprobeStream } from '../types';
import type { FileStream } from '../ffmpeg';
import { FixCodecTagOption } from '../../../../types';
import { FixCodecTagOption } from '../../../common/types';
// taken from `ffmpeg -codecs`

@ -0,0 +1,13 @@
{
"extends": ["@tsconfig/strictest", "@tsconfig/node22/tsconfig.json"],
"compilerOptions": {
"composite": true,
"noEmit": false,
"outDir": "common-ts-dist",
"tsBuildInfoFile": "common-ts-dist/tsconfig.tsbuildinfo",
},
"include": [
"src/common/**/*",
],
}

@ -3,6 +3,7 @@
{ "path": "./tsconfig.web.json" },
{ "path": "./tsconfig.main.json" },
{ "path": "./tsconfig.node.json" },
{ "path": "./tsconfig.common.json" },
],
"files": [],
}

@ -4,16 +4,18 @@
"composite": true,
"emitDeclarationOnly": true,
"outDir": "ts-dist",
"tsBuildInfoFile": "ts-dist/tsconfig.tsbuildinfo",
"outDir": "main-ts-dist",
"tsBuildInfoFile": "main-ts-dist/tsconfig.tsbuildinfo",
"lib": ["ES2024"],
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["vite/client"],
},
"references": [
{ "path": "./tsconfig.common.json" },
],
"include": [
"src/main/**/*",
"types.ts",
],
}

@ -4,6 +4,8 @@
"noEmit": true,
},
"include": [
"electron.vite.config.ts",
"i18next.config.ts",
"script/**/*",
],
}

@ -9,6 +9,7 @@
},
"references": [
{ "path": "./tsconfig.main.json" },
{ "path": "./tsconfig.common.json" },
],
"include": [
"src/renderer/**/*",

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save